fix(frontend): popupのりアクティビティがチャンクをまたいで切れる事がある問題を修正

This commit is contained in:
kakkokari-gtyih 2026-04-08 11:16:30 +09:00
commit 0a93f526dd

View file

@ -5,10 +5,10 @@
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
import { markRaw, ref, defineAsyncComponent, nextTick, effectScope, isRef, shallowReactive, watch } from 'vue';
import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import type { Component, MaybeRef } from 'vue';
import type { Component, MaybeRef, ShallowReactive } from 'vue';
import type { ComponentEmit, ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
@ -146,7 +146,7 @@ let popupIdCount = 0;
export const popups = ref<{
id: number;
component: Component;
props: Record<string, any>;
props: ShallowReactive<Record<string, any>>;
events: Record<string, any>;
}[]>([]);
@ -184,6 +184,32 @@ type ComponentEmitsObject<C extends Component, IE = OverloadToUnion<ComponentEmi
: (...args: any[]) => void;
}>;
// ref をそのまま保持せず popup 側の reactive props に同期するようにして、スコープをまたいでリアクティビティが切れるのを防止する
function normalizePopupProps<T extends Record<string, any>>(props: T): {
resolvedProps: ShallowReactive<T>;
stopSync: () => void;
} {
const resolvedProps = shallowReactive<T>({} as T) as T; // shallowReactiveの返り値はreadonlyだが、実際には書き換えるので元の型で扱う
const scope = effectScope();
scope.run(() => {
for (const [key, value] of Object.entries(props)) {
if (isRef(value)) {
watch(value, (resolvedValue) => {
resolvedProps[key as keyof T] = resolvedValue as T[keyof T];
}, { immediate: true });
} else {
resolvedProps[key as keyof T] = value;
}
}
});
return {
resolvedProps: resolvedProps as ShallowReactive<T>,
stopSync: () => scope.stop(),
};
}
// NOTE: ジェネリック型つきのコンポーネントでは、emitsの型推論がうまく働かない型変数を取り出すことはできないため
// NOTE: emitsがOverloadToUnionで対応しているオーバーロードの数を超える場合は、OverloadToUnionの個数を増やせばOK
export function popup<T extends Component>(
@ -194,20 +220,24 @@ export function popup<T extends Component>(
markRaw(component);
const id = ++popupIdCount;
const { resolvedProps, stopSync } = normalizePopupProps(props);
let disposed = false;
const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ
window.setTimeout(() => {
if (disposed) return;
disposed = true;
stopSync();
nextTick(() => {
popups.value = popups.value.filter(p => p.id !== id);
}, 0);
};
const state = {
component,
props,
events,
id,
});
};
popups.value.push(state);
popups.value.push({
component,
props: resolvedProps,
events,
id,
});
return {
dispose,
@ -242,27 +272,7 @@ export async function popupAsyncWithDialog<T extends Component>(
window.clearTimeout(timer);
closeWaiting();
markRaw(component);
const id = ++popupIdCount;
const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ
window.setTimeout(() => {
popups.value = popups.value.filter(p => p.id !== id);
}, 0);
};
const state = {
component,
props,
events,
id,
};
popups.value.push(state);
return {
dispose,
};
return popup(component, props, events);
}
export function pageWindow(path: string) {