enhance(frontend): tweak user moderation page

This commit is contained in:
syuilo 2023-08-13 21:02:25 +09:00
parent a8d7b69fbd
commit bbef2a953e
12 changed files with 126 additions and 138 deletions

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div class="bcekxzvu _margin _panel"> <div class="bcekxzvu _margin _panel">
<div class="target"> <div class="target">
<MkA v-user-preview="report.targetUserId" class="info" :to="`/user-info/${report.targetUserId}`"> <MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`">
<MkAvatar class="avatar" :user="report.targetUser" indicator/> <MkAvatar class="avatar" :user="report.targetUser" indicator/>
<div class="names"> <div class="names">
<MkUserName class="name" :user="report.targetUser"/> <MkUserName class="name" :user="report.targetUser"/>

View File

@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #value><span class="_monospace"><MkTime :time="file.createdAt" mode="detail" style="display: block;"/></span></template> <template #value><span class="_monospace"><MkTime :time="file.createdAt" mode="detail" style="display: block;"/></span></template>
</MkKeyValue> </MkKeyValue>
</div> </div>
<MkA v-if="file.user" class="user" :to="`/user-info/${file.user.id}`"> <MkA v-if="file.user" class="user" :to="`/admin/user/${file.user.id}`">
<MkUserCardMini :user="file.user"/> <MkUserCardMini :user="file.user"/>
</MkA> </MkA>
<div> <div>

View File

@ -24,12 +24,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo> <MkInfo v-if="user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
<div v-if="user.url" class="_formLinksGrid">
<FormLink :to="userPage(user)">Profile</FormLink>
<FormLink :to="user.url" :external="true">Profile (remote)</FormLink>
</div>
<FormLink v-else :to="userPage(user)">Profile</FormLink>
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink> <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
<div style="display: flex; flex-direction: column; gap: 1em;"> <div style="display: flex; flex-direction: column; gap: 1em;">
@ -57,6 +51,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkKeyValue> </MkKeyValue>
</div> </div>
<MkTextarea v-model="moderationNote" manualSave>
<template #label>Moderation note</template>
</MkTextarea>
<!--
<FormSection> <FormSection>
<template #label>ActivityPub</template> <template #label>ActivityPub</template>
@ -90,14 +89,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder> </MkFolder>
</div> </div>
</FormSection> </FormSection>
</div> -->
<div v-else-if="tab === 'moderation'" class="_gaps_m"> <FormSection>
<div class="_gaps">
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch> <MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
<div> <div>
<MkButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton> <MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
</div> </div>
<MkFolder> <MkFolder>
@ -111,10 +110,25 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #icon><i class="ti ti-badges"></i></template> <template #icon><i class="ti ti-password"></i></template>
<template #label>{{ i18n.ts.roles }}</template> <template #label>IP</template>
<div class="_gaps"> <MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
<MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> <MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
<template v-if="iAmAdmin && ips">
<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
<span class="date">{{ record.createdAt }}</span>
<span class="ip">{{ record.ip }}</span>
</div>
</template>
</MkFolder>
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
</div>
</FormSection>
</div>
<div v-else-if="tab === 'roles'" class="_gaps">
<MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
<div v-for="role in info.roles" :key="role.id" :class="$style.roleItem"> <div v-for="role in info.roles" :key="role.id" :class="$style.roleItem">
<div :class="$style.roleItemMain"> <div :class="$style.roleItemMain">
@ -130,12 +144,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
</div> </div>
</MkFolder>
<MkFolder v-if="user.host == null && iAmModerator"> <div v-else-if="tab === 'announcements'" class="_gaps">
<template #icon><i class="ti ti-speakerphone"></i></template>
<template #label>{{ i18n.ts.announcements }}</template>
<div class="_gaps">
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton> <MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton>
<MkPagination :pagination="announcementsPagination"> <MkPagination :pagination="announcementsPagination">
@ -155,30 +165,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
</MkPagination> </MkPagination>
</div> </div>
</MkFolder>
<MkFolder> <div v-else-if="tab === 'drive'" class="_gaps">
<template #icon><i class="ti ti-password"></i></template>
<template #label>IP</template>
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
<template v-if="iAmAdmin && ips">
<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;">
<span class="date">{{ record.createdAt }}</span>
<span class="ip">{{ record.ip }}</span>
</div>
</template>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-cloud"></i></template>
<template #label>{{ i18n.ts.files }}</template>
<MkFileListForAdmin :pagination="filesPagination" viewMode="grid"/> <MkFileListForAdmin :pagination="filesPagination" viewMode="grid"/>
</MkFolder>
<MkTextarea v-model="moderationNote" manualSave>
<template #label>Moderation note</template>
</MkTextarea>
</div> </div>
<div v-else-if="tab === 'chart'" class="_gaps_m"> <div v-else-if="tab === 'chart'" class="_gaps_m">
@ -230,7 +219,7 @@ import { url } from '@/config';
import { userPage, acct } from '@/filters/user'; import { userPage, acct } from '@/filters/user';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { iAmAdmin, iAmModerator, $i } from '@/account'; import { iAmAdmin, $i } from '@/account';
import MkRolePreview from '@/components/MkRolePreview.vue'; import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue';
@ -269,7 +258,6 @@ const announcementsPagination = {
let expandedRoles = $ref([]); let expandedRoles = $ref([]);
function createFetcher() { function createFetcher() {
if (iAmModerator) {
return () => Promise.all([os.api('users/show', { return () => Promise.all([os.api('users/show', {
userId: props.userId, userId: props.userId,
}), os.api('admin/show-user', { }), os.api('admin/show-user', {
@ -290,13 +278,6 @@ function createFetcher() {
await refreshUser(); await refreshUser();
}); });
}); });
} else {
return () => os.api('users/show', {
userId: props.userId,
}).then((res) => {
user = res;
});
}
} }
function refreshUser() { function refreshUser() {
@ -472,11 +453,19 @@ const headerTabs = $computed(() => [{
key: 'overview', key: 'overview',
title: i18n.ts.overview, title: i18n.ts.overview,
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',
}, iAmModerator ? { }, {
key: 'moderation', key: 'roles',
title: i18n.ts.moderation, title: i18n.ts.roles,
icon: 'ti ti-user-exclamation', icon: 'ti ti-badges',
} : null, { }, {
key: 'announcements',
title: i18n.ts.announcements,
icon: 'ti ti-speakerphone',
}, {
key: 'drive',
title: i18n.ts.drive,
icon: 'ti ti-cloud',
}, {
key: 'chart', key: 'chart',
title: i18n.ts.charts, title: i18n.ts.charts,
icon: 'ti ti-chart-line', icon: 'ti ti-chart-line',
@ -484,11 +473,11 @@ const headerTabs = $computed(() => [{
key: 'raw', key: 'raw',
title: 'Raw', title: 'Raw',
icon: 'ti ti-code', icon: 'ti ti-code',
}].filter(x => x != null)); }]);
definePageMetadata(computed(() => ({ definePageMetadata(computed(() => ({
title: user ? acct(user) : i18n.ts.userInfo, title: user ? acct(user) : i18n.ts.userInfo,
icon: 'ti ti-info-circle', icon: 'ti ti-user-exclamation',
}))); })));
</script> </script>

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in"> <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
<div v-else :class="$style.root" class="_panel"> <div v-else :class="$style.root" class="_panel">
<MkA v-for="user in moderators" :key="user.id" class="user" :to="`/user-info/${user.id}`"> <MkA v-for="user in moderators" :key="user.id" class="user" :to="`/admin/user/${user.id}`">
<MkAvatar :user="user" class="avatar" indicator/> <MkAvatar :user="user" class="avatar" indicator/>
</MkA> </MkA>
</div> </div>

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in"> <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
<div v-else class="users"> <div v-else class="users">
<MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/user-info/${user.id}`" class="user"> <MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/admin/user/${user.id}`" class="user">
<MkUserCardMini :user="user"/> <MkUserCardMini :user="user"/>
</MkA> </MkA>
</div> </div>

