From 6849d510ac81f25dfc02bcc872fa0d65d12a5f63 Mon Sep 17 00:00:00 2001 From: mattyatea <mattyacocacora0@gmail.com> Date: Sun, 10 Dec 2023 09:17:24 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=E8=A4=87=E6=95=B0=E3=83=8E=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=AE=E9=80=9A=E5=A0=B1=E6=A9=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + .../1702149469508-abusenoteselect.js | 11 ++ .../backend/src/core/GlobalEventService.ts | 1 + .../entities/AbuseUserReportEntityService.ts | 1 + .../backend/src/models/AbuseUserReport.ts | 5 + .../api/endpoints/users/report-abuse.ts | 19 ++- .../frontend/src/components/MkAbuseReport.vue | 130 +++++++++++------- .../src/components/MkAbuseReportWindow.vue | 95 +++++++++++-- .../frontend/src/scripts/get-note-menu.ts | 2 +- 10 files changed, 197 insertions(+), 69 deletions(-) create mode 100644 packages/backend/migration/1702149469508-abusenoteselect.js diff --git a/locales/index.d.ts b/locales/index.d.ts index 846a6d503d..da976aca93 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -682,6 +682,7 @@ export interface Locale { "forwardReport": string; "forwardReportIsAnonymous": string; "send": string; + "reportedNote": string; "abuseMarkAsResolved": string; "openInNewTab": string; "openInSideView": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0d84440bc8..9c26c03397 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -679,6 +679,7 @@ reporterOrigin: "通報元" forwardReport: "リモートサーバーに通報を転送する" forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。" send: "送信" +reportedNote: "通報されたノート" abuseMarkAsResolved: "対応済みにする" openInNewTab: "新しいタブで開く" openInSideView: "サイドビューで開く" diff --git a/packages/backend/migration/1702149469508-abusenoteselect.js b/packages/backend/migration/1702149469508-abusenoteselect.js new file mode 100644 index 0000000000..eb9d5b34d8 --- /dev/null +++ b/packages/backend/migration/1702149469508-abusenoteselect.js @@ -0,0 +1,11 @@ +export class Abusenoteselect1702149469508 { + name = 'Abusenoteselect1702149469508' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "notes" jsonb NOT NULL DEFAULT '[]'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "notes"`); + } +} diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index d175f21f2f..f16c18c2d6 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -157,6 +157,7 @@ export interface AdminEventTypes { targetUserId: MiUser['id'], reporterId: MiUser['id'], comment: string; + notes: any[]; }; } //#endregion diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index 97de891ece..627dddd3bc 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -33,6 +33,7 @@ export class AbuseUserReportEntityService { id: report.id, createdAt: this.idService.parse(report.id).date.toISOString(), comment: report.comment, + notes: report.notes, resolved: report.resolved, reporterId: report.reporterId, targetUserId: report.targetUserId, diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts index 593c44f66b..995fbc4018 100644 --- a/packages/backend/src/models/AbuseUserReport.ts +++ b/packages/backend/src/models/AbuseUserReport.ts @@ -60,6 +60,11 @@ export class MiAbuseUserReport { }) public comment: string; + @Column('jsonb', { + default: [], + }) + public notes: any[]; + //#region Denormalized fields @Index() @Column('varchar', { diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 3bcf44cc42..d54b01a6f1 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -3,14 +3,17 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { setImmediate } from 'node:timers/promises'; import sanitizeHtml from 'sanitize-html'; import { Inject, Injectable } from '@nestjs/common'; -import type { AbuseUserReportsRepository } from '@/models/_.js'; +import { In } from 'typeorm'; +import type { AbuseUserReportsRepository, NotesRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { MetaService } from '@/core/MetaService.js'; import { EmailService } from '@/core/EmailService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; @@ -21,7 +24,7 @@ export const meta = { requireCredential: true, - description: 'File a report.', + description: 'User a report.', errors: { noSuchUser: { @@ -49,6 +52,7 @@ export const paramDef = { properties: { userId: { type: 'string', format: 'misskey:id' }, comment: { type: 'string', minLength: 1, maxLength: 2048 }, + noteIds: { type: 'array', items: { type: 'string', format: 'misskey:id' } }, }, required: ['userId', 'comment'], } as const; @@ -59,11 +63,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + private idService: IdService, private metaService: MetaService, private emailService: EmailService, private getterService: GetterService, private roleService: RoleService, + private noteEntityService: NoteEntityService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { @@ -81,6 +89,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.cannotReportAdmin); } + const notes = ps.noteIds ? await this.notesRepository.find({ + where: { id: In(ps.noteIds) }, + }) : []; + const filteredNotes = notes.filter(note => note.userId === user.id); const report = await this.abuseUserReportsRepository.insert({ id: this.idService.gen(), targetUserId: user.id, @@ -88,6 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- reporterId: me.id, reporterHost: null, comment: ps.comment, + notes: ps.noteIds ? await this.noteEntityService.packMany(filteredNotes) : [], }).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0])); // Publish event to moderators @@ -100,9 +113,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- targetUserId: report.targetUserId, reporterId: report.reporterId, comment: report.comment, + notes: report.notes, }); } - const meta = await this.metaService.fetch(); if (meta.email) { this.emailService.sendEmail(meta.email, 'New abuse report', diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index ce7e134b70..4c8a334b23 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="bcekxzvu _margin _panel"> - <div class="target"> - <MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`"> - <MkAvatar class="avatar" :user="report.targetUser" indicator/> - <div class="names"> - <MkUserName class="name" :user="report.targetUser"/> - <MkAcct class="acct" :user="report.targetUser" style="display: block;"/> +<div :class="$style.root"> + <div :class="$style.target"> + <MkA v-user-preview="report.targetUserId" :class="$style.info" :to="`/admin/user/${report.targetUserId}`"> + <MkAvatar :class="$style.avatar" :user="report.targetUser" indicator/> + <div :class="$style.name"> + <MkUserName :class="$style.names" :user="report.targetUser"/> + <MkAcct :class="$style.names" :user="report.targetUser" style="display: block;"/> </div> </MkA> <MkKeyValue> @@ -18,9 +18,15 @@ SPDX-License-Identifier: AGPL-3.0-only <template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template> </MkKeyValue> </div> - <div class="detail"> + <div :class="$style.detail"> <div> <Mfm :text="report.comment"/> + <MkFolder v-if="report.notes.length !== 0" :class="$style.notes"> + <template #label>{{ i18n.ts.reportedNote }}</template> + <div v-for="note in report.notes" :class="$style.notes"> + <MkNoteSimple :note="note"/> + </div> + </MkFolder> </div> <hr/> <div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link">@{{ report.reporter.username }}</MkA></div> @@ -42,15 +48,28 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { dateString } from '@/filters/date.js'; - +import MkFolder from '@/components/MkFolder.vue'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; const props = defineProps<{ - report: any; + report: { + id: string; + createdAt:string; + targetUserId:Misskey.entities.User['id']; + targetUser:Misskey.entities.User & {createdAt:string;}; + reporter:Misskey.entities.User; + assignee:Misskey.entities.User['id']; + comment:string; + notes:Misskey.entities.Note['id'][]; + forwarded:boolean; + resolved:boolean; + }; }>(); const emit = defineEmits<{ @@ -69,47 +88,56 @@ function resolve() { } </script> -<style lang="scss" scoped> -.bcekxzvu { +<style lang="scss" module> +.root { display: flex; - - > .target { - width: 35%; - box-sizing: border-box; - text-align: left; - padding: 24px; - border-right: solid 1px var(--divider); - - > .info { - display: flex; - box-sizing: border-box; - align-items: center; - padding: 14px; - border-radius: 8px; - --c: rgb(255 196 0 / 15%); - background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); - background-size: 16px 16px; - - > .avatar { - width: 42px; - height: 42px; - } - - > .names { - margin-left: 0.3em; - padding: 0 8px; - flex: 1; - - > .name { - font-weight: bold; - } - } - } - } - - > .detail { - flex: 1; - padding: 24px; - } + margin: var(--margin) 0; + background: var(--panel); + border-radius: var(--radius); + overflow: clip; } + +.notes { + margin: var(--margin) 0; + padding: 0; +} + +.target { + width: 35%; + box-sizing: border-box; + text-align: left; + padding: 24px; + border-right: solid 1px var(--divider); +} +.info { + display: flex; + box-sizing: border-box; + align-items: center; + padding: 14px; + border-radius: 8px; + --c: rgb(255 196 0 / 15%); + background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); + background-size: 16px 16px; +} + +.avatar { + width: 42px; + height: 42px; +} + +.names { + margin-left: 0.3em; + padding: 0 8px; + flex: 1; +} + +.name { + font-weight: bold; +} + +.detail { + flex: 1; + padding: 24px; +} + </style> diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue index 7814681ea2..1177eb81bf 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.vue +++ b/packages/frontend/src/components/MkAbuseReportWindow.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')"> +<MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" style="overflow-x: clip;" @closed="emit('closed')"> <template #header> <i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i> <I18n :src="i18n.ts.reportAbuseOf" tag="span"> @@ -13,19 +13,45 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </I18n> </template> - <MkSpacer :marginMin="20" :marginMax="28"> - <div class="_gaps_m" :class="$style.root"> - <div class=""> - <MkTextarea v-model="comment"> - <template #label>{{ i18n.ts.details }}</template> - <template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template> - </MkTextarea> - </div> - <div class=""> - <MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton> - </div> - </div> - </MkSpacer> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + > + <template v-if="page === 0"> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps_m" :class="$style.root"> + <MkPagination v-slot="{items}" :key="user.id" :pagination="Pagination"> + <div v-for="item in items" :key="item.id" :class="$style.note"> + <MkSwitch v-model="item.isAbuseReport" @update:modelValue="pushAbuseReportNote($event,item.id)"></MkSwitch> + <MkAvatar :user="item.user" preview/> + <MkNoteSimple :note="item"/> + </div> + </MkPagination> + <div class="_buttonsCenter"> + <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + </MkSpacer> + </template> + + <template v-else-if="page === 1"> + <MkSpacer :marginMin="20" :marginMax="28"> + <div class="_gaps_m" :class="$style.root"> + <MkTextarea v-model="comment"> + <template #label>{{ i18n.ts.details }}</template> + <template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template> + </MkTextarea> + <div class="_buttonsCenter"> + <MkButton @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton> + </div> + </div> + </MkSpacer> + </template> + </Transition> </MkWindow> </template> @@ -37,23 +63,46 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; +import MkPagination from '@/components/MkPagination.vue'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; const props = defineProps<{ user: Misskey.entities.User; initialComment?: string; + initialNoteId?: Misskey.entities.Note['id']; }>(); +const Pagination = { + endpoint: 'users/notes' as const, + limit: 10, + params: { + userId: props.user.id, + }, +}; + const emit = defineEmits<{ (ev: 'closed'): void; }>(); +const abuseNotesId = ref(props.initialNoteId ? [props.initialNoteId] : []); +const page = ref(0); const uiWindow = shallowRef<InstanceType<typeof MkWindow>>(); const comment = ref(props.initialComment ?? ''); +function pushAbuseReportNote(v, id) { + if (v) { + abuseNotesId.value.push(id); + } else { + abuseNotesId.value = abuseNotesId.value.filter(noteId => noteId !== id); + } +} + function send() { os.apiWithDialog('users/report-abuse', { userId: props.user.id, comment: comment.value, + noteIds: abuseNotesId.value, }, undefined).then(res => { os.alert({ type: 'success', @@ -69,4 +118,22 @@ function send() { .root { --root-margin: 16px; } +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_x_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_x_leaveTo { + opacity: 0; + transform: translateX(-50px); +} +.note{ + display: flex; + margin: var(--margin) 0; + align-items: center; + +} </style> diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 14ada9b7f0..4f467213b8 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -102,7 +102,7 @@ export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): Men const u = note.url ?? note.uri ?? `${url}/notes/${note.id}`; os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: note.user, - initialComment: `Note: ${u}\n-----\n`, + initialNoteId: note.id, }, {}, 'closed'); }, };