Compare commits

..

3 commits

72 changed files with 1083 additions and 1139 deletions

View file

@ -2,11 +2,13 @@
### General
- Feat: ノートの下書き機能
- Feat: クリップ内でノートを検索できるように
### Client
- Feat: モデログを検索できるように
- Enhance: 設定の自動バックアップをオンにした直後に自動バックアップするように
- Enhance: ファイルアップロード前にキャプション設定を行えるように
- Enhance: ファイルアップロード時にセンシティブ設定されているか表示するように
- Enhance: ページネーション(一覧表示)の並び順を逆にできるように
- Enhance: ページネーション(一覧表示)の基準日時を指定できるように
- Fix: ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正

View file

@ -4,11 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -46,6 +48,7 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
search: { type: 'string', minLength: 1, maxLength: 100, nullable: true },
},
required: ['clipId'],
} as const;
@ -97,6 +100,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
}
if (ps.search != null) {
for (const word of ps.search!.trim().split(' ')) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.text ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
qb.orWhere('note.cw ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
}));
}
}
const notes = await query
.limit(ps.limit)
.getMany();

View file

@ -153,6 +153,9 @@ export default [
autofix: true,
}],
'vue/attribute-hyphenation': ['error', 'never'],
'vue/no-mutating-props': ['error', {
shallowOnly: true,
}],
},
},
];

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
// IDTL
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
// withSensitiveOK
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

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div>
<div :class="$style.itemBody">
<div><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
<div><i v-if="item.isSensitive" style="color: var(--MI_THEME-warn); margin-right: 0.5em;" class="ti ti-eye-exclamation"></i><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
<div :class="$style.itemInfo">
<span>{{ item.file.type }}</span>
<span v-if="item.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(item.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - item.compressedSize / item.file.size) * 100) }) }})</span>

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>

View file

@ -1,316 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { computed, isRef, onMounted, ref, shallowRef, triggerRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import type { ComputedRef, DeepReadonly, Ref, ShallowRef } from 'vue';
import { misskeyApi } from '@/utility/misskey-api.js';
const MAX_ITEMS = 30;
const MAX_QUEUE_ITEMS = 100;
const FIRST_FETCH_LIMIT = 15;
const SECOND_FETCH_LIMIT = 30;
export type MisskeyEntity = {
id: string;
createdAt: string;
_shouldInsertAd_?: boolean;
[x: string]: any;
};
export type PagingCtx<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints> = {
endpoint: E;
limit?: number;
params?: Misskey.Endpoints[E]['req'] | ComputedRef<Misskey.Endpoints[E]['req']>;
/**
* APIのような
* (APIをこの関数で使うのは若干矛盾してるけど)
*/
noPaging?: boolean;
offsetMode?: boolean;
initialId?: MisskeyEntity['id'];
initialDate?: number | null;
initialDirection?: 'newer' | 'older';
// 配列内の要素をどのような順序で並べるか
// newest: 新しいものが先頭 (default)
// oldest: 古いものが先頭
// NOTE: このようなプロパティを用意してこっち側で並びを管理せずに、Setで持っておき参照者側が好きに並び変えるような設計の方がすっきりしそうなものの、Vueのレンダリングのたびに並び替え処理が発生することになったりしそうでパフォーマンス上の懸念がある
order?: 'newest' | 'oldest';
// 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨
canFetchDetection?: 'safe' | 'limit';
};
export function usePagination<Endpoint extends keyof Misskey.Endpoints, T extends { id: string; } = (Misskey.Endpoints[Endpoint]['res'] extends (infer I)[] ? I extends { id: string } ? I : { id: string } : { id: string })>(props: {
ctx: PagingCtx<Endpoint>;
autoInit?: boolean;
autoReInit?: boolean;
useShallowRef?: boolean;
}) {
const items = props.useShallowRef ? shallowRef<T[]>([]) : ref<T[]>([]);
let aheadQueue: T[] = [];
const queuedAheadItemsCount = ref(0);
const fetching = ref(true);
const fetchingOlder = ref(false);
const fetchingNewer = ref(false);
const canFetchOlder = ref(false);
const error = ref(false);
if (props.autoReInit !== false) {
watch(() => [props.ctx.endpoint, props.ctx.params], init, { deep: true });
}
function getNewestId(): string | null | undefined {
// 様々な要因により並び順は保証されないのでソートが必要
if (aheadQueue.length > 0) {
return aheadQueue.map(x => x.id).sort().at(-1);
}
return items.value.map(x => x.id).sort().at(-1);
}
function getOldestId(): string | null | undefined {
// 様々な要因により並び順は保証されないのでソートが必要
return items.value.map(x => x.id).sort().at(0);
}
async function init(): Promise<void> {
items.value = [];
aheadQueue = [];
queuedAheadItemsCount.value = 0;
fetching.value = true;
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
await misskeyApi<T[]>(props.ctx.endpoint, {
...params,
limit: props.ctx.limit ?? FIRST_FETCH_LIMIT,
allowPartial: true,
...((props.ctx.initialId == null && props.ctx.initialDate == null) && props.ctx.initialDirection === 'newer' ? {
sinceId: '0',
} : props.ctx.initialDirection === 'newer' ? {
sinceId: props.ctx.initialId,
sinceDate: props.ctx.initialDate,
} : (props.ctx.initialId || props.ctx.initialDate) && props.ctx.initialDirection === 'older' ? {
untilId: props.ctx.initialId,
untilDate: props.ctx.initialDate,
} : {}),
}).then(res => {
// 逆順で返ってくるので
if ((props.ctx.initialId || props.ctx.initialDate) && props.ctx.initialDirection === 'newer') {
res.reverse();
}
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (i === 3) item._shouldInsertAd_ = true;
}
pushItems(res);
if (props.ctx.canFetchDetection === 'limit') {
if (res.length < FIRST_FETCH_LIMIT) {
canFetchOlder.value = false;
} else {
canFetchOlder.value = true;
}
} else if (props.ctx.canFetchDetection === 'safe' || props.ctx.canFetchDetection == null) {
if (res.length === 0 || props.ctx.noPaging) {
canFetchOlder.value = false;
} else {
canFetchOlder.value = true;
}
}
error.value = false;
fetching.value = false;
}, err => {
error.value = true;
fetching.value = false;
});
}
function reload(): Promise<void> {
return init();
}
async function fetchOlder(): Promise<void> {
if (!canFetchOlder.value || fetching.value || fetchingOlder.value || items.value.length === 0) return;
fetchingOlder.value = true;
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
await misskeyApi<T[]>(props.ctx.endpoint, {
...params,
limit: SECOND_FETCH_LIMIT,
...(props.ctx.offsetMode ? {
offset: items.value.length,
} : {
untilId: getOldestId(),
}),
}).then(res => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (i === 10) item._shouldInsertAd_ = true;
}
pushItems(res);
if (props.ctx.canFetchDetection === 'limit') {
if (res.length < FIRST_FETCH_LIMIT) {
canFetchOlder.value = false;
} else {
canFetchOlder.value = true;
}
} else if (props.ctx.canFetchDetection === 'safe' || props.ctx.canFetchDetection == null) {
if (res.length === 0) {
canFetchOlder.value = false;
} else {
canFetchOlder.value = true;
}
}
}).finally(() => {
fetchingOlder.value = false;
});
}
async function fetchNewer(options: {
toQueue?: boolean;
} = {}): Promise<void> {
fetchingNewer.value = true;
const params = props.ctx.params ? isRef(props.ctx.params) ? props.ctx.params.value : props.ctx.params : {};
await misskeyApi<T[]>(props.ctx.endpoint, {
...params,
limit: SECOND_FETCH_LIMIT,
...(props.ctx.offsetMode ? {
offset: items.value.length,
} : {
sinceId: getNewestId(),
}),
}).then(res => {
if (res.length === 0) return; // これやらないと余計なre-renderが走る
if (options.toQueue) {
aheadQueue.unshift(...res.toReversed());
if (aheadQueue.length > MAX_QUEUE_ITEMS) {
aheadQueue = aheadQueue.slice(0, MAX_QUEUE_ITEMS);
}
queuedAheadItemsCount.value = aheadQueue.length;
} else {
if (props.ctx.order === 'oldest') {
pushItems(res);
} else {
unshiftItems(res.toReversed());
}
}
}).finally(() => {
fetchingNewer.value = false;
});
}
function trim(trigger = true) {
if (items.value.length >= MAX_ITEMS) canFetchOlder.value = true;
items.value = items.value.slice(0, MAX_ITEMS);
if (props.useShallowRef && trigger) triggerRef(items);
}
function unshiftItems(newItems: T[]) {
if (newItems.length === 0) return; // これやらないと余計なre-renderが走る
items.value.unshift(...newItems.filter(x => !items.value.some(y => y.id === x.id))); // ストリーミングやポーリングのタイミングによっては重複することがあるため
trim(false);
if (props.useShallowRef) triggerRef(items);
}
function pushItems(oldItems: T[]) {
if (oldItems.length === 0) return; // これやらないと余計なre-renderが走る
items.value.push(...oldItems);
if (props.useShallowRef) triggerRef(items);
}
function prepend(item: T) {
if (items.value.some(x => x.id === item.id)) return;
items.value.unshift(item);
trim(false);
if (props.useShallowRef) triggerRef(items);
}
function enqueue(item: T) {
aheadQueue.unshift(item);
if (aheadQueue.length > MAX_QUEUE_ITEMS) {
aheadQueue.pop();
}
queuedAheadItemsCount.value = aheadQueue.length;
}
function releaseQueue() {
if (aheadQueue.length === 0) return; // これやらないと余計なre-renderが走る
unshiftItems(aheadQueue);
aheadQueue = [];
queuedAheadItemsCount.value = 0;
}
function removeItem(id: string) {
// TODO: queueからも消す
const index = items.value.findIndex(x => x.id === id);
if (index !== -1) {
items.value.splice(index, 1);
if (props.useShallowRef) triggerRef(items);
}
}
function updateItem(id: string, updator: (item: T) => T) {
// TODO: queueのも更新
const index = items.value.findIndex(x => x.id === id);
if (index !== -1) {
const item = items.value[index]!;
items.value[index] = updator(item);
if (props.useShallowRef) triggerRef(items);
}
}
function updateCtx(ctx: PagingCtx<Endpoint>) {
props.ctx = ctx;
reload();
}
function updateCtxPartial(ctx: Partial<PagingCtx<Endpoint>>) {
props.ctx = {
...props.ctx,
...ctx,
};
reload();
}
if (props.autoInit !== false) {
onMounted(() => {
init();
});
}
return {
items: items as DeepReadonly<ShallowRef<T[]>>,
queuedAheadItemsCount,
fetching,
fetchingOlder,
fetchingNewer,
canFetchOlder,
init,
reload,
fetchOlder,
fetchNewer,
unshiftItems,
prepend,
trim,
removeItem,
updateItem,
enqueue,
releaseQueue,
error,
updateCtx,
updateCtxPartial,
};
}

View file

