enhance(backend): 個人宛のお知らせはわかったを押すと過去のお知らせに表示させる (MisskeyIO#736)

This commit is contained in:
まっちゃとーにゅ 2024-09-17 08:08:44 +09:00 committed by GitHub
parent 46e4b8f8e1
commit 8e9b6dc4d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 37 additions and 28 deletions

View File

@ -138,7 +138,7 @@ export class AnnouncementService {
limit: number, limit: number,
offset: number, offset: number,
moderator: MiUser, 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'); const query = this.announcementsRepository.createQueryBuilder('announcement');
if (userId) { if (userId) {
@ -157,13 +157,14 @@ export class AnnouncementService {
.offset(offset) .offset(offset)
.getMany(); .getMany();
const reads = new Map<MiAnnouncement, number>(); const reads = announcements.length > 0
? await this.announcementReadsRepository.createQueryBuilder()
for (const announcement of announcements) { .select('"announcementId", count(*) as "reads", max("id") as "lastReadId"')
reads.set(announcement, await this.announcementReadsRepository.countBy({ .where('"announcementId" IN (:...announcementIds)', { announcementIds: announcements.map(a => a.id) })
announcementId: announcement.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({ const users = await this.usersRepository.findBy({
id: In(announcements.map(a => a.userId).filter(id => id != null)), id: In(announcements.map(a => a.userId).filter(id => id != null)),
@ -174,8 +175,8 @@ export class AnnouncementService {
return announcements.map(announcement => ({ return announcements.map(announcement => ({
...announcement, ...announcement,
...reads.get(announcement.id) ?? { reads: 0, lastReadAt: null },
userInfo: packedUsers.find(u => u.id === announcement.userId) ?? 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"', 'read.id IS NOT NULL as "isRead"',
]); ]);
query query
.andWhere( .andWhere(new Brackets((qb) => {
new Brackets((qb) => { qb.orWhere(new Brackets((nqb) => {
qb.orWhere('announcement."userId" = :userId', { userId: me.id }); nqb.andWhere('announcement."userId" = :userId', { userId: me.id });
qb.orWhere('announcement."userId" IS NULL'); nqb.andWhere(isActive ? 'read.id IS NULL' : 'read.id IS NOT NULL');
}), }));
) qb.orWhere(new Brackets((nqb) => {
.andWhere( nqb.andWhere('announcement."userId" IS NULL');
new Brackets((qb) => { nqb.andWhere('announcement."isActive" = :isActive', { isActive });
}));
}))
.andWhere(new Brackets((qb) => {
qb.orWhere('announcement."forExistingUsers" = false'); qb.orWhere('announcement."forExistingUsers" = false');
qb.orWhere('announcement.id > :userId', { userId: me.id }); qb.orWhere('announcement.id > :userId', { userId: me.id });
}), }));
);
} else { } else {
query.select([ query.select([
'announcement.*', 'announcement.*',
@ -312,12 +315,9 @@ export class AnnouncementService {
]); ]);
query.andWhere('announcement."userId" IS NULL'); query.andWhere('announcement."userId" IS NULL');
query.andWhere('announcement."forExistingUsers" = false'); query.andWhere('announcement."forExistingUsers" = false');
query.andWhere('announcement."isActive" = :isActive', { isActive });
} }
query.andWhere('announcement."isActive" = :isActive', {
isActive: isActive,
});
if (isActive) { if (isActive) {
query.orderBy({ query.orderBy({
'"isRead"': 'ASC', '"isRead"': 'ASC',

View File

@ -97,6 +97,11 @@ export const meta = {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,
}, },
lastReadAt: {
type: 'string',
optional: false, nullable: true,
format: 'date-time',
},
}, },
}, },
}, },
@ -140,6 +145,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: announcement.userId, userId: announcement.userId,
user: announcement.userInfo, user: announcement.userInfo,
reads: announcement.reads, reads: announcement.reads,
lastReadAt: announcement.lastReadAt?.toISOString() ?? null,
})); }));
}); });
} }

View File

@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._announcement.silence }} {{ i18n.ts._announcement.silence }}
<template #caption>{{ i18n.ts._announcement.silenceDescription }}</template> <template #caption>{{ i18n.ts._announcement.silenceDescription }}</template>
</MkSwitch> </MkSwitch>
<p v-if="reads">{{ i18n.tsx.nUsersRead({ n: reads }) }}</p> <p v-if="reads">{{ i18n.tsx.nUsersRead({ n: reads }) }} <span v-if="lastReadAt">(<MkTime :time="lastReadAt" mode="absolute"/>)</span></p>
<MkUserCardMini v-if="props.user.id" :user="props.user"></MkUserCardMini> <MkUserCardMini v-if="props.user.id" :user="props.user"></MkUserCardMini>
<MkButton v-if="announcement" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> <MkButton v-if="announcement" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div> </div>
@ -94,6 +94,7 @@ const closeDuration = ref<number>(props.announcement ? props.announcement.closeD
const displayOrder = ref<number>(props.announcement ? props.announcement.displayOrder : 0); const displayOrder = ref<number>(props.announcement ? props.announcement.displayOrder : 0);
const silence = ref<boolean>(props.announcement ? props.announcement.silence : false); const silence = ref<boolean>(props.announcement ? props.announcement.silence : false);
const reads = ref<number>(props.announcement ? props.announcement.reads : 0); const reads = ref<number>(props.announcement ? props.announcement.reads : 0);
const lastReadAt = ref<string | null>(props.announcement ? props.announcement.lastReadAt : null);
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void, (ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,

View File

@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
</span> </span>
<span>{{ announcement.title }}</span> <span>{{ announcement.title }}</span>
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span> <span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }} <span v-if="announcement.lastReadAt">(<MkTime :time="announcement.lastReadAt" mode="absolute"/>)</span></span>
</div> </div>
</div> </div>
</template> </template>

View File

@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription"> <MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription">
{{ i18n.ts._announcement.silence }} {{ i18n.ts._announcement.silence }}
</MkSwitch> </MkSwitch>
<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p> <p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }} <span v-if="announcement.lastReadAt">(<MkTime :time="announcement.lastReadAt" mode="absolute"/>)</span></p>
<MkUserCardMini v-if="announcement.userId" :user="announcement.user" @click="editUser(announcement)"></MkUserCardMini> <MkUserCardMini v-if="announcement.userId" :user="announcement.user" @click="editUser(announcement)"></MkUserCardMini>
<MkButton v-else class="button" inline primary @click="editUser(announcement)">{{ i18n.ts.specifyUser }}</MkButton> <MkButton v-else class="button" inline primary @click="editUser(announcement)">{{ i18n.ts.specifyUser }}</MkButton>
<div class="buttons _buttons"> <div class="buttons _buttons">

View File

@ -6183,6 +6183,8 @@ export type operations = {
userId: string | null; userId: string | null;
user: components['schemas']['UserLite'] | null; user: components['schemas']['UserLite'] | null;
reads: number; reads: number;
/** Format: date-time */
lastReadAt: string | null;
})[]; })[];
}; };
}; };