mirror of
https://github.com/misskey-dev/misskey
synced 2025-06-29 16:22:50 +02:00
Compare commits
No commits in common. "develop" and "2025.6.4-alpha.2" have entirely different histories.
develop
...
2025.6.4-a
75 changed files with 1139 additions and 1103 deletions
|
@ -2,13 +2,11 @@
|
|||
|
||||
### General
|
||||
- Feat: ノートの下書き機能
|
||||
- Feat: クリップ内でノートを検索できるように
|
||||
|
||||
### Client
|
||||
- Feat: モデログを検索できるように
|
||||
- Enhance: 設定の自動バックアップをオンにした直後に自動バックアップするように
|
||||
- Enhance: ファイルアップロード前にキャプション設定を行えるように
|
||||
- Enhance: ファイルアップロード時にセンシティブ設定されているか表示するように
|
||||
- Enhance: ページネーション(一覧表示)の並び順を逆にできるように
|
||||
- Enhance: ページネーション(一覧表示)の基準日時を指定できるように
|
||||
- Fix: ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.6.4-alpha.3",
|
||||
"version": "2025.6.4-alpha.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -4,13 +4,11 @@
|
|||
*/
|
||||
|
||||
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 = {
|
||||
|
@ -48,7 +46,6 @@ 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;
|
||||
|
@ -100,15 +97,6 @@ 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();
|
||||
|
|
|
@ -153,9 +153,6 @@ export default [
|
|||
autofix: true,
|
||||
}],
|
||||
'vue/attribute-hyphenation': ['error', 'never'],
|
||||
'vue/no-mutating-props': ['error', {
|
||||
shallowOnly: true,
|
||||
}],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkPagination :paginator="paginator">
|
||||
<MkPagination :pagination="pagination">
|
||||
<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 { Paginator } from '@/utility/paginator.js';
|
||||
import type { PagingCtx } from '@/composables/use-pagination.js';
|
||||
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
paginator: Paginator;
|
||||
pagination: PagingCtx;
|
||||
noGap?: boolean;
|
||||
extractor?: (item: any) => any;
|
||||
}>(), {
|
||||
|
|
|
@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, computed, TransitionGroup, markRaw } from 'vue';
|
||||
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, useTemplateRef, watch, computed, TransitionGroup } 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,23 +195,33 @@ const fetching = ref(true);
|
|||
|
||||
const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
|
||||
|
||||
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 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 foldersPaginator = markRaw(new Paginator('drive/folders', {
|
||||
limit: 30,
|
||||
canFetchDetection: 'limit',
|
||||
params: () => ({ // 自動でリロードしたくないためcomputedParamsは使わない
|
||||
folderId: folder.value ? folder.value.id : null,
|
||||
}),
|
||||
}));
|
||||
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 filesTimeline = makeDateGroupedTimelineComputedRef(filesPaginator.items, 'month');
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<MkPagination v-slot="{ items }" :paginator="paginator">
|
||||
<MkPagination v-slot="{ items }" :pagination="pagination">
|
||||
<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<{
|
||||
paginator: Paginator<'admin/drive/files'>;
|
||||
pagination: PagingCtx<'admin/drive/files'>;
|
||||
viewMode: 'grid' | 'list';
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
@ -187,7 +187,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
|
||||
<MkPagination :paginator="renotesPaginator">
|
||||
<MkPagination :pagination="renotesPagination" :disableAutoLoad="true">
|
||||
<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" :paginator="reactionsPaginator">
|
||||
<MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true">
|
||||
<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, markRaw, onMounted, provide, ref, useTemplateRef } from 'vue';
|
||||
import { computed, inject, 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,7 +274,6 @@ 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;
|
||||
|
@ -377,19 +376,21 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
|
|||
const tab = ref(props.initialTab);
|
||||
const reactionTabType = ref<string | null>(null);
|
||||
|
||||
const renotesPaginator = markRaw(new Paginator('notes/renotes', {
|
||||
const renotesPagination = computed(() => ({
|
||||
endpoint: 'notes/renotes',
|
||||
limit: 10,
|
||||
params: {
|
||||
noteId: appearNote.id,
|
||||
},
|
||||
}));
|
||||
|
||||
const reactionsPaginator = markRaw(new Paginator('notes/reactions', {
|
||||
const reactionsPagination = computed(() => ({
|
||||
endpoint: 'notes/reactions',
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: {
|
||||
noteId: appearNote.id,
|
||||
type: reactionTabType.value,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
useTooltip(renoteButton, async (showing) => {
|
||||
|
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
|
||||
</template>
|
||||
<div class="_spacer">
|
||||
<MkPagination :paginator="paginator" withControl>
|
||||
<MkPagination ref="pagingEl" :pagination="paging" withControl>
|
||||
<template #empty>
|
||||
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
|
||||
</template>
|
||||
|
@ -100,8 +100,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, markRaw } from 'vue';
|
||||
import { ref, shallowRef, useTemplateRef } 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';
|
||||
|
@ -110,7 +111,6 @@ 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,9 +118,12 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const paginator = markRaw(new Paginator('notes/drafts/list', {
|
||||
const paging = {
|
||||
endpoint: 'notes/drafts/list',
|
||||
limit: 10,
|
||||
}));
|
||||
} satisfies PagingCtx;
|
||||
|
||||
const pagingComponent = useTemplateRef('pagingEl');
|
||||
|
||||
const currentDraftsCount = ref(0);
|
||||
misskeyApi('notes/drafts/count').then((count) => {
|
||||
|
@ -148,7 +151,7 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
|
|||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
|
||||
paginator.reload();
|
||||
pagingComponent.value?.paginator.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,17 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkPagination :paginator="paginator" :autoLoad="autoLoad" :pullToRefresh="pullToRefresh" :withControl="withControl">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad" :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(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
|
||||
<div v-if="i > 0 && isSeparatorNeeded(pagingComponent.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(paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
|
||||
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(pagingComponent.paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
|
||||
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
|
||||
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
|
||||
<span>{{ getSeparatorInfo(pagingComponent.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,8 +31,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup generic="T extends Paginator">
|
||||
import type { Paginator } from '@/utility/paginator.js';
|
||||
<script lang="ts" setup generic="T extends PagingCtx">
|
||||
import { useTemplateRef } from 'vue';
|
||||
import type { PagingCtx } from '@/composables/use-pagination.js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -40,23 +41,24 @@ import { globalEvents, useGlobalEvent } from '@/events.js';
|
|||
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
paginator: T;
|
||||
pagination: T;
|
||||
noGap?: boolean;
|
||||
autoLoad?: boolean;
|
||||
disableAutoLoad?: boolean;
|
||||
pullToRefresh?: boolean;
|
||||
withControl?: boolean;
|
||||
}>(), {
|
||||
autoLoad: true,
|
||||
pullToRefresh: true,
|
||||
withControl: true,
|
||||
});
|
||||
|
||||
const pagingComponent = useTemplateRef('pagingComponent');
|
||||
|
||||
useGlobalEvent('noteDeleted', (noteId) => {
|
||||
props.paginator.removeItem(noteId);
|
||||
pagingComponent.value?.paginator.removeItem(noteId);
|
||||
});
|
||||
|
||||
function reload() {
|
||||
return props.paginator.reload();
|
||||
return pagingComponent.value?.paginator.reload();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<component :is="prefer.s.enablePullToRefresh && pullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => paginator.reload()" @contextmenu.prevent.stop="onContextmenu">
|
||||
<div>
|
||||
<MkPaginationControl v-if="props.withControl" :paginator="paginator" style="margin-bottom: 10px"/>
|
||||
<MkPaginationControl v-if="props.withControl" v-model:order="order" v-model:date="date" style="margin-bottom: 10px" @reload="paginator.reload()"/>
|
||||
|
||||
<!-- :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="paginator.order.value === 'oldest'">
|
||||
<MkButton v-if="!paginator.fetchingNewer.value" :class="$style.more" :wait="paginator.fetchingNewer.value" primary rounded @click="paginator.fetchNewer()">
|
||||
<div v-if="order === '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,29 +44,49 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup generic="T extends Paginator, I = UnwrapRef<T['items']>">
|
||||
<script lang="ts" setup generic="T extends PagingCtx">
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import type { UnwrapRef } from 'vue';
|
||||
import type { Paginator } from '@/utility/paginator.js';
|
||||
import type { PagingCtx } from '@/composables/use-pagination.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<{
|
||||
paginator: T;
|
||||
autoLoad?: boolean;
|
||||
pagination: T;
|
||||
disableAutoLoad?: boolean;
|
||||
displayLimit?: number;
|
||||
pullToRefresh?: boolean;
|
||||
withControl?: boolean;
|
||||
}>(), {
|
||||
autoLoad: true,
|
||||
displayLimit: 20,
|
||||
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;
|
||||
|
@ -76,27 +96,19 @@ function onContextmenu(ev: MouseEvent) {
|
|||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.reload,
|
||||
action: () => {
|
||||
props.paginator.reload();
|
||||
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: I }) => void;
|
||||
default: (props: { items: UnwrapRef<Paginator['items']> }) => void;
|
||||
}>();
|
||||
|
||||
defineExpose({
|
||||
paginator: paginator,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSelect v-model="order" :class="$style.order" :items="[{ label: i18n.ts._order.newest, value: 'newest' }, { label: i18n.ts._order.oldest, value: 'oldest' }]">
|
||||
<template #prefix><i class="ti ti-arrows-sort"></i></template>
|
||||
</MkSelect>
|
||||
<MkButton v-if="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="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="paginator.reload()"><i class="ti ti-refresh"></i></MkButton>
|
||||
<MkButton v-tooltip="i18n.ts.reload" iconOnly transparent rounded @click="emit('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 Paginator">
|
||||
<script lang="ts" setup generic="T extends PagingCtx">
|
||||
import { ref, watch } from 'vue';
|
||||
import type { Paginator } from '@/utility/paginator.js';
|
||||
import type { PagingCtx } from '@/composables/use-pagination.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
|
@ -47,35 +47,32 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
paginator: T;
|
||||
canSearch?: boolean;
|
||||
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 = 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 order = defineModel<'newest' | 'oldest'>('order', {
|
||||
default: 'newest',
|
||||
});
|
||||
|
||||
watch(date, () => {
|
||||
props.paginator.initialDate = date.value;
|
||||
props.paginator.reload();
|
||||
const date = defineModel<number | null>('date', {
|
||||
default: null,
|
||||
});
|
||||
|
||||
watch(q, () => {
|
||||
props.paginator.searchQuery.value = q.value;
|
||||
props.paginator.reload();
|
||||
const q = defineModel<string | null>('q', {
|
||||
default: null,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -56,12 +56,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup, onMounted, shallowRef, ref, markRaw } from 'vue';
|
||||
import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup, onMounted, shallowRef, ref } 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';
|
||||
|
@ -74,7 +76,6 @@ 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';
|
||||
|
@ -101,97 +102,6 @@ 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;
|
||||
|
@ -223,6 +133,17 @@ 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;
|
||||
|
@ -283,6 +204,7 @@ 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;
|
||||
|
||||
|
@ -352,18 +274,101 @@ function disconnectChannel() {
|
|||
if (connection2) connection2.dispose();
|
||||
}
|
||||
|
||||
if (store.s.realtimeMode) {
|
||||
connectChannel();
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], () => {
|
||||
function refreshEndpointAndChannel() {
|
||||
if (store.s.realtimeMode) {
|
||||
disconnectChannel();
|
||||
connectChannel();
|
||||
}
|
||||
});
|
||||
|
||||
updatePaginationQuery();
|
||||
}
|
||||
|
||||
// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる
|
||||
// IDが切り替わったら切り替え先のTLを表示させたい
|
||||
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
|
||||
|
||||
// withSensitiveはクライアントで完結する処理のため、単にリロードするだけでOK
|
||||
watch(() => props.withSensitive, reloadTimeline);
|
||||
|
||||
// 初回表示用
|
||||
refreshEndpointAndChannel();
|
||||
|
||||
const paginator = usePagination({
|
||||
ctx: paginationQuery,
|
||||
useShallowRef: true,
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnectChannel();
|
||||
});
|
||||
|
|
|
@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, markRaw, watch } from 'vue';
|
||||
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } 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,17 +62,21 @@ const props = defineProps<{
|
|||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
|
||||
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 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 MIN_POLLING_INTERVAL = 1000 * 10;
|
||||
const POLLING_INTERVAL =
|
||||
|
@ -112,14 +116,6 @@ 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);
|
||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.left">
|
||||
<slot v-if="item.type === 'event'" name="left" :event="item.data" :timestamp="item.timestamp" :delta="item.delta"></slot>
|
||||
</div>
|
||||
<div :class="[$style.center, item.type === 'date' ? $style.date : '', i === 0 ? $style.first : '', i === items.length - 1 ? $style.last : '']">
|
||||
<div :class="[$style.center, item.type === 'date' ? $style.date : '']">
|
||||
<div :class="$style.centerLine"></div>
|
||||
<div :class="$style.centerPoint"></div>
|
||||
</div>
|
||||
|
@ -143,22 +143,6 @@ const items = computed<TlItem<T>[]>(() => {
|
|||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.first {
|
||||
.centerLine {
|
||||
height: 50%;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.last {
|
||||
.centerLine {
|
||||
height: 50%;
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centerLine {
|
||||
|
|
|
@ -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><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><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>
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkPagination :paginator="paginator">
|
||||
<MkPagination :pagination="pagination">
|
||||
<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 { Paginator } from '@/utility/paginator.js';
|
||||
import type { PagingCtx } from '@/composables/use-pagination.js';
|
||||
import MkUserInfo from '@/components/MkUserInfo.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
paginator: Paginator;
|
||||
pagination: PagingCtx;
|
||||
noGap?: boolean;
|
||||
extractor?: (item: any) => any;
|
||||
}>(), {
|
||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts.recommended }}</template>
|
||||
|
||||
<MkPagination :paginator="pinnedUsersPaginator">
|
||||
<MkPagination :pagination="pinnedUsers">
|
||||
<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 :paginator="popularUsersPaginator">
|
||||
<MkPagination :pagination="popularUsers">
|
||||
<template #default="{ items }">
|
||||
<div :class="$style.users">
|
||||
<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
|
||||
|
@ -35,19 +35,20 @@ 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 { Paginator } from '@/utility/paginator.js';
|
||||
import type { PagingCtx } from '@/composables/use-pagination.js';
|
||||
|
||||
const pinnedUsersPaginator = markRaw(new Paginator('pinned-users', {
|
||||
const pinnedUsers: PagingCtx = {
|
||||
endpoint: 'pinned-users',
|
||||
noPaging: true,
|
||||
limit: 10,
|
||||
}));
|
||||
};
|
||||
|
||||
const popularUsersPaginator = markRaw(new Paginator('users', {
|
||||
const popularUsers: PagingCtx = {
|
||||
endpoint: 'users',
|
||||
limit: 10,
|
||||
noPaging: true,
|
||||
params: {
|
||||
|
@ -55,7 +56,7 @@ const popularUsersPaginator = markRaw(new Paginator('users', {
|
|||
origin: 'local',
|
||||
sort: '+follower',
|
||||
},
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
316
packages/frontend/src/composables/use-pagination.ts
Normal file
316
packages/frontend/src/composables/use-pagination.ts
Normal file
|
@ -0,0 +1,316 @@
|
|||
/*
|
||||
* 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,
|
||||
};
|
||||
}
|
|
@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :paginator="paginator">
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<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,22 +51,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, ref } from 'vue';
|
||||
import { computed, 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 paginator = markRaw(new Paginator('federation/instances', {
|
||||
const pagination = {
|
||||
endpoint: 'federation/instances' as const,
|
||||
limit: 10,
|
||||
displayLimit: 50,
|
||||
offsetMode: true,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
sort: sort.value,
|
||||
host: host.value !== '' ? host.value : null,
|
||||
...(
|
||||
|
@ -79,7 +81,7 @@ const paginator = markRaw(new Paginator('federation/instances', {
|
|||
state.value === 'notResponding' ? { notResponding: true } :
|
||||
{}),
|
||||
})),
|
||||
}));
|
||||
} as PagingCtx;
|
||||
|
||||
function getStatus(instance) {
|
||||
if (instance.isSuspended) return 'Suspended';
|
||||
|
|
|
@ -160,7 +160,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="archived">{{ i18n.ts.archived }}</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkPagination :paginator="announcementsPaginator">
|
||||
<MkPagination :pagination="announcementsPagination">
|
||||
<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 :paginator="filesPaginator" viewMode="grid"/>
|
||||
<MkFileListForAdmin :pagination="filesPagination" 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, markRaw } from 'vue';
|
||||
import { computed, defineAsyncComponent, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
|
@ -235,7 +235,6 @@ 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;
|
||||
|
@ -256,22 +255,24 @@ const silenced = ref(false);
|
|||
const suspended = ref(false);
|
||||
const isSystem = ref(false);
|
||||
const moderationNote = ref('');
|
||||
const filesPaginator = markRaw(new Paginator('admin/drive/files', {
|
||||
const filesPagination = {
|
||||
endpoint: 'admin/drive/files' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: props.userId,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
const announcementsStatus = ref<'active' | 'archived'>('active');
|
||||
|
||||
const announcementsPaginator = markRaw(new Paginator('admin/announcements/list', {
|
||||
const announcementsPagination = {
|
||||
endpoint: 'admin/announcements/list' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: props.userId,
|
||||
status: announcementsStatus.value,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
const expandedRoles = ref([]);
|
||||
|
||||
function createFetcher() {
|
||||
|
|
|
@ -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="paginator.computedParams.value.origin === 'local'">
|
||||
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params().origin === 'local'">
|
||||
<span>{{ i18n.ts.host }}</span>
|
||||
</MkInput>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<MkPagination v-slot="{items}" :paginator="paginator">
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination">
|
||||
<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, ref, markRaw } from 'vue';
|
||||
import { computed, useTemplateRef, ref } from 'vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import XAbuseReport from '@/components/MkAbuseReport.vue';
|
||||
|
@ -66,7 +66,8 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { store } from '@/store.js';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
const reports = useTemplateRef('reports');
|
||||
|
||||
const state = ref('unresolved');
|
||||
const reporterOrigin = ref('combined');
|
||||
|
@ -74,17 +75,18 @@ const targetUserOrigin = ref('combined');
|
|||
const searchUsername = ref('');
|
||||
const searchHost = ref('');
|
||||
|
||||
const paginator = markRaw(new Paginator('admin/abuse-user-reports', {
|
||||
const pagination = {
|
||||
endpoint: 'admin/abuse-user-reports' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
state: state.value,
|
||||
reporterOrigin: reporterOrigin.value,
|
||||
targetUserOrigin: targetUserOrigin.value,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
function resolved(reportId) {
|
||||
paginator.removeItem(reportId);
|
||||
reports.value?.paginator.removeItem(reportId);
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" :key="host + state" :paginator="paginator">
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<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, markRaw, ref } from 'vue';
|
||||
import { computed, 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 paginator = markRaw(new Paginator('federation/instances', {
|
||||
const pagination = {
|
||||
endpoint: 'federation/instances' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
sort: sort.value,
|
||||
host: host.value !== '' ? host.value : null,
|
||||
...(
|
||||
|
@ -85,7 +85,7 @@ const paginator = markRaw(new Paginator('federation/instances', {
|
|||
state.value === 'notResponding' ? { notResponding: true } :
|
||||
{}),
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
function getStatus(instance: Misskey.entities.FederationInstance) {
|
||||
switch (instance.suspensionState) {
|
||||
|
|
|
@ -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="paginator.computedParams.value.origin === 'local'">
|
||||
<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.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 :paginator="paginator" :viewMode="viewMode"/>
|
||||
<MkFileListForAdmin :pagination="pagination" :viewMode="viewMode"/>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
|
@ -42,22 +42,23 @@ import * as os from '@/os.js';
|
|||
import { lookupFile } from '@/utility/admin-lookup.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
import type { PagingCtx } from '@/composables/use-pagination.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 paginator = markRaw(new Paginator('admin/drive/files', {
|
||||
const pagination = {
|
||||
endpoint: 'admin/drive/files' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: 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({
|
||||
|
|
|
@ -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 :paginator="paginator">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<MkInviteCode v-for="item in items" :key="item.id" :invite="(item as any)" :onDeleted="deleted" moderator/>
|
||||
|
@ -54,7 +54,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, ref, useTemplateRef } from 'vue';
|
||||
import { computed, ref, useTemplateRef } from 'vue';
|
||||
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';
|
||||
|
@ -66,19 +67,21 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
|||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkInviteCode from '@/components/MkInviteCode.vue';
|
||||
import { definePage } from '@/page.js';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
const pagingComponent = useTemplateRef('pagingComponent');
|
||||
|
||||
const type = ref('all');
|
||||
const sort = ref('+createdAt');
|
||||
|
||||
const paginator = markRaw(new Paginator('admin/invite/list', {
|
||||
const pagination: PagingCtx = {
|
||||
endpoint: 'admin/invite/list' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
type: type.value,
|
||||
sort: sort.value,
|
||||
})),
|
||||
offsetMode: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const expiresAt = ref('');
|
||||
const noExpirationDate = ref(true);
|
||||
|
@ -97,11 +100,13 @@ async function createWithOptions() {
|
|||
text: tickets.map(x => x.code).join('\n'),
|
||||
});
|
||||
|
||||
tickets.forEach(ticket => paginator.prepend(ticket));
|
||||
tickets.forEach(ticket => pagingComponent.value?.paginator.prepend(ticket));
|
||||
}
|
||||
|
||||
function deleted(id: string) {
|
||||
paginator.removeItem(id);
|
||||
if (pagingComponent.value) {
|
||||
pagingComponent.value.paginator.removeItem(id);
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
@ -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 :paginator="paginator" canFilter>
|
||||
<MkPaginationControl v-model:order="order" v-model:date="date" v-model:q="q" canSearch canFilter @reload="paginator.reload()">
|
||||
<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, ref, markRaw, onMounted } from 'vue';
|
||||
import { computed, useTemplateRef, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XModLog from './modlog.ModLog.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
|
@ -54,25 +54,37 @@ 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 = markRaw(new Paginator('admin/show-moderation-logs', {
|
||||
limit: 20,
|
||||
canFetchDetection: 'limit',
|
||||
canSearch: true,
|
||||
computedParams: computed(() => ({
|
||||
type: type.value,
|
||||
userId: moderatorId.value === '' ? null : moderatorId.value,
|
||||
})),
|
||||
}));
|
||||
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,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
paginator.init();
|
||||
watch([order, date], () => {
|
||||
paginator.updateCtxPartial({
|
||||
order: order.value,
|
||||
initialDirection: order.value === 'oldest' ? 'newer' : 'older',
|
||||
initialDate: date.value,
|
||||
});
|
||||
}, { immediate: false });
|
||||
|
||||
const timeline = computed(() => {
|
||||
return paginator.items.value.map(x => ({
|
||||
|
@ -83,7 +95,7 @@ const timeline = computed(() => {
|
|||
});
|
||||
|
||||
function fetchMore() {
|
||||
if (paginator.order.value === 'oldest') {
|
||||
if (order.value === 'oldest') {
|
||||
paginator.fetchNewer();
|
||||
} else {
|
||||
paginator.fetchOlder();
|
||||
|
|
|
@ -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 :paginator="usersPaginator">
|
||||
<MkPagination :pagination="usersPagination">
|
||||
<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, markRaw, reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import XEditor from './roles.editor.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -66,7 +66,6 @@ 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();
|
||||
|
||||
|
@ -74,12 +73,13 @@ const props = defineProps<{
|
|||
id?: string;
|
||||
}>();
|
||||
|
||||
const usersPaginator = markRaw(new Paginator('admin/roles/users', {
|
||||
const usersPagination = {
|
||||
endpoint: 'admin/roles/users' as const,
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
roleId: props.id,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
const expandedItems = ref([]);
|
||||
|
||||
|
|
|
@ -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="paginator.computedParams.value.origin === 'local'">
|
||||
<MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'">
|
||||
<template #prefix>@</template>
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" :paginator="paginator">
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination">
|
||||
<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, markRaw, ref, watchEffect } from 'vue';
|
||||
import { computed, useTemplateRef, ref, watchEffect } from 'vue';
|
||||
import { defaultMemoryStorage } from '@/memory-storage';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
@ -69,7 +69,6 @@ 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;
|
||||
|
@ -79,6 +78,7 @@ 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,9 +86,10 @@ const state = ref(storedQuery.state ?? 'all');
|
|||
const origin = ref(storedQuery.origin ?? 'local');
|
||||
const searchUsername = ref(storedQuery.username ?? '');
|
||||
const searchHost = ref(storedQuery.hostname ?? '');
|
||||
const paginator = markRaw(new Paginator('admin/show-users', {
|
||||
const pagination = {
|
||||
endpoint: 'admin/show-users' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
sort: sort.value,
|
||||
state: state.value,
|
||||
origin: origin.value,
|
||||
|
@ -96,7 +97,7 @@ const paginator = markRaw(new Paginator('admin/show-users', {
|
|||
hostname: searchHost.value,
|
||||
})),
|
||||
offsetMode: true,
|
||||
}));
|
||||
};
|
||||
|
||||
function searchUser() {
|
||||
os.selectUser({ includeSelf: true }).then(user => {
|
||||
|
@ -120,7 +121,7 @@ async function addUser() {
|
|||
username: username,
|
||||
password: password,
|
||||
}).then(res => {
|
||||
paginator.reload();
|
||||
paginationComponent.value?.paginator.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 v-slot="{items}" :paginator="paginator" class="_gaps">
|
||||
<MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" 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, markRaw } from 'vue';
|
||||
import { ref, computed, useTemplateRef } from 'vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
@ -54,14 +54,24 @@ 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 paginator = markRaw(new Paginator('announcements', {
|
||||
const paginationCurrent = {
|
||||
endpoint: 'announcements' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
isActive: tab.value === 'current',
|
||||
})),
|
||||
}));
|
||||
params: {
|
||||
isActive: true,
|
||||
},
|
||||
};
|
||||
|
||||
const paginationPast = {
|
||||
endpoint: 'announcements' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
isActive: false,
|
||||
},
|
||||
};
|
||||
|
||||
const paginationEl = useTemplateRef('paginationEl');
|
||||
|
||||
const tab = ref('current');
|
||||
|
||||
|
@ -75,7 +85,8 @@ async function read(target) {
|
|||
if (confirm.canceled) return;
|
||||
}
|
||||
|
||||
paginator.updateItem(target.id, a => ({
|
||||
if (!paginationEl.value) return;
|
||||
paginationEl.value.paginator.updateItem(target.id, a => ({
|
||||
...a,
|
||||
isRead: true,
|
||||
}));
|
||||
|
|
|
@ -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 :paginator="featuredPaginator"/>
|
||||
<MkNotesTimeline :pagination="featuredPagination"/>
|
||||
</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="searchPaginator" :key="searchKey" :paginator="searchPaginator"/>
|
||||
<MkNotesTimeline v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
|
||||
</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, markRaw, shallowRef } from 'vue';
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
|
@ -97,7 +97,6 @@ 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();
|
||||
|
||||
|
@ -110,13 +109,14 @@ const tab = ref('overview');
|
|||
const channel = ref<Misskey.entities.Channel | null>(null);
|
||||
const favorited = ref(false);
|
||||
const searchQuery = ref('');
|
||||
const searchPaginator = shallowRef();
|
||||
const searchPagination = ref();
|
||||
const searchKey = ref('');
|
||||
const featuredPaginator = markRaw(new Paginator('channels/featured', {
|
||||
const featuredPagination = computed(() => ({
|
||||
endpoint: 'notes/featured' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: {
|
||||
channelId: props.channelId,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
useInterval(() => {
|
||||
|
@ -190,13 +190,14 @@ async function search() {
|
|||
|
||||
if (query == null) return;
|
||||
|
||||
searchPaginator.value = markRaw(new Paginator('notes/search', {
|
||||
searchPagination.value = {
|
||||
endpoint: 'notes/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
query: query,
|
||||
channelId: channel.value.id,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
searchKey.value = query;
|
||||
}
|
||||
|
|
|
@ -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="channelPaginator">
|
||||
<MkFoldableSection v-if="channelPagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkChannelList :key="key" :paginator="channelPaginator"/>
|
||||
<MkChannelList :key="key" :pagination="channelPagination"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<div v-if="tab === 'featured'">
|
||||
<MkPagination v-slot="{items}" :paginator="featuredPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagination">
|
||||
<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}" :paginator="favoritesPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="favoritesPagination">
|
||||
<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}" :paginator="followingPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="followingPagination">
|
||||
<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}" :paginator="ownedPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="ownedPagination">
|
||||
<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, markRaw, onMounted, ref, shallowRef } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
||||
import MkChannelList from '@/components/MkChannelList.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
|
@ -68,7 +68,6 @@ 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();
|
||||
|
||||
|
@ -81,27 +80,31 @@ const key = ref('');
|
|||
const tab = ref('featured');
|
||||
const searchQuery = ref('');
|
||||
const searchType = ref('nameAndDescription');
|
||||
const channelPaginator = shallowRef();
|
||||
const channelPagination = ref();
|
||||
|
||||
onMounted(() => {
|
||||
searchQuery.value = props.query ?? '';
|
||||
searchType.value = props.type ?? 'nameAndDescription';
|
||||
});
|
||||
|
||||
const featuredPaginator = markRaw(new Paginator('channels/featured', {
|
||||
const featuredPagination = {
|
||||
endpoint: 'channels/featured' as const,
|
||||
limit: 10,
|
||||
noPaging: true,
|
||||
}));
|
||||
const favoritesPaginator = markRaw(new Paginator('channels/my-favorites', {
|
||||
};
|
||||
const favoritesPagination = {
|
||||
endpoint: 'channels/my-favorites' as const,
|
||||
limit: 100,
|
||||
noPaging: true,
|
||||
}));
|
||||
const followingPaginator = markRaw(new Paginator('channels/followed', {
|
||||
};
|
||||
const followingPagination = {
|
||||
endpoint: 'channels/followed' as const,
|
||||
limit: 10,
|
||||
}));
|
||||
const ownedPaginator = markRaw(new Paginator('channels/owned', {
|
||||
};
|
||||
const ownedPagination = {
|
||||
endpoint: 'channels/owned' as const,
|
||||
limit: 10,
|
||||
}));
|
||||
};
|
||||
|
||||
async function search() {
|
||||
const query = searchQuery.value.toString().trim();
|
||||
|
@ -110,13 +113,14 @@ async function search() {
|
|||
|
||||
const type = searchType.value.toString().trim();
|
||||
|
||||
channelPaginator.value = markRaw(new Paginator('channels/search', {
|
||||
channelPagination.value = {
|
||||
endpoint: 'channels/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
query: searchQuery.value,
|
||||
type: type,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
key.value = query + type;
|
||||
}
|
||||
|
|
|
@ -23,14 +23,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<MkNotesTimeline :paginator="paginator" :detail="true"/>
|
||||
<MkNotesTimeline :pagination="pagination" :detail="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, provide, ref, markRaw } from 'vue';
|
||||
import { computed, watch, provide, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
@ -46,7 +46,6 @@ 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;
|
||||
|
@ -57,13 +56,13 @@ const props = defineProps<{
|
|||
|
||||
const clip = ref<Misskey.entities.Clip | null>(CTX_CLIP);
|
||||
const favorited = ref(false);
|
||||
const paginator = markRaw(new Paginator('clips/notes', {
|
||||
const pagination = {
|
||||
endpoint: 'clips/notes' as const,
|
||||
limit: 10,
|
||||
canSearch: true,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
clipId: props.clipId,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));
|
||||
|
||||
|
|
|
@ -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" :paginator="paginator">
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||
<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 :paginator="remotePaginator">
|
||||
<MkPagination :pagination="remotePagination">
|
||||
<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, markRaw, ref } from 'vue';
|
||||
import { computed, defineAsyncComponent, ref, useTemplateRef } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
|
@ -84,7 +84,8 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
|||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
const emojisPaginationComponent = useTemplateRef('emojisPaginationComponent');
|
||||
|
||||
const tab = ref('local');
|
||||
const query = ref<string | null>(null);
|
||||
|
@ -93,26 +94,28 @@ const host = ref<string | null>(null);
|
|||
const selectMode = ref(false);
|
||||
const selectedEmojis = ref<string[]>([]);
|
||||
|
||||
const paginator = markRaw(new Paginator('admin/emoji/list', {
|
||||
const pagination = {
|
||||
endpoint: 'admin/emoji/list' as const,
|
||||
limit: 30,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
query: (query.value && query.value !== '') ? query.value : null,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
const remotePaginator = markRaw(new Paginator('admin/emoji/list-remote', {
|
||||
const remotePagination = {
|
||||
endpoint: 'admin/emoji/list-remote' as const,
|
||||
limit: 30,
|
||||
computedParams: computed(() => ({
|
||||
params: 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 = paginator.items.value.map(item => item.id);
|
||||
selectedEmojis.value = emojisPaginationComponent.value?.paginator.items.value.map(item => item.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -129,7 +132,7 @@ const add = async (ev: MouseEvent) => {
|
|||
}, {
|
||||
done: result => {
|
||||
if (result.created) {
|
||||
paginator.prepend(result.created);
|
||||
emojisPaginationComponent.value?.paginator.prepend(result.created);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
|
@ -142,12 +145,12 @@ const edit = async (emoji) => {
|
|||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
paginator.updateItem(result.updated.id, (oldEmoji) => ({
|
||||
emojisPaginationComponent.value?.paginator.updateItem(result.updated.id, (oldEmoji) => ({
|
||||
...oldEmoji,
|
||||
...result.updated,
|
||||
}));
|
||||
} else if (result.deleted) {
|
||||
paginator.removeItem(emoji.id);
|
||||
emojisPaginationComponent.value?.paginator.removeItem(emoji.id);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
|
@ -242,7 +245,7 @@ const setCategoryBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
category: result,
|
||||
});
|
||||
paginator.reload();
|
||||
emojisPaginationComponent.value?.paginator.reload();
|
||||
};
|
||||
|
||||
const setLicenseBulk = async () => {
|
||||
|
@ -254,7 +257,7 @@ const setLicenseBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
license: result,
|
||||
});
|
||||
paginator.reload();
|
||||
emojisPaginationComponent.value?.paginator.reload();
|
||||
};
|
||||
|
||||
const addTagBulk = async () => {
|
||||
|
@ -266,7 +269,7 @@ const addTagBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
paginator.reload();
|
||||
emojisPaginationComponent.value?.paginator.reload();
|
||||
};
|
||||
|
||||
const removeTagBulk = async () => {
|
||||
|
@ -278,7 +281,7 @@ const removeTagBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
paginator.reload();
|
||||
emojisPaginationComponent.value?.paginator.reload();
|
||||
};
|
||||
|
||||
const setTagBulk = async () => {
|
||||
|
@ -290,7 +293,7 @@ const setTagBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
paginator.reload();
|
||||
emojisPaginationComponent.value?.paginator.reload();
|
||||
};
|
||||
|
||||
const delBulk = async () => {
|
||||
|
@ -302,7 +305,7 @@ const delBulk = async () => {
|
|||
await os.apiWithDialog('admin/emoji/delete-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
});
|
||||
paginator.reload();
|
||||
emojisPaginationComponent.value?.paginator.reload();
|
||||
};
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
|
|
|
@ -6,16 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div class="_gaps">
|
||||
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
|
||||
<MkNotesTimeline :paginator="paginator"/>
|
||||
<MkNotesTimeline ref="tlComponent" :pagination="pagination"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, markRaw } from 'vue';
|
||||
import { ref, computed } 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,10 +23,11 @@ const props = defineProps<{
|
|||
|
||||
const realFileId = computed(() => props.fileId);
|
||||
|
||||
const paginator = markRaw(new Paginator('drive/files/attached-notes', {
|
||||
const pagination = ref<PagingCtx>({
|
||||
endpoint: 'drive/files/attached-notes',
|
||||
limit: 10,
|
||||
params: {
|
||||
fileId: realFileId.value,
|
||||
},
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -9,29 +9,30 @@ 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'" :paginator="paginatorForNotes"/>
|
||||
<MkNotesTimeline v-else-if="tab === 'polls'" :paginator="paginatorForPolls"/>
|
||||
<MkNotesTimeline v-if="tab === 'notes'" :pagination="paginationForNotes"/>
|
||||
<MkNotesTimeline v-else-if="tab === 'polls'" :pagination="paginationForPolls"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, ref } from 'vue';
|
||||
import { 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 paginatorForNotes = markRaw(new Paginator('notes/featured', {
|
||||
const paginationForNotes = {
|
||||
endpoint: 'notes/featured' as const,
|
||||
limit: 10,
|
||||
}));
|
||||
};
|
||||
|
||||
const paginatorForPolls = markRaw(new Paginator('notes/polls/recommendation', {
|
||||
const paginationForPolls = {
|
||||
endpoint: 'notes/polls/recommendation' as const,
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
params: {
|
||||
excludeChannels: true,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const tab = ref('notes');
|
||||
</script>
|
||||
|
|
|
@ -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 :paginator="pinnedUsersPaginator"/>
|
||||
<MkUserList :pagination="pinnedUsers"/>
|
||||
</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 :paginator="popularUsersPaginator"/>
|
||||
<MkUserList :pagination="popularUsers"/>
|
||||
</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 :paginator="recentlyUpdatedUsersPaginator"/>
|
||||
<MkUserList :pagination="recentlyUpdatedUsers"/>
|
||||
</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 :paginator="recentlyRegisteredUsersPaginator"/>
|
||||
<MkUserList :pagination="recentlyRegisteredUsers"/>
|
||||
</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 :paginator="tagUsersPaginator"/>
|
||||
<MkUserList :pagination="tagUsers"/>
|
||||
</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 :paginator="popularUsersFPaginator"/>
|
||||
<MkUserList :pagination="popularUsersF"/>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection class="_margin">
|
||||
<template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
||||
<MkUserList :paginator="recentlyUpdatedUsersFPaginator"/>
|
||||
<MkUserList :pagination="recentlyUpdatedUsersF"/>
|
||||
</MkFoldableSection>
|
||||
<MkFoldableSection class="_margin">
|
||||
<template #header><i class="ti ti-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
|
||||
<MkUserList :paginator="recentlyRegisteredUsersFPaginator"/>
|
||||
<MkUserList :pagination="recentlyRegisteredUsersF"/>
|
||||
</MkFoldableSection>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, useTemplateRef, computed, markRaw } from 'vue';
|
||||
import { watch, ref, useTemplateRef, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkUserList from '@/components/MkUserList.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
|
@ -71,7 +71,6 @@ 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;
|
||||
|
@ -86,7 +85,8 @@ watch(() => props.tag, () => {
|
|||
if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null);
|
||||
});
|
||||
|
||||
const tagUsersPaginator = markRaw(new Paginator('hashtags/users', {
|
||||
const tagUsers = computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: props.tag,
|
||||
|
@ -95,66 +95,34 @@ const tagUsersPaginator = markRaw(new Paginator('hashtags/users', {
|
|||
},
|
||||
}));
|
||||
|
||||
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',
|
||||
},
|
||||
}));
|
||||
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',
|
||||
} };
|
||||
|
||||
misskeyApi('hashtags/list', {
|
||||
sort: '+attachedLocalUsers',
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<PageWithHeader>
|
||||
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
||||
<MkPagination :paginator="paginator">
|
||||
<MkPagination :pagination="pagination">
|
||||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
||||
|
||||
<template #default="{ items }">
|
||||
|
@ -20,17 +20,16 @@ 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 paginator = markRaw(new Paginator('i/favorites', {
|
||||
const pagination = {
|
||||
endpoint: 'i/favorites' as const,
|
||||
limit: 10,
|
||||
}));
|
||||
};
|
||||
|
||||
definePage(() => ({
|
||||
title: i18n.ts.favorites,
|
||||
|
|
|
@ -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}" :paginator="featuredFlashsPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredFlashsPagination">
|
||||
<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}" :paginator="myFlashsPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="myFlashsPagination">
|
||||
<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}" :paginator="likedFlashsPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="likedFlashsPagination">
|
||||
<div class="_gaps_s">
|
||||
<MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/>
|
||||
</div>
|
||||
|
@ -37,29 +37,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, ref } from 'vue';
|
||||
import { computed, 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 featuredFlashsPaginator = markRaw(new Paginator('flash/featured', {
|
||||
const featuredFlashsPagination = {
|
||||
endpoint: 'flash/featured' as const,
|
||||
limit: 5,
|
||||
offsetMode: true,
|
||||
}));
|
||||
const myFlashsPaginator = markRaw(new Paginator('flash/my', {
|
||||
};
|
||||
const myFlashsPagination = {
|
||||
endpoint: 'flash/my' as const,
|
||||
limit: 5,
|
||||
}));
|
||||
const likedFlashsPaginator = markRaw(new Paginator('flash/my-likes', {
|
||||
};
|
||||
const likedFlashsPagination = {
|
||||
endpoint: 'flash/my-likes' as const,
|
||||
limit: 5,
|
||||
}));
|
||||
};
|
||||
|
||||
function create() {
|
||||
router.push('/play/new');
|
||||
|
|
|
@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
|
||||
<div :key="tab" class="_spacer" style="--MI_SPACER-w: 800px;">
|
||||
<MkPagination :paginator="paginator">
|
||||
<div class="_spacer" style="--MI_SPACER-w: 800px;">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<template #empty><MkResult type="empty" :text="i18n.ts.noFollowRequests"/></template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests _gaps">
|
||||
|
@ -35,7 +35,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed, markRaw, ref, watch } from 'vue';
|
||||
import { useTemplateRef, computed, ref } from 'vue';
|
||||
import type { PagingCtx } from '@/composables/use-pagination.js';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { userPage, acct } from '@/filters/user.js';
|
||||
|
@ -43,35 +44,32 @@ 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 tab = ref($i?.isLocked ? 'list' : 'sent');
|
||||
const paginationComponent = useTemplateRef('paginationComponent');
|
||||
|
||||
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 });
|
||||
const pagination = computed<PagingCtx>(() => tab.value === 'list' ? {
|
||||
endpoint: 'following/requests/list',
|
||||
limit: 10,
|
||||
} : {
|
||||
endpoint: 'following/requests/sent',
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
function accept(user: Misskey.entities.UserLite) {
|
||||
os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => {
|
||||
paginator.reload();
|
||||
paginationComponent.value?.paginator.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function reject(user: Misskey.entities.UserLite) {
|
||||
os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
|
||||
paginator.reload();
|
||||
paginationComponent.value?.paginator.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function cancel(user: Misskey.entities.UserLite) {
|
||||
os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
|
||||
paginator.reload();
|
||||
paginationComponent.value?.paginator.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -93,6 +91,8 @@ const headerTabs = computed(() => [
|
|||
},
|
||||
]);
|
||||
|
||||
const tab = ref($i?.isLocked ? 'list' : 'sent');
|
||||
|
||||
definePage(() => ({
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'ti ti-user-plus',
|
||||
|
|
|
@ -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}" :paginator="recentPostsPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true">
|
||||
<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}" :paginator="popularPostsPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disableAutoLoad="true">
|
||||
<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}" :paginator="likedPostsPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
|
||||
<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}" :paginator="myPostsPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
|
||||
<div :class="$style.items">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
|
@ -44,14 +44,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, computed, markRaw } from 'vue';
|
||||
import { watch, ref, computed } 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();
|
||||
|
||||
|
@ -60,19 +59,34 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const tab = ref('explore');
|
||||
const tags = ref([]);
|
||||
const tagsRef = ref();
|
||||
|
||||
const recentPostsPaginator = markRaw(new Paginator('gallery/posts', {
|
||||
const recentPostsPagination = {
|
||||
endpoint: 'gallery/posts' as const,
|
||||
limit: 6,
|
||||
}));
|
||||
const popularPostsPaginator = markRaw(new Paginator('gallery/featured', {
|
||||
};
|
||||
const popularPostsPagination = {
|
||||
endpoint: 'gallery/featured' as const,
|
||||
noPaging: true,
|
||||
}));
|
||||
const myPostsPaginator = markRaw(new Paginator('i/gallery/posts', {
|
||||
};
|
||||
const myPostsPagination = {
|
||||
endpoint: 'i/gallery/posts' as const,
|
||||
limit: 5,
|
||||
}));
|
||||
const likedPostsPaginator = markRaw(new Paginator('i/gallery/likes', {
|
||||
};
|
||||
const likedPostsPagination = {
|
||||
endpoint: 'i/gallery/likes' as const,
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
const tagUsersPagination = computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: props.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
},
|
||||
}));
|
||||
|
||||
watch(() => props.tag, () => {
|
||||
|
|
|
@ -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}" :paginator="otherPostsPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
|
||||
<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, markRaw } from 'vue';
|
||||
import { computed, watch, ref, defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
@ -80,7 +80,6 @@ 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();
|
||||
|
||||
|
@ -90,12 +89,13 @@ const props = defineProps<{
|
|||
|
||||
const post = ref<Misskey.entities.GalleryPost | null>(null);
|
||||
const error = ref<any>(null);
|
||||
const otherPostsPaginator = markRaw(new Paginator('users/gallery/posts', {
|
||||
const otherPostsPagination = {
|
||||
endpoint: 'users/gallery/posts' as const,
|
||||
limit: 6,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: post.value.user.id,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
function fetchPost() {
|
||||
post.value = null;
|
||||
|
|
|
@ -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 }" :paginator="usersPaginator">
|
||||
<MkPagination v-slot="{ items }" :pagination="usersPagination">
|
||||
<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,9 +132,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, markRaw } from 'vue';
|
||||
import { ref, computed, watch } 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';
|
||||
|
@ -155,7 +156,6 @@ 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,7 +173,8 @@ const isMediaSilenced = ref(false);
|
|||
const faviconUrl = ref<string | null>(null);
|
||||
const moderationNote = ref('');
|
||||
|
||||
const usersPaginator = iAmModerator ? markRaw(new Paginator('admin/show-users', {
|
||||
const usersPagination = {
|
||||
endpoint: iAmModerator ? 'admin/show-users' : 'users',
|
||||
limit: 10,
|
||||
params: {
|
||||
sort: '+updatedAt',
|
||||
|
@ -181,15 +182,7 @@ const usersPaginator = iAmModerator ? markRaw(new Paginator('admin/show-users',
|
|||
hostname: props.host,
|
||||
},
|
||||
offsetMode: true,
|
||||
})) : markRaw(new Paginator('users', {
|
||||
limit: 10,
|
||||
params: {
|
||||
sort: '+updatedAt',
|
||||
state: 'all',
|
||||
hostname: props.host,
|
||||
},
|
||||
offsetMode: true,
|
||||
}));
|
||||
} satisfies PagingCtx<'admin/show-users' | 'users'>;
|
||||
|
||||
if (iAmModerator) {
|
||||
watch(moderationNote, async () => {
|
||||
|
|
|
@ -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 :paginator="paginator">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<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,8 +27,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, ref } from 'vue';
|
||||
import { computed, ref, useTemplateRef } 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';
|
||||
|
@ -38,15 +39,16 @@ 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 paginator = markRaw(new Paginator('invite/list', {
|
||||
const pagination: PagingCtx = {
|
||||
endpoint: 'invite/list' as const,
|
||||
limit: 10,
|
||||
}));
|
||||
};
|
||||
|
||||
const resetCycle = computed<null | string>(() => {
|
||||
if (!inviteLimitCycle) return null;
|
||||
|
@ -66,12 +68,14 @@ async function create() {
|
|||
text: ticket.code,
|
||||
});
|
||||
|
||||
paginator.prepend(ticket);
|
||||
pagingComponent.value?.paginator.prepend(ticket);
|
||||
update();
|
||||
}
|
||||
|
||||
function deleted(id: string) {
|
||||
paginator.removeItem(id);
|
||||
if (pagingComponent.value) {
|
||||
pagingComponent.value.paginator.removeItem(id);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
|
|
|
@ -12,38 +12,44 @@ 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 }" :paginator="paginator" class="_gaps" withControl>
|
||||
<MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps" withControl>
|
||||
<MkClipPreview v-for="item in items" :key="item.id" :clip="item" :noUserInfo="true"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<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 v-else-if="tab === 'favorites'" class="_gaps">
|
||||
<MkClipPreview v-for="item in favorites" :key="item.id" :clip="item"/>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, computed, markRaw } from 'vue';
|
||||
import { watch, ref, useTemplateRef, computed } 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';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'clips/list' as const,
|
||||
noPaging: true,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const tab = ref('my');
|
||||
|
||||
const paginator = markRaw(new Paginator('clips/list', {
|
||||
}));
|
||||
const favorites = ref<Misskey.entities.Clip[] | null>(null);
|
||||
|
||||
const favoritesPaginator = markRaw(new Paginator('clips/my-favorites', {
|
||||
}));
|
||||
const pagingComponent = useTemplateRef('pagingComponent');
|
||||
|
||||
watch(tab, async () => {
|
||||
favorites.value = await misskeyApi('clips/my-favorites');
|
||||
});
|
||||
|
||||
async function create() {
|
||||
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
|
||||
|
@ -70,15 +76,15 @@ async function create() {
|
|||
|
||||
clipsCache.delete();
|
||||
|
||||
paginator.reload();
|
||||
pagingComponent.value?.paginator.reload();
|
||||
}
|
||||
|
||||
function onClipCreated() {
|
||||
paginator.reload();
|
||||
pagingComponent.value?.paginator.reload();
|
||||
}
|
||||
|
||||
function onClipDeleted() {
|
||||
paginator.reload();
|
||||
pagingComponent.value?.paginator.reload();
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
@ -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 :paginator="membershipsPaginator" withControl>
|
||||
<MkPagination ref="paginationEl" :pagination="membershipsPagination" 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, markRaw, ref, watch } from 'vue';
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -69,7 +69,6 @@ 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();
|
||||
|
||||
|
@ -81,15 +80,17 @@ 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 membershipsPaginator = markRaw(new Paginator('users/lists/get-memberships', {
|
||||
const membershipsPagination = {
|
||||
endpoint: 'users/lists/get-memberships' as const,
|
||||
limit: 30,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
listId: props.listId,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
function fetchList() {
|
||||
misskeyApi('users/lists/show', {
|
||||
|
@ -108,7 +109,7 @@ function addUser() {
|
|||
listId: list.value.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
membershipsPaginator.reload();
|
||||
paginationEl.value?.paginator.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -124,7 +125,7 @@ async function removeUser(item, ev) {
|
|||
listId: list.value.id,
|
||||
userId: item.userId,
|
||||
}).then(() => {
|
||||
membershipsPaginator.removeItem(item.id);
|
||||
paginationEl.value?.paginator.removeItem(item.id);
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
|
@ -146,7 +147,7 @@ async function showMembershipMenu(item, ev) {
|
|||
userId: item.userId,
|
||||
withReplies,
|
||||
}).then(() => {
|
||||
membershipsPaginator.updateItem(item.id, (old) => ({
|
||||
paginationEl.value!.paginator.updateItem(item.id, (old) => ({
|
||||
...old,
|
||||
withReplies,
|
||||
}));
|
||||
|
|
|
@ -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="" :paginator="showNext === 'channel' ? nextChannelPaginator : nextUserPaginator" :noGap="true"/>
|
||||
<MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="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="" :paginator="showPrev === 'channel' ? prevChannelPaginator : prevUserPaginator" :noGap="true"/>
|
||||
<MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetchNote()"/>
|
||||
|
@ -45,9 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref, markRaw } from 'vue';
|
||||
import { computed, watch, ref } 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';
|
||||
|
@ -62,7 +63,6 @@ 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,41 +78,45 @@ const showPrev = ref<'user' | 'channel' | false>(false);
|
|||
const showNext = ref<'user' | 'channel' | false>(false);
|
||||
const error = ref();
|
||||
|
||||
const prevUserPaginator = markRaw(new Paginator('users/notes', {
|
||||
const prevUserPagination: PagingCtx = {
|
||||
endpoint: 'users/notes',
|
||||
limit: 10,
|
||||
initialId: props.noteId,
|
||||
initialDirection: 'older',
|
||||
computedParams: computed(() => note.value ? ({
|
||||
params: computed(() => note.value ? ({
|
||||
userId: note.value.userId,
|
||||
}) : undefined),
|
||||
}));
|
||||
};
|
||||
|
||||
const nextUserPaginator = markRaw(new Paginator('users/notes', {
|
||||
const nextUserPagination: PagingCtx = {
|
||||
endpoint: 'users/notes',
|
||||
limit: 10,
|
||||
initialId: props.noteId,
|
||||
initialDirection: 'newer',
|
||||
computedParams: computed(() => note.value ? ({
|
||||
params: computed(() => note.value ? ({
|
||||
userId: note.value.userId,
|
||||
}) : undefined),
|
||||
}));
|
||||
};
|
||||
|
||||
const prevChannelPaginator = markRaw(new Paginator('channels/timeline', {
|
||||
const prevChannelPagination: PagingCtx = {
|
||||
endpoint: 'channels/timeline',
|
||||
limit: 10,
|
||||
initialId: props.noteId,
|
||||
initialDirection: 'older',
|
||||
computedParams: computed(() => note.value ? ({
|
||||
params: computed(() => note.value ? ({
|
||||
channelId: note.value.channelId,
|
||||
}) : undefined),
|
||||
}));
|
||||
};
|
||||
|
||||
const nextChannelPaginator = markRaw(new Paginator('channels/timeline', {
|
||||
const nextChannelPagination: PagingCtx = {
|
||||
endpoint: 'channels/timeline',
|
||||
limit: 10,
|
||||
initialId: props.noteId,
|
||||
initialDirection: 'newer',
|
||||
computedParams: computed(() => note.value ? ({
|
||||
params: computed(() => note.value ? ({
|
||||
channelId: note.value.channelId,
|
||||
}) : undefined),
|
||||
}));
|
||||
};
|
||||
|
||||
function fetchNote() {
|
||||
showPrev.value = false;
|
||||
|
|
|
@ -10,39 +10,40 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStreamingNotificationsTimeline :class="$style.notifications" :excludeTypes="excludeTypes"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'mentions'">
|
||||
<MkNotesTimeline :paginator="mentionsPaginator"/>
|
||||
<MkNotesTimeline :pagination="mentionsPagination"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'directNotes'">
|
||||
<MkNotesTimeline :paginator="directNotesPaginator"/>
|
||||
<MkNotesTimeline :pagination="directNotesPagination"/>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, ref } from 'vue';
|
||||
import { computed, 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 mentionsPaginator = markRaw(new Paginator('notes/mentions', {
|
||||
const mentionsPagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
}));
|
||||
};
|
||||
|
||||
const directNotesPaginator = markRaw(new Paginator('notes/mentions', {
|
||||
const directNotesPagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
visibility: 'specified',
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
function setFilter(ev) {
|
||||
const typeItems = notificationTypes.map(t => ({
|
||||
|
|
|
@ -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}" :paginator="otherPostsPaginator" :class="$style.relatedPagesRoot" class="_gaps">
|
||||
<MkPagination v-slot="{items}" :pagination="otherPostsPagination" :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, markRaw } from 'vue';
|
||||
import { computed, watch, ref, defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
@ -122,7 +122,6 @@ 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();
|
||||
|
||||
|
@ -133,12 +132,13 @@ const props = defineProps<{
|
|||
|
||||
const page = ref<Misskey.entities.Page | null>(null);
|
||||
const error = ref<any>(null);
|
||||
const otherPostsPaginator = markRaw(new Paginator('users/pages', {
|
||||
const otherPostsPagination = {
|
||||
endpoint: 'users/pages' as const,
|
||||
limit: 6,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: page.value.user.id,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
const path = computed(() => props.username + '/' + props.pageName);
|
||||
|
||||
function fetchPage() {
|
||||
|
|
|
@ -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}" :paginator="featuredPagesPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
|
||||
<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}" :paginator="myPagesPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="myPagesPagination">
|
||||
<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}" :paginator="likedPagesPaginator">
|
||||
<MkPagination v-slot="{items}" :pagination="likedPagesPagination">
|
||||
<div class="_gaps">
|
||||
<MkPagePreview v-for="like in items" :key="like.page.id" :page="like.page"/>
|
||||
</div>
|
||||
|
@ -35,28 +35,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, ref } from 'vue';
|
||||
import { computed, 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 featuredPagesPaginator = markRaw(new Paginator('pages/featured', {
|
||||
const featuredPagesPagination = {
|
||||
endpoint: 'pages/featured' as const,
|
||||
noPaging: true,
|
||||
}));
|
||||
const myPagesPaginator = markRaw(new Paginator('i/pages', {
|
||||
};
|
||||
const myPagesPagination = {
|
||||
endpoint: 'i/pages' as const,
|
||||
limit: 5,
|
||||
}));
|
||||
const likedPagesPaginator = markRaw(new Paginator('i/page-likes', {
|
||||
};
|
||||
const likedPagesPagination = {
|
||||
endpoint: 'i/page-likes' as const,
|
||||
limit: 5,
|
||||
}));
|
||||
};
|
||||
|
||||
function create() {
|
||||
router.push('/pages/new');
|
||||
|
|
|
@ -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 :paginator="myGamesPaginator">
|
||||
<MkPagination :pagination="myGamesPagination" :disableAutoLoad="true">
|
||||
<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 :paginator="gamesPaginator">
|
||||
<MkPagination :pagination="gamesPagination" :disableAutoLoad="true">
|
||||
<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 { markRaw, onDeactivated, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { 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,18 +120,19 @@ 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 myGamesPaginator = markRaw(new Paginator('reversi/games', {
|
||||
const myGamesPagination = {
|
||||
endpoint: 'reversi/games' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
my: true,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const gamesPaginator = markRaw(new Paginator('reversi/games', {
|
||||
const gamesPagination = {
|
||||
endpoint: 'reversi/games' as const,
|
||||
limit: 10,
|
||||
}));
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
@ -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" :paginator="usersPaginator" :extractor="(item) => item.user"/>
|
||||
<MkUserList v-if="visible" :pagination="users" :extractor="(item) => item.user"/>
|
||||
<MkResult v-else-if="!visible" type="empty" :text="i18n.ts.nothing"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,14 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref, markRaw } from 'vue';
|
||||
import { computed, watch, ref } 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;
|
||||
|
@ -61,11 +60,12 @@ watch(() => props.roleId, () => {
|
|||
});
|
||||
}, { immediate: true });
|
||||
|
||||
const usersPaginator = markRaw(new Paginator('roles/users', {
|
||||
const users = computed(() => ({
|
||||
endpoint: 'roles/users' as const,
|
||||
limit: 30,
|
||||
computedParams: computed(() => ({
|
||||
params: {
|
||||
roleId: props.roleId,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
|
|
|
@ -103,18 +103,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="paginator">
|
||||
<MkFoldableSection v-if="notePagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkNotesTimeline :key="`searchNotes:${key}`" :paginator="paginator"/>
|
||||
<MkNotesTimeline :key="`searchNotes:${key}`" :pagination="notePagination"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, ref, shallowRef, toRef } from 'vue';
|
||||
import { host as localHost } from '@@/js/config.js';
|
||||
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 { host as localHost } from '@@/js/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -127,7 +128,6 @@ 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 paginator = shallowRef<Paginator<'notes/search'> | null>(null);
|
||||
const notePagination = ref<PagingCtx<'notes/search'>>();
|
||||
|
||||
const searchQuery = ref(toRef(props, 'query').value);
|
||||
const hostInput = ref(toRef(props, 'host').value);
|
||||
|
@ -299,12 +299,13 @@ async function search() {
|
|||
}
|
||||
}
|
||||
|
||||
paginator.value = markRaw(new Paginator('notes/search', {
|
||||
notePagination.value = {
|
||||
endpoint: 'notes/search',
|
||||
limit: 10,
|
||||
params: {
|
||||
...searchParams.value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
key.value++;
|
||||
}
|
||||
|
|
|
@ -17,16 +17,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="paginator">
|
||||
<MkFoldableSection v-if="userPagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkUserList :key="`searchUsers:${key}`" :paginator="paginator"/>
|
||||
<MkUserList :key="`searchUsers:${key}`" :pagination="userPagination"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, ref, shallowRef, toRef } from 'vue';
|
||||
import { ref, 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';
|
||||
|
@ -37,7 +38,6 @@ 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 paginator = shallowRef<Paginator<'users/search'> | null>(null);
|
||||
const userPagination = ref<PagingCtx<'users/search'>>();
|
||||
|
||||
const searchQuery = ref(toRef(props, 'query').value);
|
||||
const searchOrigin = ref(toRef(props, 'origin').value);
|
||||
|
@ -112,14 +112,15 @@ async function search() {
|
|||
}
|
||||
}
|
||||
|
||||
paginator.value = markRaw(new Paginator('users/search', {
|
||||
userPagination.value = {
|
||||
endpoint: 'users/search',
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
params: {
|
||||
query: query,
|
||||
origin: instance.federation === 'none' ? 'local' : searchOrigin.value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
key.value++;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<MkPagination :paginator="paginator">
|
||||
<FormPagination ref="list" :pagination="pagination">
|
||||
<template #empty><MkResult type="empty"/></template>
|
||||
<template #default="{items}">
|
||||
<div class="_gaps">
|
||||
|
@ -44,33 +44,35 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkFolder>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</FormPagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, markRaw } from 'vue';
|
||||
import { ref, computed, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import FormPagination 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 paginator = markRaw(new Paginator('i/apps', {
|
||||
const list = useTemplateRef('list');
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'i/apps' as const,
|
||||
limit: 100,
|
||||
noPaging: true,
|
||||
params: {
|
||||
sort: '+lastUsedAt',
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
function revoke(token) {
|
||||
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
|
||||
paginator.reload();
|
||||
list.value?.paginator.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts.manage }}</template>
|
||||
|
||||
<MkPagination :paginator="paginator" withControl>
|
||||
<MkPagination :pagination="pagination" 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, markRaw } from 'vue';
|
||||
import { computed, ref, defineAsyncComponent } 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 paginator = markRaw(new Paginator('i/webhooks/list', {
|
||||
const pagination = {
|
||||
endpoint: 'i/webhooks/list' as const,
|
||||
limit: 100,
|
||||
noPaging: true,
|
||||
}));
|
||||
};
|
||||
|
||||
async function generateToken() {
|
||||
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkTokenGenerateWindow.vue').then(x => x.default), {}, {
|
||||
|
|
|
@ -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}" :paginator="paginator">
|
||||
<MkPagination v-slot="{items}" :pagination="pagination">
|
||||
<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, markRaw, ref, watch } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { StyleValue } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
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 paginator = markRaw(new Paginator('drive/files', {
|
||||
const pagination = {
|
||||
endpoint: 'drive/files' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({ sort: sortMode.value })),
|
||||
}));
|
||||
params: computed(() => ({ sort: sortMode.value })),
|
||||
};
|
||||
|
||||
const sortOptions = [
|
||||
{ value: 'sizeDesc', displayName: i18n.ts._drivecleaner.orderBySizeDesc },
|
||||
|
|
|
@ -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 :paginator="renoteMutingPaginator" withControl>
|
||||
<MkPagination :pagination="renoteMutingPagination" 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 :paginator="mutingPaginator" withControl>
|
||||
<MkPagination :pagination="mutingPagination" 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 :paginator="blockingPaginator" withControl>
|
||||
<MkPagination :pagination="blockingPagination" 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, markRaw } from 'vue';
|
||||
import { ref, computed, watch } 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,21 +192,23 @@ 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 renoteMutingPaginator = markRaw(new Paginator('renote-mute/list', {
|
||||
const renoteMutingPagination = {
|
||||
endpoint: 'renote-mute/list' as const,
|
||||
limit: 10,
|
||||
}));
|
||||
};
|
||||
|
||||
const mutingPaginator = markRaw(new Paginator('mute/list', {
|
||||
const mutingPagination = {
|
||||
endpoint: 'mute/list' as const,
|
||||
limit: 10,
|
||||
}));
|
||||
};
|
||||
|
||||
const blockingPaginator = markRaw(new Paginator('blocking/list', {
|
||||
const blockingPagination = {
|
||||
endpoint: 'blocking/list' as const,
|
||||
limit: 10,
|
||||
}));
|
||||
};
|
||||
|
||||
const expandedRenoteMuteItems = ref([]);
|
||||
const expandedMuteItems = ref([]);
|
||||
|
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.signinHistory }}</template>
|
||||
<MkPagination :paginator="paginator" withControl>
|
||||
<MkPagination :pagination="pagination" disableAutoLoad 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, markRaw } from 'vue';
|
||||
import { computed } 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 paginator = markRaw(new Paginator('i/signin-history', {
|
||||
const pagination = {
|
||||
endpoint: 'i/signin-history' as const,
|
||||
limit: 5,
|
||||
}));
|
||||
};
|
||||
|
||||
async function change() {
|
||||
const { canceled: canceled2, result: newPassword } = await os.inputText({
|
||||
|
|
|
@ -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 :paginator="paginator"/>
|
||||
<MkNotesTimeline ref="tlComponent" class="" :pagination="pagination"/>
|
||||
</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, markRaw, ref } from 'vue';
|
||||
import { computed, ref, useTemplateRef } from 'vue';
|
||||
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { definePage } from '@/page.js';
|
||||
|
@ -28,18 +28,20 @@ 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 paginator = markRaw(new Paginator('notes/search-by-tag', {
|
||||
const pagination = {
|
||||
endpoint: 'notes/search-by-tag' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
tag: props.tag,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
const tlComponent = useTemplateRef('tlComponent');
|
||||
|
||||
async function post() {
|
||||
store.set('postFormHashtags', props.tag);
|
||||
|
@ -47,7 +49,7 @@ async function post() {
|
|||
await os.post();
|
||||
store.set('postFormHashtags', '');
|
||||
store.set('postFormWithHashtags', false);
|
||||
paginator.reload();
|
||||
tlComponent.value?.reload();
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
|
|
|
@ -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 :paginator="paginator"/>
|
||||
<MkUserList :pagination="tagUsers"/>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw } from 'vue';
|
||||
import { computed } 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 paginator = markRaw(new Paginator('hashtags/users', {
|
||||
const tagUsers = computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
computedParams: computed(() => ({
|
||||
params: {
|
||||
tag: props.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
definePage(() => ({
|
||||
|
|
|
@ -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}" :paginator="paginator" withControl>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" 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, markRaw } from 'vue';
|
||||
import { computed } 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 paginator = markRaw(new Paginator('users/clips', {
|
||||
const pagination = {
|
||||
endpoint: 'users/clips' as const,
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -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}" :paginator="paginator" withControl>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" withControl>
|
||||
<div :class="$style.stream">
|
||||
<MkNoteMediaGrid v-for="note in items" :note="note" square/>
|
||||
</div>
|
||||
|
@ -16,23 +16,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw } from 'vue';
|
||||
import { computed } 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 paginator = markRaw(new Paginator('users/notes', {
|
||||
const pagination = {
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 15,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
withFiles: true,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -5,27 +5,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="_spacer" style="--MI_SPACER-w: 700px;">
|
||||
<MkPagination v-slot="{items}" :paginator="paginator" withControl>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" withControl>
|
||||
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash" class="_margin"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw } from 'vue';
|
||||
import { computed } 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 paginator = markRaw(new Paginator('users/flashs', {
|
||||
const pagination = {
|
||||
endpoint: 'users/flashs' as const,
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<MkPagination v-slot="{items}" :paginator="type === 'following' ? followingPaginator : followersPaginator" withControl>
|
||||
<MkPagination v-slot="{items}" :pagination="type === 'following' ? followingPagination : followersPagination" 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,30 +14,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw } from 'vue';
|
||||
import { computed } 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 followingPaginator = markRaw(new Paginator('users/following', {
|
||||
const followingPagination = {
|
||||
endpoint: 'users/following' as const,
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
const followersPaginator = markRaw(new Paginator('users/followers', {
|
||||
const followersPagination = {
|
||||
endpoint: 'users/followers' as const,
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="_spacer" style="--MI_SPACER-w: 700px;">
|
||||
<MkPagination v-slot="{items}" :paginator="paginator" withControl>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" 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, markRaw } from 'vue';
|
||||
import { computed } 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 paginator = markRaw(new Paginator('users/gallery/posts', {
|
||||
const pagination = {
|
||||
endpoint: 'users/gallery/posts' as const,
|
||||
limit: 6,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -13,18 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="files">{{ i18n.ts.withFiles }}</option>
|
||||
</MkTab>
|
||||
</template>
|
||||
<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"/>
|
||||
<MkNotesTimeline :key="tab" :noGap="true" :pagination="pagination" :pullToRefresh="false" :class="$style.tl"/>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, markRaw } from 'vue';
|
||||
import { ref, computed } 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;
|
||||
|
@ -32,23 +30,23 @@ const props = defineProps<{
|
|||
|
||||
const tab = ref<string>('all');
|
||||
|
||||
const featuredPaginator = markRaw(new Paginator('users/featured-notes', {
|
||||
const pagination = computed(() => tab.value === 'featured' ? {
|
||||
endpoint: 'users/featured-notes' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
userId: props.user.id,
|
||||
},
|
||||
}));
|
||||
|
||||
const notesPaginator = markRaw(new Paginator('users/notes', {
|
||||
} : {
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: {
|
||||
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>
|
||||
|
|
|
@ -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}" :paginator="paginator" withControl>
|
||||
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" 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 { markRaw } from 'vue';
|
||||
import {} 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 paginator = markRaw(new Paginator('users/lists/list', {
|
||||
const pagination = {
|
||||
endpoint: 'users/lists/list' as const,
|
||||
noPaging: true,
|
||||
limit: 10,
|
||||
params: {
|
||||
userId: props.user.id,
|
||||
},
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -15,20 +15,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="files">{{ i18n.ts.withFiles }}</option>
|
||||
</MkTab>
|
||||
</template>
|
||||
<MkNotesTimeline v-if="tab === 'featured'" :noGap="true" :paginator="featuredPaginator" :class="$style.tl"/>
|
||||
<MkNotesTimeline v-else :noGap="true" :paginator="notesPaginator" :class="$style.tl"/>
|
||||
<MkNotesTimeline :key="tab" :noGap="true" :pagination="pagination" :class="$style.tl"/>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, markRaw } from 'vue';
|
||||
import { ref, computed } 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;
|
||||
|
@ -36,23 +34,23 @@ const props = defineProps<{
|
|||
|
||||
const tab = ref<string>('all');
|
||||
|
||||
const featuredPaginator = markRaw(new Paginator('users/featured-notes', {
|
||||
const pagination = computed(() => tab.value === 'featured' ? {
|
||||
endpoint: 'users/featured-notes' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
userId: props.user.id,
|
||||
},
|
||||
}));
|
||||
|
||||
const notesPaginator = markRaw(new Paginator('users/notes', {
|
||||
} : {
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 10,
|
||||
computedParams: computed(() => ({
|
||||
params: {
|
||||
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>
|
||||
|
|
|
@ -5,27 +5,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="_spacer" style="--MI_SPACER-w: 700px;">
|
||||
<MkPagination v-slot="{items}" :paginator="paginator" withControl>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" withControl>
|
||||
<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_margin"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw } from 'vue';
|
||||
import { computed } 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 paginator = markRaw(new Paginator('users/pages', {
|
||||
const pagination = {
|
||||
endpoint: 'users/pages' as const,
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div class="_spacer" style="--MI_SPACER-w: 700px;">
|
||||
<MkPagination v-slot="{items}" :paginator="paginator">
|
||||
<MkPagination v-slot="{items}" :pagination="pagination">
|
||||
<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, markRaw } from 'vue';
|
||||
import { computed } 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 paginator = markRaw(new Paginator('users/reactions', {
|
||||
const pagination = {
|
||||
endpoint: 'users/reactions' as const,
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
}));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -7,33 +7,35 @@ 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 :paginator="paginator"/>
|
||||
<MkNotesTimeline ref="tlComponent" :pagination="pagination"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, ref } from 'vue';
|
||||
import { ref, useTemplateRef } 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 paginator = markRaw(new Paginator('notes/mentions', {
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
params: {
|
||||
visibility: 'specified',
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const tlComponent = useTemplateRef('tlComponent');
|
||||
|
||||
function reloadTimeline() {
|
||||
return new Promise<void>((res) => {
|
||||
paginator.reload().then(() => {
|
||||
tlComponent.value?.reload().then(() => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,32 +7,34 @@ 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 :paginator="paginator"/>
|
||||
<MkNotesTimeline ref="tlComponent" :pagination="pagination"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, ref } from 'vue';
|
||||
import { ref, useTemplateRef } 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 paginator = markRaw(new Paginator('notes/mentions', {
|
||||
limit: 10,
|
||||
}));
|
||||
const tlComponent = useTemplateRef('tlComponent');
|
||||
|
||||
function reloadTimeline() {
|
||||
return new Promise<void>((res) => {
|
||||
paginator.reload().then(() => {
|
||||
tlComponent.value?.reload().then(() => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,322 +0,0 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2025.6.4-alpha.3",
|
||||
"version": "2025.6.4-alpha.2",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
|
|
@ -18274,7 +18274,6 @@ export interface operations {
|
|||
untilId?: string;
|
||||
sinceDate?: number;
|
||||
untilDate?: number;
|
||||
search?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue