summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkStreamingNotesTimeline.vue
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2025-07-18 00:28:01 +0000
committerGitHub <noreply@github.com>2025-07-18 00:28:01 +0000
commite86e9b46b3c30391296f04842529f9f8411dcbba (patch)
treebbe0f31106c393c1a742183aedd1c39ccdfa39a2 /packages/frontend/src/components/MkStreamingNotesTimeline.vue
parentMerge pull request #16197 from misskey-dev/develop (diff)
parentRelease: 2025.7.0 (diff)
downloadmisskey-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.vue231
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);