diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-11-02 15:57:55 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2023-11-02 15:57:55 +0900 |
| commit | f62ad3ed3eccfd242b2d1f1e25f00276f2bfff77 (patch) | |
| tree | 4c7280ec000d49ae69f31b97fb1043d20c3de0f1 /packages/frontend/src | |
| parent | fix(frontend): /about の連合タブのレイアウトが一部崩れてい... (diff) | |
| download | misskey-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.vue | 82 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNotifications.vue | 11 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/general.vue | 3 | ||||
| -rw-r--r-- | packages/frontend/src/store.ts | 4 |
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を同期 |