diff --git a/CHANGELOG.md b/CHANGELOG.md index c4eb644139..3c3b9f289a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ * デフォルトがオフになるので、ロールタイムラインを表示する場合はオンにしてください。 - ロールに強制的にNSFWを付与するポリシーを追加 * アップロード済みのファイルはNSFWにならない為注意してください。 +- モデレーションノートがユーザーのプロフィールページからも閲覧および編集できるようになりました。 - カスタム絵文字のライセンスを複数でセットできるようになりました。 - 管理者が予約ユーザー名を設定できるようになりました。 - Fix: フォローリクエストの通知が残る問題を修正 diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 1bb0653268..562fc5c1e6 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -292,7 +292,7 @@ export class UserEntityService implements OnModuleInit { public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>( src: User['id'] | User, - me?: { id: User['id'] } | null | undefined, + me?: { id: User['id']; isRoot: boolean; } | null | undefined, options?: { detail?: D, includeSecrets?: boolean, @@ -308,6 +308,7 @@ export class UserEntityService implements OnModuleInit { const meId = me ? me.id : null; const isMe = meId === user.id; + const iAmModerator = me ? await this.roleService.isModerator(me) : false; const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null; const pins = opts.detail ? await this.userNotePiningsRepository.createQueryBuilder('pin') @@ -411,6 +412,7 @@ export class UserEntityService implements OnModuleInit { userId: meId, targetUserId: user.id, }).then(row => row?.memo ?? null), + moderationNote: iAmModerator ? profile!.moderationNote : null, } : {}), ...(opts.detail && isMe ? { diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 57063c92de..9a52c31550 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -7,7 +7,7 @@ <!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> --> <div class="profile _gaps"> - <MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo" /> + <MkAccountMoved v-if="user.movedTo" :moved-to="user.movedTo"/> <MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/> <div :key="user.id" class="main _panel"> @@ -42,6 +42,17 @@ <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> </div> </div> + <div v-if="user.roles.length > 0" class="roles"> + <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> + <img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/> + {{ role.name }} + </span> + </div> + <div v-if="iAmModerator" class="moderationNote"> + <MkTextarea v-model="moderationNote" manual-save> + <template #label>Moderation note</template> + </MkTextarea> + </div> <div v-if="isEditingMemo || memoDraft" class="memo" :class="{'no-memo': !memoDraft}"> <div class="heading" v-text="i18n.ts.memo"/> <textarea @@ -53,12 +64,6 @@ @input="adjustMemoTextarea" /> </div> - <div v-if="user.roles.length > 0" class="roles"> - <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> - <img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/> - {{ role.name }} - </span> - </div> <div class="description"> <MkOmit> <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i"/> @@ -134,6 +139,7 @@ import MkNote from '@/components/MkNote.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import MkAccountMoved from '@/components/MkAccountMoved.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; import MkOmit from '@/components/MkOmit.vue'; import MkInfo from '@/components/MkInfo.vue'; import { getScrollPosition } from '@/scripts/scroll'; @@ -143,7 +149,7 @@ import { userPage } from '@/filters/user'; import * as os from '@/os'; import { useRouter } from '@/router'; import { i18n } from '@/i18n'; -import { $i } from '@/account'; +import { $i, iAmModerator } from '@/account'; import { dateString } from '@/filters/date'; import { confetti } from '@/scripts/confetti'; import MkNotes from '@/components/MkNotes.vue'; @@ -168,8 +174,12 @@ let rootEl = $ref<null | HTMLElement>(null); let bannerEl = $ref<null | HTMLElement>(null); let memoTextareaEl = $ref<null | HTMLElement>(null); let memoDraft = $ref(props.user.memo); - let isEditingMemo = $ref(false); +let moderationNote = $ref(props.user.moderationNote); + +watch($$(moderationNote), async () => { + await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote }); +}); const pagination = { endpoint: 'users/notes' as const, @@ -426,6 +436,10 @@ onUnmounted(() => { } } + > .moderationNote { + margin: 12px 24px 0 154px; + } + > .memo { margin: 12px 24px 0 154px; background: transparent; @@ -593,6 +607,10 @@ onUnmounted(() => { justify-content: center; } + > .moderationNote { + margin: 16px 16px 0 16px; + } + > .memo { margin: 16px 16px 0 16px; }