summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-11-02 15:57:55 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-11-02 15:57:55 +0900
commitf62ad3ed3eccfd242b2d1f1e25f00276f2bfff77 (patch)
tree4c7280ec000d49ae69f31b97fb1043d20c3de0f1 /packages/frontend/src
parentfix(frontend): /about の連合タブのレイアウトが一部崩れてい... (diff)
downloadmisskey-f62ad3ed3eccfd242b2d1f1e25f00276f2bfff77.tar.gz
misskey-f62ad3ed3eccfd242b2d1f1e25f00276f2bfff77.tar.bz2
misskey-f62ad3ed3eccfd242b2d1f1e25f00276f2bfff77.zip
feat: notification grouping
Resolve #12211
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkNotification.vue82
-rw-r--r--packages/frontend/src/components/MkNotifications.vue11
-rw-r--r--packages/frontend/src/pages/settings/general.vue3
-rw-r--r--packages/frontend/src/store.ts4
4 files changed, 94 insertions, 6 deletions
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index c507236216..ff20bc591f 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -9,9 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
+ <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>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
- <img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
+ <img v-else-if="notification.icon" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
<div
:class="[$style.subIcon, {
[$style.t_follow]: notification.type === 'follow',
@@ -39,7 +41,6 @@ SPDX-License-Identifier: AGPL-3.0-only
v-else-if="notification.type === 'reaction'"
ref="reactionRef"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
- :customEmojis="notification.note.emojis"
:noStyle="true"
style="width: 100%; height: 100%;"
/>
@@ -52,16 +53,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.user" 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'">{{ i18n.t('_notification.reactedBySomeUsers', { n: notification.reactions.length }) }}</span>
+ <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.t('_notification.renotedBySomeUsers', { n: notification.users.length }) }}</span>
<span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header>
<div>
- <MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
+ <MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i>
</MkA>
- <MkA v-else-if="notification.type === 'renote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
+ <MkA v-else-if="notification.type === 'renote' || notification.type === 'renote:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote.user"/>
<i class="ti ti-quote" :class="$style.quote"></i>
@@ -102,6 +105,24 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'app'" :class="$style.text">
<Mfm :text="notification.body" :nowrap="false"/>
</span>
+
+ <div v-if="notification.type === 'reaction:grouped'">
+ <div v-for="reaction of notification.reactions" :class="$style.reactionsItem">
+ <MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/>
+ <div :class="$style.reactionsItemReaction">
+ <MkReactionIcon
+ :reaction="reaction.reaction ? reaction.reaction.replace(/^:(\w+):$/, ':$1@.:') : reaction.reaction"
+ :noStyle="true"
+ style="width: 100%; height: 100%;"
+ />
+ </div>
+ </div>
+ </div>
+ <div v-else-if="notification.type === 'renote:grouped'">
+ <div v-for="user of notification.users" :class="$style.reactionsItem">
+ <MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
+ </div>
+ </div>
</div>
</div>
</div>
@@ -181,6 +202,29 @@ useTooltip(reactionRef, (showing) => {
display: block;
width: 100%;
height: 100%;
+}
+
+.icon_reactionGroup,
+.icon_renoteGroup {
+ display: grid;
+ align-items: center;
+ justify-items: center;
+ width: 80%;
+ height: 80%;
+ font-size: 15px;
+ border-radius: 100%;
+ color: #fff;
+}
+
+.icon_reactionGroup {
+ background: #e99a0b;
+}
+
+.icon_renoteGroup {
+ background: #36d298;
+}
+
+.icon_app {
border-radius: 6px;
}
@@ -305,6 +349,36 @@ useTooltip(reactionRef, (showing) => {
flex: 1;
}
+.reactionsItem {
+ display: inline-block;
+ position: relative;
+ width: 38px;
+ height: 38px;
+ margin-top: 8px;
+ margin-right: 8px;
+}
+
+.reactionsItemAvatar {
+ width: 100%;
+ height: 100%;
+}
+
+.reactionsItemReaction {
+ position: absolute;
+ z-index: 1;
+ bottom: -2px;
+ right: -2px;
+ width: 20px;
+ height: 20px;
+ box-sizing: border-box;
+ border-radius: 100%;
+ background: var(--panel);
+ box-shadow: 0 0 0 3px var(--panel);
+ font-size: 11px;
+ text-align: center;
+ color: #fff;
+}
+
@container (max-width: 600px) {
.root {
padding: 16px;
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 896f97a48d..8d99e440e1 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
- <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/>
+ <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList>
</template>
</MkPagination>
@@ -32,6 +32,7 @@ import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { notificationTypes } from '@/const.js';
import { infoImageUrl } from '@/instance.js';
+import { defaultStore } from '@/store.js';
const props = defineProps<{
excludeTypes?: typeof notificationTypes[number][];
@@ -39,7 +40,13 @@ const props = defineProps<{
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
-const pagination: Paging = {
+const pagination: Paging = defaultStore.state.useGroupedNotifications ? {
+ endpoint: 'i/notifications-grouped' as const,
+ limit: 20,
+ params: computed(() => ({
+ excludeTypes: props.excludeTypes ?? undefined,
+ })),
+} : {
endpoint: 'i/notifications' as const,
limit: 20,
params: computed(() => ({
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 85d038e3d1..d96c984688 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -88,6 +88,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.notificationDisplay }}</template>
<div class="_gaps_m">
+ <MkSwitch v-model="useGroupedNotifications">{{ i18n.ts.useGroupedNotifications }}</MkSwitch>
+
<MkRadios v-model="notificationPosition">
<template #label>{{ i18n.ts.position }}</template>
<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
@@ -255,6 +257,7 @@ const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificati
const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
+const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 803f2f648d..0f2e642b7b 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -373,6 +373,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
+ useGroupedNotifications: {
+ where: 'device',
+ default: true,
+ },
}));
// TODO: 他のタブと永続化されたstateを同期