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; })[]; }; };