View File

@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s"> <div class="_gaps_s">
<div v-for="item in items" :key="item.user.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedItems.includes(item.id) }]"> <div v-for="item in items" :key="item.user.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedItems.includes(item.id) }]">
<div :class="$style.userItemMain"> <div :class="$style.userItemMain">
<MkA :class="$style.userItemMainBody" :to="`/user-info/${item.user.id}`"> <MkA :class="$style.userItemMainBody" :to="`/admin/user/${item.user.id}`">
<MkUserCardMini :user="item.user"/> <MkUserCardMini :user="item.user"/>
</MkA> </MkA>
<button class="_button" :class="$style.userToggle" @click="toggleItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> <button class="_button" :class="$style.userToggle" @click="toggleItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>

View File

@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination"> <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination">
<div :class="$style.users"> <div :class="$style.users">
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/user-info/${user.id}`"> <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/admin/user/${user.id}`">
<MkUserCardMini :user="user"/> <MkUserCardMini :user="user"/>
</MkA> </MkA>
</div> </div>
@ -116,7 +116,7 @@ async function addUser() {
} }
function show(user) { function show(user) {
os.pageWindow(`/user-info/${user.id}`); os.pageWindow(`/admin/user/${user.id}`);
} }
const headerActions = $computed(() => [{ const headerActions = $computed(() => [{

View File

@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div v-else-if="tab === 'users'" class="_gaps_m"> <div v-else-if="tab === 'users'" class="_gaps_m">
<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;"> <MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`"> <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`">
<MkUserCardMini :user="user"/> <MkUserCardMini :user="user"/>
</MkA> </MkA>
</MkPagination> </MkPagination>

View File

@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s"> <div class="_gaps_s">
<div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedRenoteMuteItems.includes(item.id) }]"> <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedRenoteMuteItems.includes(item.id) }]">
<div :class="$style.userItemMain"> <div :class="$style.userItemMain">
<MkA :class="$style.userItemMainBody" :to="`/user-info/${item.mutee.id}`"> <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)">
<MkUserCardMini :user="item.mutee"/> <MkUserCardMini :user="item.mutee"/>
</MkA> </MkA>
<button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> <button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s"> <div class="_gaps_s">
<div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedMuteItems.includes(item.id) }]"> <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedMuteItems.includes(item.id) }]">
<div :class="$style.userItemMain"> <div :class="$style.userItemMain">
<MkA :class="$style.userItemMainBody" :to="`/user-info/${item.mutee.id}`"> <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)">
<MkUserCardMini :user="item.mutee"/> <MkUserCardMini :user="item.mutee"/>
</MkA> </MkA>
<button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> <button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s"> <div class="_gaps_s">
<div v-for="item in items" :key="item.blockee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedBlockItems.includes(item.id) }]"> <div v-for="item in items" :key="item.blockee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedBlockItems.includes(item.id) }]">
<div :class="$style.userItemMain"> <div :class="$style.userItemMain">
<MkA :class="$style.userItemMainBody" :to="`/user-info/${item.blockee.id}`"> <MkA :class="$style.userItemMainBody" :to="userPage(item.blockee)">
<MkUserCardMini :user="item.blockee"/> <MkUserCardMini :user="item.blockee"/>
</MkA> </MkA>
<button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> <button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>

View File

@ -42,10 +42,6 @@ export const routes = [{
}, { }, {
path: '/clips/:clipId', path: '/clips/:clipId',
component: page(() => import('./pages/clip.vue')), component: page(() => import('./pages/clip.vue')),
}, {
path: '/user-info/:userId',
component: page(() => import('./pages/user-info.vue')),
hash: 'initialTab',
}, { }, {
path: '/instance-info/:host', path: '/instance-info/:host',
component: page(() => import('./pages/instance-info.vue')), component: page(() => import('./pages/instance-info.vue')),
@ -334,6 +330,9 @@ export const routes = [{
}, { }, {
path: '/registry', path: '/registry',
component: page(() => import('./pages/registry.vue')), component: page(() => import('./pages/registry.vue')),
}, {
path: '/admin/user/:userId',
component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')),
}, { }, {
path: '/admin/file/:fileId', path: '/admin/file/:fileId',
component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')),

View File

@ -133,13 +133,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
action: () => { action: () => {
copyToClipboard(`@${user.username}@${user.host ?? host}`); copyToClipboard(`@${user.username}@${user.host ?? host}`);
}, },
}, { }, ...(iAmModerator ? [{
icon: 'ti ti-info-circle', icon: 'ti ti-user-exclamation',
text: i18n.ts.info, text: i18n.ts.moderation,
action: () => { action: () => {
router.push(`/user-info/${user.id}`); router.push(`/admin/user/${user.id}`);
}, },
}, { }] : []), {
icon: 'ti ti-rss', icon: 'ti ti-rss',
text: i18n.ts.copyRSS, text: i18n.ts.copyRSS,
action: () => { action: () => {

View File

@ -14,7 +14,7 @@ export async function lookupUser() {
if (canceled) return; if (canceled) return;
const show = (user) => { const show = (user) => {
os.pageWindow(`/user-info/${user.id}`); os.pageWindow(`/admin/user/${user.id}`);
}; };
const usernamePromise = os.api('users/show', Acct.parse(result)); const usernamePromise = os.api('users/show', Acct.parse(result));