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:
@@ -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;
|
||||
}>(), {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}>(), {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user