Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
老兄 2023-07-05 12:04:32 +08:00 committed by GitHub
commit 424e020416
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 254 additions and 89 deletions

View File

@ -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
View File

@ -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;

View File

@ -136,8 +136,10 @@ unblockConfirm: "ブロック解除しますか?"
suspendConfirm: "凍結しますか?" suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?" unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択" selectList: "リストを選択"
editList: "リストを編集"
selectChannel: "チャンネルを選択" selectChannel: "チャンネルを選択"
selectAntenna: "アンテナを選択" selectAntenna: "アンテナを選択"
editAntenna: "アンテナを編集"
selectWidget: "ウィジェットを選択" selectWidget: "ウィジェットを選択"
editWidgets: "ウィジェットを編集" editWidgets: "ウィジェットを編集"
editWidgetsExit: "編集を終了" editWidgetsExit: "編集を終了"

View File

@ -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 = {

View File

@ -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]);
}
} }
} }

View File

@ -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);
}); });

View File

@ -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); /**
* AppearIntersectionObserverによって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 => {
// unshiftOK 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({

View File

@ -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 ''; // absoluterelative使 if (props.mode === 'absolute') return ''; // absoluterelative使
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')) {
onMounted(() => {
tick(); tick();
});
onUnmounted(() => { onUnmounted(() => {
window.clearTimeout(tickId); if (tickId) window.clearInterval(tickId);
}); });
} }
</script> </script>

View File

@ -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(() => []);

View File

@ -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');

View File

@ -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[];

View File

@ -44,11 +44,22 @@ async function setAntenna() {
}); });
} }
const menu = [{ function editAntenna() {
os.pageWindow('my/antennas/' + props.column.antennaId);
}
const menu = [
{
icon: 'ti ti-pencil', icon: 'ti ti-pencil',
text: i18n.ts.selectAntenna, text: i18n.ts.selectAntenna,
action: setAntenna, action: setAntenna,
}]; },
{
icon: 'ti ti-settings',
text: i18n.ts.editAntenna,
action: editAntenna,
},
];
/* /*
function focus() { function focus() {

View File

@ -42,9 +42,20 @@ async function setList() {
}); });
} }
const menu = [{ function editList() {
os.pageWindow('my/lists/' + props.column.listId);
}
const menu = [
{
icon: 'ti ti-pencil', icon: 'ti ti-pencil',
text: i18n.ts.selectList, text: i18n.ts.selectList,
action: setList, action: setList,
}]; },
{
icon: 'ti ti-settings',
text: i18n.ts.editList,
action: editList,
},
];
</script> </script>