1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-02 17:55:52 +02:00

refactor(frontend): improve pagination implementation

This commit is contained in:
syuilo
2025-06-29 15:11:25 +09:00
parent 8bc822d829
commit f1deb89e34
68 changed files with 1067 additions and 1138 deletions

View File

@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkPagination :pagination="pagination">
<MkPagination :paginator="paginator">
<template #empty><MkResult type="empty"/></template>
<template #default="{ items }">
@@ -14,13 +14,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import type { PagingCtx } from '@/composables/use-pagination.js';
import type { Paginator } from '@/utility/paginator.js';
import MkChannelPreview from '@/components/MkChannelPreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
pagination: PagingCtx;
paginator: Paginator;
noGap?: boolean;
extractor?: (item: any) => any;
}>(), {

View File

@@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, computed, TransitionGroup } from 'vue';
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, computed, TransitionGroup, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from './MkButton.vue';
import type { MenuItem } from '@/types/menu.js';
@@ -146,10 +146,10 @@ import { prefer } from '@/preferences.js';
import { chooseFileFromPcAndUpload, selectDriveFolder } from '@/utility/drive.js';
import { store } from '@/store.js';
import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
import { usePagination } from '@/composables/use-pagination.js';
import { globalEvents, useGlobalEvent } from '@/events.js';
import { checkDragDataType, getDragData, setDragData } from '@/drag-and-drop.js';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{
initialFolder?: Misskey.entities.DriveFolder['id'] | null;
@@ -195,33 +195,23 @@ const fetching = ref(true);
const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
const filesPaginator = usePagination({
ctx: {
endpoint: 'drive/files',
limit: 30,
canFetchDetection: 'limit',
params: computed(() => ({
folderId: folder.value ? folder.value.id : null,
type: props.type,
sort: sortModeSelect.value,
})),
},
autoInit: false,
autoReInit: false,
});
const filesPaginator = markRaw(new Paginator('drive/files', {
limit: 30,
canFetchDetection: 'limit',
params: () => ({ // 自動でリロードしたくないためcomputedParamsは使わない
folderId: folder.value ? folder.value.id : null,
type: props.type,
sort: sortModeSelect.value,
}),
}));
const foldersPaginator = usePagination({
ctx: {
endpoint: 'drive/folders',
limit: 30,
canFetchDetection: 'limit',
params: computed(() => ({
folderId: folder.value ? folder.value.id : null,
})),
},
autoInit: false,
autoReInit: false,
});
const foldersPaginator = markRaw(new Paginator('drive/folders', {
limit: 30,
canFetchDetection: 'limit',
params: () => ({ // 自動でリロードしたくないためcomputedParamsは使わない
folderId: folder.value ? folder.value.id : null,
}),
}));
const filesTimeline = makeDateGroupedTimelineComputedRef(filesPaginator.items, 'month');

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div>
<MkPagination v-slot="{ items }" :pagination="pagination">
<MkPagination v-slot="{ items }" :paginator="paginator">
<div :class="[$style.fileList, { [$style.grid]: viewMode === 'grid', [$style.list]: viewMode === 'list', '_gaps_s': viewMode === 'list' }]">
<MkA
v-for="file in items"
@@ -40,15 +40,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import type { Paginator } from '@/utility/paginator.js';
import MkPagination from '@/components/MkPagination.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import bytes from '@/filters/bytes.js';
import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import type { PagingCtx } from '@/composables/use-pagination.js';
defineProps<{
pagination: PagingCtx<'admin/drive/files'>;
paginator: Paginator<'admin/drive/files'>;
viewMode: 'grid' | 'list';
}>();
</script>

View File

@@ -187,7 +187,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
</div>
<div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
<MkPagination :pagination="renotesPagination" :disableAutoLoad="true">
<MkPagination :paginator="renotesPaginator">
<template #default="{ items }">
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
<MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
@@ -204,7 +204,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span style="margin-left: 4px;">{{ $appearNote.reactions[reaction] }}</span>
</button>
</div>
<MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true">
<MkPagination v-if="reactionTabType" :key="reactionTabType" :paginator="reactionsPaginator">
<template #default="{ items }">
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
<MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
@@ -228,7 +228,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, inject, onMounted, provide, ref, useTemplateRef } from 'vue';
import { computed, inject, markRaw, onMounted, provide, ref, useTemplateRef } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
@@ -274,6 +274,7 @@ import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js';
import { globalEvents, useGlobalEvent } from '@/events.js';
import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@@ -376,21 +377,19 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
const tab = ref(props.initialTab);
const reactionTabType = ref<string | null>(null);
const renotesPagination = computed(() => ({
endpoint: 'notes/renotes',
const renotesPaginator = markRaw(new Paginator('notes/renotes', {
limit: 10,
params: {
noteId: appearNote.id,
},
}));
const reactionsPagination = computed(() => ({
endpoint: 'notes/reactions',
const reactionsPaginator = markRaw(new Paginator('notes/reactions', {
limit: 10,
params: {
computedParams: computed(() => ({
noteId: appearNote.id,
type: reactionTabType.value,
},
})),
}));
useTooltip(renoteButton, async (showing) => {

View File

@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
</template>
<div class="_spacer">
<MkPagination ref="pagingEl" :pagination="paging" withControl>
<MkPagination :paginator="paginator" withControl>
<template #empty>
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
</template>
@@ -100,9 +100,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, shallowRef, useTemplateRef } from 'vue';
import { ref, shallowRef, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import type { PagingCtx } from '@/composables/use-pagination.js';
import MkButton from '@/components/MkButton.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -111,6 +110,7 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { $i } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api';
import { Paginator } from '@/utility/paginator.js';
const emit = defineEmits<{
(ev: 'restore', draft: Misskey.entities.NoteDraft): void;
@@ -118,12 +118,9 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
const paging = {
endpoint: 'notes/drafts/list',
const paginator = markRaw(new Paginator('notes/drafts/list', {
limit: 10,
} satisfies PagingCtx;
const pagingComponent = useTemplateRef('pagingEl');
}));
const currentDraftsCount = ref(0);
misskeyApi('notes/drafts/count').then((count) => {
@@ -151,7 +148,7 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
if (canceled) return;
os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
pagingComponent.value?.paginator.reload();
paginator.reload();
});
}
</script>

View File

@@ -4,17 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad" :pullToRefresh="pullToRefresh" :withControl="withControl">
<MkPagination :paginator="paginator" :autoLoad="autoLoad" :pullToRefresh="pullToRefresh" :withControl="withControl">
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
<template #default="{ items: notes }">
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]">
<template v-for="(note, i) in notes" :key="note.id">
<div v-if="i > 0 && isSeparatorNeeded(pagingComponent.paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
<div :class="$style.date">
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(pagingComponent.paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
<span>{{ getSeparatorInfo(pagingComponent.paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
</div>
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
</div>
@@ -31,9 +31,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPagination>
</template>
<script lang="ts" setup generic="T extends PagingCtx">
import { useTemplateRef } from 'vue';
import type { PagingCtx } from '@/composables/use-pagination.js';
<script lang="ts" setup generic="T extends Paginator">
import type { Paginator } from '@/utility/paginator.js';
import MkNote from '@/components/MkNote.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
@@ -41,24 +40,23 @@ import { globalEvents, useGlobalEvent } from '@/events.js';
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
const props = withDefaults(defineProps<{
pagination: T;
paginator: T;
noGap?: boolean;
disableAutoLoad?: boolean;
autoLoad?: boolean;
pullToRefresh?: boolean;
withControl?: boolean;
}>(), {
autoLoad: true,
pullToRefresh: true,
withControl: true,
});
const pagingComponent = useTemplateRef('pagingComponent');
useGlobalEvent('noteDeleted', (noteId) => {
pagingComponent.value?.paginator.removeItem(noteId);
props.paginator.removeItem(noteId);
});
function reload() {
return pagingComponent.value?.paginator.reload();
return props.paginator.reload();
}
defineExpose({

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component :is="prefer.s.enablePullToRefresh && pullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => paginator.reload()" @contextmenu.prevent.stop="onContextmenu">
<div>
<MkPaginationControl v-if="props.withControl" v-model:order="order" v-model:date="date" style="margin-bottom: 10px" @reload="paginator.reload()"/>
<MkPaginationControl v-if="props.withControl" :paginator="paginator" style="margin-bottom: 10px"/>
<!-- :css="prefer.s.animation" にしたいけどバグる(おそらくvueのバグ) https://github.com/misskey-dev/misskey/issues/16078 -->
<Transition
@@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else key="_root_" class="_gaps">
<slot :items="paginator.items.value" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
<div v-if="order === 'oldest'">
<MkButton v-if="!paginator.fetchingNewer.value" :class="$style.more" :wait="paginator.fetchingNewer.value" primary rounded @click="paginator.fetchNewer">
<div v-if="paginator.order.value === 'oldest'">
<MkButton v-if="!paginator.fetchingNewer.value" :class="$style.more" :wait="paginator.fetchingNewer.value" primary rounded @click="paginator.fetchNewer()">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else/>
</div>
<div v-else v-show="paginator.canFetchOlder.value">
<MkButton v-if="!paginator.fetchingOlder.value" :class="$style.more" :wait="paginator.fetchingOlder.value" primary rounded @click="paginator.fetchOlder">
<MkButton v-if="!paginator.fetchingOlder.value" :class="$style.more" :wait="paginator.fetchingOlder.value" primary rounded @click="paginator.fetchOlder()">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else/>
@@ -44,49 +44,29 @@ SPDX-License-Identifier: AGPL-3.0-only
</component>
</template>
<script lang="ts" setup generic="T extends PagingCtx">
<script lang="ts" setup generic="T extends Paginator, I = UnwrapRef<T['items']>">
import { isLink } from '@@/js/is-link.js';
import { ref, watch } from 'vue';
import { onMounted, watch } from 'vue';
import type { UnwrapRef } from 'vue';
import type { PagingCtx } from '@/composables/use-pagination.js';
import type { Paginator } from '@/utility/paginator.js';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { usePagination } from '@/composables/use-pagination.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import MkPaginationControl from '@/components/MkPaginationControl.vue';
import * as os from '@/os.js';
type Paginator = ReturnType<typeof usePagination<T['endpoint']>>;
const props = withDefaults(defineProps<{
pagination: T;
disableAutoLoad?: boolean;
displayLimit?: number;
paginator: T;
autoLoad?: boolean;
pullToRefresh?: boolean;
withControl?: boolean;
}>(), {
displayLimit: 20,
autoLoad: true,
pullToRefresh: true,
withControl: false,
});
const order = ref<'newest' | 'oldest'>(props.pagination.order ?? 'newest');
const date = ref<number | null>(null);
const paginator: Paginator = usePagination({
ctx: props.pagination,
});
watch([order, date], () => {
paginator.updateCtx({
...props.pagination,
order: order.value,
initialDirection: order.value === 'oldest' ? 'newer' : 'older',
initialDate: date.value,
});
}, { immediate: false });
function onContextmenu(ev: MouseEvent) {
if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== '') return;
@@ -96,19 +76,27 @@ function onContextmenu(ev: MouseEvent) {
icon: 'ti ti-refresh',
text: i18n.ts.reload,
action: () => {
paginator.reload();
props.paginator.reload();
},
}], ev);
}
if (props.autoLoad) {
onMounted(() => {
props.paginator.init();
});
}
if (props.paginator.computedParams) {
watch(props.paginator.computedParams, () => {
props.paginator.reload();
}, { immediate: false, deep: true });
}
defineSlots<{
empty: () => void;
default: (props: { items: UnwrapRef<Paginator['items']> }) => void;
default: (props: { items: I }) => void;
}>();
defineExpose({
paginator: paginator,
});
</script>
<style lang="scss" module>

View File

@@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="order" :class="$style.order" :items="[{ label: i18n.ts._order.newest, value: 'newest' }, { label: i18n.ts._order.oldest, value: 'oldest' }]">
<template #prefix><i class="ti ti-arrows-sort"></i></template>
</MkSelect>
<MkButton v-if="canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
<MkButton v-if="paginator.canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
<MkButton v-if="canFilter" v-tooltip="i18n.ts.filter" iconOnly transparent rounded :active="filterOpened" @click="filterOpened = !filterOpened"><i class="ti ti-filter"></i></MkButton>
<MkButton v-tooltip="i18n.ts.dateAndTime" iconOnly transparent rounded :active="date != null" @click="date = date == null ? Date.now() : null"><i class="ti ti-calendar-clock"></i></MkButton>
<MkButton v-tooltip="i18n.ts.reload" iconOnly transparent rounded @click="emit('reload')"><i class="ti ti-refresh"></i></MkButton>
<MkButton v-tooltip="i18n.ts.reload" iconOnly transparent rounded @click="paginator.reload()"><i class="ti ti-refresh"></i></MkButton>
</div>
<MkInput
@@ -37,9 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
<script lang="ts" setup generic="T extends PagingCtx">
<script lang="ts" setup generic="T extends Paginator">
import { ref, watch } from 'vue';
import type { PagingCtx } from '@/composables/use-pagination.js';
import type { Paginator } from '@/utility/paginator.js';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
@@ -47,32 +47,35 @@ import MkInput from '@/components/MkInput.vue';
import { formatDateTimeString } from '@/utility/format-time-string.js';
const props = withDefaults(defineProps<{
canSearch?: boolean;
paginator: T;
canFilter?: boolean;
filterOpened?: boolean;
}>(), {
canSearch: false,
canFilter: false,
filterOpened: false,
});
const emit = defineEmits<{
(ev: 'reload'): void;
}>();
const searchOpened = ref(false);
const filterOpened = ref(props.filterOpened);
const order = defineModel<'newest' | 'oldest'>('order', {
default: 'newest',
const order = ref<'newest' | 'oldest'>('newest');
const date = ref<number | null>(null);
const q = ref<string | null>(null);
watch(order, () => {
props.paginator.order.value = order.value;
props.paginator.initialDirection = order.value === 'oldest' ? 'newer' : 'older';
props.paginator.reload();
});
const date = defineModel<number | null>('date', {
default: null,
watch(date, () => {
props.paginator.initialDate = date.value;
props.paginator.reload();
});
const q = defineModel<string | null>('q', {
default: null,
watch(q, () => {
props.paginator.searchQuery.value = q.value;
props.paginator.reload();
});
</script>

View File

@@ -56,14 +56,12 @@ 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 { 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 MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream } from '@/stream.js';
import * as sound from '@/utility/sound.js';
@@ -76,6 +74,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 +101,97 @@ provide('inTimeline', true);
provide('tl_withSensitive', computed(() => props.withSensitive));
provide('inChannel', computed(() => props.src === 'channel'));
let paginator: Paginator;
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,17 +223,6 @@ onUnmounted(() => {
}
});
type TimelineQueryType = {
antennaId?: string,
withRenotes?: boolean,
withReplies?: boolean,
withFiles?: boolean,
visibility?: string,
listId?: string,
channelId?: string,
roleId?: string
};
let adInsertionCounter = 0;
const MIN_POLLING_INTERVAL = 1000 * 10;
@@ -204,7 +283,6 @@ function prepend(note: Misskey.entities.Note) {
let connection: Misskey.ChannelConnection | null = null;
let connection2: Misskey.ChannelConnection | null = null;
let paginationQuery: PagingCtx;
const stream = store.s.realtimeMode ? useStream() : null;
@@ -274,100 +352,13 @@ 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,
};
}
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();

View File

@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue';
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, markRaw, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import type { notificationTypes } from '@@/js/const.js';
@@ -53,8 +53,8 @@ import { i18n } from '@/i18n.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { prefer } from '@/preferences.js';
import { store } from '@/store.js';
import { usePagination } from '@/composables/use-pagination.js';
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
excludeTypes?: typeof notificationTypes[number][];
@@ -62,21 +62,17 @@ const props = defineProps<{
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 paginator = prefer.s.useGroupedNotifications ? markRaw(new Paginator('i/notifications-grouped', {
limit: 20,
computedParams: computed(() => ({
excludeTypes: props.excludeTypes ?? undefined,
})),
})) : markRaw(new Paginator('i/notifications', {
limit: 20,
computedParams: computed(() => ({
excludeTypes: props.excludeTypes ?? undefined,
})),
}));
const MIN_POLLING_INTERVAL = 1000 * 10;
const POLLING_INTERVAL =
@@ -116,6 +112,14 @@ function reload() {
let connection: Misskey.ChannelConnection<Misskey.Channels['main']> | null = null;
onMounted(() => {
paginator.init();
if (paginator.computedParams) {
watch(paginator.computedParams, () => {
paginator.reload();
}, { immediate: false, deep: true });
}
if (store.s.realtimeMode) {
connection = useStream().useChannel('main');
connection.on('notification', onNotification);

View File

@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkPagination :pagination="pagination">
<MkPagination :paginator="paginator">
<template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template>
<template #default="{ items }">
@@ -16,13 +16,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import type { PagingCtx } from '@/composables/use-pagination.js';
import type { Paginator } from '@/utility/paginator.js';
import MkUserInfo from '@/components/MkUserInfo.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
pagination: PagingCtx;
paginator: Paginator;
noGap?: boolean;
extractor?: (item: any) => any;
}>(), {

View File

@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.recommended }}</template>
<MkPagination :pagination="pinnedUsers">
<MkPagination :paginator="pinnedUsersPaginator">
<template #default="{ items }">
<div :class="$style.users">
<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.popularUsers }}</template>
<MkPagination :pagination="popularUsers">
<MkPagination :paginator="popularUsersPaginator">
<template #default="{ items }">
<div :class="$style.users">
<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
@@ -35,20 +35,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { markRaw } from 'vue';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue';
import MkPagination from '@/components/MkPagination.vue';
import type { PagingCtx } from '@/composables/use-pagination.js';
import { Paginator } from '@/utility/paginator.js';
const pinnedUsers: PagingCtx = {
endpoint: 'pinned-users',
const pinnedUsersPaginator = markRaw(new Paginator('pinned-users', {
noPaging: true,
limit: 10,
};
}));
const popularUsers: PagingCtx = {
endpoint: 'users',
const popularUsersPaginator = markRaw(new Paginator('users', {
limit: 10,
noPaging: true,
params: {
@@ -56,7 +55,7 @@ const popularUsers: PagingCtx = {
origin: 'local',
sort: '+follower',
},
};
}));
</script>
<style lang="scss" module>