@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSplit>
</div>
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :paginator="paginator">
<div :class="$style.items">
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.item" :to="`/instance-info/${instance.host}`">
<MkInstanceCardMini :instance="instance"/>
@ -51,24 +51,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, markRaw, ref } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue';
import type { PagingCtx } from '@/composables/use-pagination.js';
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
import FormSplit from '@/components/form/split.vue';
import { i18n } from '@/i18n.js';
import { Paginator } from '@/utility/paginator.js';
const host = ref('');
const state = ref('federating');
const sort = ref('+pubSub');
const pagination = {
endpoint: 'federation/instances' as const,
const paginator = markRaw(new Paginator('federation/instances', {
limit: 10,
displayLimit: 50,
offsetMode: true,
params: computed(() => ({
computedParams: computed(() => ({
sort: sort.value,
host: host.value !== '' ? host.value : null,
...(
@ -81,7 +79,7 @@ const pagination = {
state.value === 'notResponding' ? { notResponding: true } :
{}),
})),
} as PagingCtx;
}));
function getStatus(instance) {
if (instance.isSuspended) return 'Suspended';

View file

@ -160,7 +160,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="archived">{{ i18n.ts.archived }}</option>
</MkSelect>
<MkPagination :pagination="announcementsPagination">
<MkPagination :paginator="announcementsPaginator">
<template #default="{ items }">
<div class="_gaps_s">
<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
@ -179,7 +179,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else-if="tab === 'drive'" class="_gaps">
<MkFileListForAdmin :pagination="filesPagination" viewMode="grid"/>
<MkFileListForAdmin :paginator="filesPaginator" viewMode="grid"/>
</div>
<div v-else-if="tab === 'chart'" class="_gaps_m">
@ -211,7 +211,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, watch, ref } from 'vue';
import { computed, defineAsyncComponent, watch, ref, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import MkChart from '@/components/MkChart.vue';
@ -235,6 +235,7 @@ import { i18n } from '@/i18n.js';
import { iAmAdmin, $i, iAmModerator } from '@/i.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{
userId: string;
@ -255,24 +256,22 @@ const silenced = ref(false);
const suspended = ref(false);
const isSystem = ref(false);
const moderationNote = ref('');
const filesPagination = {
endpoint: 'admin/drive/files' as const,
const filesPaginator = markRaw(new Paginator('admin/drive/files', {
limit: 10,
params: computed(() => ({
computedParams: computed(() => ({
userId: props.userId,
})),
};
}));
const announcementsStatus = ref<'active' | 'archived'>('active');
const announcementsPagination = {
endpoint: 'admin/announcements/list' as const,
const announcementsPaginator = markRaw(new Paginator('admin/announcements/list', {
limit: 10,
params: computed(() => ({
computedParams: computed(() => ({
userId: props.userId,
status: announcementsStatus.value,
})),
};
}));
const expandedRoles = ref([]);
function createFetcher() {

View file

@ -41,13 +41,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" :spellcheck="false">
<span>{{ i18n.ts.username }}</span>
</MkInput>
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params().origin === 'local'">
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" :spellcheck="false" :disabled="paginator.computedParams.value.origin === 'local'">
<span>{{ i18n.ts.host }}</span>
</MkInput>
</div>
-->
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination">
<MkPagination v-slot="{items}" :paginator="paginator">
<div class="_gaps">
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
</div>
@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, useTemplateRef, ref } from 'vue';
import { computed, ref, markRaw } from 'vue';
import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue';
import XAbuseReport from '@/components/MkAbuseReport.vue';
@ -66,8 +66,7 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import { store } from '@/store.js';
const reports = useTemplateRef('reports');
import { Paginator } from '@/utility/paginator.js';
const state = ref('unresolved');
const reporterOrigin = ref('combined');
@ -75,18 +74,17 @@ const targetUserOrigin = ref('combined');
const searchUsername = ref('');
const searchHost = ref('');
const pagination = {
endpoint: 'admin/abuse-user-reports' as const,
const paginator = markRaw(new Paginator('admin/abuse-user-reports', {
limit: 10,
params: computed(() => ({
computedParams: computed(() => ({
state: state.value,
reporterOrigin: reporterOrigin.value,
targetUserOrigin: targetUserOrigin.value,
})),
};
}));
function resolved(reportId) {
reports.value?.paginator.removeItem(reportId);
paginator.removeItem(reportId);
}
const headerActions = computed(() => []);

View file

@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSplit>
</div>
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
<MkPagination v-slot="{items}" :key="host + state" :paginator="paginator">
<div :class="$style.instances">
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.instance" :to="`/instance-info/${instance.host}`">
<MkInstanceCardMini :instance="instance"/>
@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { computed, ref } from 'vue';
import { computed, markRaw, ref } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue';
@ -64,15 +64,15 @@ import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
import FormSplit from '@/components/form/split.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { Paginator } from '@/utility/paginator.js';
const host = ref('');
const state = ref('federating');
const sort = ref('+pubSub');
const pagination = {
endpoint: 'federation/instances' as const,
const paginator = markRaw(new Paginator('federation/instances', {
limit: 10,
offsetMode: true,
params: computed(() => ({
computedParams: computed(() => ({
sort: sort.value,
host: host.value !== '' ? host.value : null,
...(
@ -85,7 +85,7 @@ const pagination = {
state.value === 'notResponding' ? { notResponding: true } :
{}),
})),
};
}));
function getStatus(instance: Misskey.entities.FederationInstance) {
switch (instance.suspensionState) {

View file

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="local">{{ i18n.ts.local }}</option>
<option value="remote">{{ i18n.ts.remote }}</option>
</MkSelect>
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="paginator.computedParams.value.origin === 'local'">
<template #label>{{ i18n.ts.host }}</template>
</MkInput>
</div>
@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>MIME type</template>
</MkInput>
</div>
<MkFileListForAdmin :pagination="pagination" :viewMode="viewMode"/>
<MkFileListForAdmin :paginator="paginator" :viewMode="viewMode"/>
</div>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, markRaw, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
@ -42,23 +42,22 @@ import * as os from '@/os.js';
import { lookupFile } from '@/utility/admin-lookup.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import type { PagingCtx } from '@/composables/use-pagination.js';
import { Paginator } from '@/utility/paginator.js';
const origin = ref<Misskey.entities.AdminDriveFilesRequest['origin']>('local');
const type = ref<string | null>(null);
const searchHost = ref('');
const userId = ref('');
const viewMode = ref<'grid' | 'list'>('grid');
const pagination = {
endpoint: 'admin/drive/files' as const,
const paginator = markRaw(new Paginator('admin/drive/files', {
limit: 10,
params: computed(() => ({
computedParams: computed(() => ({
type: (type.value && type.value !== '') ? type.value : null,
userId: (userId.value && userId.value !== '') ? userId.value : null,
origin: origin.value,
hostname: (searchHost.value && searchHost.value !== '') ? searchHost.value : null,
})),
} satisfies PagingCtx<'admin/drive/files'>;
}));
function clear() {
os.confirm({

View file

@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
<template #label>{{ i18n.ts.expirationDate }}</template>
</MkInput>
<MkInput v-model="createCount" type="number" min="1">
<MkInput v-model="createCount" type="number" :min="1">
<template #label>{{ i18n.ts.createCount }}</template>
</MkInput>
<MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton>
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="-usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.descendingOrder }})</option>
</MkSelect>
</div>
<MkPagination ref="pagingComponent" :pagination="pagination">
<MkPagination :paginator="paginator">
<template #default="{ items }">
<div class="_gaps_s">
<MkInviteCode v-for="item in items" :key="item.id" :invite="(item as any)" :onDeleted="deleted" moderator/>
@ -54,8 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref, useTemplateRef } from 'vue';
import type { PagingCtx } from '@/composables/use-pagination.js';
import { computed, markRaw, ref, useTemplateRef } from 'vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -67,21 +66,19 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkInviteCode from '@/components/MkInviteCode.vue';
import { definePage } from '@/page.js';
const pagingComponent = useTemplateRef('pagingComponent');
import { Paginator } from '@/utility/paginator.js';
const type = ref('all');
const sort = ref('+createdAt');
const pagination: PagingCtx = {
endpoint: 'admin/invite/list' as const,
const paginator = markRaw(new Paginator('admin/invite/list', {
limit: 10,
params: computed(() => ({
computedParams: computed(() => ({
type: type.value,
sort: sort.value,
})),
offsetMode: true,
};
}));
const expiresAt = ref('');
const noExpirationDate = ref(true);
@ -100,13 +97,11 @@ async function createWithOptions() {
text: tickets.map(x => x.code).join('\n'),
});
tickets.forEach(ticket => pagingComponent.value?.paginator.prepend(ticket));
tickets.forEach(ticket => paginator.prepend(ticket));
}
function deleted(id: string) {
if (pagingComponent.value) {
pagingComponent.value.paginator.removeItem(id);
}
paginator.removeItem(id);
}
const headerActions = computed(() => []);

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 900px;">
<div class="_gaps">
<MkPaginationControl v-model:order="order" v-model:date="date" v-model:q="q" canSearch canFilter @reload="paginator.reload()">
<MkPaginationControl :paginator="paginator" canFilter>
<MkSelect v-model="type" style="margin: 0; flex: 1;">
<template #label>{{ i18n.ts.type }}</template>
<option :value="null">{{ i18n.ts.all }}</option>
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, useTemplateRef, ref, watch } from 'vue';
import { computed, ref, markRaw, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import XModLog from './modlog.ModLog.vue';
import MkSelect from '@/components/MkSelect.vue';
@ -54,37 +54,25 @@ import MkTl from '@/components/MkTl.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js';
import { usePagination } from '@/composables/use-pagination.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import MkButton from '@/components/MkButton.vue';
import MkPaginationControl from '@/components/MkPaginationControl.vue';
import { Paginator } from '@/utility/paginator.js';
const order = ref<'newest' | 'oldest'>('newest');
const date = ref<number | null>(null);
const type = ref<string | null>(null);
const moderatorId = ref('');
const q = ref<string | null>(null);
const paginator = usePagination({
ctx: {
endpoint: 'admin/show-moderation-logs',
limit: 20,
canFetchDetection: 'limit',
params: computed(() => ({
type: type.value,
userId: moderatorId.value === '' ? null : moderatorId.value,
search: q.value,
})),
},
});
const paginator = markRaw(new Paginator('admin/show-moderation-logs', {
limit: 20,
canFetchDetection: 'limit',
canSearch: true,
computedParams: computed(() => ({
type: type.value,
userId: moderatorId.value === '' ? null : moderatorId.value,
})),
}));
watch([order, date], () => {
paginator.updateCtxPartial({
order: order.value,
initialDirection: order.value === 'oldest' ? 'newer' : 'older',
initialDate: date.value,
});
}, { immediate: false });
paginator.init();
const timeline = computed(() => {
return paginator.items.value.map(x => ({
@ -95,7 +83,7 @@ const timeline = computed(() => {
});
function fetchMore() {
if (order.value === 'oldest') {
if (paginator.order.value === 'oldest') {
paginator.fetchNewer();
} else {
paginator.fetchOlder();

View file

@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<MkButton primary rounded @click="assign"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
<MkPagination :pagination="usersPagination">
<MkPagination :paginator="usersPaginator">
<template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template>
<template #default="{ items }">
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue';
import { computed, markRaw, reactive, ref } from 'vue';
import XEditor from './roles.editor.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
@ -66,6 +66,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkPagination from '@/components/MkPagination.vue';
import { useRouter } from '@/router.js';
import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
@ -73,13 +74,12 @@ const props = defineProps<{
id?: string;
}>();
const usersPagination = {
endpoint: 'admin/roles/users' as const,
const usersPaginator = markRaw(new Paginator('admin/roles/users', {
limit: 20,
params: computed(() => ({
computedParams: computed(() => ({
roleId: props.id,
})),
};
}));
const expandedItems = ref([]);

View file

@ -38,13 +38,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix>@</template>
<template #label>{{ i18n.ts.username }}</template>
</MkInput>
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'">
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="paginator.computedParams.value.origin === 'local'">
<template #prefix>@</template>
<template #label>{{ i18n.ts.host }}</template>
</MkInput>
</div>
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination">
<MkPagination v-slot="{items}" :paginator="paginator">
<div :class="$style.users">
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/admin/user/${user.id}`">
<MkUserCardMini :user="user"/>
@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, useTemplateRef, ref, watchEffect } from 'vue';
import { computed, markRaw, ref, watchEffect } from 'vue';
import { defaultMemoryStorage } from '@/memory-storage';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@ -69,6 +69,7 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { dateString } from '@/filters/date.js';
import { Paginator } from '@/utility/paginator.js';
type SearchQuery = {
sort?: string;
@ -78,7 +79,6 @@ type SearchQuery = {
hostname?: string;
};
const paginationComponent = useTemplateRef('paginationComponent');
const storedQuery = JSON.parse(defaultMemoryStorage.getItem('admin-users-query') ?? '{}') as SearchQuery;
const sort = ref(storedQuery.sort ?? '+createdAt');
@ -86,10 +86,9 @@ const state = ref(storedQuery.state ?? 'all');
const origin = ref(storedQuery.origin ?? 'local');
const searchUsername = ref(storedQuery.username ?? '');
const searchHost = ref(storedQuery.hostname ?? '');
const pagination = {
endpoint: 'admin/show-users' as const,
const paginator = markRaw(new Paginator('admin/show-users', {
limit: 10,
params: computed(() => ({
computedParams: computed(() => ({
sort: sort.value,
state: state.value,
origin: origin.value,
@ -97,7 +96,7 @@ const pagination = {
hostname: searchHost.value,
})),
offsetMode: true,
};
}));
function searchUser() {
os.selectUser({ includeSelf: true }).then(user => {
@ -121,7 +120,7 @@ async function addUser() {
username: username,
password: password,
}).then(res => {
paginationComponent.value?.paginator.reload();
paginator.reload();
});
}

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_spacer" style="--MI_SPACER-w: 800px;">
<div class="_gaps">
<MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo>
<MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps">
<MkPagination v-slot="{items}" :paginator="paginator" class="_gaps">
<section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement">
<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
<div :class="$style.header">
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed, useTemplateRef } from 'vue';
import { ref, computed, markRaw } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
@ -54,24 +54,14 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { $i } from '@/i.js';
import { updateCurrentAccountPartial } from '@/accounts.js';
import { Paginator } from '@/utility/paginator.js';
const paginationCurrent = {
endpoint: 'announcements' as const,
const paginator = markRaw(new Paginator('announcements', {
limit: 10,
params: {
isActive: true,
},
};
const paginationPast = {
endpoint: 'announcements' as const,
limit: 10,
params: {
isActive: false,
},
};
const paginationEl = useTemplateRef('paginationEl');
computedParams: computed(() => ({
isActive: tab.value === 'current',
})),
}));
const tab = ref('current');
@ -85,8 +75,7 @@ async function read(target) {
if (confirm.canceled) return;
}
if (!paginationEl.value) return;
paginationEl.value.paginator.updateItem(target.id, a => ({
paginator.updateItem(target.id, a => ({
...a,
isRead: true,
}));

View file

@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStreamingNotesTimeline :key="channelId" src="channel" :channel="channelId"/>
</div>
<div v-else-if="tab === 'featured'">
<MkNotesTimeline :pagination="featuredPagination"/>
<MkNotesTimeline :paginator="featuredPaginator"/>
</div>
<div v-else-if="tab === 'search'">
<div v-if="notesSearchAvailable" class="_gaps">
@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
<MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
</div>
<MkNotesTimeline v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
<MkNotesTimeline v-if="searchPaginator" :key="searchKey" :paginator="searchPaginator"/>
</div>
<div v-else>
<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
@ -70,7 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import { computed, watch, ref, markRaw, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js';
@ -97,6 +97,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { notesSearchAvailable } from '@/utility/check-permissions.js';
import { miLocalStorage } from '@/local-storage.js';
import { useRouter } from '@/router.js';
import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
@ -109,14 +110,13 @@ const tab = ref('overview');
const channel = ref<Misskey.entities.Channel | null>(null);
const favorited = ref(false);
const searchQuery = ref('');
const searchPagination = ref();
const searchPaginator = shallowRef();
const searchKey = ref('');
const featuredPagination = computed(() => ({
endpoint: 'notes/featured' as const,
const featuredPaginator = markRaw(new Paginator('channels/featured', {
limit: 10,
params: {
computedParams: computed(() => ({
channelId: props.channelId,
},
})),
}));
useInterval(() => {
@ -190,14 +190,13 @@ async function search() {
if (query == null) return;
searchPagination.value = {
endpoint: 'notes/search',
searchPaginator.value = markRaw(new Paginator('notes/search', {
limit: 10,
params: {
query: query,
channelId: channel.value.id,
},
};
}));
searchKey.value = query;
}

View file

@ -18,27 +18,27 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>
<MkFoldableSection v-if="channelPagination">
<MkFoldableSection v-if="channelPaginator">
<template #header>{{ i18n.ts.searchResult }}</template>
<MkChannelList :key="key" :pagination="channelPagination"/>
<MkChannelList :key="key" :paginator="channelPaginator"/>
</MkFoldableSection>
</div>
<div v-if="tab === 'featured'">
<MkPagination v-slot="{items}" :pagination="featuredPagination">
<MkPagination v-slot="{items}" :paginator="featuredPaginator">
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'favorites'">
<MkPagination v-slot="{items}" :pagination="favoritesPagination">
<MkPagination v-slot="{items}" :paginator="favoritesPaginator">
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'following'">
<MkPagination v-slot="{items}" :pagination="followingPagination">
<MkPagination v-slot="{items}" :paginator="followingPaginator">
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else-if="tab === 'owned'">
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="ownedPagination">
<MkPagination v-slot="{items}" :paginator="ownedPaginator">
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
</div>
@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { computed, markRaw, onMounted, ref, shallowRef } from 'vue';
import MkChannelPreview from '@/components/MkChannelPreview.vue';
import MkChannelList from '@/components/MkChannelList.vue';
import MkPagination from '@/components/MkPagination.vue';
@ -68,6 +68,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
@ -80,31 +81,27 @@ const key = ref('');
const tab = ref('featured');
const searchQuery = ref('');
const searchType = ref('nameAndDescription');
const channelPagination = ref();
const channelPaginator = shallowRef();
onMounted(() => {
searchQuery.value = props.query ?? '';
searchType.value = props.type ?? 'nameAndDescription';
});
const featuredPagination = {
endpoint: 'channels/featured' as const,
const featuredPaginator = markRaw(new Paginator('channels/featured', {
limit: 10,
noPaging: true,
};
const favoritesPagination = {
endpoint: 'channels/my-favorites' as const,
}));
const favoritesPaginator = markRaw(new Paginator('channels/my-favorites', {
limit: 100,
noPaging: true,
};
const followingPagination = {
endpoint: 'channels/followed' as const,
}));
const followingPaginator = markRaw(new Paginator('channels/followed', {
limit: 10,
};
const ownedPagination = {
endpoint: 'channels/owned' as const,
}));
const ownedPaginator = markRaw(new Paginator('channels/owned', {
limit: 10,
};
}));
async function search() {
const query = searchQuery.value.toString().trim();
@ -113,14 +110,13 @@ async function search() {
const type = searchType.value.toString().trim();
channelPagination.value = {
endpoint: 'channels/search',
channelPaginator.value = markRaw(new Paginator('channels/search', {
limit: 10,
params: {
query: searchQuery.value,
type: type,
},
};
}));
key.value = query + type;
}

View file

@ -23,14 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<MkNotesTimeline :pagination="pagination" :detail="true"/>
<MkNotesTimeline :paginator="paginator" :detail="true"/>
</div>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { computed, watch, provide, ref } from 'vue';
import { computed, watch, provide, ref, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js';
@ -46,6 +46,7 @@ import { isSupportShare } from '@/utility/navigator.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { genEmbedCode } from '@/utility/get-embed-code.js';
import { assertServerContext, serverContext } from '@/server-context.js';
import { Paginator } from '@/utility/paginator.js';
// context
const CTX_CLIP = !$i && assertServerContext(serverContext, 'clip') ? serverContext.clip : null;
@ -56,13 +57,13 @@ const props = defineProps<{
const clip = ref<Misskey.entities.Clip | null>(CTX_CLIP);
const favorited = ref(false);
const pagination = {
endpoint: 'clips/notes' as const,
const paginator = markRaw(new Paginator('clips/notes', {
limit: 10,
params: computed(() => ({
canSearch: true,
computedParams: computed(() => ({
clipId: props.clipId,
})),
};
}));
const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));

View file

@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton inline @click="setLicenseBulk">Set License</MkButton>
<MkButton inline danger @click="delBulk">Delete</MkButton>
</div>
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
<MkPagination ref="emojisPaginationComponent" :paginator="paginator">
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
<template #default="{items}">
<div class="ldhfsamy">
@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.host }}</template>
</MkInput>
</FormSplit>
<MkPagination :pagination="remotePagination">
<MkPagination :paginator="remotePaginator">
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
<template #default="{items}">
<div class="ldhfsamy">
@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref, useTemplateRef } from 'vue';
import { computed, defineAsyncComponent, markRaw, ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkPagination from '@/components/MkPagination.vue';
@ -84,8 +84,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
const emojisPaginationComponent = useTemplateRef('emojisPaginationComponent');
import { Paginator } from '@/utility/paginator.js';
const tab = ref('local');
const query = ref<string | null>(null);
@ -94,28 +93,26 @@ const host = ref<string | null>(null);
const selectMode = ref(false);
const selectedEmojis = ref<string[]>([]);
const pagination = {
endpoint: 'admin/emoji/list' as const,
const paginator = markRaw(new Paginator('admin/emoji/list', {
limit: 30,
params: computed(() => ({
computedParams: computed(() => ({
query: (query.value && query.value !== '') ? query.value : null,
})),
};
}));
const remotePagination = {
endpoint: 'admin/emoji/list-remote' as const,
const remotePaginator = markRaw(new Paginator('admin/emoji/list-remote', {
limit: 30,
params: computed(() => ({
computedParams: computed(() => ({
query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null,
host: (host.value && host.value !== '') ? host.value : null,
})),
};
}));
const selectAll = () => {
if (selectedEmojis.value.length > 0) {
selectedEmojis.value = [];
} else {
selectedEmojis.value = emojisPaginationComponent.value?.paginator.items.value.map(item => item.id);
selectedEmojis.value = paginator.items.value.map(item => item.id);
}
};
@ -132,7 +129,7 @@ const add = async (ev: MouseEvent) => {
}, {
done: result => {
if (result.created) {
emojisPaginationComponent.value?.paginator.prepend(result.created);
paginator.prepend(result.created);
}
},
closed: () => dispose(),
@ -145,12 +142,12 @@ const edit = async (emoji) => {
}, {
done: result => {
if (result.updated) {
emojisPaginationComponent.value?.paginator.updateItem(result.updated.id, (oldEmoji) => ({
paginator.updateItem(result.updated.id, (oldEmoji) => ({
...oldEmoji,
...result.updated,
}));
} else if (result.deleted) {
emojisPaginationComponent.value?.paginator.removeItem(emoji.id);
paginator.removeItem(emoji.id);
}
},
closed: () => dispose(),
@ -245,7 +242,7 @@ const setCategoryBulk = async () => {
ids: selectedEmojis.value,
category: result,
});
emojisPaginationComponent.value?.paginator.reload();
paginator.reload();
};
const setLicenseBulk = async () => {
@ -257,7 +254,7 @@ const setLicenseBulk = async () => {
ids: selectedEmojis.value,
license: result,
});
emojisPaginationComponent.value?.paginator.reload();
paginator.reload();
};
const addTagBulk = async () => {
@ -269,7 +266,7 @@ const addTagBulk = async () => {
ids: selectedEmojis.value,
aliases: result.split(' '),
});
emojisPaginationComponent.value?.paginator.reload();
paginator.reload();
};
const removeTagBulk = async () => {
@ -281,7 +278,7 @@ const removeTagBulk = async () => {
ids: selectedEmojis.value,
aliases: result.split(' '),
});
emojisPaginationComponent.value?.paginator.reload();
paginator.reload();
};
const setTagBulk = async () => {
@ -293,7 +290,7 @@ const setTagBulk = async () => {
ids: selectedEmojis.value,
aliases: result.split(' '),
});
emojisPaginationComponent.value?.paginator.reload();
paginator.reload();
};
const delBulk = async () => {
@ -305,7 +302,7 @@ const delBulk = async () => {
await os.apiWithDialog('admin/emoji/delete-bulk', {
ids: selectedEmojis.value,
});
emojisPaginationComponent.value?.paginator.reload();
paginator.reload();
};
const headerActions = computed(() => [{

View file

@ -6,16 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
<MkNotesTimeline ref="tlComponent" :pagination="pagination"/>
<MkNotesTimeline :paginator="paginator"/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, markRaw } from 'vue';
import { i18n } from '@/i18n.js';
import type { PagingCtx } from '@/composables/use-pagination.js';
import MkInfo from '@/components/MkInfo.vue';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
fileId: string;
@ -23,11 +23,10 @@ const props = defineProps<{
const realFileId = computed(() => props.fileId);
const pagination = ref<PagingCtx>({
endpoint: 'drive/files/attached-notes',
const paginator = markRaw(new Paginator('drive/files/attached-notes', {
limit: 10,
params: {
fileId: realFileId.value,
},
});
}));
</script>

View file

@ -9,30 +9,29 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="notes">{{ i18n.ts.notes }}</option>
<option value="polls">{{ i18n.ts.poll }}</option>
</MkTab>
<MkNotesTimeline v-if="tab === 'notes'" :pagination="paginationForNotes"/>
<MkNotesTimeline v-else-if="tab === 'polls'" :pagination="paginationForPolls"/>
<MkNotesTimeline v-if="tab === 'notes'" :paginator="paginatorForNotes"/>
<MkNotesTimeline v-else-if="tab === 'polls'" :paginator="paginatorForPolls"/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { markRaw, ref } from 'vue';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkTab from '@/components/MkTab.vue';
import { i18n } from '@/i18n.js';
import { Paginator } from '@/utility/paginator.js';
const paginationForNotes = {
endpoint: 'notes/featured' as const,
const paginatorForNotes = markRaw(new Paginator('notes/featured', {
limit: 10,
};
}));
const paginationForPolls = {
endpoint: 'notes/polls/recommendation' as const,
const paginatorForPolls = markRaw(new Paginator('notes/polls/recommendation', {
limit: 10,
offsetMode: true,
params: {
excludeChannels: true,
},
};
}));
const tab = ref('notes');
</script>

View file

@ -13,19 +13,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="tag == null">
<MkFoldableSection class="_margin" persistKey="explore-pinned-users">
<template #header><i class="ti ti-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
<MkUserList :pagination="pinnedUsers"/>
<MkUserList :paginator="pinnedUsersPaginator"/>
</MkFoldableSection>
<MkFoldableSection class="_margin" persistKey="explore-popular-users">
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
<MkUserList :pagination="popularUsers"/>
<MkUserList :paginator="popularUsersPaginator"/>
</MkFoldableSection>
<MkFoldableSection class="_margin" persistKey="explore-recently-updated-users">
<template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
<MkUserList :pagination="recentlyUpdatedUsers"/>
<MkUserList :paginator="recentlyUpdatedUsersPaginator"/>
</MkFoldableSection>
<MkFoldableSection class="_margin" persistKey="explore-recently-registered-users">
<template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template>
<MkUserList :pagination="recentlyRegisteredUsers"/>
<MkUserList :paginator="recentlyRegisteredUsersPaginator"/>
</MkFoldableSection>
</template>
</div>
@ -41,21 +41,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFoldableSection v-if="tag != null" :key="`${tag}`" class="_margin">
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
<MkUserList :pagination="tagUsers"/>
<MkUserList :paginator="tagUsersPaginator"/>
</MkFoldableSection>
<template v-if="tag == null">
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
<MkUserList :pagination="popularUsersF"/>
<MkUserList :paginator="popularUsersFPaginator"/>
</MkFoldableSection>
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
<MkUserList :pagination="recentlyUpdatedUsersF"/>
<MkUserList :paginator="recentlyUpdatedUsersFPaginator"/>
</MkFoldableSection>
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
<MkUserList :pagination="recentlyRegisteredUsersF"/>
<MkUserList :paginator="recentlyRegisteredUsersFPaginator"/>
</MkFoldableSection>
</template>
</div>
@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { watch, ref, useTemplateRef, computed } from 'vue';
import { watch, ref, useTemplateRef, computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkUserList from '@/components/MkUserList.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
@ -71,6 +71,7 @@ import MkTab from '@/components/MkTab.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
tag?: string;
@ -85,8 +86,7 @@ watch(() => props.tag, () => {
if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null);
});
const tagUsers = computed(() => ({
endpoint: 'hashtags/users' as const,
const tagUsersPaginator = markRaw(new Paginator('hashtags/users', {
limit: 30,
params: {
tag: props.tag,
@ -95,34 +95,66 @@ const tagUsers = computed(() => ({
},
}));
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
state: 'alive',
origin: 'local',
sort: '+follower',
} };
const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
origin: 'local',
sort: '+updatedAt',
} };
const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
origin: 'local',
state: 'alive',
sort: '+createdAt',
} };
const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
state: 'alive',
origin: 'remote',
sort: '+follower',
} };
const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
origin: 'combined',
sort: '+updatedAt',
} };
const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
origin: 'combined',
sort: '+createdAt',
} };
const pinnedUsersPaginator = markRaw(new Paginator('pinned-users', {
noPaging: true,
}));
const popularUsersPaginator = markRaw(new Paginator('users', {
limit: 10,
noPaging: true,
params: {
state: 'alive',
origin: 'local',
sort: '+follower',
},
}));
const recentlyUpdatedUsersPaginator = markRaw(new Paginator('users', {
limit: 10,
noPaging: true,
params: {
origin: 'local',
sort: '+updatedAt',
},
}));
const recentlyRegisteredUsersPaginator = markRaw(new Paginator('users', {
limit: 10,
noPaging: true,
params: {
origin: 'local',
state: 'alive',
sort: '+createdAt',
},
}));
const popularUsersFPaginator = markRaw(new Paginator('users', {
limit: 10,
noPaging: true,
params: {
state: 'alive',
origin: 'remote',
sort: '+follower',
},
}));
const recentlyUpdatedUsersFPaginator = markRaw(new Paginator('users', {
limit: 10,
noPaging: true,
params: {
origin: 'combined',
sort: '+updatedAt',
},
}));
const recentlyRegisteredUsersFPaginator = markRaw(new Paginator('users', {
limit: 10,
noPaging: true,
params: {
origin: 'combined',
sort: '+createdAt',
},
}));
misskeyApi('hashtags/list', {
sort: '+attachedLocalUsers',

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader>
<div class="_spacer" style="--MI_SPACER-w: 800px;">
<MkPagination :pagination="pagination">
<MkPagination :paginator="paginator">
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
<template #default="{ items }">
@ -20,16 +20,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { markRaw } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkNote from '@/components/MkNote.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { Paginator } from '@/utility/paginator.js';
const pagination = {
endpoint: 'i/favorites' as const,
const paginator = markRaw(new Paginator('i/favorites', {
limit: 10,
};
}));
definePage(() => ({
title: i18n.ts.favorites,

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
<div class="_spacer" style="--MI_SPACER-w: 700px;">
<div v-if="tab === 'featured'">
<MkPagination v-slot="{items}" :pagination="featuredFlashsPagination">
<MkPagination v-slot="{items}" :paginator="featuredFlashsPaginator">
<div class="_gaps_s">
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
</div>
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="tab === 'my'">
<div class="_gaps">
<MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="myFlashsPagination">
<MkPagination v-slot="{items}" :paginator="myFlashsPaginator">
<div class="_gaps_s">
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
</div>
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else-if="tab === 'liked'">
<MkPagination v-slot="{items}" :pagination="likedFlashsPagination">
<MkPagination v-slot="{items}" :paginator="likedFlashsPaginator">
<div class="_gaps_s">
<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/>
</div>
@ -37,31 +37,29 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, markRaw, ref } from 'vue';
import MkFlashPreview from '@/components/MkFlashPreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { useRouter } from '@/router.js';
import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
const tab = ref('featured');
const featuredFlashsPagination = {
endpoint: 'flash/featured' as const,
const featuredFlashsPaginator = markRaw(new Paginator('flash/featured', {
limit: 5,
offsetMode: true,
};
const myFlashsPagination = {
endpoint: 'flash/my' as const,
}));
const myFlashsPaginator = markRaw(new Paginator('flash/my', {
limit: 5,
};
const likedFlashsPagination = {
endpoint: 'flash/my-likes' as const,
}));
const likedFlashsPaginator = markRaw(new Paginator('flash/my-likes', {
limit: 5,
};
}));
function create() {
router.push('/play/new');

View file

@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
<div class="_spacer" style="--MI_SPACER-w: 800px;">
<MkPagination ref="paginationComponent" :pagination="pagination">
<div :key="tab" class="_spacer" style="--MI_SPACER-w: 800px;">
<MkPagination :paginator="paginator">
<template #empty><MkResult type="empty" :text="i18n.ts.noFollowRequests"/></template>
<template #default="{items}">
<div class="mk-follow-requests _gaps">
@ -35,8 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { useTemplateRef, computed, ref } from 'vue';
import type { PagingCtx } from '@/composables/use-pagination.js';
import { computed, markRaw, ref, watch } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import { userPage, acct } from '@/filters/user.js';
@ -44,32 +43,35 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { $i } from '@/i.js';
import { Paginator } from '@/utility/paginator.js';
const paginationComponent = useTemplateRef('paginationComponent');
const tab = ref($i?.isLocked ? 'list' : 'sent');
const pagination = computed<PagingCtx>(() => tab.value === 'list' ? {
endpoint: 'following/requests/list',
limit: 10,
} : {
endpoint: 'following/requests/sent',
limit: 10,
});
let paginator: Paginator<'following/requests/list' | 'following/requests/sent'>;
watch(tab, (newTab) => {
if (newTab === 'list') {
paginator = markRaw(new Paginator('following/requests/list', { limit: 10 }));
} else {
paginator = markRaw(new Paginator('following/requests/sent', { limit: 10 }));
}
}, { immediate: true });
function accept(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => {
paginationComponent.value?.paginator.reload();
paginator.reload();
});
}
function reject(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
paginationComponent.value?.paginator.reload();
paginator.reload();
});
}
function cancel(user: Misskey.entities.UserLite) {
os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
paginationComponent.value?.paginator.reload();
paginator.reload();
});
}
@ -91,8 +93,6 @@ const headerTabs = computed(() => [
},
]);
const tab = ref($i?.isLocked ? 'list' : 'sent');
definePage(() => ({
title: i18n.ts.followRequests,
icon: 'ti ti-user-plus',

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="tab === 'explore'">
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template>
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true">
<MkPagination v-slot="{items}" :paginator="recentPostsPaginator">
<div :class="$style.items">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFoldableSection>
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template>
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disableAutoLoad="true">
<MkPagination v-slot="{items}" :paginator="popularPostsPaginator">
<div :class="$style.items">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFoldableSection>
</div>
<div v-else-if="tab === 'liked'">
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
<MkPagination v-slot="{items}" :paginator="likedPostsPaginator">
<div :class="$style.items">
<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
</div>
@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else-if="tab === 'my'">
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA>
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
<MkPagination v-slot="{items}" :paginator="myPostsPaginator">
<div :class="$style.items">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
@ -44,13 +44,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { watch, ref, computed } from 'vue';
import { watch, ref, computed, markRaw } from 'vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
@ -59,34 +60,19 @@ const props = defineProps<{
}>();
const tab = ref('explore');
const tags = ref([]);
const tagsRef = ref();
const recentPostsPagination = {
endpoint: 'gallery/posts' as const,
const recentPostsPaginator = markRaw(new Paginator('gallery/posts', {
limit: 6,
};
const popularPostsPagination = {
endpoint: 'gallery/featured' as const,
}));
const popularPostsPaginator = markRaw(new Paginator('gallery/featured', {
noPaging: true,
};
const myPostsPagination = {
endpoint: 'i/gallery/posts' as const,
}));
const myPostsPaginator = markRaw(new Paginator('i/gallery/posts', {
limit: 5,
};
const likedPostsPagination = {
endpoint: 'i/gallery/likes' as const,
}));
const likedPostsPaginator = markRaw(new Paginator('i/gallery/likes', {
limit: 5,
};
const tagUsersPagination = computed(() => ({
endpoint: 'hashtags/users' as const,
limit: 30,
params: {
tag: props.tag,
origin: 'combined',
sort: '+follower',
},
}));
watch(() => props.tag, () => {

View file

@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkContainer :max-height="300" :foldable="true" class="other">
<template #icon><i class="ti ti-clock"></i></template>
<template #header>{{ i18n.ts.recentPosts }}</template>
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
<MkPagination v-slot="{items}" :paginator="otherPostsPaginator">
<div class="sdrarzaf">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, watch, ref, defineAsyncComponent } from 'vue';
import { computed, watch, ref, defineAsyncComponent, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js';
@ -80,6 +80,7 @@ import { $i } from '@/i.js';
import { isSupportShare } from '@/utility/navigator.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { useRouter } from '@/router.js';
import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
@ -89,13 +90,12 @@ const props = defineProps<{
const post = ref<Misskey.entities.GalleryPost | null>(null);
const error = ref<any>(null);
const otherPostsPagination = {
endpoint: 'users/gallery/posts' as const,
const otherPostsPaginator = markRaw(new Paginator('users/gallery/posts', {
limit: 6,
params: computed(() => ({
computedParams: computed(() => ({
userId: post.value.user.id,
})),
};
}));
function fetchPost() {
post.value = null;

View file

@ -115,7 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div v-else-if="tab === 'users'" class="_gaps_m">
<MkPagination v-slot="{ items }" :pagination="usersPagination">
<MkPagination v-slot="{ items }" :paginator="usersPaginator">
<div :class="$style.users">
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${user.updatedAt ? dateString(user.updatedAt) : 'unknown'}`" :to="`/admin/user/${user.id}`">
<MkUserCardMini :user="user"/>
@ -132,10 +132,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import type { ChartSrc } from '@/components/MkChart.vue';
import type { PagingCtx } from '@/composables/use-pagination.js';
import MkChart from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue';
import FormLink from '@/components/form/link.vue';
@ -156,6 +155,7 @@ import MkPagination from '@/components/MkPagination.vue';
import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
import { dateString } from '@/filters/date.js';
import MkTextarea from '@/components/MkTextarea.vue';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
host: string;
@ -173,8 +173,7 @@ const isMediaSilenced = ref(false);
const faviconUrl = ref<string | null>(null);
const moderationNote = ref('');
const usersPagination = {
endpoint: iAmModerator ? 'admin/show-users' : 'users',
const usersPaginator = iAmModerator ? markRaw(new Paginator('admin/show-users', {
limit: 10,
params: {
sort: '+updatedAt',
@ -182,7 +181,15 @@ const usersPagination = {
hostname: props.host,
},
offsetMode: true,
} satisfies PagingCtx<'admin/show-users' | 'users'>;
})) : markRaw(new Paginator('users', {
limit: 10,
params: {
sort: '+updatedAt',
state: 'all',
hostname: props.host,
},
offsetMode: true,
}));
if (iAmModerator) {
watch(moderationNote, async () => {

View file

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton>
<div v-if="currentInviteLimit !== null">{{ i18n.tsx.createLimitRemaining({ limit: currentInviteLimit }) }}</div>
<MkPagination ref="pagingComponent" :pagination="pagination">
<MkPagination :paginator="paginator">
<template #default="{ items }">
<div class="_gaps_s">
<MkInviteCode v-for="item in (items as Misskey.entities.InviteCode[])" :key="item.id" :invite="item" :onDeleted="deleted"/>
@ -27,9 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref, useTemplateRef } from 'vue';
import { computed, markRaw, ref } from 'vue';
import * as Misskey from 'misskey-js';
import type { PagingCtx } from '@/composables/use-pagination.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -39,16 +38,15 @@ import MkInviteCode from '@/components/MkInviteCode.vue';
import { definePage } from '@/page.js';
import { instance } from '@/instance.js';
import { $i } from '@/i.js';
import { Paginator } from '@/utility/paginator.js';
const pagingComponent = useTemplateRef('pagingComponent');
const currentInviteLimit = ref<null | number>(null);
const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number;
const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number;
const pagination: PagingCtx = {
endpoint: 'invite/list' as const,
const paginator = markRaw(new Paginator('invite/list', {
limit: 10,
};
}));
const resetCycle = computed<null | string>(() => {
if (!inviteLimitCycle) return null;
@ -68,14 +66,12 @@ async function create() {
text: ticket.code,
});
pagingComponent.value?.paginator.prepend(ticket);
paginator.prepend(ticket);
update();
}
function deleted(id: string) {
if (pagingComponent.value) {
pagingComponent.value.paginator.removeItem(id);
}
paginator.removeItem(id);
update();
}

View file

@ -12,44 +12,38 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="tab === 'my'" class="_gaps">
<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps" withControl>
<MkPagination v-slot="{ items }" :paginator="paginator" class="_gaps" withControl>
<MkClipPreview v-for="item in items" :key="item.id" :clip="item" :noUserInfo="true"/>
</MkPagination>
</div>
<div v-else-if="tab === 'favorites'" class="_gaps">
<MkClipPreview v-for="item in favorites" :key="item.id" :clip="item"/>
<div v-else-if="tab === 'favorites'">
<MkPagination v-slot="{ items }" :paginator="favoritesPaginator" class="_gaps" withControl>
<MkClipPreview v-for="item in items" :key="item.id" :clip="item" :noUserInfo="true"/>
</MkPagination>
</div>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { watch, ref, useTemplateRef, computed } from 'vue';
import { watch, ref, computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import MkClipPreview from '@/components/MkClipPreview.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { clipsCache } from '@/cache.js';
const pagination = {
endpoint: 'clips/list' as const,
noPaging: true,
limit: 10,
};
import { Paginator } from '@/utility/paginator.js';
const tab = ref('my');
const favorites = ref<Misskey.entities.Clip[] | null>(null);
const paginator = markRaw(new Paginator('clips/list', {
}));
const pagingComponent = useTemplateRef('pagingComponent');
watch(tab, async () => {
favorites.value = await misskeyApi('clips/my-favorites');
});
const favoritesPaginator = markRaw(new Paginator('clips/my-favorites', {
}));
async function create() {
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
@ -76,15 +70,15 @@ async function create() {
clipsCache.delete();
pagingComponent.value?.paginator.reload();
paginator.reload();
}
function onClipCreated() {
pagingComponent.value?.paginator.reload();
paginator.reload();
}
function onClipDeleted() {
pagingComponent.value?.paginator.reload();
paginator.reload();
}
const headerActions = computed(() => []);

View file

@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
<MkPagination ref="paginationEl" :pagination="membershipsPagination" withControl>
<MkPagination :paginator="membershipsPaginator" withControl>
<template #default="{ items }">
<div class="_gaps_s">
<div v-for="item in items" :key="item.id">
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref, useTemplateRef, watch } from 'vue';
import { computed, markRaw, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@ -69,6 +69,7 @@ import { ensureSignin } from '@/i.js';
import MkPagination from '@/components/MkPagination.vue';
import { mainRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { Paginator } from '@/utility/paginator.js';
const $i = ensureSignin();
@ -80,17 +81,15 @@ const props = defineProps<{
listId: string;
}>();
const paginationEl = useTemplateRef('paginationEl');
const list = ref<Misskey.entities.UserList | null>(null);
const isPublic = ref(false);
const name = ref('');
const membershipsPagination = {
endpoint: 'users/lists/get-memberships' as const,
const membershipsPaginator = markRaw(new Paginator('users/lists/get-memberships', {
limit: 30,
params: computed(() => ({
computedParams: computed(() => ({
listId: props.listId,
})),
};
}));
function fetchList() {
misskeyApi('users/lists/show', {
@ -109,7 +108,7 @@ function addUser() {
listId: list.value.id,
userId: user.id,
}).then(() => {
paginationEl.value?.paginator.reload();
membershipsPaginator.reload();
});
});
}
@ -125,7 +124,7 @@ async function removeUser(item, ev) {
listId: list.value.id,
userId: item.userId,
}).then(() => {
paginationEl.value?.paginator.removeItem(item.id);
membershipsPaginator.removeItem(item.id);
});
},
}], ev.currentTarget ?? ev.target);
@ -147,7 +146,7 @@ async function showMembershipMenu(item, ev) {
userId: item.userId,
withReplies,
}).then(() => {
paginationEl.value!.paginator.updateItem(item.id, (old) => ({
membershipsPaginator.updateItem(item.id, (old) => ({
...old,
withReplies,
}));

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
<div v-if="note">
<div v-if="showNext" class="_margin">
<MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/>
<MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :paginator="showNext === 'channel' ? nextChannelPaginator : nextUserPaginator" :noGap="true"/>
</div>
<div class="_margin">
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-if="showPrev" class="_margin">
<MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/>
<MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :paginator="showPrev === 'channel' ? prevChannelPaginator : prevUserPaginator" :noGap="true"/>
</div>
</div>
<MkError v-else-if="error" @retry="fetchNote()"/>
@ -45,10 +45,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import { computed, watch, ref, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import type { PagingCtx } from '@/composables/use-pagination.js';
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
@ -63,6 +62,7 @@ import { pleaseLogin } from '@/utility/please-login.js';
import { getAppearNote } from '@/utility/get-appear-note.js';
import { serverContext, assertServerContext } from '@/server-context.js';
import { $i } from '@/i.js';
import { Paginator } from '@/utility/paginator.js';
// context
const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null;
@ -78,45 +78,41 @@ const showPrev = ref<'user' | 'channel' | false>(false);
const showNext = ref<'user' | 'channel' | false>(false);
const error = ref();
const prevUserPagination: PagingCtx = {
endpoint: 'users/notes',
const prevUserPaginator = markRaw(new Paginator('users/notes', {
limit: 10,
initialId: props.noteId,
initialDirection: 'older',
params: computed(() => note.value ? ({
computedParams: computed(() => note.value ? ({
userId: note.value.userId,
}) : undefined),
};
}));
const nextUserPagination: PagingCtx = {
endpoint: 'users/notes',
const nextUserPaginator = markRaw(new Paginator('users/notes', {
limit: 10,
initialId: props.noteId,
initialDirection: 'newer',
params: computed(() => note.value ? ({
computedParams: computed(() => note.value ? ({
userId: note.value.userId,
}) : undefined),
};
}));
const prevChannelPagination: PagingCtx = {
endpoint: 'channels/timeline',
const prevChannelPaginator = markRaw(new Paginator('channels/timeline', {
limit: 10,
initialId: props.noteId,
initialDirection: 'older',
params: computed(() => note.value ? ({
computedParams: computed(() => note.value ? ({
channelId: note.value.channelId,
}) : undefined),
};
}));
const nextChannelPagination: PagingCtx = {
endpoint: 'channels/timeline',
const nextChannelPaginator = markRaw(new Paginator('channels/timeline', {
limit: 10,
initialId: props.noteId,
initialDirection: 'newer',
params: computed(() => note.value ? ({
computedParams: computed(() => note.value ? ({
channelId: note.value.channelId,
}) : undefined),
};
}));
function fetchNote() {
showPrev.value = false;

View file

@ -10,40 +10,39 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStreamingNotificationsTimeline :class="$style.notifications" :excludeTypes="excludeTypes"/>
</div>
<div v-else-if="tab === 'mentions'">
<MkNotesTimeline :pagination="mentionsPagination"/>
<MkNotesTimeline :paginator="mentionsPaginator"/>
</div>
<div v-else-if="tab === 'directNotes'">
<MkNotesTimeline :pagination="directNotesPagination"/>
<MkNotesTimeline :paginator="directNotesPaginator"/>
</div>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, markRaw, ref } from 'vue';
import { notificationTypes } from '@@/js/const.js';
import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { Paginator } from '@/utility/paginator.js';
const tab = ref('all');
const includeTypes = ref<string[] | null>(null);
const excludeTypes = computed(() => includeTypes.value ? notificationTypes.filter(t => !includeTypes.value.includes(t)) : null);
const mentionsPagination = {
endpoint: 'notes/mentions' as const,
const mentionsPaginator = markRaw(new Paginator('notes/mentions', {
limit: 10,
};
}));
const directNotesPagination = {
endpoint: 'notes/mentions' as const,
const directNotesPaginator = markRaw(new Paginator('notes/mentions', {
limit: 10,
params: {
visibility: 'specified',
},
};
}));
function setFilter(ev) {
const typeItems = notificationTypes.map(t => ({

View file

@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkContainer :max-height="300" :foldable="true" class="other">
<template #icon><i class="ti ti-clock"></i></template>
<template #header>{{ i18n.ts.recentPosts }}</template>
<MkPagination v-slot="{items}" :pagination="otherPostsPagination" :class="$style.relatedPagesRoot" class="_gaps">
<MkPagination v-slot="{items}" :paginator="otherPostsPaginator" :class="$style.relatedPagesRoot" class="_gaps">
<MkPagePreview v-for="page in items" :key="page.id" :page="page" :class="$style.relatedPagesItem"/>
</MkPagination>
</MkContainer>
@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, watch, ref, defineAsyncComponent } from 'vue';
import { computed, watch, ref, defineAsyncComponent, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js';
@ -122,6 +122,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { useRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
@ -132,13 +133,12 @@ const props = defineProps<{
const page = ref<Misskey.entities.Page | null>(null);
const error = ref<any>(null);
const otherPostsPagination = {
endpoint: 'users/pages' as const,
const otherPostsPaginator = markRaw(new Paginator('users/pages', {
limit: 6,
params: computed(() => ({
computedParams: computed(() => ({
userId: page.value.user.id,
})),
};
}));
const path = computed(() => props.username + '/' + props.pageName);
function fetchPage() {

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
<div class="_spacer" style="--MI_SPACER-w: 700px;">
<div v-if="tab === 'featured'">
<MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
<MkPagination v-slot="{items}" :paginator="featuredPagesPaginator">
<div class="_gaps">
<MkPagePreview v-for="page in items" :key="page.id" :page="page"/>
</div>
@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="tab === 'my'" class="_gaps">
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="myPagesPagination">
<MkPagination v-slot="{items}" :paginator="myPagesPaginator">
<div class="_gaps">
<MkPagePreview v-for="page in items" :key="page.id" :page="page"/>
</div>
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else-if="tab === 'liked'">
<MkPagination v-slot="{items}" :pagination="likedPagesPagination">
<MkPagination v-slot="{items}" :paginator="likedPagesPaginator">
<div class="_gaps">
<MkPagePreview v-for="like in items" :key="like.page.id" :page="like.page"/>
</div>
@ -35,30 +35,28 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, markRaw, ref } from 'vue';
import MkPagePreview from '@/components/MkPagePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { useRouter } from '@/router.js';
import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
const tab = ref('featured');
const featuredPagesPagination = {
endpoint: 'pages/featured' as const,
const featuredPagesPaginator = markRaw(new Paginator('pages/featured', {
noPaging: true,
};
const myPagesPagination = {
endpoint: 'i/pages' as const,
}));
const myPagesPaginator = markRaw(new Paginator('i/pages', {
limit: 5,
};
const likedPagesPagination = {
endpoint: 'i/page-likes' as const,
}));
const likedPagesPaginator = markRaw(new Paginator('i/page-likes', {
limit: 5,
};
}));
function create() {
router.push('/pages/new');

View file

@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-if="$i" :defaultOpen="true">
<template #label>{{ i18n.ts._reversi.myGames }}</template>
<MkPagination :pagination="myGamesPagination" :disableAutoLoad="true">
<MkPagination :paginator="myGamesPaginator">
<template #default="{ items }">
<div :class="$style.gamePreviews">
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts._reversi.allGames }}</template>
<MkPagination :pagination="gamesPagination" :disableAutoLoad="true">
<MkPagination :paginator="gamesPaginator">
<template #default="{ items }">
<div :class="$style.gamePreviews">
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onDeactivated, onMounted, onUnmounted, ref } from 'vue';
import { markRaw, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -120,19 +120,18 @@ import { useRouter } from '@/router.js';
import * as os from '@/os.js';
import { pleaseLogin } from '@/utility/please-login.js';
import * as sound from '@/utility/sound.js';
import { Paginator } from '@/utility/paginator.js';
const myGamesPagination = {
endpoint: 'reversi/games' as const,
const myGamesPaginator = markRaw(new Paginator('reversi/games', {
limit: 10,
params: {
my: true,
},
};
}));
const gamesPagination = {
endpoint: 'reversi/games' as const,
const gamesPaginator = markRaw(new Paginator('reversi/games', {
limit: 10,
};
}));
const router = useRouter();

View file

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="tab === 'users'" class="_spacer" style="--MI_SPACER-w: 1200px;">
<div class="_gaps_s">
<div v-if="role">{{ role.description }}</div>
<MkUserList v-if="visible" :pagination="users" :extractor="(item) => item.user"/>
<MkUserList v-if="visible" :paginator="usersPaginator" :extractor="(item) => item.user"/>
<MkResult v-else-if="!visible" type="empty" :text="i18n.ts.nothing"/>
</div>
</div>
@ -23,13 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import { computed, watch, ref, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/utility/misskey-api.js';
import MkUserList from '@/components/MkUserList.vue';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue';
import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{
roleId: string;
@ -60,12 +61,11 @@ watch(() => props.roleId, () => {
});
}, { immediate: true });
const users = computed(() => ({
endpoint: 'roles/users' as const,
const usersPaginator = markRaw(new Paginator('roles/users', {
limit: 30,
params: {
computedParams: computed(() => ({
roleId: props.roleId,
},
})),
}));
const headerTabs = computed(() => [{

View file

@ -103,19 +103,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<MkFoldableSection v-if="notePagination">
<MkFoldableSection v-if="paginator">
<template #header>{{ i18n.ts.searchResult }}</template>
<MkNotesTimeline :key="`searchNotes:${key}`" :pagination="notePagination"/>
<MkNotesTimeline :key="`searchNotes:${key}`" :paginator="paginator"/>
</MkFoldableSection>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, shallowRef, toRef } from 'vue';
import type * as Misskey from 'misskey-js';
import type { PagingCtx } from '@/composables/use-pagination.js';
import { $i } from '@/i.js';
import { computed, markRaw, ref, shallowRef, toRef } from 'vue';
import { host as localHost } from '@@/js/config.js';
import type * as Misskey from 'misskey-js';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import * as os from '@/os.js';
@ -128,6 +127,7 @@ import MkInput from '@/components/MkInput.vue';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{
query?: string;
@ -144,7 +144,7 @@ const props = withDefaults(defineProps<{
const router = useRouter();
const key = ref(0);
const notePagination = ref<PagingCtx<'notes/search'>>();
const paginator = shallowRef<Paginator<'notes/search'> | null>(null);
const searchQuery = ref(toRef(props, 'query').value);
const hostInput = ref(toRef(props, 'host').value);
@ -299,13 +299,12 @@ async function search() {
}
}
notePagination.value = {
endpoint: 'notes/search',
paginator.value = markRaw(new Paginator('notes/search', {
limit: 10,
params: {
...searchParams.value,
},
};
}));
key.value++;
}

View file

@ -17,17 +17,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
</div>
<MkFoldableSection v-if="userPagination">
<MkFoldableSection v-if="paginator">
<template #header>{{ i18n.ts.searchResult }}</template>
<MkUserList :key="`searchUsers:${key}`" :pagination="userPagination"/>
<MkUserList :key="`searchUsers:${key}`" :paginator="paginator"/>
</MkFoldableSection>
</div>
</template>
<script lang="ts" setup>
import { ref, toRef } from 'vue';
import { markRaw, ref, shallowRef, toRef } from 'vue';
import type { Endpoints } from 'misskey-js';
import type { PagingCtx } from '@/composables/use-pagination.js';
import MkUserList from '@/components/MkUserList.vue';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
@ -38,6 +37,7 @@ import * as os from '@/os.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { useRouter } from '@/router.js';
import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{
query?: string,
@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{
const router = useRouter();
const key = ref(0);
const userPagination = ref<PagingCtx<'users/search'>>();
const paginator = shallowRef<Paginator<'users/search'> | null>(null);
const searchQuery = ref(toRef(props, 'query').value);
const searchOrigin = ref(toRef(props, 'origin').value);
@ -112,15 +112,14 @@ async function search() {
}
}
userPagination.value = {
endpoint: 'users/search',
paginator.value = markRaw(new Paginator('users/search', {
limit: 10,
offsetMode: true,
params: {
query: query,
origin: instance.federation === 'none' ? 'local' : searchOrigin.value,
},
};
}));
key.value++;
}

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<FormPagination ref="list" :pagination="pagination">
<MkPagination :paginator="paginator">
<template #empty><MkResult type="empty"/></template>
<template #default="{items}">
<div class="_gaps">
@ -44,35 +44,33 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
</div>
</template>
</FormPagination>
</MkPagination>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, useTemplateRef } from 'vue';
import { ref, computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import FormPagination from '@/components/MkPagination.vue';
import MkPagination from '@/components/MkPagination.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import { Paginator } from '@/utility/paginator.js';
const list = useTemplateRef('list');
const pagination = {
endpoint: 'i/apps' as const,
const paginator = markRaw(new Paginator('i/apps', {
limit: 100,
noPaging: true,
params: {
sort: '+lastUsedAt',
},
};
}));
function revoke(token) {
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
list.value?.paginator.reload();
paginator.reload();
});
}

View file

@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.manage }}</template>
<MkPagination :pagination="pagination" withControl>
<MkPagination :paginator="paginator" withControl>
<template #default="{items}">
<div class="_gaps">
<FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`">
@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref, defineAsyncComponent } from 'vue';
import { computed, ref, defineAsyncComponent, markRaw } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
@ -72,14 +72,14 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import { Paginator } from '@/utility/paginator.js';
const isDesktop = ref(window.innerWidth >= 1100);
const pagination = {
endpoint: 'i/webhooks/list' as const,
const paginator = markRaw(new Paginator('i/webhooks/list', {
limit: 100,
noPaging: true,
};
}));
async function generateToken() {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkTokenGenerateWindow.vue').then(x => x.default), {}, {

View file

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option v-for="x in sortOptions" :key="x.value" :value="x.value">{{ x.displayName }}</option>
</MkSelect>
<div v-if="!fetching">
<MkPagination v-slot="{items}" :pagination="pagination">
<MkPagination v-slot="{items}" :paginator="paginator">
<div class="_gaps">
<div
v-for="file in items" :key="file.id"
@ -48,9 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import type { StyleValue } from 'vue';
import { computed, markRaw, ref, watch } from 'vue';
import tinycolor from 'tinycolor2';
import type { StyleValue } from 'vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import MkPagination from '@/components/MkPagination.vue';
@ -60,13 +60,13 @@ import bytes from '@/filters/bytes.js';
import { definePage } from '@/page.js';
import MkSelect from '@/components/MkSelect.vue';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
import { Paginator } from '@/utility/paginator.js';
const sortMode = ref('+size');
const pagination = {
endpoint: 'drive/files' as const,
const paginator = markRaw(new Paginator('drive/files', {
limit: 10,
params: computed(() => ({ sort: sortMode.value })),
};
computedParams: computed(() => ({ sort: sortMode.value })),
}));
const sortOptions = [
{ value: 'sizeDesc', displayName: i18n.ts._drivecleaner.orderBySizeDesc },

View file

@ -80,7 +80,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-repeat-off"></i></template>
<template #label><SearchLabel>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</SearchLabel></template>
<MkPagination :pagination="renoteMutingPagination" withControl>
<MkPagination :paginator="renoteMutingPaginator" withControl>
<template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template>
<template #default="{ items }">
@ -111,7 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.mutedUsers }}</template>
<MkPagination :pagination="mutingPagination" withControl>
<MkPagination :paginator="mutingPaginator" withControl>
<template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template>
<template #default="{ items }">
@ -144,7 +144,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-ban"></i></template>
<template #label>{{ i18n.ts.blockedUsers }}</template>
<MkPagination :pagination="blockingPagination" withControl>
<MkPagination :paginator="blockingPaginator" withControl>
<template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template>
<template #default="{ items }">
@ -174,7 +174,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, markRaw } from 'vue';
import XEmojiMute from './mute-block.emoji-mute.vue';
import XInstanceMute from './mute-block.instance-mute.vue';
import XWordMute from './mute-block.word-mute.vue';
@ -192,23 +192,21 @@ import MkSwitch from '@/components/MkSwitch.vue';
import { reloadAsk } from '@/utility/reload-ask.js';
import { prefer } from '@/preferences.js';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import { Paginator } from '@/utility/paginator.js';
const $i = ensureSignin();
const renoteMutingPagination = {
endpoint: 'renote-mute/list' as const,
const renoteMutingPaginator = markRaw(new Paginator('renote-mute/list', {
limit: 10,
};
}));
const mutingPagination = {
endpoint: 'mute/list' as const,
const mutingPaginator = markRaw(new Paginator('mute/list', {
limit: 10,
};
}));
const blockingPagination = {
endpoint: 'blocking/list' as const,
const blockingPaginator = markRaw(new Paginator('blocking/list', {
limit: 10,
};
}));
const expandedRenoteMuteItems = ref([]);
const expandedMuteItems = ref([]);

View file

@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<template #label>{{ i18n.ts.signinHistory }}</template>
<MkPagination :pagination="pagination" disableAutoLoad withControl>
<MkPagination :paginator="paginator" withControl>
<template #default="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, markRaw } from 'vue';
import X2fa from './2fa.vue';
import FormSection from '@/components/form/section.vue';
import FormSlot from '@/components/form/slot.vue';
@ -64,11 +64,11 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import { Paginator } from '@/utility/paginator.js';
const pagination = {
endpoint: 'i/signin-history' as const,
const paginator = markRaw(new Paginator('i/signin-history', {
limit: 5,
};
}));
async function change() {
const { canceled: canceled2, result: newPassword } = await os.inputText({

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px;">
<MkNotesTimeline ref="tlComponent" class="" :pagination="pagination"/>
<MkNotesTimeline :paginator="paginator"/>
</div>
<template v-if="$i" #footer>
<div :class="$style.footer">
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref, useTemplateRef } from 'vue';
import { computed, markRaw, ref } from 'vue';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkButton from '@/components/MkButton.vue';
import { definePage } from '@/page.js';
@ -28,20 +28,18 @@ import { $i } from '@/i.js';
import { store } from '@/store.js';
import * as os from '@/os.js';
import { genEmbedCode } from '@/utility/get-embed-code.js';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
tag: string;
}>();
const pagination = {
endpoint: 'notes/search-by-tag' as const,
const paginator = markRaw(new Paginator('notes/search-by-tag', {
limit: 10,
params: computed(() => ({
computedParams: computed(() => ({
tag: props.tag,
})),
};
const tlComponent = useTemplateRef('tlComponent');
}));
async function post() {
store.set('postFormHashtags', props.tag);
@ -49,7 +47,7 @@ async function post() {
await os.post();
store.set('postFormHashtags', '');
store.set('postFormWithHashtags', false);
tlComponent.value?.reload();
paginator.reload();
}
const headerActions = computed(() => [{

View file

@ -7,29 +7,29 @@ SPDX-License-Identifier: AGPL-3.0-only
<PageWithHeader>
<div class="_spacer" style="--MI_SPACER-w: 1200px;">
<div class="_gaps_s">
<MkUserList :pagination="tagUsers"/>
<MkUserList :paginator="paginator"/>
</div>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, markRaw } from 'vue';
import MkUserList from '@/components/MkUserList.vue';
import { definePage } from '@/page.js';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
tag: string;
}>();
const tagUsers = computed(() => ({
endpoint: 'hashtags/users' as const,
const paginator = markRaw(new Paginator('hashtags/users', {
limit: 30,
params: {
computedParams: computed(() => ({
tag: props.tag,
origin: 'combined',
sort: '+follower',
},
})),
}));
definePage(() => ({

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_spacer" style="--MI_SPACER-w: 700px;">
<div>
<MkPagination v-slot="{items}" :pagination="pagination" withControl>
<MkPagination v-slot="{items}" :paginator="paginator" withControl>
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" :class="$style.item" class="_panel _margin">
<b>{{ item.name }}</b>
<div v-if="item.description" :class="$style.description">{{ item.description }}</div>
@ -17,21 +17,21 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkPagination from '@/components/MkPagination.vue';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
user: Misskey.entities.User;
}>();
const pagination = {
endpoint: 'users/clips' as const,
const paginator = markRaw(new Paginator('users/clips', {
limit: 20,
params: computed(() => ({
computedParams: computed(() => ({
userId: props.user.id,
})),
};
}));
</script>
<style lang="scss" module>

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_spacer" style="--MI_SPACER-w: 1100px;">
<div :class="$style.root">
<MkPagination v-slot="{items}" :pagination="pagination" withControl>
<MkPagination v-slot="{items}" :paginator="paginator" withControl>
<div :class="$style.stream">
<MkNoteMediaGrid v-for="note in items" :note="note" square/>
</div>
@ -16,24 +16,23 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue';
import MkPagination from '@/components/MkPagination.vue';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
user: Misskey.entities.UserDetailed;
}>();
const pagination = {
endpoint: 'users/notes' as const,
const paginator = markRaw(new Paginator('users/notes', {
limit: 15,
params: computed(() => ({
computedParams: computed(() => ({
userId: props.user.id,
withFiles: true,
})),
};
}));
</script>
<style lang="scss" module>

View file

@ -5,27 +5,27 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_spacer" style="--MI_SPACER-w: 700px;">
<MkPagination v-slot="{items}" :pagination="pagination" withControl>
<MkPagination v-slot="{items}" :paginator="paginator" withControl>
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash" class="_margin"/>
</MkPagination>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkFlashPreview from '@/components/MkFlashPreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
user: Misskey.entities.User;
}>();
const pagination = {
endpoint: 'users/flashs' as const,
const paginator = markRaw(new Paginator('users/flashs', {
limit: 20,
params: computed(() => ({
computedParams: computed(() => ({
userId: props.user.id,
})),
};
}));
</script>

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div>
<MkPagination v-slot="{items}" :pagination="type === 'following' ? followingPagination : followersPagination" withControl>
<MkPagination v-slot="{items}" :paginator="type === 'following' ? followingPaginator : followersPaginator" withControl>
<div :class="$style.users">
<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" :user="user"/>
</div>
@ -14,31 +14,30 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkUserInfo from '@/components/MkUserInfo.vue';
import MkPagination from '@/components/MkPagination.vue';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
user: Misskey.entities.User;
type: 'following' | 'followers';
}>();
const followingPagination = {
endpoint: 'users/following' as const,
const followingPaginator = markRaw(new Paginator('users/following', {
limit: 20,
params: computed(() => ({
computedParams: computed(() => ({
userId: props.user.id,
})),
};
}));
const followersPagination = {
endpoint: 'users/followers' as const,
const followersPaginator = markRaw(new Paginator('users/followers', {
limit: 20,
params: computed(() => ({
computedParams: computed(() => ({
userId: props.user.id,
})),
};
}));
</script>
<style lang="scss" module>

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_spacer" style="--MI_SPACER-w: 700px;">
<MkPagination v-slot="{items}" :pagination="pagination" withControl>
<MkPagination v-slot="{items}" :paginator="paginator" withControl>
<div :class="$style.root">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
@ -14,23 +14,23 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import { Paginator } from '@/utility/paginator.js';
const props = withDefaults(defineProps<{
user: Misskey.entities.User;
}>(), {
});
const pagination = {
endpoint: 'users/gallery/posts' as const,
const paginator = markRaw(new Paginator('users/gallery/posts', {
limit: 6,
params: computed(() => ({
computedParams: computed(() => ({
userId: props.user.id,
})),
};
}));
</script>
<style lang="scss" module>

View file

@ -13,16 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="files">{{ i18n.ts.withFiles }}</option>
</MkTab>
</template>
<MkNotesTimeline :key="tab" :noGap="true" :pagination="pagination" :pullToRefresh="false" :class="$style.tl"/>
<MkNotesTimeline v-if="tab === 'featured'" :noGap="true" :paginator="featuredPaginator" :pullToRefresh="false" :class="$style.tl"/>
<MkNotesTimeline v-else :noGap="true" :paginator="notesPaginator" :pullToRefresh="false" :class="$style.tl"/>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkTab from '@/components/MkTab.vue';
import { i18n } from '@/i18n.js';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
user: Misskey.entities.UserDetailed;
@ -30,23 +32,23 @@ const props = defineProps<{
const tab = ref<string>('all');
const pagination = computed(() => tab.value === 'featured' ? {
endpoint: 'users/featured-notes' as const,
const featuredPaginator = markRaw(new Paginator('users/featured-notes', {
limit: 10,
params: {
userId: props.user.id,
},
} : {
endpoint: 'users/notes' as const,
}));
const notesPaginator = markRaw(new Paginator('users/notes', {
limit: 10,
params: {
computedParams: computed(() => ({
userId: props.user.id,
withRenotes: tab.value === 'all',
withReplies: tab.value === 'all',
withChannelNotes: tab.value === 'all',
withFiles: tab.value === 'files',
},
});
})),
}));
</script>
<style lang="scss" module>

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<div class="_spacer" style="--MI_SPACER-w: 700px;">
<div>
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" withControl>
<MkPagination v-slot="{items}" :paginator="paginator" withControl>
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/list/${ list.id }`">
<div>{{ list.name }}</div>
<MkAvatars :userIds="list.userIds"/>
@ -19,24 +19,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import {} from 'vue';
import { markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkPagination from '@/components/MkPagination.vue';
import MkStickyContainer from '@/components/global/MkStickyContainer.vue';
import MkAvatars from '@/components/MkAvatars.vue';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
user: Misskey.entities.UserDetailed;
}>();
const pagination = {
endpoint: 'users/lists/list' as const,
const paginator = markRaw(new Paginator('users/lists/list', {
noPaging: true,
limit: 10,
params: {
userId: props.user.id,
},
};
}));
</script>
<style lang="scss" module>

View file

@ -15,18 +15,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="files">{{ i18n.ts.withFiles }}</option>
</MkTab>
</template>
<MkNotesTimeline :key="tab" :noGap="true" :pagination="pagination" :class="$style.tl"/>
<MkNotesTimeline v-if="tab === 'featured'" :noGap="true" :paginator="featuredPaginator" :class="$style.tl"/>
<MkNotesTimeline v-else :noGap="true" :paginator="notesPaginator" :class="$style.tl"/>
</MkStickyContainer>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkTab from '@/components/MkTab.vue';
import { i18n } from '@/i18n.js';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
user: Misskey.entities.UserDetailed;
@ -34,23 +36,23 @@ const props = defineProps<{
const tab = ref<string>('all');
const pagination = computed(() => tab.value === 'featured' ? {
endpoint: 'users/featured-notes' as const,
const featuredPaginator = markRaw(new Paginator('users/featured-notes', {
limit: 10,
params: {
userId: props.user.id,
},
} : {
endpoint: 'users/notes' as const,
}));
const notesPaginator = markRaw(new Paginator('users/notes', {
limit: 10,
params: {
computedParams: computed(() => ({
userId: props.user.id,
withRenotes: tab.value === 'all',
withReplies: tab.value === 'all',
withChannelNotes: tab.value === 'all',
withFiles: tab.value === 'files',
},
});
})),
}));
</script>
<style lang="scss" module>

View file

@ -5,27 +5,27 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_spacer" style="--MI_SPACER-w: 700px;">
<MkPagination v-slot="{items}" :pagination="pagination" withControl>
<MkPagination v-slot="{items}" :paginator="paginator" withControl>
<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_margin"/>
</MkPagination>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkPagePreview from '@/components/MkPagePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
user: Misskey.entities.User;
}>();
const pagination = {
endpoint: 'users/pages' as const,
const paginator = markRaw(new Paginator('users/pages', {
limit: 20,
params: computed(() => ({
computedParams: computed(() => ({
userId: props.user.id,
})),
};
}));
</script>

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_spacer" style="--MI_SPACER-w: 700px;">
<MkPagination v-slot="{items}" :pagination="pagination">
<MkPagination v-slot="{items}" :paginator="paginator">
<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="_panel _margin">
<div :class="$style.header">
<MkAvatar :class="$style.avatar" :user="user"/>
@ -19,23 +19,23 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import MkPagination from '@/components/MkPagination.vue';
import MkNote from '@/components/MkNote.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import { Paginator } from '@/utility/paginator.js';
const props = defineProps<{
user: Misskey.entities.User;
}>();
const pagination = {
endpoint: 'users/reactions' as const,
const paginator = markRaw(new Paginator('users/reactions', {
limit: 20,
params: computed(() => ({
computedParams: computed(() => ({
userId: props.user.id,
})),
};
}));
</script>
<style lang="scss" module>

View file

@ -7,35 +7,33 @@ SPDX-License-Identifier: AGPL-3.0-only
<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.direct }}</template>
<MkNotesTimeline ref="tlComponent" :pagination="pagination"/>
<MkNotesTimeline :paginator="paginator"/>
</XColumn>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import { markRaw, ref } from 'vue';
import XColumn from './column.vue';
import type { Column } from '@/deck.js';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import { i18n } from '@/i18n.js';
import { Paginator } from '@/utility/paginator.js';
defineProps<{
column: Column;
isStacked: boolean;
}>();
const pagination = {
endpoint: 'notes/mentions' as const,
const paginator = markRaw(new Paginator('notes/mentions', {
limit: 10,
params: {
visibility: 'specified',
},
};
const tlComponent = useTemplateRef('tlComponent');
}));
function reloadTimeline() {
return new Promise<void>((res) => {
tlComponent.value?.reload().then(() => {
paginator.reload().then(() => {
res();
});
});

View file

@ -7,34 +7,32 @@ SPDX-License-Identifier: AGPL-3.0-only
<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.mentions }}</template>
<MkNotesTimeline ref="tlComponent" :pagination="pagination"/>
<MkNotesTimeline :paginator="paginator"/>
</XColumn>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import { markRaw, ref } from 'vue';
import XColumn from './column.vue';
import type { Column } from '@/deck.js';
import { i18n } from '@/i18n.js';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import { Paginator } from '@/utility/paginator.js';
defineProps<{
column: Column;
isStacked: boolean;
}>();
const tlComponent = useTemplateRef('tlComponent');
const paginator = markRaw(new Paginator('notes/mentions', {
limit: 10,
}));
function reloadTimeline() {
return new Promise<void>((res) => {
tlComponent.value?.reload().then(() => {
paginator.reload().then(() => {
res();
});
});
}
const pagination = {
endpoint: 'notes/mentions' as const,
limit: 10,
};
</script>

View file

@ -0,0 +1,322 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ref, shallowRef, triggerRef } from 'vue';
import * as Misskey from 'misskey-js';
import type { ComputedRef, DeepReadonly, Ref, ShallowRef } from 'vue';
import { misskeyApi } from '@/utility/misskey-api.js';
const MAX_ITEMS = 30;
const MAX_QUEUE_ITEMS = 100;
const FIRST_FETCH_LIMIT = 15;
const SECOND_FETCH_LIMIT = 30;
export type MisskeyEntity = {
id: string;
createdAt: string;
_shouldInsertAd_?: boolean;
[x: string]: any;
};
export class Paginator<Endpoint extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, T extends { id: string; } = (Misskey.Endpoints[Endpoint]['res'] extends (infer I)[] ? I extends { id: string } ? I : { id: string } : { id: string })> {
/**
*
*/
public items: ShallowRef<T[]> | Ref<T[]>;
public queuedAheadItemsCount = ref(0);
public fetching = ref(true);
public fetchingOlder = ref(false);
public fetchingNewer = ref(false);
public canFetchOlder = ref(false);
public canSearch = false;
public error = ref(false);
private endpoint: Endpoint;
private limit: number;
private params: Misskey.Endpoints[Endpoint]['req'] | (() => Misskey.Endpoints[Endpoint]['req']);
public computedParams: ComputedRef<Misskey.Endpoints[Endpoint]['req']> | null;
public initialId: MisskeyEntity['id'] | null = null;
public initialDate: number | null = null;
public initialDirection: 'newer' | 'older';
private offsetMode: boolean;
public noPaging: boolean;
public searchQuery = ref<null | string>('');
private searchParamName: string;
private canFetchDetection: 'safe' | 'limit' | null = null;
private aheadQueue: T[] = [];
private useShallowRef: boolean;
// 配列内の要素をどのような順序で並べるか
// newest: 新しいものが先頭 (default)
// oldest: 古いものが先頭
// NOTE: このようなプロパティを用意してこっち側で並びを管理せずに、Setで持っておき参照者側が好きに並び変えるような設計の方がすっきりしそうなものの、Vueのレンダリングのたびに並び替え処理が発生することになったりしそうでパフォーマンス上の懸念がある
public order: Ref<'newest' | 'oldest'>;
constructor(endpoint: Endpoint, props: {
limit?: number;
params?: Misskey.Endpoints[Endpoint]['req'] | (() => Misskey.Endpoints[Endpoint]['req']);
computedParams?: ComputedRef<Misskey.Endpoints[Endpoint]['req']>;
/**
* APIのような
* (APIをこの関数で使うのは若干矛盾してるけど)
*/
noPaging?: boolean;
offsetMode?: boolean;
initialId?: MisskeyEntity['id'];
initialDate?: number | null;
initialDirection?: 'newer' | 'older';
order?: 'newest' | 'oldest';
// 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨
canFetchDetection?: 'safe' | 'limit';
useShallowRef?: boolean;
canSearch?: boolean;
searchParamName?: keyof Misskey.Endpoints[Endpoint]['req'];
}) {
this.endpoint = endpoint;
this.useShallowRef = props.useShallowRef ?? false;
this.items = this.useShallowRef ? shallowRef([] as T[]) : ref([] as T[]);
this.limit = props.limit ?? FIRST_FETCH_LIMIT;
this.params = props.params ?? {};
this.computedParams = props.computedParams ?? null;
this.order = ref(props.order ?? 'newest');
this.initialId = props.initialId ?? null;
this.initialDate = props.initialDate ?? null;
this.initialDirection = props.initialDirection ?? 'older';
this.canFetchDetection = props.canFetchDetection ?? null;
this.noPaging = props.noPaging ?? false;
this.offsetMode = props.offsetMode ?? false;
this.canSearch = props.canSearch ?? false;
this.searchParamName = props.searchParamName ?? 'search';
this.getNewestId = this.getNewestId.bind(this);
this.getOldestId = this.getOldestId.bind(this);
this.init = this.init.bind(this);
this.reload = this.reload.bind(this);
this.fetchOlder = this.fetchOlder.bind(this);
this.fetchNewer = this.fetchNewer.bind(this);
this.unshiftItems = this.unshiftItems.bind(this);
this.pushItems = this.pushItems.bind(this);
this.prepend = this.prepend.bind(this);
this.enqueue = this.enqueue.bind(this);
this.releaseQueue = this.releaseQueue.bind(this);
this.removeItem = this.removeItem.bind(this);
this.updateItem = this.updateItem.bind(this);
}
private getNewestId(): string | null | undefined {
// 様々な要因により並び順は保証されないのでソートが必要
if (this.aheadQueue.length > 0) {
return this.aheadQueue.map(x => x.id).sort().at(-1);
}
return this.items.value.map(x => x.id).sort().at(-1);
}
private getOldestId(): string | null | undefined {
// 様々な要因により並び順は保証されないのでソートが必要
return this.items.value.map(x => x.id).sort().at(0);
}
public async init(): Promise<void> {
this.items.value = [];
this.aheadQueue = [];
this.queuedAheadItemsCount.value = 0;
this.fetching.value = true;
await misskeyApi<T[]>(this.endpoint, {
...(typeof this.params === 'function' ? this.params() : this.params),
...(this.computedParams ? this.computedParams.value : {}),
...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}),
limit: this.limit ?? FIRST_FETCH_LIMIT,
allowPartial: true,
...((this.initialId == null && this.initialDate == null) && this.initialDirection === 'newer' ? {
sinceId: '0',
} : this.initialDirection === 'newer' ? {
sinceId: this.initialId ?? undefined,
sinceDate: this.initialDate ?? undefined,
} : (this.initialId || this.initialDate) && this.initialDirection === 'older' ? {
untilId: this.initialId ?? undefined,
untilDate: this.initialDate ?? undefined,
} : {}),
}).then(res => {
// 逆順で返ってくるので
if ((this.initialId || this.initialDate) && this.initialDirection === 'newer') {
res.reverse();
}
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (i === 3) item._shouldInsertAd_ = true;
}
this.pushItems(res);
if (this.canFetchDetection === 'limit') {
if (res.length < FIRST_FETCH_LIMIT) {
this.canFetchOlder.value = false;
} else {
this.canFetchOlder.value = true;
}
} else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) {
if (res.length === 0 || this.noPaging) {
this.canFetchOlder.value = false;
} else {
this.canFetchOlder.value = true;
}
}
this.error.value = false;
this.fetching.value = false;
}, err => {
this.error.value = true;
this.fetching.value = false;
});
}
public reload(): Promise<void> {
return this.init();
}
public async fetchOlder(): Promise<void> {
if (!this.canFetchOlder.value || this.fetching.value || this.fetchingOlder.value || this.items.value.length === 0) return;
this.fetchingOlder.value = true;
await misskeyApi<T[]>(this.endpoint, {
...(typeof this.params === 'function' ? this.params() : this.params),
...(this.computedParams ? this.computedParams.value : {}),
...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}),
limit: SECOND_FETCH_LIMIT,
...(this.offsetMode ? {
offset: this.items.value.length,
} : {
untilId: this.getOldestId(),
}),
}).then(res => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (i === 10) item._shouldInsertAd_ = true;
}
this.pushItems(res);
if (this.canFetchDetection === 'limit') {
if (res.length < FIRST_FETCH_LIMIT) {
this.canFetchOlder.value = false;
} else {
this.canFetchOlder.value = true;
}
} else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) {
if (res.length === 0) {
this.canFetchOlder.value = false;
} else {
this.canFetchOlder.value = true;
}
}
}).finally(() => {
this.fetchingOlder.value = false;
});
}
public async fetchNewer(options: {
toQueue?: boolean;
} = {}): Promise<void> {
this.fetchingNewer.value = true;
await misskeyApi<T[]>(this.endpoint, {
...(typeof this.params === 'function' ? this.params() : this.params),
...(this.computedParams ? this.computedParams.value : {}),
...(this.searchQuery.value != null && this.searchQuery.value.trim() !== '' ? { [this.searchParamName]: this.searchQuery.value } : {}),
limit: SECOND_FETCH_LIMIT,
...(this.offsetMode ? {
offset: this.items.value.length,
} : {
sinceId: this.getNewestId(),
}),
}).then(res => {
if (res.length === 0) return; // これやらないと余計なre-renderが走る
if (options.toQueue) {
this.aheadQueue.unshift(...res.toReversed());
if (this.aheadQueue.length > MAX_QUEUE_ITEMS) {
this.aheadQueue = this.aheadQueue.slice(0, MAX_QUEUE_ITEMS);
}
this.queuedAheadItemsCount.value = this.aheadQueue.length;
} else {
if (this.order.value === 'oldest') {
this.pushItems(res);
} else {
this.unshiftItems(res.toReversed());
}
}
}).finally(() => {
this.fetchingNewer.value = false;
});
}
public trim(trigger = true): void {
if (this.items.value.length >= MAX_ITEMS) this.canFetchOlder.value = true;
this.items.value = this.items.value.slice(0, MAX_ITEMS);
if (this.useShallowRef && trigger) triggerRef(this.items);
}
public unshiftItems(newItems: T[]): void {
if (newItems.length === 0) return; // これやらないと余計なre-renderが走る
this.items.value.unshift(...newItems.filter(x => !this.items.value.some(y => y.id === x.id))); // ストリーミングやポーリングのタイミングによっては重複することがあるため
this.trim(false);
if (this.useShallowRef) triggerRef(this.items);
}
public pushItems(oldItems: T[]): void {
if (oldItems.length === 0) return; // これやらないと余計なre-renderが走る
this.items.value.push(...oldItems);
if (this.useShallowRef) triggerRef(this.items);
}
public prepend(item: T): void {
if (this.items.value.some(x => x.id === item.id)) return;
this.items.value.unshift(item);
this.trim(false);
if (this.useShallowRef) triggerRef(this.items);
}
public enqueue(item: T): void {
this.aheadQueue.unshift(item);
if (this.aheadQueue.length > MAX_QUEUE_ITEMS) {
this.aheadQueue.pop();
}
this.queuedAheadItemsCount.value = this.aheadQueue.length;
}
public releaseQueue(): void {
if (this.aheadQueue.length === 0) return; // これやらないと余計なre-renderが走る
this.unshiftItems(this.aheadQueue);
this.aheadQueue = [];
this.queuedAheadItemsCount.value = 0;
}
public removeItem(id: string): void {
// TODO: queueからも消す
const index = this.items.value.findIndex(x => x.id === id);
if (index !== -1) {
this.items.value.splice(index, 1);
if (this.useShallowRef) triggerRef(this.items);
}
}
public updateItem(id: string, updator: (item: T) => T): void {
// TODO: queueのも更新
const index = this.items.value.findIndex(x => x.id === id);
if (index !== -1) {
const item = this.items.value[index]!;
this.items.value[index] = updator(item);
if (this.useShallowRef) triggerRef(this.items);
}
}
}

View file

@ -18274,6 +18274,7 @@ export interface operations {
untilId?: string;
sinceDate?: number;
untilDate?: number;
search?: string | null;
};
};
};