diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-05-10 07:58:26 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-10 07:58:26 +0900 |
| commit | e1cd7c94fb13f8e49667b17554d22ce8de627a2a (patch) | |
| tree | a064c4b0937160cf1e26697dcfe18de8f2eb0144 /packages/frontend/src/use | |
| parent | Bump version to 2025.5.1-alpha.0 (diff) | |
| download | misskey-e1cd7c94fb13f8e49667b17554d22ce8de627a2a.tar.gz misskey-e1cd7c94fb13f8e49667b17554d22ce8de627a2a.tar.bz2 misskey-e1cd7c94fb13f8e49667b17554d22ce8de627a2a.zip | |
refactor(frontend): use* 関数の格納場所のフォルダ名を composables に変更 (#16004)
* refactor(frontend): use* 関数の格納場所を正式名称(composables)に変更
* migrate
* move useLoading
Diffstat (limited to 'packages/frontend/src/use')
| -rw-r--r-- | packages/frontend/src/use/use-chart-tooltip.ts | 63 | ||||
| -rw-r--r-- | packages/frontend/src/use/use-form.ts | 57 | ||||
| -rw-r--r-- | packages/frontend/src/use/use-leave-guard.ts | 50 | ||||
| -rw-r--r-- | packages/frontend/src/use/use-mutation-observer.ts | 21 | ||||
| -rw-r--r-- | packages/frontend/src/use/use-note-capture.ts | 283 | ||||
| -rw-r--r-- | packages/frontend/src/use/use-pagination.ts | 258 | ||||
| -rw-r--r-- | packages/frontend/src/use/use-scroll-position-keeper.ts | 77 | ||||
| -rw-r--r-- | packages/frontend/src/use/use-tooltip.ts | 106 |
8 files changed, 0 insertions, 915 deletions
diff --git a/packages/frontend/src/use/use-chart-tooltip.ts b/packages/frontend/src/use/use-chart-tooltip.ts deleted file mode 100644 index bba64fc6ee..0000000000 --- a/packages/frontend/src/use/use-chart-tooltip.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onUnmounted, onDeactivated, ref } from 'vue'; -import * as os from '@/os.js'; -import MkChartTooltip from '@/components/MkChartTooltip.vue'; - -export function useChartTooltip(opts: { position: 'top' | 'middle' } = { position: 'top' }) { - const tooltipShowing = ref(false); - const tooltipX = ref(0); - const tooltipY = ref(0); - const tooltipTitle = ref<string | null>(null); - const tooltipSeries = ref<{ - backgroundColor: string; - borderColor: string; - text: string; - }[] | null>(null); - const { dispose: disposeTooltipComponent } = os.popup(MkChartTooltip, { - showing: tooltipShowing, - x: tooltipX, - y: tooltipY, - title: tooltipTitle, - series: tooltipSeries, - }, {}); - - onUnmounted(() => { - disposeTooltipComponent(); - }); - - onDeactivated(() => { - tooltipShowing.value = false; - }); - - function handler(context) { - if (context.tooltip.opacity === 0) { - tooltipShowing.value = false; - return; - } - - tooltipTitle.value = context.tooltip.title[0]; - tooltipSeries.value = context.tooltip.body.map((b, i) => ({ - backgroundColor: context.tooltip.labelColors[i].backgroundColor, - borderColor: context.tooltip.labelColors[i].borderColor, - text: b.lines[0], - })); - - const rect = context.chart.canvas.getBoundingClientRect(); - - tooltipShowing.value = true; - tooltipX.value = rect.left + window.scrollX + context.tooltip.caretX; - if (opts.position === 'top') { - tooltipY.value = rect.top + window.scrollY; - } else if (opts.position === 'middle') { - tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY; - } - } - - return { - handler, - }; -} diff --git a/packages/frontend/src/use/use-form.ts b/packages/frontend/src/use/use-form.ts deleted file mode 100644 index 1c93557413..0000000000 --- a/packages/frontend/src/use/use-form.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { computed, reactive, watch } from 'vue'; -import type { Reactive } from 'vue'; -import { deepEqual } from '@/utility/deep-equal'; - -function copy<T>(v: T): T { - return JSON.parse(JSON.stringify(v)); -} - -function unwrapReactive<T>(v: Reactive<T>): T { - return JSON.parse(JSON.stringify(v)); -} - -export function useForm<T extends Record<string, any>>(initialState: T, save: (newState: T) => Promise<void>) { - const currentState = reactive<T>(copy(initialState)); - const previousState = reactive<T>(copy(initialState)); - - const modifiedStates = reactive<Record<keyof T, boolean>>({} as any); - for (const key in currentState) { - modifiedStates[key] = false; - } - const modified = computed(() => Object.values(modifiedStates).some(v => v)); - const modifiedCount = computed(() => Object.values(modifiedStates).filter(v => v).length); - - watch([currentState, previousState], () => { - for (const key in modifiedStates) { - modifiedStates[key] = !deepEqual(currentState[key], previousState[key]); - } - }, { deep: true }); - - async function _save() { - await save(unwrapReactive(currentState)); - for (const key in currentState) { - previousState[key] = copy(currentState[key]); - } - } - - function discard() { - for (const key in currentState) { - currentState[key] = copy(previousState[key]); - } - } - - return { - state: currentState, - savedState: previousState, - modifiedStates, - modified, - modifiedCount, - save: _save, - discard, - }; -} diff --git a/packages/frontend/src/use/use-leave-guard.ts b/packages/frontend/src/use/use-leave-guard.ts deleted file mode 100644 index 395c12a756..0000000000 --- a/packages/frontend/src/use/use-leave-guard.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Ref } from 'vue'; - -export function useLeaveGuard(enabled: Ref<boolean>) { - /* TODO - const setLeaveGuard = inject('setLeaveGuard'); - - if (setLeaveGuard) { - setLeaveGuard(async () => { - if (!enabled.value) return false; - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.leaveConfirm, - }); - - return canceled; - }); - } else { - onBeforeRouteLeave(async (to, from) => { - if (!enabled.value) return true; - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.leaveConfirm, - }); - - return !canceled; - }); - } - */ - - /* - function onBeforeLeave(ev: BeforeUnloadEvent) { - if (enabled.value) { - ev.preventDefault(); - ev.returnValue = ''; - } - } - - window.addEventListener('beforeunload', onBeforeLeave); - onUnmounted(() => { - window.removeEventListener('beforeunload', onBeforeLeave); - }); - */ -} diff --git a/packages/frontend/src/use/use-mutation-observer.ts b/packages/frontend/src/use/use-mutation-observer.ts deleted file mode 100644 index 7b774022dc..0000000000 --- a/packages/frontend/src/use/use-mutation-observer.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onUnmounted, watch } from 'vue'; -import type { Ref } from 'vue'; - -export function useMutationObserver(targetNodeRef: Ref<HTMLElement | null | undefined>, options: MutationObserverInit, callback: MutationCallback): void { - const observer = new MutationObserver(callback); - - watch(targetNodeRef, (targetNode) => { - if (targetNode) { - observer.observe(targetNode, options); - } - }, { immediate: true }); - - onUnmounted(() => { - observer.disconnect(); - }); -} diff --git a/packages/frontend/src/use/use-note-capture.ts b/packages/frontend/src/use/use-note-capture.ts deleted file mode 100644 index 2f33c25a0a..0000000000 --- a/packages/frontend/src/use/use-note-capture.ts +++ /dev/null @@ -1,283 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onUnmounted } from 'vue'; -import * as Misskey from 'misskey-js'; -import { EventEmitter } from 'eventemitter3'; -import type { Reactive, Ref } from 'vue'; -import { useStream } from '@/stream.js'; -import { $i } from '@/i.js'; -import { store } from '@/store.js'; -import { misskeyApi } from '@/utility/misskey-api.js'; -import { prefer } from '@/preferences.js'; -import { globalEvents } from '@/events.js'; - -export const noteEvents = new EventEmitter<{ - [ev: `reacted:${string}`]: (ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }) => void; - [ev: `unreacted:${string}`]: (ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }) => void; - [ev: `pollVoted:${string}`]: (ctx: { userId: Misskey.entities.User['id']; choice: string; }) => void; -}>(); - -const fetchEvent = new EventEmitter<{ - [id: string]: Pick<Misskey.entities.Note, 'reactions' | 'reactionEmojis'>; -}>(); - -const pollingQueue = new Map<string, { - referenceCount: number; - lastAddedAt: number; -}>(); - -function pollingEnqueue(note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>) { - if (pollingQueue.has(note.id)) { - const data = pollingQueue.get(note.id)!; - pollingQueue.set(note.id, { - ...data, - referenceCount: data.referenceCount + 1, - lastAddedAt: Date.now(), - }); - } else { - pollingQueue.set(note.id, { - referenceCount: 1, - lastAddedAt: Date.now(), - }); - } -} - -function pollingDequeue(note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>) { - const data = pollingQueue.get(note.id); - if (data == null) return; - - if (data.referenceCount === 1) { - pollingQueue.delete(note.id); - } else { - pollingQueue.set(note.id, { - ...data, - referenceCount: data.referenceCount - 1, - }); - } -} - -const CAPTURE_MAX = 30; -const MIN_POLLING_INTERVAL = 1000 * 10; -const POLLING_INTERVAL = - prefer.s.pollingInterval === 1 ? MIN_POLLING_INTERVAL * 1.5 * 1.5 : - prefer.s.pollingInterval === 2 ? MIN_POLLING_INTERVAL * 1.5 : - prefer.s.pollingInterval === 3 ? MIN_POLLING_INTERVAL : - MIN_POLLING_INTERVAL; - -window.setInterval(() => { - const ids = [...pollingQueue.entries()] - .filter(([k, v]) => Date.now() - v.lastAddedAt < 1000 * 60 * 5) // 追加されてから一定時間経過したものは省く - .map(([k, v]) => k) - .sort((a, b) => (a > b ? -1 : 1)) // 新しいものを優先するためにIDで降順ソート - .slice(0, CAPTURE_MAX); - - if (ids.length === 0) return; - if (window.document.hidden) return; - - // まとめてリクエストするのではなく、個別にHTTPリクエスト投げてCDNにキャッシュさせた方がサーバーの負荷低減には良いかもしれない? - misskeyApi('notes/show-partial-bulk', { - noteIds: ids, - }).then((items) => { - for (const item of items) { - fetchEvent.emit(item.id, { - reactions: item.reactions, - reactionEmojis: item.reactionEmojis, - }); - } - }); -}, POLLING_INTERVAL); - -function pollingSubscribe(props: { - note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>; - $note: ReactiveNoteData; -}) { - const { note, $note } = props; - - function onFetched(data: Pick<Misskey.entities.Note, 'reactions' | 'reactionEmojis'>): void { - $note.reactions = data.reactions; - $note.reactionCount = Object.values(data.reactions).reduce((a, b) => a + b, 0); - $note.reactionEmojis = data.reactionEmojis; - } - - pollingEnqueue(note); - fetchEvent.on(note.id, onFetched); - - onUnmounted(() => { - pollingDequeue(note); - fetchEvent.off(note.id, onFetched); - }); -} - -function realtimeSubscribe(props: { - note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>; -}): void { - const note = props.note; - const connection = useStream(); - - function onStreamNoteUpdated(noteData): void { - const { type, id, body } = noteData; - - if (id !== note.id) return; - - switch (type) { - case 'reacted': { - noteEvents.emit(`reacted:${id}`, { - userId: body.userId, - reaction: body.reaction, - emoji: body.emoji, - }); - break; - } - - case 'unreacted': { - noteEvents.emit(`unreacted:${id}`, { - userId: body.userId, - reaction: body.reaction, - emoji: body.emoji, - }); - break; - } - - case 'pollVoted': { - noteEvents.emit(`pollVoted:${id}`, { - userId: body.userId, - choice: body.choice, - }); - break; - } - - case 'deleted': { - globalEvents.emit('noteDeleted', id); - break; - } - } - } - - function capture(withHandler = false): void { - connection.send('sr', { id: note.id }); - if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); - } - - function decapture(withHandler = false): void { - connection.send('un', { id: note.id }); - if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated); - } - - function onStreamConnected() { - capture(false); - } - - capture(true); - connection.on('_connected_', onStreamConnected); - - onUnmounted(() => { - decapture(true); - connection.off('_connected_', onStreamConnected); - }); -} - -type ReactiveNoteData = Reactive<{ - reactions: Misskey.entities.Note['reactions']; - reactionCount: Misskey.entities.Note['reactionCount']; - reactionEmojis: Misskey.entities.Note['reactionEmojis']; - myReaction: Misskey.entities.Note['myReaction']; - pollChoices: NonNullable<Misskey.entities.Note['poll']>['choices']; -}>; - -export function useNoteCapture(props: { - note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>; - parentNote: Misskey.entities.Note | null; - $note: ReactiveNoteData; -}) { - const { note, parentNote, $note } = props; - - noteEvents.on(`reacted:${note.id}`, onReacted); - noteEvents.on(`unreacted:${note.id}`, onUnreacted); - noteEvents.on(`pollVoted:${note.id}`, onPollVoted); - - let latestReactedKey: string | null = null; - let latestUnreactedKey: string | null = null; - let latestPollVotedKey: string | null = null; - - function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void { - const newReactedKey = `${ctx.userId}:${ctx.reaction}`; - if (newReactedKey === latestReactedKey) return; - latestReactedKey = newReactedKey; - - if (ctx.emoji && !(ctx.emoji.name in $note.reactionEmojis)) { - $note.reactionEmojis[ctx.emoji.name] = ctx.emoji.url; - } - - const currentCount = $note.reactions[ctx.reaction] || 0; - - $note.reactions[ctx.reaction] = currentCount + 1; - $note.reactionCount += 1; - - if ($i && (ctx.userId === $i.id)) { - $note.myReaction = ctx.reaction; - } - } - - function onUnreacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void { - const newUnreactedKey = `${ctx.userId}:${ctx.reaction}`; - if (newUnreactedKey === latestUnreactedKey) return; - latestUnreactedKey = newUnreactedKey; - - const currentCount = $note.reactions[ctx.reaction] || 0; - - $note.reactions[ctx.reaction] = Math.max(0, currentCount - 1); - $note.reactionCount = Math.max(0, $note.reactionCount - 1); - if ($note.reactions[ctx.reaction] === 0) delete $note.reactions[ctx.reaction]; - - if ($i && (ctx.userId === $i.id)) { - $note.myReaction = null; - } - } - - function onPollVoted(ctx: { userId: Misskey.entities.User['id']; choice: string; }): void { - const newPollVotedKey = `${ctx.userId}:${ctx.choice}`; - if (newPollVotedKey === latestPollVotedKey) return; - latestPollVotedKey = newPollVotedKey; - - const choices = [...$note.pollChoices]; - choices[ctx.choice] = { - ...choices[ctx.choice], - votes: choices[ctx.choice].votes + 1, - ...($i && (ctx.userId === $i.id) ? { - isVoted: true, - } : {}), - }; - - $note.pollChoices = choices; - } - - onUnmounted(() => { - noteEvents.off(`reacted:${note.id}`, onReacted); - noteEvents.off(`unreacted:${note.id}`, onUnreacted); - noteEvents.off(`pollVoted:${note.id}`, onPollVoted); - }); - - // 投稿からある程度経過している(=タイムラインを遡って表示した)ノートは、イベントが発生する可能性が低いためそもそも購読しない - // ただし「リノートされたばかりの過去のノート」(= parentNoteが存在し、かつparentNoteの投稿日時が最近)はイベント発生が考えられるため購読する - // TODO: デバイスとサーバーの時計がズレていると不具合の元になるため、ズレを検知して警告を表示するなどのケアが必要かもしれない - if (parentNote == null) { - if ((Date.now() - new Date(note.createdAt).getTime()) > 1000 * 60 * 5) { // 5min - // リノートで表示されているノートでもないし、投稿からある程度経過しているので購読しない - return; - } - } else { - if ((Date.now() - new Date(parentNote.createdAt).getTime()) > 1000 * 60 * 5) { // 5min - // リノートで表示されているノートだが、リノートされてからある程度経過しているので購読しない - return; - } - } - - if ($i && store.s.realtimeMode) { - realtimeSubscribe(props); - } else { - pollingSubscribe(props); - } -} diff --git a/packages/frontend/src/use/use-pagination.ts b/packages/frontend/src/use/use-pagination.ts deleted file mode 100644 index f1042985bf..0000000000 --- a/packages/frontend/src/use/use-pagination.ts +++ /dev/null @@ -1,258 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { computed, isRef, onMounted, ref, shallowRef, triggerRef, watch } from 'vue'; -import * as Misskey from 'misskey-js'; -import type { ComputedRef, DeepReadonly, Ref, ShallowRef } from 'vue'; -import { misskeyApi } from '@/utility/misskey-api.js'; - -const MAX_ITEMS = 30; -const MAX_QUEUE_ITEMS = 100; -const FIRST_FETCH_LIMIT = 15; -const SECOND_FETCH_LIMIT = 30; - -export type MisskeyEntity = { - id: string; - createdAt: string; - _shouldInsertAd_?: boolean; - [x: string]: any; -}; - -export type PagingCtx<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints> = { - endpoint: E; - limit?: number; - params?: Misskey.Endpoints[E]['req'] | ComputedRef<Misskey.Endpoints[E]['req']>; - - /** - * 検索APIのような、ページング不可なエンドポイントを利用する場合 - * (そのようなAPIをこの関数で使うのは若干矛盾してるけど) - */ - noPaging?: boolean; - - offsetMode?: boolean; - - baseId?: MisskeyEntity['id']; - direction?: 'newer' | 'older'; -}; - -export function usePagination<T extends MisskeyEntity>(props: { - ctx: PagingCtx; - 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 canFetchOlder = ref(false); - const error = ref(false); - - // パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど) - 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.baseId && props.ctx.direction === 'newer' ? { - sinceId: props.ctx.baseId, - } : props.ctx.baseId && props.ctx.direction === 'older' ? { - untilId: props.ctx.baseId, - } : {}), - }).then(res => { - // 逆順で返ってくるので - if (props.ctx.baseId && props.ctx.direction === 'newer') { - res.reverse(); - } - - for (let i = 0; i < res.length; i++) { - const item = res[i]; - if (i === 3) item._shouldInsertAd_ = true; - } - - if (res.length === 0 || props.ctx.noPaging) { - pushItems(res); - canFetchOlder.value = false; - } else { - pushItems(res); - 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; - } - - if (res.length === 0) { - canFetchOlder.value = false; - fetchingOlder.value = false; - } else { - pushItems(res); - canFetchOlder.value = true; - fetchingOlder.value = false; - } - }, err => { - fetchingOlder.value = false; - }); - } - - async function fetchNewer(options: { - toQueue?: boolean; - } = {}): Promise<void> { - 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 { - unshiftItems(res.toReversed()); - } - }); - } - - 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); - } - } - - onMounted(() => { - init(); - }); - - return { - items: items as DeepReadonly<ShallowRef<T[]>>, - queuedAheadItemsCount, - fetching, - fetchingOlder, - canFetchOlder, - init, - reload, - fetchOlder, - fetchNewer, - unshiftItems, - prepend, - trim, - removeItem, - updateItem, - enqueue, - releaseQueue, - error, - }; -} diff --git a/packages/frontend/src/use/use-scroll-position-keeper.ts b/packages/frontend/src/use/use-scroll-position-keeper.ts deleted file mode 100644 index b584171cbe..0000000000 --- a/packages/frontend/src/use/use-scroll-position-keeper.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { throttle } from 'throttle-debounce'; -import { nextTick, onActivated, onDeactivated, onUnmounted, watch } from 'vue'; -import type { Ref } from 'vue'; - -// note render skippingがオンだとズレるため、遷移直前にスクロール範囲に表示されているdata-scroll-anchor要素を特定して、復元時に当該要素までスクロールするようにする - -// TODO: data-scroll-anchor がひとつも存在しない場合、または手動で useAnchor みたいなフラグをfalseで呼ばれた場合、単純にスクロール位置を使用する処理にフォールバックするようにする - -export function useScrollPositionKeeper(scrollContainerRef: Ref<HTMLElement | null | undefined>): void { - let anchorId: string | null = null; - let ready = true; - - watch(scrollContainerRef, (el) => { - if (!el) return; - - const onScroll = () => { - if (!el) return; - if (!ready) return; - - const scrollContainerRect = el.getBoundingClientRect(); - const viewPosition = scrollContainerRect.height / 2; - - const anchorEls = el.querySelectorAll('[data-scroll-anchor]'); - for (let i = anchorEls.length - 1; i > -1; i--) { // 下から見た方が速い - const anchorEl = anchorEls[i] as HTMLElement; - const anchorRect = anchorEl.getBoundingClientRect(); - const anchorTop = anchorRect.top; - const anchorBottom = anchorRect.bottom; - if (anchorTop <= viewPosition && anchorBottom >= viewPosition) { - anchorId = anchorEl.getAttribute('data-scroll-anchor'); - break; - } - } - }; - - // ほんとはscrollイベントじゃなくてonBeforeDeactivatedでやりたい - // https://github.com/vuejs/vue/issues/9454 - // https://github.com/vuejs/rfcs/pull/284 - el.addEventListener('scroll', throttle(1000, onScroll), { passive: true }); - }, { - immediate: true, - }); - - const restore = () => { - if (!anchorId) return; - const scrollContainer = scrollContainerRef.value; - if (!scrollContainer) return; - const scrollAnchorEl = scrollContainer.querySelector(`[data-scroll-anchor="${anchorId}"]`); - if (!scrollAnchorEl) return; - scrollAnchorEl.scrollIntoView({ - behavior: 'instant', - block: 'center', - inline: 'center', - }); - }; - - onDeactivated(() => { - ready = false; - }); - - onActivated(() => { - restore(); - nextTick(() => { - restore(); - window.setTimeout(() => { - restore(); - - ready = true; - }, 100); - }); - }); -} diff --git a/packages/frontend/src/use/use-tooltip.ts b/packages/frontend/src/use/use-tooltip.ts deleted file mode 100644 index af76a3a1e8..0000000000 --- a/packages/frontend/src/use/use-tooltip.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ref, watch, onUnmounted } from 'vue'; -import type { Ref } from 'vue'; - -export function useTooltip( - elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>, - onShow: (showing: Ref<boolean>) => void, - delay = 300, -): void { - let isHovering = false; - - // iOS(Androidも?)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、それを無視するためのフラグ - // 無視しないと、画面に触れてないのにツールチップが出たりし、ユーザビリティが損なわれる - // TODO: 一度でもタップすると二度とマウスでツールチップ出せなくなるのをどうにかする 定期的にfalseに戻すとか...? - let shouldIgnoreMouseover = false; - - let timeoutId: number; - - let changeShowingState: (() => void) | null; - - let autoHidingTimer; - - const open = () => { - close(); - if (!isHovering) return; - if (elRef.value == null) return; - const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; - if (!window.document.body.contains(el)) return; // openしようとしたときに既に元要素がDOMから消えている場合があるため - - const showing = ref(true); - onShow(showing); - changeShowingState = () => { - showing.value = false; - }; - - autoHidingTimer = window.setInterval(() => { - if (elRef.value == null || !window.document.body.contains(elRef.value instanceof Element ? elRef.value : elRef.value.$el)) { - if (!isHovering) return; - isHovering = false; - window.clearTimeout(timeoutId); - close(); - window.clearInterval(autoHidingTimer); - } - }, 1000); - }; - - const close = () => { - if (changeShowingState != null) { - changeShowingState(); - changeShowingState = null; - } - }; - - const onMouseover = () => { - if (isHovering) return; - if (shouldIgnoreMouseover) return; - isHovering = true; - timeoutId = window.setTimeout(open, delay); - }; - - const onMouseleave = () => { - if (!isHovering) return; - isHovering = false; - window.clearTimeout(timeoutId); - window.clearInterval(autoHidingTimer); - close(); - }; - - const onTouchstart = () => { - shouldIgnoreMouseover = true; - if (isHovering) return; - isHovering = true; - timeoutId = window.setTimeout(open, delay); - }; - - const onTouchend = () => { - if (!isHovering) return; - isHovering = false; - window.clearTimeout(timeoutId); - window.clearInterval(autoHidingTimer); - close(); - }; - - const stop = watch(elRef, () => { - if (elRef.value) { - stop(); - const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; - el.addEventListener('mouseover', onMouseover, { passive: true }); - el.addEventListener('mouseleave', onMouseleave, { passive: true }); - el.addEventListener('touchstart', onTouchstart, { passive: true }); - el.addEventListener('touchend', onTouchend, { passive: true }); - el.addEventListener('click', close, { passive: true }); - } - }, { - immediate: true, - flush: 'post', - }); - - onUnmounted(() => { - close(); - }); -} |