diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2025-07-18 00:28:01 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-18 00:28:01 +0000 |
| commit | e86e9b46b3c30391296f04842529f9f8411dcbba (patch) | |
| tree | bbe0f31106c393c1a742183aedd1c39ccdfa39a2 /packages/frontend/src/components/MkStreamingNotesTimeline.vue | |
| parent | Merge pull request #16197 from misskey-dev/develop (diff) | |
| parent | Release: 2025.7.0 (diff) | |
| download | misskey-e86e9b46b3c30391296f04842529f9f8411dcbba.tar.gz misskey-e86e9b46b3c30391296f04842529f9f8411dcbba.tar.bz2 misskey-e86e9b46b3c30391296f04842529f9f8411dcbba.zip | |
Merge pull request #16244 from misskey-dev/develop
Release: 2025.7.0
Diffstat (limited to 'packages/frontend/src/components/MkStreamingNotesTimeline.vue')
| -rw-r--r-- | packages/frontend/src/components/MkStreamingNotesTimeline.vue | 231 |
1 files changed, 121 insertions, 110 deletions
diff --git a/packages/frontend/src/components/MkStreamingNotesTimeline.vue b/packages/frontend/src/components/MkStreamingNotesTimeline.vue index 7e72840b7b..3e50bdefd2 100644 --- a/packages/frontend/src/components/MkStreamingNotesTimeline.vue +++ b/packages/frontend/src/components/MkStreamingNotesTimeline.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNote v-else :class="$style.note" :note="note" :withHardMute="true" :data-scroll-anchor="note.id"/> </template> </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"> + <button v-show="paginator.canFetchOlder.value" key="_more_" :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 :inline="true"/> </button> @@ -56,14 +56,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup, onMounted, shallowRef, ref } from 'vue'; +import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup, onMounted, shallowRef, ref, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import { useInterval } from '@@/js/use-interval.js'; +import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; import { getScrollContainer, scrollToTop } from '@@/js/scroll.js'; import type { BasicTimelineType } from '@/timelines.js'; -import type { PagingCtx } from '@/composables/use-pagination.js'; import type { SoundStore } from '@/preferences/def.js'; -import { usePagination } from '@/composables/use-pagination.js'; +import type { IPaginator, MisskeyEntity } from '@/utility/paginator.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import { useStream } from '@/stream.js'; import * as sound from '@/utility/sound.js'; @@ -76,6 +76,7 @@ import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import { globalEvents, useGlobalEvent } from '@/events.js'; import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js'; +import { Paginator } from '@/utility/paginator.js'; const props = withDefaults(defineProps<{ src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; @@ -102,6 +103,97 @@ provide('inTimeline', true); provide('tl_withSensitive', computed(() => props.withSensitive)); provide('inChannel', computed(() => props.src === 'channel')); +let paginator: IPaginator<Misskey.entities.Note>; + +if (props.src === 'antenna') { + paginator = markRaw(new Paginator('antennas/notes', { + computedParams: computed(() => ({ + antennaId: props.antenna!, + })), + useShallowRef: true, + })); +} else if (props.src === 'home') { + paginator = markRaw(new Paginator('notes/timeline', { + computedParams: computed(() => ({ + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + })), + useShallowRef: true, + })); +} else if (props.src === 'local') { + paginator = markRaw(new Paginator('notes/local-timeline', { + computedParams: computed(() => ({ + withRenotes: props.withRenotes, + withReplies: props.withReplies, + withFiles: props.onlyFiles ? true : undefined, + })), + useShallowRef: true, + })); +} else if (props.src === 'social') { + paginator = markRaw(new Paginator('notes/hybrid-timeline', { + computedParams: computed(() => ({ + withRenotes: props.withRenotes, + withReplies: props.withReplies, + withFiles: props.onlyFiles ? true : undefined, + })), + useShallowRef: true, + })); +} else if (props.src === 'global') { + paginator = markRaw(new Paginator('notes/global-timeline', { + computedParams: computed(() => ({ + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + })), + useShallowRef: true, + })); +} else if (props.src === 'mentions') { + paginator = markRaw(new Paginator('notes/mentions', { + useShallowRef: true, + })); +} else if (props.src === 'directs') { + paginator = markRaw(new Paginator('notes/mentions', { + params: { + visibility: 'specified', + }, + useShallowRef: true, + })); +} else if (props.src === 'list') { + paginator = markRaw(new Paginator('notes/user-list-timeline', { + computedParams: computed(() => ({ + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + listId: props.list!, + })), + useShallowRef: true, + })); +} else if (props.src === 'channel') { + paginator = markRaw(new Paginator('channels/timeline', { + computedParams: computed(() => ({ + channelId: props.channel!, + })), + useShallowRef: true, + })); +} else if (props.src === 'role') { + paginator = markRaw(new Paginator('roles/notes', { + computedParams: computed(() => ({ + roleId: props.role!, + })), + useShallowRef: true, + })); +} else { + throw new Error('Unrecognized timeline type: ' + props.src); +} + +onMounted(() => { + paginator.init(); + + if (paginator.computedParams) { + watch(paginator.computedParams, () => { + paginator.reload(); + }, { immediate: false, deep: true }); + } +}); + function isTop() { if (scrollContainer == null) return true; if (rootEl.value == null) return true; @@ -133,16 +225,19 @@ onUnmounted(() => { } }); -type TimelineQueryType = { - antennaId?: string, - withRenotes?: boolean, - withReplies?: boolean, - withFiles?: boolean, - visibility?: string, - listId?: string, - channelId?: string, - roleId?: string -}; +const visibility = useDocumentVisibility(); +let isPausingUpdate = false; + +watch(visibility, () => { + if (visibility.value === 'hidden') { + isPausingUpdate = true; + } else { // 'visible' + isPausingUpdate = false; + if (isTop()) { + releaseQueue(); + } + } +}); let adInsertionCounter = 0; @@ -157,7 +252,7 @@ if (!store.s.realtimeMode) { // TODO: 先頭のノートの作成日時が1日以上前であれば流速が遅いTLと見做してインターバルを通常より延ばす useInterval(async () => { paginator.fetchNewer({ - toQueue: !isTop(), + toQueue: !isTop() || isPausingUpdate, }); }, POLLING_INTERVAL, { immediate: false, @@ -166,7 +261,7 @@ if (!store.s.realtimeMode) { useGlobalEvent('notePosted', (note) => { paginator.fetchNewer({ - toQueue: !isTop(), + toQueue: !isTop() || isPausingUpdate, }); }); } @@ -177,17 +272,17 @@ useGlobalEvent('noteDeleted', (noteId) => { function releaseQueue() { paginator.releaseQueue(); - scrollToTop(rootEl.value); + scrollToTop(rootEl.value!); } -function prepend(note: Misskey.entities.Note) { +function prepend(note: Misskey.entities.Note & MisskeyEntity) { adInsertionCounter++; if (instance.notesPerOneAd > 0 && adInsertionCounter % instance.notesPerOneAd === 0) { note._shouldInsertAd_ = true; } - if (isTop()) { + if (isTop() && !isPausingUpdate) { paginator.prepend(note); } else { paginator.enqueue(note); @@ -202,13 +297,13 @@ function prepend(note: Misskey.entities.Note) { } } -let connection: Misskey.ChannelConnection | null = null; -let connection2: Misskey.ChannelConnection | null = null; -let paginationQuery: PagingCtx; +let connection: Misskey.IChannelConnection | null = null; +let connection2: Misskey.IChannelConnection | null = null; const stream = store.s.realtimeMode ? useStream() : null; function connectChannel() { + if (stream == null) return; if (props.src === 'antenna') { if (props.antenna == null) return; connection = stream.useChannel('antenna', { @@ -274,100 +369,17 @@ function disconnectChannel() { if (connection2) connection2.dispose(); } -function updatePaginationQuery() { - let endpoint: keyof Misskey.Endpoints | null; - let query: TimelineQueryType | null; - - if (props.src === 'antenna') { - endpoint = 'antennas/notes'; - query = { - antennaId: props.antenna, - }; - } else if (props.src === 'home') { - endpoint = 'notes/timeline'; - query = { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - }; - } else if (props.src === 'local') { - endpoint = 'notes/local-timeline'; - query = { - withRenotes: props.withRenotes, - withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, - }; - } else if (props.src === 'social') { - endpoint = 'notes/hybrid-timeline'; - query = { - withRenotes: props.withRenotes, - withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, - }; - } else if (props.src === 'global') { - endpoint = 'notes/global-timeline'; - query = { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - }; - } else if (props.src === 'mentions') { - endpoint = 'notes/mentions'; - query = null; - } else if (props.src === 'directs') { - endpoint = 'notes/mentions'; - query = { - visibility: 'specified', - }; - } else if (props.src === 'list') { - endpoint = 'notes/user-list-timeline'; - query = { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - listId: props.list, - }; - } else if (props.src === 'channel') { - endpoint = 'channels/timeline'; - query = { - channelId: props.channel, - }; - } else if (props.src === 'role') { - endpoint = 'roles/notes'; - query = { - roleId: props.role, - }; - } else { - throw new Error('Unrecognized timeline type: ' + props.src); - } - - paginationQuery = { - endpoint: endpoint, - limit: 10, - params: query, - }; +if (store.s.realtimeMode) { + connectChannel(); } -function refreshEndpointAndChannel() { +watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], () => { if (store.s.realtimeMode) { disconnectChannel(); connectChannel(); } - - updatePaginationQuery(); -} - -// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる -// IDが切り替わったら切り替え先のTLを表示させたい -watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel); - -// withSensitiveはクライアントで完結する処理のため、単にリロードするだけでOK -watch(() => props.withSensitive, reloadTimeline); - -// 初回表示用 -refreshEndpointAndChannel(); - -const paginator = usePagination({ - ctx: paginationQuery, - useShallowRef: true, }); +watch(() => props.withSensitive, reloadTimeline); onUnmounted(() => { disconnectChannel(); @@ -512,7 +524,6 @@ defineExpose({ 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); |