Merge branch 'misskey-dev:develop' into develop
This commit is contained in:
commit
424e020416
@ -21,6 +21,10 @@
|
|||||||
### Client
|
### Client
|
||||||
- Fix: サーバーメトリクスが90度傾いている
|
- Fix: サーバーメトリクスが90度傾いている
|
||||||
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
|
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
|
||||||
|
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるようになりました
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
|
||||||
|
|
||||||
## 13.13.2
|
## 13.13.2
|
||||||
|
|
||||||
|
2
locales/index.d.ts
vendored
2
locales/index.d.ts
vendored
@ -139,8 +139,10 @@ export interface Locale {
|
|||||||
"suspendConfirm": string;
|
"suspendConfirm": string;
|
||||||
"unsuspendConfirm": string;
|
"unsuspendConfirm": string;
|
||||||
"selectList": string;
|
"selectList": string;
|
||||||
|
"editList": string;
|
||||||
"selectChannel": string;
|
"selectChannel": string;
|
||||||
"selectAntenna": string;
|
"selectAntenna": string;
|
||||||
|
"editAntenna": string;
|
||||||
"selectWidget": string;
|
"selectWidget": string;
|
||||||
"editWidgets": string;
|
"editWidgets": string;
|
||||||
"editWidgetsExit": string;
|
"editWidgetsExit": string;
|
||||||
|
@ -136,8 +136,10 @@ unblockConfirm: "ブロック解除しますか?"
|
|||||||
suspendConfirm: "凍結しますか?"
|
suspendConfirm: "凍結しますか?"
|
||||||
unsuspendConfirm: "解凍しますか?"
|
unsuspendConfirm: "解凍しますか?"
|
||||||
selectList: "リストを選択"
|
selectList: "リストを選択"
|
||||||
|
editList: "リストを編集"
|
||||||
selectChannel: "チャンネルを選択"
|
selectChannel: "チャンネルを選択"
|
||||||
selectAntenna: "アンテナを選択"
|
selectAntenna: "アンテナを選択"
|
||||||
|
editAntenna: "アンテナを編集"
|
||||||
selectWidget: "ウィジェットを選択"
|
selectWidget: "ウィジェットを選択"
|
||||||
editWidgets: "ウィジェットを編集"
|
editWidgets: "ウィジェットを編集"
|
||||||
editWidgetsExit: "編集を終了"
|
editWidgetsExit: "編集を終了"
|
||||||
|
@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||||
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||||
import type httpSignature from '@peertube/http-signature';
|
import type httpSignature from '@peertube/http-signature';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ export class QueueService {
|
|||||||
if (content == null) return null;
|
if (content == null) return null;
|
||||||
if (to == null) return null;
|
if (to == null) return null;
|
||||||
|
|
||||||
const data = {
|
const data: DeliverJobData = {
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
@ -88,6 +88,38 @@ export class QueueService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ApDeliverManager-DeliverManager.execute()からinboxesを突っ込んでaddBulkしたい
|
||||||
|
* @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください
|
||||||
|
* @param content IActivity | null
|
||||||
|
* @param inboxes `Map<string, boolean>` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox)
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
||||||
|
const opts = {
|
||||||
|
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
||||||
|
backoff: {
|
||||||
|
type: 'custom',
|
||||||
|
},
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.deliverQueue.addBulk(Array.from(inboxes.entries()).map(d => ({
|
||||||
|
name: d[0],
|
||||||
|
data: {
|
||||||
|
user,
|
||||||
|
content,
|
||||||
|
to: d[0],
|
||||||
|
isSharedInbox: d[1],
|
||||||
|
} as DeliverJobData,
|
||||||
|
opts,
|
||||||
|
})));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
||||||
const data = {
|
const data = {
|
||||||
|
@ -7,6 +7,8 @@ import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
|||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
|
import { ThinUser } from '@/queue/types.js';
|
||||||
|
|
||||||
interface IRecipe {
|
interface IRecipe {
|
||||||
type: string;
|
type: string;
|
||||||
@ -21,10 +23,10 @@ interface IDirectRecipe extends IRecipe {
|
|||||||
to: RemoteUser;
|
to: RemoteUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
|
const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
|
||||||
recipe.type === 'Followers';
|
recipe.type === 'Followers';
|
||||||
|
|
||||||
const isDirect = (recipe: any): recipe is IDirectRecipe =>
|
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
|
||||||
recipe.type === 'Direct';
|
recipe.type === 'Direct';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -46,11 +48,11 @@ export class ApDeliverManagerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deliver activity to followers
|
* Deliver activity to followers
|
||||||
|
* @param actor
|
||||||
* @param activity Activity
|
* @param activity Activity
|
||||||
* @param from Followee
|
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) {
|
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) {
|
||||||
const manager = new DeliverManager(
|
const manager = new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
@ -64,11 +66,12 @@ export class ApDeliverManagerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deliver activity to user
|
* Deliver activity to user
|
||||||
|
* @param actor
|
||||||
* @param activity Activity
|
* @param activity Activity
|
||||||
* @param to Target user
|
* @param to Target user
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) {
|
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) {
|
||||||
const manager = new DeliverManager(
|
const manager = new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
@ -81,7 +84,7 @@ export class ApDeliverManagerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) {
|
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) {
|
||||||
return new DeliverManager(
|
return new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
@ -94,12 +97,15 @@ export class ApDeliverManagerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DeliverManager {
|
class DeliverManager {
|
||||||
private actor: { id: User['id']; host: null; };
|
private actor: ThinUser;
|
||||||
private activity: any;
|
private activity: IActivity | null;
|
||||||
private recipes: IRecipe[] = [];
|
private recipes: IRecipe[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
* @param userEntityService
|
||||||
|
* @param followingsRepository
|
||||||
|
* @param queueService
|
||||||
* @param actor Actor
|
* @param actor Actor
|
||||||
* @param activity Activity to deliver
|
* @param activity Activity to deliver
|
||||||
*/
|
*/
|
||||||
@ -109,9 +115,15 @@ class DeliverManager {
|
|||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
|
|
||||||
actor: { id: User['id']; host: null; },
|
actor: { id: User['id']; host: null; },
|
||||||
activity: any,
|
activity: IActivity | null,
|
||||||
) {
|
) {
|
||||||
this.actor = actor;
|
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
||||||
|
if (actor.host != null) throw new Error('actor.host must be null');
|
||||||
|
|
||||||
|
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
||||||
|
this.actor = {
|
||||||
|
id: actor.id,
|
||||||
|
};
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,9 +167,8 @@ class DeliverManager {
|
|||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async execute() {
|
public async execute() {
|
||||||
if (!this.userEntityService.isLocalUser(this.actor)) return;
|
|
||||||
|
|
||||||
// The value flags whether it is shared or not.
|
// The value flags whether it is shared or not.
|
||||||
|
// key: inbox URL, value: whether it is sharedInbox
|
||||||
const inboxes = new Map<string, boolean>();
|
const inboxes = new Map<string, boolean>();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -201,9 +212,6 @@ class DeliverManager {
|
|||||||
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
||||||
|
|
||||||
// deliver
|
// deliver
|
||||||
for (const inbox of inboxes) {
|
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||||
// inbox[0]: inbox, inbox[1]: whether it is sharedInbox
|
|
||||||
this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,6 +103,13 @@ export class StreamingApiServerService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const globalEv = new EventEmitter();
|
||||||
|
|
||||||
|
this.redisForSub.on('message', (_: string, data: string) => {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
globalEv.emit('message', parsed);
|
||||||
|
});
|
||||||
|
|
||||||
this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: {
|
this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: {
|
||||||
stream: MainStreamConnection,
|
stream: MainStreamConnection,
|
||||||
user: LocalUser | null;
|
user: LocalUser | null;
|
||||||
@ -112,12 +119,11 @@ export class StreamingApiServerService {
|
|||||||
|
|
||||||
const ev = new EventEmitter();
|
const ev = new EventEmitter();
|
||||||
|
|
||||||
async function onRedisMessage(_: string, data: string): Promise<void> {
|
function onRedisMessage(data: any): void {
|
||||||
const parsed = JSON.parse(data);
|
ev.emit(data.channel, data.message);
|
||||||
ev.emit(parsed.channel, parsed.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.redisForSub.on('message', onRedisMessage);
|
globalEv.on('message', onRedisMessage);
|
||||||
|
|
||||||
await stream.listen(ev, connection);
|
await stream.listen(ev, connection);
|
||||||
|
|
||||||
@ -137,7 +143,7 @@ export class StreamingApiServerService {
|
|||||||
connection.once('close', () => {
|
connection.once('close', () => {
|
||||||
ev.removeAllListeners();
|
ev.removeAllListeners();
|
||||||
stream.dispose();
|
stream.dispose();
|
||||||
this.redisForSub.off('message', onRedisMessage);
|
globalEv.off('message', onRedisMessage);
|
||||||
this.#connections.delete(connection);
|
this.#connections.delete(connection);
|
||||||
if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
|
if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
|
||||||
});
|
});
|
||||||
|
@ -21,14 +21,14 @@
|
|||||||
|
|
||||||
<div v-else ref="rootEl">
|
<div v-else ref="rootEl">
|
||||||
<div v-show="pagination.reversed && more" key="_more_" class="_margin">
|
<div v-show="pagination.reversed && more" key="_more_" class="_margin">
|
||||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMoreAhead">
|
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMoreAhead">
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else class="loading"/>
|
<MkLoading v-else class="loading"/>
|
||||||
</div>
|
</div>
|
||||||
<slot :items="items" :fetching="fetching || moreFetching"></slot>
|
<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
|
||||||
<div v-show="!pagination.reversed && more" key="_more_" class="_margin">
|
<div v-show="!pagination.reversed && more" key="_more_" class="_margin">
|
||||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMore">
|
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMore">
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else class="loading"/>
|
<MkLoading v-else class="loading"/>
|
||||||
@ -50,6 +50,7 @@ import { i18n } from '@/i18n';
|
|||||||
|
|
||||||
const SECOND_FETCH_LIMIT = 30;
|
const SECOND_FETCH_LIMIT = 30;
|
||||||
const TOLERANCE = 16;
|
const TOLERANCE = 16;
|
||||||
|
const APPEAR_MINIMUM_INTERVAL = 600;
|
||||||
|
|
||||||
export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
|
export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
|
||||||
endpoint: E;
|
endpoint: E;
|
||||||
@ -71,6 +72,16 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints>
|
|||||||
|
|
||||||
pageEl?: HTMLElement;
|
pageEl?: HTMLElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MisskeyEntityMap = Map<string, MisskeyEntity>;
|
||||||
|
|
||||||
|
function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
|
||||||
|
return entities.map(en => [en.id, en]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): MisskeyEntityMap {
|
||||||
|
return new Map([...map, ...arrayToEntries(entities)]);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { infoImageUrl } from '@/instance';
|
import { infoImageUrl } from '@/instance';
|
||||||
@ -94,21 +105,38 @@ let backed = $ref(false);
|
|||||||
|
|
||||||
let scrollRemove = $ref<(() => void) | null>(null);
|
let scrollRemove = $ref<(() => void) | null>(null);
|
||||||
|
|
||||||
const items = ref<MisskeyEntity[]>([]);
|
/**
|
||||||
const queue = ref<MisskeyEntity[]>([]);
|
* 表示するアイテムのソース
|
||||||
|
* 最新が0番目
|
||||||
|
*/
|
||||||
|
const items = ref<MisskeyEntityMap>(new Map());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* タブが非アクティブなどの場合に更新を貯めておく
|
||||||
|
* 最新が0番目
|
||||||
|
*/
|
||||||
|
const queue = ref<MisskeyEntityMap>(new Map());
|
||||||
|
|
||||||
const offset = ref(0);
|
const offset = ref(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初期化中かどうか(trueならMkLoadingで全て隠す)
|
||||||
|
*/
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
const moreFetching = ref(false);
|
const moreFetching = ref(false);
|
||||||
const more = ref(false);
|
const more = ref(false);
|
||||||
|
const preventAppearFetchMore = ref(false);
|
||||||
|
const preventAppearFetchMoreTimer = ref<number | null>(null);
|
||||||
const isBackTop = ref(false);
|
const isBackTop = ref(false);
|
||||||
const empty = computed(() => items.value.length === 0);
|
const empty = computed(() => items.value.size === 0);
|
||||||
const error = ref(false);
|
const error = ref(false);
|
||||||
const {
|
const {
|
||||||
enableInfiniteScroll,
|
enableInfiniteScroll,
|
||||||
} = defaultStore.reactiveState;
|
} = defaultStore.reactiveState;
|
||||||
|
|
||||||
const contentEl = $computed(() => props.pagination.pageEl ?? rootEl);
|
const contentEl = $computed(() => props.pagination.pageEl ?? rootEl);
|
||||||
const scrollableElement = $computed(() => getScrollContainer(contentEl));
|
const scrollableElement = $computed(() => contentEl ? getScrollContainer(contentEl) : document.body);
|
||||||
|
|
||||||
const visibility = useDocumentVisibility();
|
const visibility = useDocumentVisibility();
|
||||||
|
|
||||||
@ -133,9 +161,9 @@ watch([() => props.pagination.reversed, $$(scrollableElement)], () => {
|
|||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
watch($$(rootEl), () => {
|
watch($$(rootEl), () => {
|
||||||
scrollObserver.disconnect();
|
scrollObserver?.disconnect();
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (rootEl) scrollObserver.observe(rootEl);
|
if (rootEl) scrollObserver?.observe(rootEl);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -155,12 +183,12 @@ if (props.pagination.params && isRef(props.pagination.params)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(queue, (a, b) => {
|
watch(queue, (a, b) => {
|
||||||
if (a.length === 0 && b.length === 0) return;
|
if (a.size === 0 && b.size === 0) return;
|
||||||
emit('queue', queue.value.length);
|
emit('queue', queue.value.size);
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
queue.value = [];
|
queue.value = new Map();
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
await os.api(props.pagination.endpoint, {
|
await os.api(props.pagination.endpoint, {
|
||||||
@ -173,11 +201,11 @@ async function init(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (res.length === 0 || props.pagination.noPaging) {
|
if (res.length === 0 || props.pagination.noPaging) {
|
||||||
items.value = res;
|
concatItems(res);
|
||||||
more.value = false;
|
more.value = false;
|
||||||
} else {
|
} else {
|
||||||
if (props.pagination.reversed) moreFetching.value = true;
|
if (props.pagination.reversed) moreFetching.value = true;
|
||||||
items.value = res;
|
concatItems(res);
|
||||||
more.value = true;
|
more.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,12 +219,13 @@ async function init(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reload = (): Promise<void> => {
|
const reload = (): Promise<void> => {
|
||||||
items.value = [];
|
items.value = new Map();
|
||||||
|
queue.value = new Map();
|
||||||
return init();
|
return init();
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMore = async (): Promise<void> => {
|
const fetchMore = async (): Promise<void> => {
|
||||||
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
||||||
moreFetching.value = true;
|
moreFetching.value = true;
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
await os.api(props.pagination.endpoint, {
|
await os.api(props.pagination.endpoint, {
|
||||||
@ -205,7 +234,7 @@ const fetchMore = async (): Promise<void> => {
|
|||||||
...(props.pagination.offsetMode ? {
|
...(props.pagination.offsetMode ? {
|
||||||
offset: offset.value,
|
offset: offset.value,
|
||||||
} : {
|
} : {
|
||||||
untilId: items.value[items.value.length - 1].id,
|
untilId: Array.from(items.value.keys())[items.value.size - 1],
|
||||||
}),
|
}),
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
for (let i = 0; i < res.length; i++) {
|
for (let i = 0; i < res.length; i++) {
|
||||||
@ -217,7 +246,7 @@ const fetchMore = async (): Promise<void> => {
|
|||||||
const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight();
|
const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight();
|
||||||
const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY;
|
const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY;
|
||||||
|
|
||||||
items.value = items.value.concat(_res);
|
items.value = concatMapWithArray(items.value, _res);
|
||||||
|
|
||||||
return nextTick(() => {
|
return nextTick(() => {
|
||||||
if (scrollableElement) {
|
if (scrollableElement) {
|
||||||
@ -237,7 +266,7 @@ const fetchMore = async (): Promise<void> => {
|
|||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
items.value = items.value.concat(res);
|
items.value = concatMapWithArray(items.value, res);
|
||||||
more.value = false;
|
more.value = false;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
}
|
}
|
||||||
@ -248,7 +277,7 @@ const fetchMore = async (): Promise<void> => {
|
|||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
items.value = items.value.concat(res);
|
items.value = concatMapWithArray(items.value, res);
|
||||||
more.value = true;
|
more.value = true;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
}
|
}
|
||||||
@ -260,7 +289,7 @@ const fetchMore = async (): Promise<void> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchMoreAhead = async (): Promise<void> => {
|
const fetchMoreAhead = async (): Promise<void> => {
|
||||||
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
||||||
moreFetching.value = true;
|
moreFetching.value = true;
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
await os.api(props.pagination.endpoint, {
|
await os.api(props.pagination.endpoint, {
|
||||||
@ -269,14 +298,14 @@ const fetchMoreAhead = async (): Promise<void> => {
|
|||||||
...(props.pagination.offsetMode ? {
|
...(props.pagination.offsetMode ? {
|
||||||
offset: offset.value,
|
offset: offset.value,
|
||||||
} : {
|
} : {
|
||||||
sinceId: items.value[items.value.length - 1].id,
|
sinceId: Array.from(items.value.keys())[items.value.size - 1],
|
||||||
}),
|
}),
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (res.length === 0) {
|
if (res.length === 0) {
|
||||||
items.value = items.value.concat(res);
|
items.value = concatMapWithArray(items.value, res);
|
||||||
more.value = false;
|
more.value = false;
|
||||||
} else {
|
} else {
|
||||||
items.value = items.value.concat(res);
|
items.value = concatMapWithArray(items.value, res);
|
||||||
more.value = true;
|
more.value = true;
|
||||||
}
|
}
|
||||||
offset.value += res.length;
|
offset.value += res.length;
|
||||||
@ -286,7 +315,32 @@ const fetchMoreAhead = async (): Promise<void> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl, TOLERANCE);
|
/**
|
||||||
|
* Appear(IntersectionObserver)によってfetchMoreが呼ばれる場合、
|
||||||
|
* APPEAR_MINIMUM_INTERVALミリ秒以内に2回fetchMoreが呼ばれるのを防ぐ
|
||||||
|
*/
|
||||||
|
const fetchMoreApperTimeoutFn = (): void => {
|
||||||
|
preventAppearFetchMore.value = false;
|
||||||
|
preventAppearFetchMoreTimer.value = null;
|
||||||
|
};
|
||||||
|
const fetchMoreAppearTimeout = (): void => {
|
||||||
|
preventAppearFetchMore.value = true;
|
||||||
|
preventAppearFetchMoreTimer.value = window.setTimeout(fetchMoreApperTimeoutFn, APPEAR_MINIMUM_INTERVAL);
|
||||||
|
};
|
||||||
|
|
||||||
|
const appearFetchMore = async (): Promise<void> => {
|
||||||
|
if (preventAppearFetchMore.value) return;
|
||||||
|
await fetchMore();
|
||||||
|
fetchMoreAppearTimeout();
|
||||||
|
};
|
||||||
|
|
||||||
|
const appearFetchMoreAhead = async (): Promise<void> => {
|
||||||
|
if (preventAppearFetchMore.value) return;
|
||||||
|
await fetchMoreAhead();
|
||||||
|
fetchMoreAppearTimeout();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl!, TOLERANCE);
|
||||||
|
|
||||||
watch(visibility, () => {
|
watch(visibility, () => {
|
||||||
if (visibility.value === 'hidden') {
|
if (visibility.value === 'hidden') {
|
||||||
@ -308,10 +362,15 @@ watch(visibility, () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最新のものとして1つだけアイテムを追加する
|
||||||
|
* ストリーミングから降ってきたアイテムはこれで追加する
|
||||||
|
* @param item アイテム
|
||||||
|
*/
|
||||||
const prepend = (item: MisskeyEntity): void => {
|
const prepend = (item: MisskeyEntity): void => {
|
||||||
// 初回表示時はunshiftだけでOK
|
if (items.value.size === 0) {
|
||||||
if (!rootEl) {
|
items.value.set(item.id, item);
|
||||||
items.value.unshift(item);
|
fetching.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,38 +378,55 @@ const prepend = (item: MisskeyEntity): void => {
|
|||||||
else prependQueue(item);
|
else prependQueue(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新着アイテムをitemsの先頭に追加し、displayLimitを適用する
|
||||||
|
* @param newItems 新しいアイテムの配列
|
||||||
|
*/
|
||||||
function unshiftItems(newItems: MisskeyEntity[]) {
|
function unshiftItems(newItems: MisskeyEntity[]) {
|
||||||
const length = newItems.length + items.value.length;
|
const length = newItems.length + items.value.size;
|
||||||
items.value = [...newItems, ...items.value].slice(0, props.displayLimit);
|
items.value = new Map([...arrayToEntries(newItems), ...items.value].slice(0, props.displayLimit));
|
||||||
|
|
||||||
|
if (length >= props.displayLimit) more.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 古いアイテムをitemsの末尾に追加し、displayLimitを適用する
|
||||||
|
* @param oldItems 古いアイテムの配列
|
||||||
|
*/
|
||||||
|
function concatItems(oldItems: MisskeyEntity[]) {
|
||||||
|
const length = oldItems.length + items.value.size;
|
||||||
|
items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, props.displayLimit));
|
||||||
|
|
||||||
if (length >= props.displayLimit) more.value = true;
|
if (length >= props.displayLimit) more.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeQueue() {
|
function executeQueue() {
|
||||||
if (queue.value.length === 0) return;
|
unshiftItems(Array.from(queue.value.values()));
|
||||||
unshiftItems(queue.value);
|
queue.value = new Map();
|
||||||
queue.value = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function prependQueue(newItem: MisskeyEntity) {
|
function prependQueue(newItem: MisskeyEntity) {
|
||||||
queue.value.unshift(newItem);
|
queue.value = new Map([[newItem.id, newItem], ...queue.value].slice(0, props.displayLimit) as [string, MisskeyEntity][]);
|
||||||
if (queue.value.length >= props.displayLimit) {
|
|
||||||
queue.value.pop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* アイテムを末尾に追加する(使うの?)
|
||||||
|
*/
|
||||||
const appendItem = (item: MisskeyEntity): void => {
|
const appendItem = (item: MisskeyEntity): void => {
|
||||||
items.value.push(item);
|
items.value.set(item.id, item);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeItem = (finder: (item: MisskeyEntity) => boolean) => {
|
const removeItem = (id: string) => {
|
||||||
const i = items.value.findIndex(finder);
|
items.value.delete(id);
|
||||||
items.value.splice(i, 1);
|
queue.value.delete(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => MisskeyEntity): void => {
|
const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => MisskeyEntity): void => {
|
||||||
const i = items.value.findIndex(item => item.id === id);
|
const item = items.value.get(id);
|
||||||
items.value[i] = replacer(items.value[i]);
|
if (item) items.value.set(id, replacer(item));
|
||||||
|
|
||||||
|
const queueItem = queue.value.get(id);
|
||||||
|
if (queueItem) queue.value.set(id, replacer(queueItem));
|
||||||
};
|
};
|
||||||
|
|
||||||
const inited = init();
|
const inited = init();
|
||||||
@ -364,7 +440,7 @@ onDeactivated(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function toBottom() {
|
function toBottom() {
|
||||||
scrollToBottom(contentEl);
|
scrollToBottom(contentEl!);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -388,7 +464,11 @@ onBeforeUnmount(() => {
|
|||||||
clearTimeout(timerForSetPause);
|
clearTimeout(timerForSetPause);
|
||||||
timerForSetPause = null;
|
timerForSetPause = null;
|
||||||
}
|
}
|
||||||
scrollObserver.disconnect();
|
if (preventAppearFetchMoreTimer.value) {
|
||||||
|
clearTimeout(preventAppearFetchMoreTimer.value);
|
||||||
|
preventAppearFetchMoreTimer.value = null;
|
||||||
|
}
|
||||||
|
scrollObserver?.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import isChromatic from 'chromatic/isChromatic';
|
import isChromatic from 'chromatic/isChromatic';
|
||||||
import { onUnmounted } from 'vue';
|
import { onMounted, onUnmounted } from 'vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { dateTimeFormat } from '@/scripts/intl-const';
|
import { dateTimeFormat } from '@/scripts/intl-const';
|
||||||
|
|
||||||
@ -29,11 +29,12 @@ const invalid = Number.isNaN(_time);
|
|||||||
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
||||||
|
|
||||||
let now = $ref((props.origin ?? new Date()).getTime());
|
let now = $ref((props.origin ?? new Date()).getTime());
|
||||||
|
const ago = $computed(() => (now - _time) / 1000/*ms*/);
|
||||||
|
|
||||||
const relative = $computed<string>(() => {
|
const relative = $computed<string>(() => {
|
||||||
if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない
|
if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない
|
||||||
if (invalid) return i18n.ts._ago.invalid;
|
if (invalid) return i18n.ts._ago.invalid;
|
||||||
|
|
||||||
const ago = (now - _time) / 1000/*ms*/;
|
|
||||||
return (
|
return (
|
||||||
ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() }) :
|
ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() }) :
|
||||||
ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() }) :
|
ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() }) :
|
||||||
@ -47,19 +48,25 @@ const relative = $computed<string>(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let tickId: number;
|
let tickId: number;
|
||||||
|
let currentInterval: number;
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
now = props.origin ?? (new Date()).getTime();
|
now = (new Date()).getTime();
|
||||||
const ago = (now - _time) / 1000/*ms*/;
|
const nextInterval = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
|
||||||
const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
|
|
||||||
|
|
||||||
tickId = window.setTimeout(tick, next);
|
if (currentInterval !== nextInterval) {
|
||||||
|
if (tickId) window.clearInterval(tickId);
|
||||||
|
currentInterval = nextInterval;
|
||||||
|
tickId = window.setInterval(tick, nextInterval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.mode === 'relative' || props.mode === 'detail') {
|
if (!invalid && props.origin === null && (props.mode === 'relative' || props.mode === 'detail')) {
|
||||||
tick();
|
onMounted(() => {
|
||||||
|
tick();
|
||||||
|
});
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.clearTimeout(tickId);
|
if (tickId) window.clearInterval(tickId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -75,7 +75,7 @@ const pagination = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function resolved(reportId) {
|
function resolved(reportId) {
|
||||||
reports.removeItem(item => item.id === reportId);
|
reports.removeItem(reportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
@ -144,7 +144,7 @@ const edit = (emoji) => {
|
|||||||
...result.updated,
|
...result.updated,
|
||||||
}));
|
}));
|
||||||
} else if (result.deleted) {
|
} else if (result.deleted) {
|
||||||
emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
|
emojisPaginationComponent.value.removeItem(emoji.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}, 'closed');
|
}, 'closed');
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
// structredCloneが遅いため
|
// structredCloneが遅いため
|
||||||
// SEE: http://var.blog.jp/archives/86038606.html
|
// SEE: http://var.blog.jp/archives/86038606.html
|
||||||
|
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
||||||
|
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
|
||||||
|
|
||||||
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
|
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
|
||||||
|
|
||||||
|
@ -44,11 +44,22 @@ async function setAntenna() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = [{
|
function editAntenna() {
|
||||||
icon: 'ti ti-pencil',
|
os.pageWindow('my/antennas/' + props.column.antennaId);
|
||||||
text: i18n.ts.selectAntenna,
|
}
|
||||||
action: setAntenna,
|
|
||||||
}];
|
const menu = [
|
||||||
|
{
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
text: i18n.ts.selectAntenna,
|
||||||
|
action: setAntenna,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'ti ti-settings',
|
||||||
|
text: i18n.ts.editAntenna,
|
||||||
|
action: editAntenna,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
function focus() {
|
function focus() {
|
||||||
|
@ -42,9 +42,20 @@ async function setList() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = [{
|
function editList() {
|
||||||
icon: 'ti ti-pencil',
|
os.pageWindow('my/lists/' + props.column.listId);
|
||||||
text: i18n.ts.selectList,
|
}
|
||||||
action: setList,
|
|
||||||
}];
|
const menu = [
|
||||||
|
{
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
text: i18n.ts.selectList,
|
||||||
|
action: setList,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'ti ti-settings',
|
||||||
|
text: i18n.ts.editList,
|
||||||
|
action: editList,
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user