From 5fb6847419ba75edc33270071f4990244669e262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 1 Nov 2023 13:34:05 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=E6=9C=AA=E8=AA=AD=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E6=95=B0=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=20(#11982)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 未読通知数を表示できるように * 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 --- CHANGELOG.md | 2 + .../backend/assets/tabler-badges/bell.png | Bin 0 -> 1774 bytes .../backend/src/core/NotificationService.ts | 4 +- .../src/core/entities/UserEntityService.ts | 39 ++++++++++++++---- .../backend/src/models/json-schema/user.ts | 4 ++ packages/backend/test/e2e/users.ts | 2 + packages/frontend/src/boot/main-boot.ts | 11 ++++- .../frontend/src/components/MkLaunchPad.vue | 22 ++++++++-- .../src/components/MkNotifications.vue | 6 ++- packages/frontend/src/navbar.ts | 9 ++++ .../src/pages/settings/notifications.vue | 4 +- packages/frontend/src/style.scss | 13 ++++++ packages/frontend/src/ui/_common_/common.vue | 3 +- .../src/ui/_common_/navbar-for-mobile.vue | 11 ++++- packages/frontend/src/ui/_common_/navbar.vue | 22 +++++++++- packages/frontend/src/ui/classic.sidebar.vue | 11 ++++- packages/frontend/src/ui/deck.vue | 12 +++++- packages/frontend/src/ui/universal.vue | 12 +++++- packages/misskey-js/etc/misskey-js.api.md | 5 ++- packages/misskey-js/src/entities.ts | 1 + .../sw/src/scripts/create-notification.ts | 7 ++++ packages/sw/src/types.ts | 1 + 22 files changed, 173 insertions(+), 28 deletions(-) create mode 100644 packages/backend/assets/tabler-badges/bell.png diff --git a/CHANGELOG.md b/CHANGELOG.md index feabb8e0f9..77b01aad39 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 0000000000000000000000000000000000000000..ab3b2a110f668fa064c6bb93a8df1a4ffae7c602 GIT binary patch literal 1774 zcmVi#_{?t{_0_>S#Jlzz8hYHdGNr+KYr5PJU;CeZ&-{z zz@0G7M9$;s3h|5g(^v}jT7j_Lj$#%dC!t@mLJmtj*kP*`2-i(0V7`p-B_&9OO*UGA zFkJk1~xz!o*Q6PaV>glJASdmj)pD4knDwTB_ZkmSCC=Kk&w&aYpryY z)q`a8R!HIBS@0;#D(*uf-om;PAg;G6HlR0W;Su(AD(b4+3rD01TNG~wT?d4V(6tMn zs49{peJbn_k>?Y3H^4+7_S5|sv?~g<;}cJNi&Xd<`k57+XwV-NEvy>rsULJei86F| zK$Lh&NF|jXh{6hl{x;?cYQvRhxK6;@fC6iUt@2vKM&A#$x-Jl25BUhH9R&~Ghio9q z)2}R+aG`^)^k6HkKv)d~1mV^4OSsk#o_d|3XV<_nt;!b_A83^o2+J#APV4m)So=D} zN|P0^EJpnyI9=xJIi%Wx)|pW3U@fcOzxf5O@|(3<&EA#pbD-ZuEq8|>xkB~+y=9nx z#^U}$!XXd87r@anAUqd#O(5deY4MyGA7$N!L|tx}VJhUb_T&06Rd5)_#{-?VQRu9j zExp}Yrjz$MD{;cZ|6WCWKF;lH2sQemFVMm8;GVE-K&YGp-z%8W-CYfbKl~L&Q8uQ8~f1|0;M_;}bpm|3)HXuwsl9+M#xwvTsdKEV!>K}$-0b;d|k$0u&^HkGV z(brE)(a2@@Ys@!kk>4?VZ^i( z5tEA)U!{0)qmjx+6mFJc5wBYpnsuTi8+)n4@?XWE-GptI!AXsJzu8Dc{${(=Ry0n3dsAS%nHsWH0#fgFu0a2`x445sjj zNd>~BFBnX=RDntJ3M_gH#0ta;#0ta;#0ps2(1hJkGBTN#TOeajfpWdY-=sA#6AH8= z#jtw2Y{0Qq8;^Z#eBNn98F?z+W0j!+r){lg52KF#WpU`1EUW+Cb&AvpGlQ zi*=jJB+;x7MFG`O`Q7F!i-tALO2*rzEvFJLA3z$XqRuybW^i3#SX(}!u)3Q`;xrO@ zvLdwUc@Q&L2d1#)GjPZP=!dFfn zS-F;Gz_Dx{zFnT`TafyAt!4SHqi_C{`=@xf;7# z;ZlFURzMy@DONYqvE%->6dRbzO;0Hn6B9kAlSNPhbeK-6ika`sSxWe!!^ zD??rLW70}YF+(U7ZQ|?gfllyNwxZ)`!nbMo^Sgm)^VoS#?zj|gd2V{Y=maNxA0{FD zmHc+T4$~EWl38sV>{BwYT$aFhUqIC@EK$%^GQFOUejfpE^5(RGNP*>E4Z@|EmDP#Y zLHwnTE4Z3#Y}iFW2=38(uEiACJSGiH3$ItROzG`CcI@Net%06{s3sZb17(ybn2y^2 z)wHECEeEmknPb`QR&4|#N4Sh7Y}6XT@e$TZxZJjFKzu92aj^}qP+!S)k2B`@AJnf>D2<5i QUjP6A07*qoM6N<$g17KpssI20 literal 0 HcmV?d00001 diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index c6d5023e65..7c3672c67a 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 09a7e579f0..17e7988176 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 75f3286eff..37bdcbe281 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 520d9b14e4..1867525cc8 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 800a3b079f..b11d0db043 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 321acc0fbc..87f15a110f 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 2510977877..1d51e08f78 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 c9fb8a931d..86ec8650f9 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 0a6806ae60..8f389086c9 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 38bac3b7c3..029bf48c84 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 e96f8585c7..a51a28dc3b 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 fa1ed15ed6..c63e489c71 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'