diff --git a/CHANGELOG_CHERRYPICK.md b/CHANGELOG_CHERRYPICK.md index 0bff14d1ac..43531efe08 100644 --- a/CHANGELOG_CHERRYPICK.md +++ b/CHANGELOG_CHERRYPICK.md @@ -34,6 +34,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE ### General - Feat: 사용자 메뉴에서 서버를 뮤트할 수 있음 (kokonect-link/cherrypick#502) - 이전 빌드에 추가된 기능은 관리자 전용이며, 이 빌드에서 추가된 기능은 일반 사용자용 기능입니다. +- Feat: 새 노트 알림을 묶어서 표시 (yojo-art/cherrypick#328) ### Client - Enhance: (Friendly) 모바일 환경에서 계정 목록을 표시할 때 내 프로필을 표시함 diff --git a/locales/en-US.yml b/locales/en-US.yml index 7c9a39b8f4..3aed3c7be2 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -171,6 +171,7 @@ receiveFollowRequest: "Follow request received" followRequestAccepted: "Follow request accepted" mention: "Mention" mentions: "Mentions" +newNotes: "New notes" directNotes: "Direct notes" importAndExport: "Import / Export" import: "Import" @@ -2715,6 +2716,7 @@ _notification: reactedBySomeUsers: "{n} users reacted" likedBySomeUsers: "{n} users liked your note" renotedBySomeUsers: "Renote from {n} users" + notedBySomeUsers: "There are {n} new notes" followedBySomeUsers: "Followed by {n} users" flushNotification: "Clear notifications" _types: diff --git a/locales/index.d.ts b/locales/index.d.ts index c6dbde1ceb..f1d69e6e88 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -711,6 +711,10 @@ export interface Locale extends ILocale { * あなた宛て */ "mentions": string; + /** + * 新規投稿 + */ + "newNotes": string; /** * ダイレクト投稿 */ @@ -10668,6 +10672,10 @@ export interface Locale extends ILocale { * {n}人がリノートしました */ "renotedBySomeUsers": ParameterizedString<"n">; + /** + * {n}件の新しい投稿があります + */ + "notedBySomeUsers": ParameterizedString<"n">; /** * {n}人にフォローされました */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 48cc052e1e..cd4de67b4b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -171,6 +171,7 @@ receiveFollowRequest: "フォローリクエストされました" followRequestAccepted: "フォローが承認されました" mention: "メンション" mentions: "あなた宛て" +newNotes: "新規投稿" directNotes: "ダイレクト投稿" importAndExport: "インポートとエクスポート" import: "インポート" @@ -2811,6 +2812,7 @@ _notification: reactedBySomeUsers: "{n}人がリアクションしました" likedBySomeUsers: "{n}人がいいねしました" renotedBySomeUsers: "{n}人がリノートしました" + notedBySomeUsers: "{n}件の新しい投稿があります" followedBySomeUsers: "{n}人にフォローされました" flushNotification: "通知の履歴をリセットする" exportOfXCompleted: "{x}のエクスポートが完了しました" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index d1938e567d..ca36aab18d 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -171,6 +171,7 @@ receiveFollowRequest: "새로운 팔로우 요청이 있어요" followRequestAccepted: "팔로우가 수락되었어요" mention: "멘션" mentions: "받은 멘션" +newNotes: "새 노트" directNotes: "다이렉트 노트" importAndExport: "가져오기 및 내보내기" import: "가져오기" @@ -2741,6 +2742,7 @@ _notification: reactedBySomeUsers: "{n}명이 리액션했어요" likedBySomeUsers: "{n}명이 좋아요를 눌렀어요" renotedBySomeUsers: "{n}명이 리노트했어요" + notedBySomeUsers: "{n}개의 새 노트가 있어요" followedBySomeUsers: "{n}명에게 팔로우됨" flushNotification: "모든 알림 지우기" exportOfXCompleted: "{x} 내보내기에 성공했어요." diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 3bcd358270..45b6845595 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -142,6 +142,27 @@ export class NotificationEntityService implements OnModuleInit { note: noteIfNeed, users, }); + } else if (notification.type === 'note:grouped') { + const users = (await Promise.all(notification.notifierIds.map(notifier => { + const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(notifier) : null; + if (packedUser) { + return packedUser; + } + + return this.userEntityService.pack(notifier, {id: meId}); + }))).filter(x => x != null); + // if all users have been deleted, don't show this notification + if (users.length === 0) { + return null; + } + + return await awaitAll({ + id: notification.id, + createdAt: new Date(notification.createdAt).toISOString(), + type: notification.type, + noteIds: notification.noteIds, + users, + }); } // #endregion @@ -213,6 +234,7 @@ export class NotificationEntityService implements OnModuleInit { if ('notifierId' in notification) userIds.push(notification.notifierId); if (notification.type === 'reaction:grouped') userIds.push(...notification.reactions.map(x => x.userId)); if (notification.type === 'renote:grouped') userIds.push(...notification.userIds); + if (notification.type === 'note:grouped') userIds.push(...notification.notifierIds); } const users = userIds.length > 0 ? await this.usersRepository.find({ where: { id: In(userIds) }, diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 647cac7397..5b846cdfb2 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -140,4 +140,10 @@ export type MiGroupedNotification = MiNotification | { createdAt: string; noteId: MiNote['id']; userIds: string[]; +} | { + type: 'note:grouped'; + id: string; + createdAt: string; + notifierIds: MiUser['id'][]; + noteIds: string[]; }; diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts index dc6ffd3e02..833a74fe12 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts @@ -157,6 +157,24 @@ export default class extends Endpoint { // eslint- prevGroupedNotification.id = notification.id; continue; } + if (prev.type === 'note' && notification.type === 'note') { + if (prevGroupedNotification.type !== 'note:grouped') { + groupedNotifications[groupedNotifications.length - 1] = { + type: 'note:grouped', + id: '', + createdAt: notification.createdAt, + noteIds: [notification.noteId], + notifierIds: [prev.notifierId!], + }; + prevGroupedNotification = groupedNotifications.at(-1)!; + } + if (!(prevGroupedNotification as FilterUnionByProperty).notifierIds.includes(notification.notifierId)) { + (prevGroupedNotification as FilterUnionByProperty).notifierIds.push(notification.notifierId!); + } + (prevGroupedNotification as FilterUnionByProperty).noteIds.push(notification.noteId!); + prevGroupedNotification.id = notification.id; + continue; + } groupedNotifications.push(notification); } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 805d060919..e7c9e3b3ad 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -44,6 +44,7 @@ export const groupedNotificationTypes = [ ...notificationTypes, 'reaction:grouped', 'renote:grouped', + 'note:grouped', ] as const; export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const; diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 7530655d99..077c9a1f52 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -67,6 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }} {{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }} {{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }} + {{ i18n.tsx._notification.notedBySomeUsers({ n: notification.noteIds.length }) }} {{ notification.header }} @@ -156,6 +158,11 @@ SPDX-License-Identifier: AGPL-3.0-only +
+
+ +
+
@@ -268,7 +275,8 @@ const rejectGroupInvitation = () => { .icon_reactionGroup, .icon_reactionGroupHeart, -.icon_renoteGroup { +.icon_renoteGroup, +.icon_noteGroup { display: grid; align-items: center; justify-items: center; @@ -291,6 +299,10 @@ const rejectGroupInvitation = () => { background: var(--eventRenote); } +.icon_noteGroup { + background: var(--eventRenote); +} + .icon_app { border-radius: 6px; } diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 04b1efc45f..a7cbac5945 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -41,11 +41,18 @@ import { globalEvents } from '@/events.js'; const props = defineProps<{ excludeTypes?: typeof notificationTypes[number][]; + notUseGrouped?: boolean; }>(); const pagingComponent = shallowRef>(); -const pagination = computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? { +const pagination = computed(() => props.notUseGrouped ? { + endpoint: 'i/notifications' as const, + limit: 20, + params: computed(() => ({ + excludeTypes: props.excludeTypes ?? undefined, + })), +} : defaultStore.reactiveState.useGroupedNotifications.value ? { endpoint: 'i/notifications-grouped' as const, limit: 20, params: computed(() => ({ diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 0f87d9617a..7b396ee275 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -11,6 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ +
@@ -37,6 +40,7 @@ import { globalEvents } from '@/events.js'; const tab = ref('all'); const includeTypes = ref(null); const excludeTypes = computed(() => includeTypes.value ? notificationTypes.filter(t => !includeTypes.value.includes(t)) : undefined); +const newNoteExcludeTypes = computed(() => notificationTypes.filter(t => !['note'].includes(t))); const props = defineProps<{ disableRefreshButton?: boolean; @@ -96,6 +100,10 @@ const headerTabs = computed(() => [{ key: 'all', title: i18n.ts.all, icon: 'ti ti-point', +}, { + key: 'newNote', + title: i18n.ts.newNotes, + icon: 'ti ti-pencil', }, { key: 'mentions', title: i18n.ts.mentions,