summaryrefslogtreecommitdiff
path: root/packages/frontend/src/use
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2025-05-10 07:58:26 +0900
committerGitHub <noreply@github.com>2025-05-10 07:58:26 +0900
commite1cd7c94fb13f8e49667b17554d22ce8de627a2a (patch)
treea064c4b0937160cf1e26697dcfe18de8f2eb0144 /packages/frontend/src/use
parentBump version to 2025.5.1-alpha.0 (diff)
downloadmisskey-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.ts63
-rw-r--r--packages/frontend/src/use/use-form.ts57
-rw-r--r--packages/frontend/src/use/use-leave-guard.ts50
-rw-r--r--packages/frontend/src/use/use-mutation-observer.ts21
-rw-r--r--packages/frontend/src/use/use-note-capture.ts283
-rw-r--r--packages/frontend/src/use/use-pagination.ts258
-rw-r--r--packages/frontend/src/use/use-scroll-position-keeper.ts77
-rw-r--r--packages/frontend/src/use/use-tooltip.ts106
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();
- });
-}