mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-04-30 18:05:51 +02:00
Compare commits
2 Commits
renovate/m
...
2026.4.0-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
665adfccb7 | ||
|
|
973b5b50a9 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.4.0-beta.1",
|
||||
"version": "2026.4.0-beta.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
||||
|
||||
import { markRaw, ref, defineAsyncComponent, nextTick, effectScope, isRef, shallowReactive, watch } from 'vue';
|
||||
import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { Component, MaybeRef, ShallowReactive } from 'vue';
|
||||
import type { Component, MaybeRef } 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';
|
||||
@@ -145,7 +145,7 @@ let popupIdCount = 0;
|
||||
export const popups = ref<{
|
||||
id: number;
|
||||
component: Component;
|
||||
props: ShallowReactive<Record<string, any>>;
|
||||
props: Record<string, any>;
|
||||
events: Record<string, any>;
|
||||
}[]>([]);
|
||||
|
||||
@@ -183,32 +183,6 @@ 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>(
|
||||
@@ -219,24 +193,20 @@ export function popup<T extends Component>(
|
||||
markRaw(component);
|
||||
|
||||
const id = ++popupIdCount;
|
||||
const { resolvedProps, stopSync } = normalizePopupProps(props);
|
||||
let disposed = false;
|
||||
const dispose = () => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
stopSync();
|
||||
|
||||
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
|
||||
nextTick(() => {
|
||||
popups.value = popups.value.filter(p => p.id !== id);
|
||||
});
|
||||
};
|
||||
|
||||
popups.value.push({
|
||||
const state = {
|
||||
component,
|
||||
props: resolvedProps,
|
||||
props,
|
||||
events,
|
||||
id,
|
||||
});
|
||||
};
|
||||
|
||||
popups.value.push(state);
|
||||
|
||||
return {
|
||||
dispose,
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineAsyncComponent, ref, watch } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { popup } from '@/os.js';
|
||||
import { markRaw, shallowRef, ref, watch } from 'vue';
|
||||
import type MkEmojiPickerDialog__TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
|
||||
import { popup, popupAsyncWithDialog } from '@/os.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
/**
|
||||
@@ -15,53 +15,77 @@ import { prefer } from '@/preferences.js';
|
||||
* 一度表示したダイアログを連続で使用できることが望ましいシーンでの利用が想定される。
|
||||
*/
|
||||
class EmojiPicker {
|
||||
private anchorElement: Ref<HTMLElement | null> = ref(null);
|
||||
private manualShowing = ref(false);
|
||||
private onChosen?: (emoji: string) => void;
|
||||
private onClosed?: () => void;
|
||||
private loadedComponent: typeof MkEmojiPickerDialog__TypeReferenceOnly | null = null;
|
||||
private emojisRef = ref<string[]>([]);
|
||||
|
||||
constructor() {
|
||||
// nop
|
||||
}
|
||||
|
||||
public async init() {
|
||||
const emojisRef = ref<string[]>([]);
|
||||
|
||||
watch([prefer.r.emojiPaletteForMain, prefer.r.emojiPalettes], () => {
|
||||
emojisRef.value = prefer.s.emojiPaletteForMain == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForMain)?.emojis ?? [];
|
||||
}, {
|
||||
immediate: true,
|
||||
public init() {
|
||||
// コンポーネントをプリロードしてキャッシュしておく。
|
||||
// iOS PWA では await を挟むとユーザーアクティベーションが失われfocusが効かなくなるため、
|
||||
// show() 呼び出し時には同期的に popup() できるよう事前にコンポーネントを解決しておく。
|
||||
import('@/components/MkEmojiPickerDialog.vue').then(m => {
|
||||
this.loadedComponent = markRaw(m.default);
|
||||
}).catch(err => {
|
||||
console.error('[EmojiPicker] Failed to preload MkEmojiPickerDialog:', err);
|
||||
});
|
||||
|
||||
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
|
||||
anchorElement: this.anchorElement,
|
||||
pinnedEmojis: emojisRef,
|
||||
asReactionPicker: false,
|
||||
manualShowing: this.manualShowing,
|
||||
choseAndClose: false,
|
||||
watch([prefer.r.emojiPaletteForMain, prefer.r.emojiPalettes], () => {
|
||||
this.emojisRef.value = prefer.s.emojiPaletteForMain == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForMain)?.emojis ?? [];
|
||||
}, {
|
||||
done: emoji => {
|
||||
if (this.onChosen) this.onChosen(emoji);
|
||||
},
|
||||
close: () => {
|
||||
this.manualShowing.value = false;
|
||||
},
|
||||
closed: () => {
|
||||
this.anchorElement.value = null;
|
||||
if (this.onClosed) this.onClosed();
|
||||
},
|
||||
immediate: true,
|
||||
});
|
||||
}
|
||||
|
||||
public show(
|
||||
anchorElement: HTMLElement,
|
||||
onChosen?: EmojiPicker['onChosen'],
|
||||
onClosed?: EmojiPicker['onClosed'],
|
||||
onChosen?: (emoji: string) => void,
|
||||
onClosed?: () => void,
|
||||
) {
|
||||
this.anchorElement.value = anchorElement;
|
||||
this.manualShowing.value = true;
|
||||
this.onChosen = onChosen;
|
||||
this.onClosed = onClosed;
|
||||
const anchorRef = shallowRef(anchorElement);
|
||||
|
||||
if (this.loadedComponent) {
|
||||
// コンポーネント解決済みのため同期的に popup() できる。
|
||||
// ユーザーアクティベーションコンテキストが維持されiOSでもfocusが機能する。
|
||||
const { dispose } = popup(this.loadedComponent, {
|
||||
anchorElement: anchorRef,
|
||||
pinnedEmojis: this.emojisRef,
|
||||
asReactionPicker: false,
|
||||
choseAndClose: false,
|
||||
}, {
|
||||
done: (emoji: string) => {
|
||||
if (onChosen) onChosen(emoji);
|
||||
},
|
||||
closed: () => {
|
||||
if (onClosed) onClosed();
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// フォールバック: 初回タップがプリロード完了前
|
||||
popupAsyncWithDialog(
|
||||
import('@/components/MkEmojiPickerDialog.vue').then(m => {
|
||||
this.loadedComponent = markRaw(m.default);
|
||||
return this.loadedComponent;
|
||||
}),
|
||||
{
|
||||
anchorElement: anchorRef,
|
||||
pinnedEmojis: this.emojisRef,
|
||||
asReactionPicker: false,
|
||||
choseAndClose: false,
|
||||
},
|
||||
{
|
||||
done: (emoji: string) => {
|
||||
if (onChosen) onChosen(emoji);
|
||||
},
|
||||
closed: () => {
|
||||
if (onClosed) onClosed();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,57 +4,85 @@
|
||||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { defineAsyncComponent, ref, watch } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { popup } from '@/os.js';
|
||||
import { markRaw, shallowRef, ref, watch } from 'vue';
|
||||
import type MkEmojiPickerDialog__TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
|
||||
import { popup, popupAsyncWithDialog } from '@/os.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
class ReactionPicker {
|
||||
private anchorElement: Ref<HTMLElement | null> = ref(null);
|
||||
private manualShowing = ref(false);
|
||||
private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
|
||||
private onChosen?: (reaction: string) => void;
|
||||
private onClosed?: () => void;
|
||||
private loadedComponent: typeof MkEmojiPickerDialog__TypeReferenceOnly | null = null;
|
||||
private reactionsRef = ref<string[]>([]);
|
||||
|
||||
constructor() {
|
||||
// nop
|
||||
}
|
||||
|
||||
public async init() {
|
||||
const reactionsRef = ref<string[]>([]);
|
||||
public init() {
|
||||
// コンポーネントをプリロードしてキャッシュしておく。
|
||||
// iOS PWA では await を挟むとユーザーアクティベーションが失われfocusが効かなくなるため、
|
||||
// show() 呼び出し時には同期的に popup() できるよう事前にコンポーネントを解決しておく。
|
||||
import('@/components/MkEmojiPickerDialog.vue').then(m => {
|
||||
this.loadedComponent = markRaw(m.default);
|
||||
}).catch(err => {
|
||||
console.error('[ReactionPicker] Failed to preload MkEmojiPickerDialog:', err);
|
||||
});
|
||||
|
||||
watch([prefer.r.emojiPaletteForReaction, prefer.r.emojiPalettes], () => {
|
||||
reactionsRef.value = prefer.s.emojiPaletteForReaction == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForReaction)?.emojis ?? [];
|
||||
this.reactionsRef.value = prefer.s.emojiPaletteForReaction == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForReaction)?.emojis ?? [];
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
|
||||
anchorElement: this.anchorElement,
|
||||
pinnedEmojis: reactionsRef,
|
||||
asReactionPicker: true,
|
||||
targetNote: this.targetNote,
|
||||
manualShowing: this.manualShowing,
|
||||
}, {
|
||||
done: reaction => {
|
||||
if (this.onChosen) this.onChosen(reaction);
|
||||
},
|
||||
close: () => {
|
||||
this.manualShowing.value = false;
|
||||
},
|
||||
closed: () => {
|
||||
this.anchorElement.value = null;
|
||||
if (this.onClosed) this.onClosed();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public show(anchorElement: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
|
||||
this.anchorElement.value = anchorElement;
|
||||
this.targetNote.value = targetNote;
|
||||
this.manualShowing.value = true;
|
||||
this.onChosen = onChosen;
|
||||
this.onClosed = onClosed;
|
||||
public show(
|
||||
anchorElement: HTMLElement | null,
|
||||
targetNote: Misskey.entities.Note | null,
|
||||
onChosen?: (reaction: string) => void,
|
||||
onClosed?: () => void,
|
||||
) {
|
||||
const anchorRef = shallowRef(anchorElement);
|
||||
const targetNoteRef = ref(targetNote);
|
||||
|
||||
if (this.loadedComponent) {
|
||||
// 通常パス: コンポーネント解決済みのため同期的に popup() できる。
|
||||
// ユーザーアクティベーションコンテキストが維持されiOSでもfocusが機能する。
|
||||
const { dispose } = popup(this.loadedComponent, {
|
||||
anchorElement: anchorRef,
|
||||
pinnedEmojis: this.reactionsRef,
|
||||
asReactionPicker: true,
|
||||
targetNote: targetNoteRef,
|
||||
}, {
|
||||
done: (reaction: string) => {
|
||||
if (onChosen) onChosen(reaction);
|
||||
},
|
||||
closed: () => {
|
||||
if (onClosed) onClosed();
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// フォールバック: 初回タップがプリロード完了前
|
||||
popupAsyncWithDialog(
|
||||
import('@/components/MkEmojiPickerDialog.vue').then(m => {
|
||||
this.loadedComponent = markRaw(m.default);
|
||||
return this.loadedComponent;
|
||||
}),
|
||||
{
|
||||
anchorElement: anchorRef,
|
||||
pinnedEmojis: this.reactionsRef,
|
||||
asReactionPicker: true,
|
||||
targetNote: targetNoteRef,
|
||||
},
|
||||
{
|
||||
done: (reaction: string) => {
|
||||
if (onChosen) onChosen(reaction);
|
||||
},
|
||||
closed: () => {
|
||||
if (onClosed) onClosed();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2026.4.0-beta.1",
|
||||
"version": "2026.4.0-beta.2",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
||||
Reference in New Issue
Block a user