Skip to content

Commit

Permalink
Enhance:新着ノート通知があった時まとめるように (#387)
Browse files Browse the repository at this point in the history
  • Loading branch information
penginn-net authored Sep 19, 2024
1 parent b761657 commit 64c50d7
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 5 deletions.
8 changes: 8 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,10 @@ export interface Locale extends ILocale {
* あなた宛て
*/
"mentions": string;
/**
* 新規投稿
*/
"newNotes": string;
/**
* ダイレクト投稿
*/
Expand Down Expand Up @@ -10645,6 +10649,10 @@ export interface Locale extends ILocale {
* {n}人がリノートしました
*/
"renotedBySomeUsers": ParameterizedString<"n">;
/**
* {n}件の新しい投稿
*/
"notedBySomeUsers": ParameterizedString<"n">;
/**
* {n}人にフォローされました
*/
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ receiveFollowRequest: "フォローリクエストされました"
followRequestAccepted: "フォローが承認されました"
mention: "メンション"
mentions: "あなた宛て"
newNotes: "新規投稿"
directNotes: "ダイレクト投稿"
importAndExport: "インポートとエクスポート"
import: "インポート"
Expand Down Expand Up @@ -2812,6 +2813,7 @@ _notification:
reactedBySomeUsers: "{n}人がリアクションしました"
likedBySomeUsers: "{n}人がいいねしました"
renotedBySomeUsers: "{n}人がリノートしました"
notedBySomeUsers: "{n}件の新しい投稿"
followedBySomeUsers: "{n}人にフォローされました"
flushNotification: "通知の履歴をリセットする"

Expand Down
26 changes: 24 additions & 2 deletions packages/backend/src/core/entities/NotificationEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class NotificationEntityService implements OnModuleInit {
async #packInternal <T extends MiNotification | MiGroupedNotification> (
src: T,
meId: MiUser['id'],
// eslint-disable-next-line @typescript-eslint/ban-types

options: {
checkValidNotifier?: boolean;
},
Expand Down Expand Up @@ -143,6 +143,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

Expand Down Expand Up @@ -207,6 +228,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) },
Expand Down Expand Up @@ -239,7 +261,7 @@ export class NotificationEntityService implements OnModuleInit {
public async pack(
src: MiNotification | MiGroupedNotification,
meId: MiUser['id'],
// eslint-disable-next-line @typescript-eslint/ban-types

options: {
checkValidNotifier?: boolean;
},
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/models/Notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,10 @@ export type MiGroupedNotification = MiNotification | {
createdAt: string;
noteId: MiNote['id'];
userIds: string[];
} | {
type: 'note:grouped';
id: string;
createdAt: string;
noteIds: string[];
notifierIds: MiUser['id'][];
};
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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<MiGroupedNotification, 'type', 'note:grouped'>).notifierIds.includes(notification.notifierId)) {
(prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'note:grouped'>).notifierIds.push(notification.notifierId!);
}
(prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'note:grouped'>).noteIds.push(notification.noteId!);
prevGroupedNotification.id = notification.id;
continue;
}

groupedNotifications.push(notification);
}
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const groupedNotificationTypes = [
...notificationTypes,
'reaction:grouped',
'renote:grouped',
'note:grouped',
] as const;

export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const;
Expand Down
4 changes: 2 additions & 2 deletions packages/cherrypick-js/src/autogen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18806,8 +18806,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote')[];
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[];
};
};
};
Expand Down
13 changes: 13 additions & 0 deletions packages/frontend/src/components/MkNotification.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'note:grouped'" :class="[$style.icon, $style.icon_noteGroup]"><i class="ti ti-pencil" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="'icon' in notification" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
Expand Down Expand Up @@ -62,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
<span v-else-if="notification.type === 'note:grouped'">{{ i18n.tsx._notification.notedBySomeUsers({ n: notification.noteIds.length }) }}</span>
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime" :mode="defaultStore.state.enableAbsoluteTime ? 'absolute' : 'relative'"/>
Expand Down Expand Up @@ -147,6 +149,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
</div>
</div>
<div v-else-if="notification.type === 'note:grouped'">
<div v-for="user of notification.users" :key="user.id" :class="$style.reactionsItem">
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
</div>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -243,6 +250,7 @@ const rejectGroupInvitation = () => {
height: 100%;
}

.icon_noteGroup,
.icon_reactionGroup,
.icon_reactionGroupHeart,
.icon_renoteGroup {
Expand All @@ -268,6 +276,11 @@ const rejectGroupInvitation = () => {
background: var(--eventRenote);
}

.icon_noteGroup {

background: var(--eventRenote);
}

.icon_app {
border-radius: 6px;
}
Expand Down
9 changes: 8 additions & 1 deletion packages/frontend/src/components/MkNotifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,18 @@ import { globalEvents } from '@/events.js';

const props = defineProps<{
excludeTypes?: typeof notificationTypes[number][];
notUseGrouped?: boolean;
}>();

const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();

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(() => ({
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export const notificationTypes = [
'roleAssigned',
'achievementEarned',
'app',
'test',
'pollVote',
] as const;
export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const;

Expand Down
8 changes: 8 additions & 0 deletions packages/frontend/src/pages/notifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="tab === 'all'" key="all">
<XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/>
</div>
<div v-else-if="tab === 'newNote'" key="newNote">
<XNotifications :class="$style.notifications" :excludeTypes="newNoteExcludeTypes" :notUseGrouped="true"/>
</div>
<div v-else-if="tab === 'mentions'" key="mention">
<MkNotes :pagination="mentionsPagination" :notification="true"/>
</div>
Expand Down Expand Up @@ -38,6 +41,7 @@ import { flushNotification } from '@/scripts/check-nortification-delete.js';
const tab = ref('all');
const includeTypes = ref<string[] | null>(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;
Expand Down Expand Up @@ -103,6 +107,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,
Expand Down

0 comments on commit 64c50d7

Please sign in to comment.