From 8e9b6dc4d18825c98975cd5b424cdfd5a817f2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 17 Sep 2024 08:08:44 +0900 Subject: [PATCH] =?UTF-8?q?enhance(backend):=20=E5=80=8B=E4=BA=BA=E5=AE=9B?= =?UTF-8?q?=E3=81=AE=E3=81=8A=E7=9F=A5=E3=82=89=E3=81=9B=E3=81=AF=E3=82=8F?= =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=9F=E3=82=92=E6=8A=BC=E3=81=99=E3=81=A8?= =?UTF-8?q?=E9=81=8E=E5=8E=BB=E3=81=AE=E3=81=8A=E7=9F=A5=E3=82=89=E3=81=9B?= =?UTF-8?q?=E3=81=AB=E8=A1=A8=E7=A4=BA=E3=81=95=E3=81=9B=E3=82=8B=20(Missk?= =?UTF-8?q?eyIO#736)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/core/AnnouncementService.ts | 50 +++++++++---------- .../api/endpoints/admin/announcements/list.ts | 6 +++ .../MkUserAnnouncementEditDialog.vue | 3 +- packages/frontend/src/pages/admin-user.vue | 2 +- .../src/pages/admin/announcements.vue | 2 +- packages/misskey-js/src/autogen/types.ts | 2 + 6 files changed, 37 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index 394b47c99..07a8a7833 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -138,7 +138,7 @@ export class AnnouncementService { limit: number, offset: number, moderator: MiUser, - ): Promise<(MiAnnouncement & { userInfo: Packed<'UserLite'> | null, reads: number })[]> { + ): Promise<(MiAnnouncement & { userInfo: Packed<'UserLite'> | null, reads: number, lastReadAt: Date | null })[]> { const query = this.announcementsRepository.createQueryBuilder('announcement'); if (userId) { @@ -157,13 +157,14 @@ export class AnnouncementService { .offset(offset) .getMany(); - const reads = new Map(); - - for (const announcement of announcements) { - reads.set(announcement, await this.announcementReadsRepository.countBy({ - announcementId: announcement.id, - })); - } + const reads = announcements.length > 0 + ? await this.announcementReadsRepository.createQueryBuilder() + .select('"announcementId", count(*) as "reads", max("id") as "lastReadId"') + .where('"announcementId" IN (:...announcementIds)', { announcementIds: announcements.map(a => a.id) }) + .groupBy('"announcementId"') + .getRawMany<{ announcementId: string, reads: number, lastReadId: string | null }>() + .then(rs => new Map(rs.map(r => [r.announcementId, { reads: r.reads, lastReadAt: r.lastReadId ? this.idService.parse(r.lastReadId).date : null }]))) + : new Map(); const users = await this.usersRepository.findBy({ id: In(announcements.map(a => a.userId).filter(id => id != null)), @@ -174,8 +175,8 @@ export class AnnouncementService { return announcements.map(announcement => ({ ...announcement, + ...reads.get(announcement.id) ?? { reads: 0, lastReadAt: null }, userInfo: packedUsers.find(u => u.id === announcement.userId) ?? null, - reads: reads.get(announcement) ?? 0, })); } @@ -293,18 +294,20 @@ export class AnnouncementService { 'read.id IS NOT NULL as "isRead"', ]); query - .andWhere( - new Brackets((qb) => { - qb.orWhere('announcement."userId" = :userId', { userId: me.id }); - qb.orWhere('announcement."userId" IS NULL'); - }), - ) - .andWhere( - new Brackets((qb) => { - qb.orWhere('announcement."forExistingUsers" = false'); - qb.orWhere('announcement.id > :userId', { userId: me.id }); - }), - ); + .andWhere(new Brackets((qb) => { + qb.orWhere(new Brackets((nqb) => { + nqb.andWhere('announcement."userId" = :userId', { userId: me.id }); + nqb.andWhere(isActive ? 'read.id IS NULL' : 'read.id IS NOT NULL'); + })); + qb.orWhere(new Brackets((nqb) => { + nqb.andWhere('announcement."userId" IS NULL'); + nqb.andWhere('announcement."isActive" = :isActive', { isActive }); + })); + })) + .andWhere(new Brackets((qb) => { + qb.orWhere('announcement."forExistingUsers" = false'); + qb.orWhere('announcement.id > :userId', { userId: me.id }); + })); } else { query.select([ 'announcement.*', @@ -312,12 +315,9 @@ export class AnnouncementService { ]); query.andWhere('announcement."userId" IS NULL'); query.andWhere('announcement."forExistingUsers" = false'); + query.andWhere('announcement."isActive" = :isActive', { isActive }); } - query.andWhere('announcement."isActive" = :isActive', { - isActive: isActive, - }); - if (isActive) { query.orderBy({ '"isRead"': 'ASC', diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index c8719ffb8..573652ac5 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -97,6 +97,11 @@ export const meta = { type: 'number', optional: false, nullable: false, }, + lastReadAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, }, }, }, @@ -140,6 +145,7 @@ export default class extends Endpoint { // eslint- userId: announcement.userId, user: announcement.userInfo, reads: announcement.reads, + lastReadAt: announcement.lastReadAt?.toISOString() ?? null, })); }); } diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index ec46dac34..7fee7c1b1 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.silence }} -

{{ i18n.tsx.nUsersRead({ n: reads }) }}

+

{{ i18n.tsx.nUsersRead({ n: reads }) }} ()

{{ i18n.ts.delete }} @@ -94,6 +94,7 @@ const closeDuration = ref(props.announcement ? props.announcement.closeD const displayOrder = ref(props.announcement ? props.announcement.displayOrder : 0); const silence = ref(props.announcement ? props.announcement.silence : false); const reads = ref(props.announcement ? props.announcement.reads : 0); +const lastReadAt = ref(props.announcement ? props.announcement.lastReadAt : null); const emit = defineEmits<{ (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 3a9a6c775..4f339e550 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ announcement.title }} - {{ i18n.ts.messageRead }} + {{ i18n.ts.messageRead }} () diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index afd97bc06..7a0f12844 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.silence }} -

{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}

+

{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }} ()

{{ i18n.ts.specifyUser }}
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 92b3b7769..ae000aca0 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -6183,6 +6183,8 @@ export type operations = { userId: string | null; user: components['schemas']['UserLite'] | null; reads: number; + /** Format: date-time */ + lastReadAt: string | null; })[]; }; };