diff --git a/CHANGELOG.md b/CHANGELOG.md index e20c0c6e40..fd54648ba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - 最大でも黄色いエリア内にデコレーションを収めることを推奨します。 - 画像は512x512pxを推奨します。 - Enhance: すでにフォローしたすべての人の返信をTLに追加できるように +- Enhance: 未読の通知数を表示できるように - Enhance: ローカリゼーションの更新 - Enhance: 依存関係の更新 @@ -53,6 +54,7 @@ - Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善 - Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました - 相手がMisskey v2023.11.0以降である必要があります +- Enhance: チャンネル取得時のパフォーマンスを向上 - Fix: リストTLに自分のフォロワー限定投稿が含まれない問題を修正 - Fix: ローカルタイムラインに投稿者自身の投稿への返信が含まれない問題を修正 - Fix: 自分のフォローしているユーザーの自分のフォローしていないユーザーの visibility: followers な投稿への返信がストリーミングで流れてくる問題を修正 @@ -63,6 +65,7 @@ - Fix: リノートをリノートできるのを修正 - Fix: アクセストークンを削除すると、通知が取得できなくなる場合がある問題を修正 - Fix: 自身の宛先なしダイレクト投稿がストリーミングで流れてこない問題を修正 +- Fix: サーバーサイドからのテスト通知を正しく行えるように修正 ## 2023.10.2 diff --git a/package.json b/package.json index 9ffcde67ff..42f7e5a4ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.11.0-beta.6-prismisskey.2", + "version": "2023.11.0-beta.7-prismisskey.1", "codename": "nasubi", "repository": { "type": "git", @@ -49,14 +49,14 @@ "cssnano": "6.0.1", "js-yaml": "4.1.0", "postcss": "8.4.31", - "terser": "5.22.0", + "terser": "5.24.0", "typescript": "5.2.2" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "6.9.0", - "@typescript-eslint/parser": "6.9.0", + "@typescript-eslint/eslint-plugin": "6.9.1", + "@typescript-eslint/parser": "6.9.1", "cross-env": "7.0.3", - "cypress": "13.3.3", + "cypress": "13.4.0", "eslint": "8.52.0", "start-server-and-test": "2.0.1" }, diff --git a/packages/backend/assets/tabler-badges/bell.png b/packages/backend/assets/tabler-badges/bell.png new file mode 100644 index 0000000000..ab3b2a110f Binary files /dev/null and b/packages/backend/assets/tabler-badges/bell.png differ diff --git a/packages/backend/package.json b/packages/backend/package.json index a461e3a21c..0c4879a297 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -66,17 +66,17 @@ "@discordapp/twemoji": "14.1.2", "@fastify/accepts": "4.2.0", "@fastify/cookie": "9.1.0", - "@fastify/cors": "8.4.0", + "@fastify/cors": "8.4.1", "@fastify/express": "2.3.0", "@fastify/http-proxy": "9.2.1", "@fastify/multipart": "8.0.0", - "@fastify/static": "6.11.2", + "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", "@nestjs/common": "10.2.7", "@nestjs/core": "10.2.7", "@nestjs/testing": "10.2.7", "@peertube/http-signature": "1.7.0", - "@simplewebauthn/server": "8.3.4", + "@simplewebauthn/server": "8.3.5", "@sinonjs/fake-timers": "11.2.2", "@swc/cli": "0.1.62", "@swc/core": "1.3.95", @@ -87,7 +87,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "4.12.6", + "bullmq": "4.12.7", "cacheable-lookup": "7.0.0", "cbor": "9.0.1", "chalk": "5.3.0", @@ -138,7 +138,7 @@ "probe-image-size": "7.2.3", "promise-limit": "2.7.0", "pug": "3.0.2", - "punycode": "2.3.0", + "punycode": "2.3.1", "pureimage": "0.3.17", "qrcode": "1.5.3", "random-seed": "0.3.0", @@ -175,7 +175,7 @@ "@simplewebauthn/typescript-types": "8.3.4", "@swc/jest": "0.2.29", "@types/accepts": "1.3.6", - "@types/archiver": "5.3.4", + "@types/archiver": "6.0.0", "@types/bcryptjs": "2.4.5", "@types/body-parser": "1.19.4", "@types/cbor": "6.0.0", @@ -183,14 +183,14 @@ "@types/content-disposition": "0.5.7", "@types/fluent-ffmpeg": "2.1.23", "@types/http-link-header": "1.0.4", - "@types/jest": "29.5.6", + "@types/jest": "29.5.7", "@types/js-yaml": "4.0.8", "@types/jsdom": "21.1.4", "@types/jsonld": "1.5.11", "@types/jsrsasign": "10.5.11", "@types/mime-types": "2.1.3", "@types/ms": "0.7.33", - "@types/node": "20.8.9", + "@types/node": "20.8.10", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.13", "@types/oauth": "0.9.3", @@ -213,8 +213,8 @@ "@types/vary": "1.1.2", "@types/web-push": "3.6.2", "@types/ws": "8.5.8", - "@typescript-eslint/eslint-plugin": "6.9.0", - "@typescript-eslint/parser": "6.9.0", + "@typescript-eslint/eslint-plugin": "6.9.1", + "@typescript-eslint/parser": "6.9.1", "aws-sdk-client-mock": "3.0.0", "cross-env": "7.0.3", "eslint": "8.52.0", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index ef59a80950..b25554b229 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -258,7 +258,7 @@ export function loadConfig(): Config { clientEntry: clientManifest['src/_boot_.ts'], clientManifestExists: clientManifestExists, perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000, - perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300, + perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), pidFile: config.pidFile, }; 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/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts index dd72953c7d..9e66834dfa 100644 --- a/packages/backend/src/core/entities/ChannelEntityService.ts +++ b/packages/backend/src/core/entities/ChannelEntityService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NoteUnreadsRepository, NotesRepository } from '@/models/_.js'; +import type { ChannelFavoritesRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, NotesRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/Blocking.js'; import type { MiUser } from '@/models/User.js'; @@ -31,9 +31,6 @@ export class ChannelEntityService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.noteUnreadsRepository) - private noteUnreadsRepository: NoteUnreadsRepository, - @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -54,13 +51,6 @@ export class ChannelEntityService { const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null; - const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({ - where: { - noteChannelId: channel.id, - userId: meId, - }, - }) : undefined; - const isFollowing = meId ? await this.channelFollowingsRepository.exist({ where: { followerId: meId, @@ -99,7 +89,7 @@ export class ChannelEntityService { ...(me ? { isFollowing, isFavorited, - hasUnreadNote, + hasUnreadNote: false, // 後方互換性のため } : {}), ...(detailed ? { 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/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b03381a3f3..0e6a4d2e36 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -45,7 +45,7 @@ export const meta = { limit: { duration: ms('1hour'), - max: 10, + max: 20, }, errors: { diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index dfdc044caa..49092fba63 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -720,7 +720,7 @@ describe('クリップ', () => { test('を追加できる。', async () => { await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); const res = await show({ clipId: aliceClip.id }); - assert.strictEqual(res.lastClippedAt, new Date(res.lastClippedAt ?? '').toISOString()); + assert.strictEqual(res.lastClippedAt, res.lastClippedAt ? new Date(res.lastClippedAt).toISOString() : null); assert.deepStrictEqual((await notes({ clipId: aliceClip.id })).map(x => x.id), [aliceNote.id]); // 他人の非公開ノートも突っ込める 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/package.json b/packages/frontend/package.json index fe35519d27..97b1ac5a9a 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -10,7 +10,7 @@ "build-storybook": "pnpm build-storybook-pre && storybook build", "chromatic": "chromatic", "test": "vitest --run", - "test-and-coverage": "vitest --run --coverage", + "test-and-coverage": "vitest --run --coverage --globals", "typecheck": "vue-tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.{ts,vue}\"", "lint": "pnpm typecheck && pnpm eslint" @@ -20,7 +20,7 @@ "@github/webauthn-json": "2.1.1", "@rollup/plugin-alias": "5.0.1", "@rollup/plugin-json": "6.0.1", - "@rollup/plugin-replace": "5.0.4", + "@rollup/plugin-replace": "5.0.5", "@rollup/pluginutils": "5.0.5", "@syuilo/aiscript": "0.16.0", "@tabler/icons-webfont": "2.37.0", @@ -30,7 +30,7 @@ "astring": "1.8.6", "autosize": "6.0.1", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.5", - "broadcast-channel": "5.5.1", + "broadcast-channel": "6.0.0", "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "buraha": "0.0.1", "canvas-confetti": "1.6.1", @@ -39,7 +39,7 @@ "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", - "chromatic": "7.5.4", + "chromatic": "7.6.0", "compare-versions": "6.1.0", "cropperjs": "2.0.0-beta.4", "date-fns": "2.30.0", @@ -55,9 +55,9 @@ "mfm-js": "0.23.3", "misskey-js": "workspace:*", "photoswipe": "5.4.2", - "punycode": "2.3.0", + "punycode": "2.3.1", "querystring": "0.2.1", - "rollup": "4.1.4", + "rollup": "4.2.0", "sanitize-html": "2.11.0", "shiki": "^0.14.5", "sass": "1.69.5", @@ -78,30 +78,30 @@ "vuedraggable": "next" }, "devDependencies": { - "@storybook/addon-actions": "7.5.1", - "@storybook/addon-essentials": "7.5.1", - "@storybook/addon-interactions": "7.5.1", - "@storybook/addon-links": "7.5.1", - "@storybook/addon-storysource": "7.5.1", - "@storybook/addons": "7.5.1", - "@storybook/blocks": "7.5.1", - "@storybook/core-events": "7.5.1", + "@storybook/addon-actions": "7.5.2", + "@storybook/addon-essentials": "7.5.2", + "@storybook/addon-interactions": "7.5.2", + "@storybook/addon-links": "7.5.2", + "@storybook/addon-storysource": "7.5.2", + "@storybook/addons": "7.5.2", + "@storybook/blocks": "7.5.2", + "@storybook/core-events": "7.5.2", "@storybook/jest": "0.2.3", - "@storybook/manager-api": "7.5.1", - "@storybook/preview-api": "7.5.1", - "@storybook/react": "7.5.1", - "@storybook/react-vite": "7.5.1", + "@storybook/manager-api": "7.5.2", + "@storybook/preview-api": "7.5.2", + "@storybook/react": "7.5.2", + "@storybook/react-vite": "7.5.2", "@storybook/testing-library": "0.2.2", - "@storybook/theming": "7.5.1", - "@storybook/types": "7.5.1", - "@storybook/vue3": "7.5.1", - "@storybook/vue3-vite": "7.5.1", - "@testing-library/vue": "7.0.0", + "@storybook/theming": "7.5.2", + "@storybook/types": "7.5.2", + "@storybook/vue3": "7.5.2", + "@storybook/vue3-vite": "7.5.2", + "@testing-library/vue": "8.0.0", "@types/escape-regexp": "0.0.2", - "@types/estree": "1.0.3", + "@types/estree": "1.0.4", "@types/matter-js": "0.19.2", "@types/micromatch": "4.0.4", - "@types/node": "20.8.9", + "@types/node": "20.8.10", "@types/punycode": "2.1.1", "@types/sanitize-html": "2.9.3", "@types/throttle-debounce": "5.0.1", @@ -109,13 +109,13 @@ "@types/uuid": "9.0.6", "@types/websocket": "1.0.8", "@types/ws": "8.5.8", - "@typescript-eslint/eslint-plugin": "6.9.0", - "@typescript-eslint/parser": "6.9.0", + "@typescript-eslint/eslint-plugin": "6.9.1", + "@typescript-eslint/parser": "6.9.1", "@vitest/coverage-v8": "0.34.6", "@vue/runtime-core": "3.3.7", "acorn": "8.11.2", "cross-env": "7.0.3", - "cypress": "13.3.3", + "cypress": "13.4.0", "eslint": "8.52.0", "eslint-plugin-import": "2.29.0", "eslint-plugin-vue": "9.18.1", @@ -129,7 +129,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "start-server-and-test": "2.0.1", - "storybook": "7.5.1", + "storybook": "7.5.2", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "summaly": "github:misskey-dev/summaly", "vite-plugin-turbosnap": "1.0.3", 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 29872122bf..10ad4b2fe3 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
-