mirror of
https://github.com/kokonect-link/cherrypick
synced 2024-11-27 14:28:53 +09:00
feat: 새 노트 알림을 묶어서 표시 (yojo-art/cherrypick#328)
This commit is contained in:
parent
d25dc69eed
commit
e9d7399f12
@ -34,6 +34,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE
|
||||
### General
|
||||
- Feat: 사용자 메뉴에서 서버를 뮤트할 수 있음 (kokonect-link/cherrypick#502)
|
||||
- 이전 빌드에 추가된 기능은 관리자 전용이며, 이 빌드에서 추가된 기능은 일반 사용자용 기능입니다.
|
||||
- Feat: 새 노트 알림을 묶어서 표시 (yojo-art/cherrypick#328)
|
||||
|
||||
### Client
|
||||
- Enhance: (Friendly) 모바일 환경에서 계정 목록을 표시할 때 내 프로필을 표시함
|
||||
|
@ -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:
|
||||
|
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
@ -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}人にフォローされました
|
||||
*/
|
||||
|
@ -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}のエクスポートが完了しました"
|
||||
|
@ -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} 내보내기에 성공했어요."
|
||||
|
@ -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) },
|
||||
|
@ -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[];
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ export const groupedNotificationTypes = [
|
||||
...notificationTypes,
|
||||
'reaction:grouped',
|
||||
'renote:grouped',
|
||||
'note:grouped',
|
||||
] as const;
|
||||
|
||||
export const obsoleteNotificationTypes = ['pollVote'/*, 'groupInvited'*/] as const;
|
||||
|
@ -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/>
|
||||
<MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/>
|
||||
@ -67,6 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<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 === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'note:grouped'">{{ i18n.tsx._notification.notedBySomeUsers({ n: notification.noteIds.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'"/>
|
||||
</header>
|
||||
@ -156,6 +158,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>
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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(() => ({
|
||||
|
@ -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>
|
||||
@ -37,6 +40,7 @@ import { globalEvents } from '@/events.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;
|
||||
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user