Feat: 未読通知数を表示できるように (#11982)
* 未読通知数を表示できるように * Update Changelog * オプトインにする * Fix lint * (add) テスト通知のプッシュ通知を追加 * add test * フロントエンドの表示上限を99に変更 * Make it default on * 共通スタイルをくくりだす * Update Changelog * tweak * Update UserEntityService.ts * rename * Update navbar-for-mobile.vue --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
e85b8217c0
commit
5fb6847419
22 changed files with 173 additions and 28 deletions
BIN
packages/backend/assets/tabler-badges/bell.png
Normal file
BIN
packages/backend/assets/tabler-badges/bell.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
|
@ -144,7 +144,9 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||
|
||||
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
||||
// テスト通知の場合は即時発行
|
||||
const interval = notification.type === 'test' ? 0 : 2000;
|
||||
setTimeout(interval, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
|
||||
if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return;
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
|
|||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js';
|
||||
import { MiNotification } from '@/models/Notification.js';
|
||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
@ -235,17 +236,34 @@ export class UserEntityService implements OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async getHasUnreadNotification(userId: MiUser['id']): Promise<boolean> {
|
||||
public async getNotificationsInfo(userId: MiUser['id']): Promise<{
|
||||
hasUnread: boolean;
|
||||
unreadCount: number;
|
||||
}> {
|
||||
const response = {
|
||||
hasUnread: false,
|
||||
unreadCount: 0,
|
||||
};
|
||||
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||
|
||||
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${userId}`,
|
||||
'+',
|
||||
'-',
|
||||
'COUNT', 1);
|
||||
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
|
||||
if (!latestReadNotificationId) {
|
||||
response.unreadCount = await this.redisClient.xlen(`notificationTimeline:${userId}`);
|
||||
} else {
|
||||
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${userId}`,
|
||||
'+',
|
||||
latestReadNotificationId,
|
||||
);
|
||||
|
||||
return latestNotificationId != null && (latestReadNotificationId == null || latestReadNotificationId < latestNotificationId);
|
||||
response.unreadCount = (latestNotificationIdsRes.length - 1 >= 0) ? latestNotificationIdsRes.length - 1 : 0;
|
||||
}
|
||||
|
||||
if (response.unreadCount > 0) {
|
||||
response.hasUnread = true;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -331,6 +349,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
...announcement,
|
||||
})) : null;
|
||||
|
||||
const notificationsInfo = isMe && opts.detail ? await this.getNotificationsInfo(user.id) : null;
|
||||
|
||||
const packed = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
|
@ -449,8 +469,9 @@ export class UserEntityService implements OnModuleInit {
|
|||
unreadAnnouncements,
|
||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||
hasUnreadChannel: false, // 後方互換性のため
|
||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||
hasUnreadNotification: notificationsInfo?.hasUnread, // 後方互換性のため
|
||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
||||
mutedWords: profile!.mutedWords,
|
||||
mutedInstances: profile!.mutedInstances,
|
||||
mutingNotificationTypes: [], // 後方互換性のため
|
||||
|
|
|
@ -399,6 +399,10 @@ export const packedMeDetailedOnlySchema = {
|
|||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
unreadNotificationsCount: {
|
||||
type: 'number',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
mutedWords: {
|
||||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
|
|
|
@ -164,6 +164,7 @@ describe('ユーザー', () => {
|
|||
hasUnreadAntenna: user.hasUnreadAntenna,
|
||||
hasUnreadChannel: user.hasUnreadChannel,
|
||||
hasUnreadNotification: user.hasUnreadNotification,
|
||||
unreadNotificationsCount: user.unreadNotificationsCount,
|
||||
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
|
||||
unreadAnnouncements: user.unreadAnnouncements,
|
||||
mutedWords: user.mutedWords,
|
||||
|
@ -414,6 +415,7 @@ describe('ユーザー', () => {
|
|||
assert.strictEqual(response.hasUnreadAntenna, false);
|
||||
assert.strictEqual(response.hasUnreadChannel, false);
|
||||
assert.strictEqual(response.hasUnreadNotification, false);
|
||||
assert.strictEqual(response.unreadNotificationsCount, 0);
|
||||
assert.strictEqual(response.hasPendingReceivedFollowRequest, false);
|
||||
assert.deepStrictEqual(response.unreadAnnouncements, []);
|
||||
assert.deepStrictEqual(response.mutedWords, []);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue