sharkey/packages/frontend/src/pages/admin-user.vue

683 lines
20 KiB
Vue
Raw Normal View History

<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
2023-05-19 16:20:53 +09:00
<MkSpacer :contentMax="600" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
2023-01-06 13:40:17 +09:00
<div v-if="tab === 'overview'" class="_gaps_m">
2023-01-05 21:04:56 +09:00
<div class="aeakzknw">
2023-01-16 14:18:11 +09:00
<MkAvatar class="avatar" :user="user" indicator link preview/>
2022-06-23 13:39:28 +09:00
<div class="body">
2023-02-08 20:07:19 +09:00
<span class="name"><MkUserName class="name" :user="user"/></span>
2022-06-23 13:39:28 +09:00
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
2022-07-03 00:15:03 +09:00
<span class="state">
2023-10-18 10:00:48 +09:00
<span v-if="!approved" class="silenced">{{ i18n.ts.notApproved }}</span>
<span v-if="approved && !user.host" class="moderator">{{ i18n.ts.approved }}</span>
2022-07-03 00:15:03 +09:00
<span v-if="suspended" class="suspended">Suspended</span>
<span v-if="silenced" class="silenced">Silenced</span>
<span v-if="moderator" class="moderator">Moderator</span>
</span>
2022-06-23 13:39:28 +09:00
</div>
</div>
2022-01-04 15:36:14 +09:00
2023-01-05 21:04:56 +09:00
<MkInfo v-if="user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
2022-06-26 14:17:55 +09:00
2023-01-05 21:04:56 +09:00
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
2022-01-04 15:36:14 +09:00
2023-01-05 21:04:56 +09:00
<div style="display: flex; flex-direction: column; gap: 1em;">
<MkKeyValue :copy="user.id" oneline>
<template #key>ID</template>
<template #value><span class="_monospace">{{ user.id }}</span></template>
2022-01-04 15:36:14 +09:00
</MkKeyValue>
<!-- 要る
2023-01-05 21:04:56 +09:00
<MkKeyValue v-if="ips.length > 0" :copy="user.id" oneline>
<template #key>IP (recent)</template>
<template #value><span class="_monospace">{{ ips[0].ip }}</span></template>
</MkKeyValue>
-->
2023-01-05 21:04:56 +09:00
<MkKeyValue oneline>
2022-07-02 12:22:52 +09:00
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
</MkKeyValue>
2023-01-05 21:04:56 +09:00
<MkKeyValue v-if="info" oneline>
2022-07-02 12:22:52 +09:00
<template #key>{{ i18n.ts.lastActiveDate }}</template>
<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
</MkKeyValue>
2023-01-05 21:04:56 +09:00
<MkKeyValue v-if="info" oneline>
2022-07-03 00:15:03 +09:00
<template #key>{{ i18n.ts.email }}</template>
<template #value><span class="_monospace">{{ info.email }}</span></template>
</MkKeyValue>
2022-01-04 15:36:14 +09:00
</div>
<MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
</MkTextarea>
2023-10-16 01:35:55 +09:00
<FormSection v-if="user.host">
<template #label>ActivityPub</template>
2023-01-06 13:40:17 +09:00
<div class="_gaps_m">
2023-01-05 21:04:56 +09:00
<div style="display: flex; flex-direction: column; gap: 1em;">
2023-10-16 01:35:55 +09:00
<MkKeyValue oneline>
2023-01-05 21:04:56 +09:00
<template #key>{{ i18n.ts.instanceInfo }}</template>
2023-10-01 04:53:52 +09:00
<template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="ph-caret-right ph-bold ph-lg"></i></MkA></template>
2023-01-05 21:04:56 +09:00
</MkKeyValue>
<MkKeyValue oneline>
<template #key>{{ i18n.ts.updatedAt }}</template>
2023-10-16 01:35:55 +09:00
<template #value><MkTime mode="detail" :time="user.lastFetchedAt"/></template>
2023-01-05 21:04:56 +09:00
</MkKeyValue>
</div>
<MkButton @click="updateRemoteUser"><i class="ph-arrows-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.updateRemoteUser }}</MkButton>
2023-01-05 21:04:56 +09:00
</div>
</FormSection>
<FormSection>
<div class="_gaps">
<MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch>
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
<MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch>
<div>
2023-10-01 04:53:52 +09:00
<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ph-key ph-bold ph-lg"></i> {{ i18n.ts.resetPassword }}</MkButton>
</div>
<MkFolder>
2023-10-01 07:46:42 +09:00
<template #icon><i class="ph-scroll ph-bold ph-lg"></i></template>
<template #label>{{ i18n.ts._role.policies }}</template>
<div class="_gaps">
<div v-for="policy in Object.keys(info.policies)" :key="policy">
{{ policy }} ... {{ info.policies[policy] }}
</div>
</div>
</MkFolder>
<MkFolder>
2023-10-01 04:53:52 +09:00
<template #icon><i class="ph-password ph-bold ph-lg"></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>
2023-12-10 02:11:17 +09:00
<div>
2024-01-01 06:26:19 +09:00
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ph-user-circle ph-bold ph-lg"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
<MkButton v-if="iAmModerator" inline danger @click="unsetUserBanner"><i class="ph-photo ph-bold ph-lg"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
</div>
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
</div>
</FormSection>
</div>
<div v-else-if="tab === 'roles'" class="_gaps">
2023-10-01 04:53:52 +09:00
<MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.assign }}</MkButton>
<div v-for="role in info.roles" :key="role.id">
<div :class="$style.roleItemMain">
<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/>
2024-03-03 01:36:49 +09:00
<button class="_button" @click="toggleRoleItem(role)"><i class="ph-caret-down ph-bold ph-lg"></i></button>
2023-10-01 04:53:52 +09:00
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ph-x ph-bold ph-lg"></i></button>
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ph-prohibit ph-bold ph-lg"></i></button>
</div>
<div v-if="expandedRoles.includes(role.id)" :class="$style.roleItemSub">
<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div>
<div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div>
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
</div>
</div>
</div>
<div v-else-if="tab === 'announcements'" class="_gaps">
2023-10-01 04:53:52 +09:00
<MkButton primary rounded @click="createAnnouncement"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.new }}</MkButton>
<MkPagination :pagination="announcementsPagination">
<template #default="{ items }">
<div class="_gaps_s">
<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
<span style="margin-right: 0.5em;">
2023-10-01 04:53:52 +09:00
<i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i>
<i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i>
<i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i>
<i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" style="color: var(--success);"></i>
</span>
<span>{{ announcement.title }}</span>
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
</div>
2022-07-03 00:15:03 +09:00
</div>
</template>
</MkPagination>
</div>
<div v-else-if="tab === 'drive'" class="_gaps">
<MkFileListForAdmin :pagination="filesPagination" viewMode="grid"/>
2022-07-03 00:15:03 +09:00
</div>
2023-01-06 13:40:17 +09:00
<div v-else-if="tab === 'chart'" class="_gaps_m">
2022-06-22 00:10:34 +09:00
<div class="cmhjzshm">
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
2022-07-20 22:24:26 +09:00
<option value="per-user-notes">{{ i18n.ts.notes }}</option>
2022-06-22 00:10:34 +09:00
</MkSelect>
</div>
<div class="charts">
<div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div>
2022-06-22 00:10:34 +09:00
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
<div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
2022-06-22 00:10:34 +09:00
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
</div>
</div>
</div>
2023-01-06 13:40:17 +09:00
<div v-else-if="tab === 'raw'" class="_gaps_m">
<MkObjectView v-if="info && $i.isAdmin" tall :value="info">
</MkObjectView>
<MkObjectView tall :value="user">
</MkObjectView>
</div>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkChart from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue';
2023-01-07 15:09:46 +09:00
import MkTextarea from '@/components/MkTextarea.vue';
2023-01-07 14:59:54 +09:00
import MkSwitch from '@/components/MkSwitch.vue';
2022-01-04 15:36:14 +09:00
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
2023-01-06 09:41:14 +09:00
import MkButton from '@/components/MkButton.vue';
2023-01-09 09:41:25 +09:00
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
2023-01-07 15:09:46 +09:00
import MkSelect from '@/components/MkSelect.vue';
2022-01-04 15:36:14 +09:00
import FormSuspense from '@/components/form/suspense.vue';
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import MkInfo from '@/components/MkInfo.vue';
2023-09-19 16:37:43 +09:00
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
2023-09-19 16:37:43 +09:00
import { url } from '@/config.js';
2023-12-21 11:36:45 +09:00
import { acct } from '@/filters/user.js';
2023-09-19 16:37:43 +09:00
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { iAmAdmin, $i, iAmModerator } from '@/account.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
const props = withDefaults(defineProps<{
userId: string;
initialTab?: string;
}>(), {
initialTab: 'overview',
});
const tab = ref(props.initialTab);
const chartSrc = ref('per-user-notes');
const user = ref<null | Misskey.entities.UserDetailed>();
const init = ref<ReturnType<typeof createFetcher>>();
const info = ref<any>();
const ips = ref<Misskey.entities.AdminGetUserIpsResponse | null>(null);
const ap = ref<any>(null);
const moderator = ref(false);
const silenced = ref(false);
2023-12-23 10:09:23 +09:00
const approved = ref(false);
const suspended = ref(false);
2023-12-23 10:09:23 +09:00
const markedAsNSFW = ref(false);
const moderationNote = ref('');
const filesPagination = {
endpoint: 'admin/drive/files' as const,
limit: 10,
params: computed(() => ({
userId: props.userId,
})),
};
const announcementsPagination = {
endpoint: 'admin/announcements/list' as const,
limit: 10,
params: computed(() => ({
userId: props.userId,
})),
};
const expandedRoles = ref([]);
function createFetcher() {
return () => Promise.all([misskeyApi('users/show', {
userId: props.userId,
}), misskeyApi('admin/show-user', {
userId: props.userId,
}), iAmAdmin ? misskeyApi('admin/get-user-ips', {
userId: props.userId,
}) : Promise.resolve(null)]).then(([_user, _info, _ips]) => {
user.value = _user;
info.value = _info;
ips.value = _ips;
moderator.value = info.value.isModerator;
silenced.value = info.value.isSilenced;
2023-12-23 10:09:23 +09:00
approved.value = info.value.approved;
suspended.value = info.value.isSuspended;
moderationNote.value = info.value.moderationNote;
watch(moderationNote, async () => {
await misskeyApi('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
await refreshUser();
});
});
}
function refreshUser() {
init.value = createFetcher();
}
2021-04-23 12:00:07 +09:00
async function updateRemoteUser() {
await os.apiWithDialog('federation/update-remote-user', { userId: user.value.id });
refreshUser();
}
async function resetPassword() {
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.resetPasswordConfirm,
});
if (confirm.canceled) {
return;
} else {
const { password } = await misskeyApi('admin/reset-password', {
userId: user.value.id,
});
os.alert({
type: 'success',
text: i18n.tsx.newPasswordIs({ password }),
});
}
}
2021-04-23 12:00:07 +09:00
async function toggleNSFW(v) {
const confirm = await os.confirm({
type: 'warning',
text: v ? i18n.ts.nsfwConfirm : i18n.ts.unNsfwConfirm,
});
if (confirm.canceled) {
2023-12-23 10:09:23 +09:00
markedAsNSFW.value = !v;
} else {
2024-01-09 10:57:57 +09:00
await misskeyApi(v ? 'admin/nsfw-user' : 'admin/unnsfw-user', { userId: user.value.id });
await refreshUser();
}
}
async function toggleSilence(v) {
const confirm = await os.confirm({
type: 'warning',
text: v ? i18n.ts.silenceConfirm : i18n.ts.unsilenceConfirm,
});
if (confirm.canceled) {
2023-12-23 10:09:23 +09:00
silenced.value = !v;
} else {
2024-01-09 10:57:57 +09:00
await misskeyApi(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: user.value.id });
await refreshUser();
}
}
async function toggleSuspend(v) {
const confirm = await os.confirm({
type: 'warning',
text: v ? i18n.ts.suspendConfirm : i18n.ts.unsuspendConfirm,
});
if (confirm.canceled) {
suspended.value = !v;
} else {
await misskeyApi(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: user.value.id });
await refreshUser();
}
}
2021-04-23 12:00:07 +09:00
2023-11-19 10:18:57 +09:00
async function unsetUserAvatar() {
2023-12-10 02:11:17 +09:00
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.unsetUserAvatarConfirm,
});
if (confirm.canceled) return;
const process = async () => {
await misskeyApi('admin/unset-user-avatar', { userId: user.value.id });
2023-12-10 02:11:17 +09:00
os.success();
};
await process().catch(err => {
os.alert({
type: 'error',
text: err.toString(),
});
});
refreshUser();
}
2023-11-19 10:18:57 +09:00
async function unsetUserBanner() {
2023-12-10 02:11:17 +09:00
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.unsetUserBannerConfirm,
});
if (confirm.canceled) return;
const process = async () => {
await misskeyApi('admin/unset-user-banner', { userId: user.value.id });
2023-12-10 02:11:17 +09:00
os.success();
};
await process().catch(err => {
os.alert({
type: 'error',
text: err.toString(),
});
});
refreshUser();
}
async function deleteAllFiles() {
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.deleteAllFilesConfirm,
});
if (confirm.canceled) return;
const process = async () => {
await misskeyApi('admin/delete-all-files-of-a-user', { userId: user.value.id });
os.success();
};
await process().catch(err => {
os.alert({
type: 'error',
text: err.toString(),
});
});
await refreshUser();
}
async function deleteAccount() {
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.deleteAccountConfirm,
});
if (confirm.canceled) return;
const typed = await os.inputText({
text: i18n.tsx.typeToConfirm({ x: user.value?.username }),
});
if (typed.canceled) return;
if (typed.result === user.value?.username) {
await os.apiWithDialog('admin/delete-account', {
userId: user.value.id,
});
} else {
os.alert({
type: 'error',
text: 'input not match',
});
}
}
async function assignRole() {
const roles = await misskeyApi('admin/roles/list');
const { canceled, result: roleId } = await os.select({
title: i18n.ts._role.chooseRoleToAssign,
items: roles.map(r => ({ text: r.name, value: r.id })),
});
if (canceled) return;
const { canceled: canceled2, result: period } = await os.select({
title: i18n.ts.period,
items: [{
value: 'indefinitely', text: i18n.ts.indefinitely,
}, {
value: 'oneHour', text: i18n.ts.oneHour,
}, {
value: 'oneDay', text: i18n.ts.oneDay,
}, {
value: 'oneWeek', text: i18n.ts.oneWeek,
}, {
value: 'oneMonth', text: i18n.ts.oneMonth,
}],
default: 'indefinitely',
});
if (canceled2) return;
const expiresAt = period === 'indefinitely' ? null
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
: null;
await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.value.id, expiresAt });
refreshUser();
}
async function unassignRole(role, ev) {
os.popupMenu([{
text: i18n.ts.unassign,
2023-10-01 04:53:52 +09:00
icon: 'ph-x ph-bold ph-lg',
danger: true,
action: async () => {
await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.value.id });
refreshUser();
},
}], ev.currentTarget ?? ev.target);
}
function toggleRoleItem(role) {
if (expandedRoles.value.includes(role.id)) {
expandedRoles.value = expandedRoles.value.filter(x => x !== role.id);
} else {
expandedRoles.value.push(role.id);
}
}
function createAnnouncement() {
os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
user: user.value,
}, {}, 'closed');
}
function editAnnouncement(announcement) {
os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
user: user.value,
announcement,
}, {}, 'closed');
}
watch(() => props.userId, () => {
init.value = createFetcher();
}, {
immediate: true,
});
2021-04-23 12:00:07 +09:00
watch(user, () => {
misskeyApi('ap/get', {
uri: user.value.uri ?? `${url}/users/${user.value.id}`,
}).then(res => {
ap.value = res;
});
});
2021-04-23 12:00:07 +09:00
const headerActions = computed(() => []);
2021-04-23 12:00:07 +09:00
const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
icon: 'ph-info ph-bold ph-lg',
}, {
key: 'roles',
title: i18n.ts.roles,
icon: 'ph-seal-check ph-bold ph-lg',
}, {
key: 'announcements',
title: i18n.ts.announcements,
icon: 'ph-megaphone ph-bold ph-lg',
}, {
key: 'drive',
title: i18n.ts.drive,
icon: 'ph-cloud ph-bold ph-lg',
}, {
key: 'chart',
title: i18n.ts.charts,
icon: 'ph-chart-line ph-bold ph-lg',
}, {
key: 'raw',
title: 'Raw',
icon: 'ph-code ph-bold ph-lg',
}]);
2021-04-23 12:00:07 +09:00
definePageMetadata(() => ({
title: user.value ? acct(user.value) : i18n.ts.userInfo,
2023-10-01 04:53:52 +09:00
icon: 'ph-warning-circle ph-bold ph-lg',
}));
</script>
2021-04-23 12:00:07 +09:00
<style lang="scss" scoped>
2021-04-23 12:00:07 +09:00
.aeakzknw {
2022-06-23 13:39:28 +09:00
display: flex;
align-items: center;
2021-04-23 12:00:07 +09:00
> .avatar {
display: block;
width: 64px;
height: 64px;
2022-06-23 13:39:28 +09:00
margin-right: 16px;
}
> .body {
flex: 1;
overflow: hidden;
> .name {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> .sub {
display: block;
width: 100%;
font-size: 85%;
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
2022-07-03 00:15:03 +09:00
> .state {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 4px;
&:empty {
display: none;
}
> .suspended, > .silenced, > .moderator {
display: inline-block;
border: solid 1px;
border-radius: var(--radius-sm);
2022-07-03 00:15:03 +09:00
padding: 2px 6px;
font-size: 85%;
}
> .suspended {
color: var(--error);
border-color: var(--error);
}
> .silenced {
color: var(--warn);
border-color: var(--warn);
}
> .moderator {
color: var(--success);
border-color: var(--success);
}
}
2021-04-23 12:00:07 +09:00
}
}
2022-06-22 00:10:34 +09:00
.cmhjzshm {
> .selects {
display: flex;
margin: 0 0 16px 0;
}
> .charts {
> .label {
margin-bottom: 12px;
font-weight: bold;
}
}
}
2023-10-18 09:41:36 +09:00
.casdwq {
.silenced {
color: var(--warn);
border-color: var(--warn);
}
.moderator {
color: var(--success);
border-color: var(--success);
}
}
2021-04-23 12:00:07 +09:00
</style>
<style lang="scss" module>
.ip {
display: flex;
> :global(.date) {
opacity: 0.7;
}
> :global(.ip) {
margin-left: auto;
}
}
.roleItemMain {
display: flex;
}
.role {
flex: 1;
min-width: 0;
margin-right: 8px;
}
.roleItemSub {
padding: 6px 12px;
font-size: 85%;
color: var(--fgTransparentWeak);
}
.roleUnassign {
width: 32px;
height: 32px;
margin-left: 8px;
align-self: center;
}
.announcementItem {
display: flex;
padding: 8px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
}
</style>