{{ i18n.ts._visibility.followers }}
@@ -54,6 +54,7 @@ const props = withDefaults(defineProps<{
isSilenced: boolean;
localOnly: boolean;
src?: HTMLElement;
+ isReplyVisibilitySpecified?: boolean;
}>(), {
});
diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts
index 0bac4d0b7c..9e41926a97 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend/src/const.ts
@@ -75,6 +75,7 @@ export const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
+ 'mentionLimit',
'canInvite',
'inviteLimit',
'inviteLimitCycle',
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
@@ -62,6 +67,7 @@ import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { deepClone } from '@/scripts/clone.js';
+import { rolesCache } from '@/cache.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -77,6 +83,8 @@ const props = defineProps<{
const v = ref(deepClone(props.modelValue));
+const roles = await rolesCache.fetch();
+
watch(() => props.modelValue, () => {
if (JSON.stringify(props.modelValue) === JSON.stringify(v.value)) return;
v.value = deepClone(props.modelValue);
@@ -92,6 +100,7 @@ const type = computed({
if (t === 'and') v.value.values = [];
if (t === 'or') v.value.values = [];
if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' };
+ if (t === 'roleAssignedTo') v.value.roleId = '';
if (t === 'createdLessThan') v.value.sec = 86400;
if (t === 'createdMoreThan') v.value.sec = 86400;
if (t === 'followersLessThanOrEq') v.value.value = 10;
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index ad9df35dbf..eb8a59b34f 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -160,6 +160,25 @@ SPDX-License-Identifier: AGPL-3.0-only