diff --git a/CHANGELOG.md b/CHANGELOG.md
index f52226a635..bd35691871 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,8 @@
- Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正
- Fix: チャートのラベルが消えている問題を修正
- Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正
+- Fix: 設定のバックアップ作成時に名前を入力しなかった場合、ローカライゼーションがおかしくなる問題を修正
+- Fix: ページ`/admin/emojis`の絵文字編集ダイアログで「リアクションとして使えるロール」を追加する際に何も選択せずOKを押下すると画面が固まる問題を修正
- Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正
### Server
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index e606fe368c..7f20e0b1a2 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -290,7 +290,7 @@ export async function openAccountMenu(opts: {
text: i18n.ts.profile,
to: `/@${ $i.username }`,
avatar: $i,
- }, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
+ }, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
type: 'parent' as const,
icon: 'ti ti-plus',
text: i18n.ts.addAccount,
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 4b7584faaa..4577d37c08 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -38,11 +38,6 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
-
{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}
@@ -64,7 +59,7 @@ import MkSelect from '@/components/MkSelect.vue';
import { i18n } from '@/i18n.js';
type Input = {
- type: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
+ type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
placeholder?: string | null;
autocomplete?: string;
default: string | number | null;
@@ -74,22 +69,17 @@ type Input = {
type Select = {
items: {
- value: string;
+ value: any;
text: string;
}[];
- groupedItems: {
- label: string;
- items: {
- value: string;
- text: string;
- }[];
- }[];
default: string | null;
};
+type Result = string | number | true | null;
+
const props = withDefaults(defineProps<{
type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting';
- title: string;
+ title?: string;
text?: string;
input?: Input;
select?: Select;
@@ -113,7 +103,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'done', v: { canceled: boolean; result: any }): void;
+ (ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void;
(ev: 'closed'): void;
}>();
@@ -139,8 +129,11 @@ const okButtonDisabledReason = computed
();
const dialog = shallowRef>();
-const selected = ref([]);
+const selected = ref([]);
function ok() {
emit('done', selected.value);
@@ -57,7 +57,7 @@ function cancel() {
dialog.value?.close();
}
-function onChangeSelection(files: Misskey.entities.DriveFile[]) {
- selected.value = files;
+function onChangeSelection(v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]) {
+ selected.value = v;
}
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 59f4b51522..adcea839ee 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'done', v: any): void;
+ (ev: 'done', v: string): void;
(ev: 'close'): void;
(ev: 'closed'): void;
}>();
@@ -64,7 +64,7 @@ const emit = defineEmits<{
const modal = shallowRef>();
const picker = shallowRef>();
-function chosen(emoji: any) {
+function chosen(emoji: string) {
emit('done', emoji);
if (props.choseAndClose) {
modal.value?.close();
diff --git a/packages/frontend/src/components/MkEmojiPickerWindow.vue b/packages/frontend/src/components/MkEmojiPickerWindow.vue
deleted file mode 100644
index 6952943345..0000000000
--- a/packages/frontend/src/components/MkEmojiPickerWindow.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 0d8734799c..deedc5badb 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -21,37 +21,37 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
- ({{ i18n.ts.optional }})
- {{ form[item].description }}
+
+
+ ({{ i18n.ts.optional }})
+ {{ v.description }}
-
- ({{ i18n.ts.optional }})
- {{ form[item].description }}
+
+ ({{ i18n.ts.optional }})
+ {{ v.description }}
-
- ({{ i18n.ts.optional }})
- {{ form[item].description }}
+
+ ({{ i18n.ts.optional }})
+ {{ v.description }}
-
-
- {{ form[item].description }}
+
+
+ {{ v.description }}
-
- ({{ i18n.ts.optional }})
-
+
+ ({{ i18n.ts.optional }})
+
-
- ({{ i18n.ts.optional }})
-
+
+ ({{ i18n.ts.optional }})
+
-
- ({{ i18n.ts.optional }})
- {{ form[item].description }}
+
+ ({{ i18n.ts.optional }})
+ {{ v.description }}
-
-
+
+
@@ -72,19 +72,21 @@ import MkSelect from './MkSelect.vue';
import MkRange from './MkRange.vue';
import MkButton from './MkButton.vue';
import MkRadios from './MkRadios.vue';
+import type { Form } from '@/scripts/form.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
const props = defineProps<{
title: string;
- form: any;
+ form: Form;
}>();
const emit = defineEmits<{
(ev: 'done', v: {
- canceled?: boolean;
- result?: any;
+ canceled: true;
+ } | {
+ result: Record;
}): void;
(ev: 'closed'): void;
}>();
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index a4fde6b701..c561e84a23 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -7,9 +7,9 @@
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
import { EventEmitter } from 'eventemitter3';
-import insertTextAtCursor from 'insert-text-at-cursor';
import * as Misskey from 'misskey-js';
-import type { ComponentProps } from 'vue-component-type-helpers';
+import type { ComponentProps as CP } from 'vue-component-type-helpers';
+import type { Form, GetFormResultType } from '@/scripts/form.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
@@ -19,7 +19,6 @@ import MkToast from '@/components/MkToast.vue';
import MkDialog from '@/components/MkDialog.vue';
import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
-import MkEmojiPickerWindow from '@/components/MkEmojiPickerWindow.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue';
import { MenuItem } from '@/types/menu.js';
@@ -28,15 +27,15 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
export const openingWindowsCount = ref(0);
-export const apiWithDialog = ((
- endpoint: string,
- data: Record = {},
+export const apiWithDialog = ((
+ endpoint: E,
+ data: P = {} as any,
token?: string | null | undefined,
) => {
const promise = misskeyApi(endpoint, data, token);
promiseDialog(promise, null, async (err) => {
- let title = null;
- let text = err.message + '\n' + (err as any).id;
+ let title: string | undefined;
+ let text = err.message + '\n' + err.id;
if (err.code === 'INTERNAL_ERROR') {
title = i18n.ts.internalServerError;
text = i18n.ts.internalServerErrorDescription;
@@ -88,7 +87,7 @@ export const apiWithDialog = ((
export function promiseDialog>(
promise: T,
onSuccess?: ((res: any) => void) | null,
- onFailure?: ((err: Error) => void) | null,
+ onFailure?: ((err: Misskey.api.APIError) => void) | null,
text?: string,
): T {
const showing = ref(true);
@@ -149,14 +148,30 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
// 使い物にならないので、代わりに ['$props'] から色々省くことで emit の型を生成する
// FIXME: 何故か *.ts ファイルからだと型がうまく取れない?ことがあるのをなんとかしたい
type ComponentEmit = T extends new () => { $props: infer Props }
- ? EmitsExtractor
- : never;
+ ? [keyof Pick>] extends [never]
+ ? Record // *.ts ファイルから型がうまく取れないとき用(これがないと {} になって型エラーがうるさい)
+ : EmitsExtractor
+ : T extends (...args: any) => any
+ ? ReturnType extends { [x: string]: any; __ctx?: { [x: string]: any; props: infer Props } }
+ ? [keyof Pick>] extends [never]
+ ? Record
+ : EmitsExtractor
+ : never
+ : never;
+
+// props に ref を許可するようにする
+type ComponentProps = { [K in keyof CP]: CP[K] | Ref[K]> };
type EmitsExtractor = {
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize : K extends string ? never : K]: T[K];
};
-export async function popup(component: T, props: ComponentProps, events: ComponentEmit = {} as ComponentEmit, disposeEvent?: keyof ComponentEmit) {
+export async function popup(
+ component: T,
+ props: ComponentProps,
+ events: ComponentEmit = {} as ComponentEmit,
+ disposeEvent?: keyof ComponentEmit,
+): Promise<{ dispose: () => void }> {
markRaw(component);
const id = ++popupIdCount;
@@ -197,12 +212,12 @@ export function toast(message: string) {
export function alert(props: {
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
}): Promise {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, props, {
- done: result => {
+ done: () => {
resolve();
},
}, 'closed');
@@ -211,12 +226,12 @@ export function alert(props: {
export function confirm(props: {
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
okText?: string;
cancelText?: string;
}): Promise<{ canceled: boolean }> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
...props,
showCancelButton: true,
@@ -237,13 +252,15 @@ export function actions(props: {
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
actions: T;
-}): Promise<{ canceled: true; result: undefined; } | {
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
canceled: false; result: T[number]['value'];
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
...props,
actions: props.actions.map(a => ({
@@ -262,19 +279,50 @@ export function actions;
+export function inputText(props: {
+ type?: 'text' | 'email' | 'password' | 'url';
+ title?: string;
+ text?: string;
placeholder?: string | null;
autocomplete?: string;
default?: string | null;
minLength?: number;
maxLength?: number;
-}): Promise<{ canceled: true; result: undefined; } | {
- canceled: false; result: string;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: string | null;
+}>;
+export function inputText(props: {
+ type?: 'text' | 'email' | 'password' | 'url';
+ title?: string;
+ text?: string;
+ placeholder?: string | null;
+ autocomplete?: string;
+ default?: string | null;
+ minLength?: number;
+ maxLength?: number;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: string | null;
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
title: props.title,
text: props.text,
@@ -282,7 +330,7 @@ export function inputText(props: {
type: props.type,
placeholder: props.placeholder,
autocomplete: props.autocomplete,
- default: props.default,
+ default: props.default ?? null,
minLength: props.minLength,
maxLength: props.maxLength,
},
@@ -294,16 +342,41 @@ export function inputText(props: {
});
}
+// default が指定されていたら result は null になり得ないことを保証する overload function
export function inputNumber(props: {
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
+ placeholder?: string | null;
+ autocomplete?: string;
+ default: number;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: number;
+}>;
+export function inputNumber(props: {
+ title?: string;
+ text?: string;
placeholder?: string | null;
autocomplete?: string;
default?: number | null;
-}): Promise<{ canceled: true; result: undefined; } | {
- canceled: false; result: number;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: number | null;
+}>;
+export function inputNumber(props: {
+ title?: string;
+ text?: string;
+ placeholder?: string | null;
+ autocomplete?: string;
+ default?: number | null;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: number | null;
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
title: props.title,
text: props.text,
@@ -311,7 +384,7 @@ export function inputNumber(props: {
type: 'number',
placeholder: props.placeholder,
autocomplete: props.autocomplete,
- default: props.default,
+ default: props.default ?? null,
},
}, {
done: result => {
@@ -322,34 +395,38 @@ export function inputNumber(props: {
}
export function inputDate(props: {
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
placeholder?: string | null;
- default?: Date | null;
-}): Promise<{ canceled: true; result: undefined; } | {
+ default?: string | null;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
canceled: false; result: Date;
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
title: props.title,
text: props.text,
input: {
type: 'date',
placeholder: props.placeholder,
- default: props.default,
+ default: props.default ?? null,
},
}, {
done: result => {
- resolve(result ? { result: new Date(result.result), canceled: false } : { canceled: true });
+ resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
},
}, 'closed');
});
}
-export function authenticateDialog(): Promise<{ canceled: true; result: undefined; } | {
+export function authenticateDialog(): Promise<{
+ canceled: true; result: undefined;
+} | {
canceled: false; result: { password: string; token: string | null; };
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkPasswordDialog, {}, {
done: result => {
resolve(result ? { canceled: false, result } : { canceled: true, result: undefined });
@@ -358,34 +435,53 @@ export function authenticateDialog(): Promise<{ canceled: true; result: undefine
});
}
+// default が指定されていたら result は null になり得ないことを保証する overload function
export function select(props: {
- title?: string | null;
- text?: string | null;
- default?: string | null;
-} & ({
+ title?: string;
+ text?: string;
+ default: string;
items: {
value: C;
text: string;
}[];
+}): Promise<{
+ canceled: true; result: undefined;
} | {
- groupedItems: {
- label: string;
- items: {
- value: C;
- text: string;
- }[];
- }[];
-})): Promise<{ canceled: true; result: undefined; } | {
canceled: false; result: C;
+}>;
+export function select(props: {
+ title?: string;
+ text?: string;
+ default?: string | null;
+ items: {
+ value: C;
+ text: string;
+ }[];
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: C | null;
+}>;
+export function select(props: {
+ title?: string;
+ text?: string;
+ default?: string | null;
+ items: {
+ value: C;
+ text: string;
+ }[];
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: C | null;
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
title: props.title,
text: props.text,
select: {
items: props.items,
- groupedItems: props.groupedItems,
- default: props.default,
+ default: props.default ?? null,
},
}, {
done: result => {
@@ -396,7 +492,7 @@ export function select(props: {
}
export function success(): Promise {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
const showing = ref(true);
window.setTimeout(() => {
showing.value = false;
@@ -411,7 +507,7 @@ export function success(): Promise {
}
export function waiting(): Promise {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
const showing = ref(true);
popup(MkWaitingDialog, {
success: false,
@@ -422,9 +518,9 @@ export function waiting(): Promise {
});
}
-export function form(title, form) {
- return new Promise((resolve, reject) => {
- popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form }, {
+export function form(title: string, f: F): Promise<{ canceled: true } | { result: GetFormResultType }> {
+ return new Promise(resolve => {
+ popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
done: result => {
resolve(result);
},
@@ -433,7 +529,7 @@ export function form(title, form) {
}
export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
includeSelf: opts.includeSelf,
localOnly: opts.localOnly,
@@ -446,7 +542,7 @@ export async function selectUser(opts: { includeSelf?: boolean; localOnly?: bool
}
export async function selectDriveFile(multiple: boolean): Promise {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
type: 'file',
multiple,
@@ -460,23 +556,23 @@ export async function selectDriveFile(multiple: boolean): Promise {
+export async function selectDriveFolder(multiple: boolean): Promise {
+ return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
type: 'folder',
multiple,
}, {
done: folders => {
if (folders) {
- resolve(multiple ? folders : folders[0]);
+ resolve(folders);
}
},
}, 'closed');
});
}
-export async function pickEmoji(src: HTMLElement | null, opts) {
- return new Promise((resolve, reject) => {
+export async function pickEmoji(src: HTMLElement, opts: ComponentProps): Promise {
+ return new Promise(resolve => {
popup(MkEmojiPickerDialog, {
src,
...opts,
@@ -492,7 +588,7 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
aspectRatio: number;
uploadFolder?: string | null;
}): Promise {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
file: image,
aspectRatio: options.aspectRatio,
@@ -505,67 +601,13 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
});
}
-type AwaitType =
- T extends Promise ? U :
- T extends (...args: any[]) => Promise ? V :
- T;
-let openingEmojiPicker: AwaitType> | null = null;
-let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
-export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) {
- if (openingEmojiPicker) return;
-
- activeTextarea = initialTextarea;
-
- const textareas = document.querySelectorAll('textarea, input');
- for (const textarea of Array.from(textareas)) {
- textarea.addEventListener('focus', () => {
- activeTextarea = textarea;
- });
- }
-
- const observer = new MutationObserver(records => {
- for (const record of records) {
- for (const node of Array.from(record.addedNodes).filter(node => node instanceof HTMLElement) as HTMLElement[]) {
- const textareas = node.querySelectorAll('textarea, input') as NodeListOf>;
- for (const textarea of Array.from(textareas).filter(textarea => textarea.dataset.preventEmojiInsert == null)) {
- if (document.activeElement === textarea) activeTextarea = textarea;
- textarea.addEventListener('focus', () => {
- activeTextarea = textarea;
- });
- }
- }
- }
- });
-
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- attributes: false,
- characterData: false,
- });
-
- openingEmojiPicker = await popup(MkEmojiPickerWindow, {
- src,
- ...opts,
- }, {
- chosen: emoji => {
- insertTextAtCursor(activeTextarea, emoji);
- },
- closed: () => {
- openingEmojiPicker!.dispose();
- openingEmojiPicker = null;
- observer.disconnect();
- },
- });
-}
-
-export function popupMenu(items: MenuItem[] | Ref