diff --git a/CHANGELOG.md b/CHANGELOG.md index feabb8e0f..77b01aad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - 最大でも黄色いエリア内にデコレーションを収めることを推奨します。 - 画像は512x512pxを推奨します。 - Enhance: すでにフォローしたすべての人の返信をTLに追加できるように +- Enhance: 未読の通知数を表示できるように - Enhance: ローカリゼーションの更新 - Enhance: 依存関係の更新 @@ -63,6 +64,7 @@ - Fix: リノートをリノートできるのを修正 - Fix: アクセストークンを削除すると、通知が取得できなくなる場合がある問題を修正 - Fix: 自身の宛先なしダイレクト投稿がストリーミングで流れてこない問題を修正 +- Fix: サーバーサイドからのテスト通知を正しく行えるように修正 ## 2023.10.2 diff --git a/packages/backend/assets/tabler-badges/bell.png b/packages/backend/assets/tabler-badges/bell.png new file mode 100644 index 000000000..ab3b2a110 Binary files /dev/null and b/packages/backend/assets/tabler-badges/bell.png differ diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index c6d5023e6..7c3672c67 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -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; diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 09a7e579f..17e798817 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -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 { + 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: [], // 後方互換性のため diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 75f3286ef..37bdcbe28 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -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, diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 520d9b14e..1867525cc 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -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, []); diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 800a3b079..b11d0db04 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -226,11 +226,18 @@ export async function mainBoot() { }); main.on('readAllNotifications', () => { - updateAccount({ hasUnreadNotification: false }); + updateAccount({ + hasUnreadNotification: false, + unreadNotificationsCount: 0, + }); }); main.on('unreadNotification', () => { - updateAccount({ hasUnreadNotification: true }); + const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1; + updateAccount({ + hasUnreadNotification: true, + unreadNotificationsCount, + }); }); main.on('unreadMention', () => { diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index 321acc0fb..87f15a110 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -7,16 +7,18 @@ SPDX-License-Identifier: AGPL-3.0-only
-
@@ -218,6 +221,12 @@ watch(defaultStore.reactiveState.menuDisplay, () => { color: var(--navIndicator); font-size: 8px; animation: blink 1s infinite; + + &:has(.itemIndicateValueIcon) { + animation: none; + left: auto; + right: 20px; + } } &:hover { diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 251097787..1d51e08f7 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -52,7 +52,12 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -485,5 +490,10 @@ body { color: var(--indicator); font-size: 16px; animation: blink 1s infinite; + + &:has(.itemIndicateValueIcon) { + animation: none; + font-size: 12px; + } } diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index c9fb8a931..86ec8650f 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -27,7 +27,12 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -444,6 +449,11 @@ $widgets-hide-threshold: 1090px; color: var(--indicator); font-size: 16px; animation: blink 1s infinite; + + &:has(.itemIndicateValueIcon) { + animation: none; + font-size: 12px; + } } .menuDrawerBg { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 0a6806ae6..8f389086c 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2488,6 +2488,7 @@ type MeDetailed = UserDetailed & { hasUnreadMessagingMessage: boolean; hasUnreadNotification: boolean; hasUnreadSpecifiedNotes: boolean; + unreadNotificationsCount: number; hideOnlineStatus: boolean; injectFeaturedNote: boolean; integrations: Record; @@ -3023,8 +3024,8 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts // src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts -// src/entities.ts:115:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts -// src/entities.ts:611:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts +// src/entities.ts:612:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 38bac3b7c..029bf48c8 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -106,6 +106,7 @@ export type MeDetailed = UserDetailed & { hasUnreadMessagingMessage: boolean; hasUnreadNotification: boolean; hasUnreadSpecifiedNotes: boolean; + unreadNotificationsCount: number; hideOnlineStatus: boolean; injectFeaturedNote: boolean; integrations: Record; diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts index e96f8585c..a51a28dc3 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -225,6 +225,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif data, }]; + case 'test': + return [t('_notification.testNotification'), { + body: t('_notification.notificationWillBeDisplayedLikeThis'), + badge: iconUrl('bell'), + data, + }]; + default: return null; } diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts index fa1ed15ed..c63e489c7 100644 --- a/packages/sw/src/types.ts +++ b/packages/sw/src/types.ts @@ -41,6 +41,7 @@ export type BadgeNames = | 'antenna' | 'arrow-back-up' | 'at' + | 'bell' | 'chart-arrows' | 'circle-check' | 'medal'