diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-05-09 17:40:08 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-09 17:40:08 +0900 |
| commit | 8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11 (patch) | |
| tree | ae0d3573bd5a3175bc6174d33129dc64205a1436 /packages/frontend/src/components/MkStreamingNotificationsTimeline.vue | |
| parent | refactor (diff) | |
| download | misskey-8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11.tar.gz misskey-8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11.tar.bz2 misskey-8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11.zip | |
Feat: No websocket mode (#15851)
* wip
* wip
* wip
* wip
* Update MkTimeline.vue
* wip
* wip
* wip
* Update MkTimeline.vue
* Update use-pagination.ts
* wip
* wip
* Update MkTimeline.vue
* Update MkTimeline.vue
* wip
* wip
* Update MkTimeline.vue
* Update MkTimeline.vue
* Update MkTimeline.vue
* wip
* Update use-pagination.ts
* wip
* Update use-pagination.ts
* Update MkNotifications.vue
* Update MkNotifications.vue
* wip
* wip
* wip
* Update use-note-capture.ts
* Update use-note-capture.ts
* Update use-note-capture.ts
* wip
* wip
* wip
* wip
* Update MkNoteDetailed.vue
* wip
* wip
* Update MkTimeline.vue
* wip
* fix
* Update MkTimeline.vue
* wip
* test
* Revert "test"
This reverts commit 3375619396c54dcda5e564eb1da444c2391208c9.
* Update use-pagination.ts
* test
* Revert "test"
This reverts commit 42c53c830e28485d2fb49061fa7cdeee31bc6a22.
* test
* Revert "test"
This reverts commit c4f8cda4aa1cec9d1eb97557145f3ad3d2d0e469.
* Update style.scss
* Update MkTimeline.vue
* Update MkTimeline.vue
* Update MkTimeline.vue
* ✌️
* Update MkTimeline.vue
* wip
* wip
* test
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* Update MkTimeline.vue
* wip
* tweak navbar
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update home.vue
* wip
* refactor
* wip
* wip
* Update note.vue
* Update navbar.vue
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* wip
* Update MkStreamingNotificationsTimeline.vue
* Update use-pagination.ts
* wip
* improve perf
* wip
* Update MkNotesTimeline.vue
* wip
* megre
* Update use-pagination.ts
* Update use-pagination.ts
* Update MkStreamingNotesTimeline.vue
* Update use-pagination.ts
* Update CHANGELOG.md
* Update CHANGELOG.md
* Update CHANGELOG.md
Diffstat (limited to 'packages/frontend/src/components/MkStreamingNotificationsTimeline.vue')
| -rw-r--r-- | packages/frontend/src/components/MkStreamingNotificationsTimeline.vue | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue b/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue new file mode 100644 index 0000000000..931f6ae115 --- /dev/null +++ b/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue @@ -0,0 +1,199 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reload()"> + <MkLoading v-if="paginator.fetching.value"/> + + <MkError v-else-if="paginator.error.value" @retry="paginator.init()"/> + + <div v-else-if="paginator.items.value.length === 0" key="_empty_"> + <slot name="empty"><MkResult type="empty" :text="i18n.ts.noNotifications"/></slot> + </div> + + <div v-else ref="rootEl"> + <component + :is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + :moveClass="$style.transition_x_move" + tag="div" + > + <div v-for="(notification, i) in paginator.items.value" :key="notification.id" :data-scroll-anchor="notification.id" :class="$style.item"> + <div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, notification.createdAt)" :class="$style.date"> + <span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt).prevText }}</span> + <span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span> + <span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span> + </div> + <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.content" :note="notification.note" :withHardMute="true"/> + <XNotification v-else :class="$style.content" :notification="notification" :withTime="true" :full="true"/> + </div> + </component> + <button v-show="paginator.canFetchOlder.value" key="_more_" v-appear="prefer.s.enableInfiniteScroll ? paginator.fetchOlder : null" :disabled="paginator.fetchingOlder.value" class="_button" :class="$style.more" @click="paginator.fetchOlder"> + <div v-if="!paginator.fetchingOlder.value">{{ i18n.ts.loadMore }}</div> + <MkLoading v-else/> + </button> + </div> +</component> +</template> + +<script lang="ts" setup> +import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue'; +import * as Misskey from 'misskey-js'; +import { useInterval } from '@@/js/use-interval.js'; +import type { notificationTypes } from '@@/js/const.js'; +import XNotification from '@/components/MkNotification.vue'; +import MkNote from '@/components/MkNote.vue'; +import { useStream } from '@/stream.js'; +import { i18n } from '@/i18n.js'; +import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; +import { prefer } from '@/preferences.js'; +import { store } from '@/store.js'; +import { usePagination } from '@/use/use-pagination.js'; +import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js'; + +const props = defineProps<{ + excludeTypes?: typeof notificationTypes[number][]; +}>(); + +const rootEl = useTemplateRef('rootEl'); + +const paginator = usePagination({ + ctx: prefer.s.useGroupedNotifications ? { + endpoint: 'i/notifications-grouped' as const, + limit: 20, + params: computed(() => ({ + excludeTypes: props.excludeTypes ?? undefined, + })), + } : { + endpoint: 'i/notifications' as const, + limit: 20, + params: computed(() => ({ + excludeTypes: props.excludeTypes ?? undefined, + })), + }, +}); + +const MIN_POLLING_INTERVAL = 1000 * 10; +const POLLING_INTERVAL = + prefer.s.pollingInterval === 1 ? MIN_POLLING_INTERVAL * 1.5 * 1.5 : + prefer.s.pollingInterval === 2 ? MIN_POLLING_INTERVAL * 1.5 : + prefer.s.pollingInterval === 3 ? MIN_POLLING_INTERVAL : + MIN_POLLING_INTERVAL; + +if (!store.s.realtimeMode) { + useInterval(async () => { + paginator.fetchNewer({ + toQueue: false, + }); + }, POLLING_INTERVAL, { + immediate: false, + afterMounted: true, + }); +} + +function onNotification(notification) { + const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false; + if (isMuted || window.document.visibilityState === 'visible') { + if (store.s.realtimeMode) { + useStream().send('readNotification'); + } + } + + if (!isMuted) { + paginator.prepend(notification); + } +} + +function reload() { + return paginator.reload(); +} + +let connection: Misskey.ChannelConnection<Misskey.Channels['main']> | null = null; + +onMounted(() => { + if (store.s.realtimeMode) { + connection = useStream().useChannel('main'); + connection.on('notification', onNotification); + connection.on('notificationFlushed', reload); + } +}); + +onUnmounted(() => { + if (connection) connection.dispose(); +}); + +defineExpose({ + reload, +}); +</script> + +<style lang="scss" module> +.transition_x_move { + transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); +} + +.transition_x_enterActive { + transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); + + &.content, + .content { + /* Skip Note Rendering有効時、TransitionGroupで通知を追加するときに一瞬がくっとなる問題を抑制する */ + content-visibility: visible !important; + } +} + +.transition_x_leaveActive { + transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1); +} + +.transition_x_enterFrom { + opacity: 0; + transform: translateY(max(-64px, -100%)); +} + +@supports (interpolate-size: allow-keywords) { + .transition_x_enterFrom { + interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要 + height: 0; + } +} + +.transition_x_leaveTo { + opacity: 0; +} + +.notifications { + container-type: inline-size; + background: var(--MI_THEME-panel); +} + +.item { + border-bottom: solid 0.5px var(--MI_THEME-divider); +} + +.date { + display: flex; + font-size: 85%; + align-items: center; + justify-content: center; + gap: 1em; + opacity: 0.75; + padding: 8px 8px; + margin: 0 auto; + border-bottom: solid 0.5px var(--MI_THEME-divider); +} + +.more { + display: block; + width: 100%; + box-sizing: border-box; + padding: 16px; + background: var(--MI_THEME-panel); + border-top: solid 0.5px var(--MI_THEME-divider); +} +</style> |