Merge 23e5927bcd
into 8cbbb80e3f
This commit is contained in:
commit
62809df02b
|
@ -53,6 +53,7 @@
|
||||||
- Fix: カラムの名前が正しくリスト/チャンネルの名前にならない問題を修正
|
- Fix: カラムの名前が正しくリスト/チャンネルの名前にならない問題を修正
|
||||||
- Fix: 複数のメンションを1行に記述した場合に、サジェストが正しく表示されない問題を修正
|
- Fix: 複数のメンションを1行に記述した場合に、サジェストが正しく表示されない問題を修正
|
||||||
- Fix: メンションとしての条件を満たしていても、特定の条件(`-`が含まれる場合など)で正しくサジェストされない問題を一部修正
|
- Fix: メンションとしての条件を満たしていても、特定の条件(`-`が含まれる場合など)で正しくサジェストされない問題を一部修正
|
||||||
|
- Fix: ユーザーの前後ノートを閲覧する機能が壊れている問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Feat: サーバー管理コマンド
|
- Feat: サーバー管理コマンド
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkPagination :paginator="paginator" :autoLoad="autoLoad" :pullToRefresh="pullToRefresh" :withControl="withControl">
|
<MkPagination :paginator="paginator" :direction="direction" :autoLoad="autoLoad" :pullToRefresh="pullToRefresh" :withControl="withControl">
|
||||||
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
<template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
|
||||||
|
|
||||||
<template #default="{ items: notes }">
|
<template #default="{ items: notes }">
|
||||||
|
@ -50,11 +50,14 @@ import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-sep
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
paginator: T;
|
paginator: T;
|
||||||
noGap?: boolean;
|
noGap?: boolean;
|
||||||
|
|
||||||
|
direction?: 'up' | 'down' | 'both';
|
||||||
autoLoad?: boolean;
|
autoLoad?: boolean;
|
||||||
pullToRefresh?: boolean;
|
pullToRefresh?: boolean;
|
||||||
withControl?: boolean;
|
withControl?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
autoLoad: true,
|
autoLoad: true,
|
||||||
|
direction: 'down',
|
||||||
pullToRefresh: true,
|
pullToRefresh: true,
|
||||||
withControl: true,
|
withControl: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,15 +25,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else key="_root_" class="_gaps">
|
<div v-else key="_root_" class="_gaps">
|
||||||
<slot :items="unref(paginator.items)" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
|
<div v-if="direction === 'up' || direction === 'both'" v-show="upButtonVisible">
|
||||||
<div v-if="paginator.order.value === 'oldest'">
|
<MkButton v-if="!upButtonLoading" :class="$style.more" primary rounded @click="upButtonClick">
|
||||||
<MkButton v-if="!paginator.fetchingNewer.value" :class="$style.more" :wait="paginator.fetchingNewer.value" primary rounded @click="paginator.fetchNewer()">
|
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else/>
|
<MkLoading v-else/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-show="paginator.canFetchOlder.value">
|
<slot :items="unref(paginator.items)" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
|
||||||
<MkButton v-if="!paginator.fetchingOlder.value" :class="$style.more" :wait="paginator.fetchingOlder.value" primary rounded @click="paginator.fetchOlder()">
|
<div v-if="direction === 'down' || direction === 'both'" v-show="downButtonVisible">
|
||||||
|
<MkButton v-if="!downButtonLoading" :class="$style.more" primary rounded @click="downButtonClick">
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else/>
|
<MkLoading v-else/>
|
||||||
|
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup generic="T extends IPaginator">
|
<script lang="ts" setup generic="T extends IPaginator">
|
||||||
import { isLink } from '@@/js/is-link.js';
|
import { isLink } from '@@/js/is-link.js';
|
||||||
import { onMounted, watch, unref } from 'vue';
|
import { onMounted, computed, watch, unref } from 'vue';
|
||||||
import type { UnwrapRef } from 'vue';
|
import type { UnwrapRef } from 'vue';
|
||||||
import type { IPaginator } from '@/utility/paginator.js';
|
import type { IPaginator } from '@/utility/paginator.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -58,11 +58,20 @@ import * as os from '@/os.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
paginator: T;
|
paginator: T;
|
||||||
|
|
||||||
|
// ページネーションを進める方向
|
||||||
|
// up: 上方向
|
||||||
|
// down: 下方向 (default)
|
||||||
|
// both: 双方向
|
||||||
|
// NOTE: この方向はページネーションの方向であって、アイテムの並び順ではない
|
||||||
|
direction?: 'up' | 'down' | 'both';
|
||||||
|
|
||||||
autoLoad?: boolean;
|
autoLoad?: boolean;
|
||||||
pullToRefresh?: boolean;
|
pullToRefresh?: boolean;
|
||||||
withControl?: boolean;
|
withControl?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
autoLoad: true,
|
autoLoad: true,
|
||||||
|
direction: 'down',
|
||||||
pullToRefresh: true,
|
pullToRefresh: true,
|
||||||
withControl: false,
|
withControl: false,
|
||||||
});
|
});
|
||||||
|
@ -93,6 +102,36 @@ if (props.paginator.computedParams) {
|
||||||
}, { immediate: false, deep: true });
|
}, { immediate: false, deep: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const upButtonVisible = computed(() => {
|
||||||
|
return props.paginator.order.value === 'oldest' ? props.paginator.canFetchOlder.value : props.paginator.canFetchNewer.value;
|
||||||
|
});
|
||||||
|
const upButtonLoading = computed(() => {
|
||||||
|
return props.paginator.order.value === 'oldest' ? props.paginator.fetchingOlder.value : props.paginator.fetchingNewer.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function upButtonClick() {
|
||||||
|
if (props.paginator.order.value === 'oldest') {
|
||||||
|
props.paginator.fetchOlder();
|
||||||
|
} else {
|
||||||
|
props.paginator.fetchNewer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downButtonVisible = computed(() => {
|
||||||
|
return props.paginator.order.value === 'oldest' ? props.paginator.canFetchNewer.value : props.paginator.canFetchOlder.value;
|
||||||
|
});
|
||||||
|
const downButtonLoading = computed(() => {
|
||||||
|
return props.paginator.order.value === 'oldest' ? props.paginator.fetchingNewer.value : props.paginator.fetchingOlder.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function downButtonClick() {
|
||||||
|
if (props.paginator.order.value === 'oldest') {
|
||||||
|
props.paginator.fetchNewer();
|
||||||
|
} else {
|
||||||
|
props.paginator.fetchOlder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
empty: () => void;
|
empty: () => void;
|
||||||
default: (props: { items: UnwrapRef<T['items']> }) => void;
|
default: (props: { items: UnwrapRef<T['items']> }) => void;
|
||||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
|
<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
|
||||||
<div v-if="note">
|
<div v-if="note">
|
||||||
<div v-if="showNext" class="_margin">
|
<div v-if="showNext" class="_margin">
|
||||||
<MkNotesTimeline :withControl="false" :pullToRefresh="false" class="" :paginator="showNext === 'channel' ? nextChannelPaginator : nextUserPaginator" :noGap="true"/>
|
<MkNotesTimeline direction="up" :withControl="false" :pullToRefresh="false" class="" :paginator="showNext === 'channel' ? nextChannelPaginator : nextUserPaginator" :noGap="true"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="_margin">
|
<div class="_margin">
|
||||||
|
@ -81,7 +81,6 @@ const error = ref();
|
||||||
const prevUserPaginator = markRaw(new Paginator('users/notes', {
|
const prevUserPaginator = markRaw(new Paginator('users/notes', {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
initialId: props.noteId,
|
initialId: props.noteId,
|
||||||
initialDirection: 'older',
|
|
||||||
computedParams: computed(() => note.value ? ({
|
computedParams: computed(() => note.value ? ({
|
||||||
userId: note.value.userId,
|
userId: note.value.userId,
|
||||||
}) : undefined),
|
}) : undefined),
|
||||||
|
@ -99,7 +98,6 @@ const nextUserPaginator = markRaw(new Paginator('users/notes', {
|
||||||
const prevChannelPaginator = markRaw(new Paginator('channels/timeline', {
|
const prevChannelPaginator = markRaw(new Paginator('channels/timeline', {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
initialId: props.noteId,
|
initialId: props.noteId,
|
||||||
initialDirection: 'older',
|
|
||||||
computedParams: computed(() => note.value && note.value.channelId != null ? ({
|
computedParams: computed(() => note.value && note.value.channelId != null ? ({
|
||||||
channelId: note.value.channelId,
|
channelId: note.value.channelId,
|
||||||
}) : undefined),
|
}) : undefined),
|
||||||
|
|
|
@ -37,6 +37,7 @@ export interface IPaginator<T = unknown, _T = T & MisskeyEntity> {
|
||||||
fetchingOlder: Ref<boolean>;
|
fetchingOlder: Ref<boolean>;
|
||||||
fetchingNewer: Ref<boolean>;
|
fetchingNewer: Ref<boolean>;
|
||||||
canFetchOlder: Ref<boolean>;
|
canFetchOlder: Ref<boolean>;
|
||||||
|
canFetchNewer: Ref<boolean>;
|
||||||
canSearch: boolean;
|
canSearch: boolean;
|
||||||
error: Ref<boolean>;
|
error: Ref<boolean>;
|
||||||
computedParams: ComputedRef<Misskey.Endpoints[PaginatorCompatibleEndpointPaths]['req'] | null | undefined> | null;
|
computedParams: ComputedRef<Misskey.Endpoints[PaginatorCompatibleEndpointPaths]['req'] | null | undefined> | null;
|
||||||
|
@ -77,6 +78,7 @@ export class Paginator<
|
||||||
public fetchingOlder = ref(false);
|
public fetchingOlder = ref(false);
|
||||||
public fetchingNewer = ref(false);
|
public fetchingNewer = ref(false);
|
||||||
public canFetchOlder = ref(false);
|
public canFetchOlder = ref(false);
|
||||||
|
public canFetchNewer = ref(false);
|
||||||
public canSearch = false;
|
public canSearch = false;
|
||||||
public error = ref(false);
|
public error = ref(false);
|
||||||
private endpoint: Endpoint;
|
private endpoint: Endpoint;
|
||||||
|
@ -85,7 +87,12 @@ export class Paginator<
|
||||||
public computedParams: ComputedRef<E['req'] | null | undefined> | null;
|
public computedParams: ComputedRef<E['req'] | null | undefined> | null;
|
||||||
public initialId: MisskeyEntity['id'] | null = null;
|
public initialId: MisskeyEntity['id'] | null = null;
|
||||||
public initialDate: number | null = null;
|
public initialDate: number | null = null;
|
||||||
|
|
||||||
|
// 初回読み込み時、initialIdを基準にそれより新しいものを取得するか古いものを取得するか
|
||||||
|
// newer: initialIdより新しいものを取得する
|
||||||
|
// older: initialIdより古いものを取得する (default)
|
||||||
public initialDirection: 'newer' | 'older';
|
public initialDirection: 'newer' | 'older';
|
||||||
|
|
||||||
private offsetMode: boolean;
|
private offsetMode: boolean;
|
||||||
public noPaging: boolean;
|
public noPaging: boolean;
|
||||||
public searchQuery = ref<null | string>('');
|
public searchQuery = ref<null | string>('');
|
||||||
|
@ -116,6 +123,7 @@ export class Paginator<
|
||||||
initialId?: MisskeyEntity['id'];
|
initialId?: MisskeyEntity['id'];
|
||||||
initialDate?: number | null;
|
initialDate?: number | null;
|
||||||
initialDirection?: 'newer' | 'older';
|
initialDirection?: 'newer' | 'older';
|
||||||
|
|
||||||
order?: 'newest' | 'oldest';
|
order?: 'newest' | 'oldest';
|
||||||
|
|
||||||
// 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨
|
// 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨
|
||||||
|
@ -222,15 +230,15 @@ export class Paginator<
|
||||||
|
|
||||||
if (this.canFetchDetection === 'limit') {
|
if (this.canFetchDetection === 'limit') {
|
||||||
if (apiRes.length < FIRST_FETCH_LIMIT) {
|
if (apiRes.length < FIRST_FETCH_LIMIT) {
|
||||||
this.canFetchOlder.value = false;
|
(this.initialDirection === 'older' ? this.canFetchOlder : this.canFetchNewer).value = false;
|
||||||
} else {
|
} else {
|
||||||
this.canFetchOlder.value = true;
|
(this.initialDirection === 'older' ? this.canFetchOlder : this.canFetchNewer).value = true;
|
||||||
}
|
}
|
||||||
} else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) {
|
} else if (this.canFetchDetection === 'safe' || this.canFetchDetection == null) {
|
||||||
if (apiRes.length === 0 || this.noPaging) {
|
if (apiRes.length === 0 || this.noPaging) {
|
||||||
this.canFetchOlder.value = false;
|
(this.initialDirection === 'older' ? this.canFetchOlder : this.canFetchNewer).value = false;
|
||||||
} else {
|
} else {
|
||||||
this.canFetchOlder.value = true;
|
(this.initialDirection === 'older' ? this.canFetchOlder : this.canFetchNewer).value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,7 +281,11 @@ export class Paginator<
|
||||||
if (i === 10) item._shouldInsertAd_ = true;
|
if (i === 10) item._shouldInsertAd_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pushItems(apiRes);
|
if (this.order.value === 'oldest') {
|
||||||
|
this.unshiftItems(apiRes.toReversed(), true);
|
||||||
|
} else {
|
||||||
|
this.pushItems(apiRes);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.canFetchDetection === 'limit') {
|
if (this.canFetchDetection === 'limit') {
|
||||||
if (apiRes.length < FIRST_FETCH_LIMIT) {
|
if (apiRes.length < FIRST_FETCH_LIMIT) {
|
||||||
|
@ -313,7 +325,11 @@ export class Paginator<
|
||||||
|
|
||||||
this.fetchingNewer.value = false;
|
this.fetchingNewer.value = false;
|
||||||
|
|
||||||
if (apiRes == null || apiRes.length === 0) return; // これやらないと余計なre-renderが走る
|
if (apiRes == null || apiRes.length === 0) {
|
||||||
|
this.canFetchNewer.value = false;
|
||||||
|
// 余計なre-renderを防止するためここで終了
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (options.toQueue) {
|
if (options.toQueue) {
|
||||||
this.aheadQueue.unshift(...apiRes.toReversed());
|
this.aheadQueue.unshift(...apiRes.toReversed());
|
||||||
|
@ -325,9 +341,19 @@ export class Paginator<
|
||||||
if (this.order.value === 'oldest') {
|
if (this.order.value === 'oldest') {
|
||||||
this.pushItems(apiRes);
|
this.pushItems(apiRes);
|
||||||
} else {
|
} else {
|
||||||
this.unshiftItems(apiRes.toReversed());
|
this.unshiftItems(apiRes.toReversed(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.canFetchDetection === 'limit') {
|
||||||
|
if (apiRes.length < FIRST_FETCH_LIMIT) {
|
||||||
|
this.canFetchNewer.value = false;
|
||||||
|
} else {
|
||||||
|
this.canFetchNewer.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// canFetchDetectionが'safe'の場合・apiRes.length === 0 の場合は apiRes.length === 0 の場合に canFetchNewer.value = false になるが、
|
||||||
|
// 余計な re-render を防ぐために上部で処理している。そのため、ここでは何もしない
|
||||||
}
|
}
|
||||||
|
|
||||||
public trim(trigger = true): void {
|
public trim(trigger = true): void {
|
||||||
|
@ -336,10 +362,10 @@ export class Paginator<
|
||||||
if (this.useShallowRef && trigger) triggerRef(this.items);
|
if (this.useShallowRef && trigger) triggerRef(this.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unshiftItems(newItems: T[]): void {
|
public unshiftItems(newItems: T[], noTrim = false): void {
|
||||||
if (newItems.length === 0) return; // これやらないと余計なre-renderが走る
|
if (newItems.length === 0) return; // これやらないと余計なre-renderが走る
|
||||||
this.items.value.unshift(...newItems.filter(x => !this.items.value.some(y => y.id === x.id))); // ストリーミングやポーリングのタイミングによっては重複することがあるため
|
this.items.value.unshift(...newItems.filter(x => !this.items.value.some(y => y.id === x.id))); // ストリーミングやポーリングのタイミングによっては重複することがあるため
|
||||||
this.trim(false);
|
if (!noTrim) this.trim(true);
|
||||||
if (this.useShallowRef) triggerRef(this.items);
|
if (this.useShallowRef) triggerRef(this.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue