From d429f810a9ea3fda9efed2ff51483d25a288ecc9 Mon Sep 17 00:00:00 2001 From: Ebise Lutica <7106976+EbiseLutica@users.noreply.github.com> Date: Thu, 13 Apr 2023 00:31:22 +0900 Subject: [PATCH 001/136] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41353c346b..df2265727d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ ## 13.11.2 +### Note +- 13.11.0または13.11.1から13.11.2以降にアップデートする場合、Redisのカスタム絵文字のキャッシュを削除する必要があります(https://github.com/misskey-dev/misskey/issues/10502#issuecomment-1502790755 参照) + ### General - チャンネルの検索用ページの追加 From 2217d0c050d16aa195a65780ffd6c450ba202dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= Date: Sun, 10 Dec 2023 17:53:38 +0900 Subject: [PATCH 002/136] refactor(frontend): remove redundant class names (#12618) --- ...lugin-unwind-css-module-class-name.test.ts | 3 +- ...lup-plugin-unwind-css-module-class-name.ts | 223 +++++++++++++++++- .../frontend/src/components/MkCodeEditor.vue | 2 +- .../src/components/MkNoteDetailed.vue | 2 +- packages/frontend/src/components/MkSwitch.vue | 5 +- packages/frontend/src/pages/admin-user.vue | 7 +- .../src/pages/admin/modlog.ModLog.vue | 5 +- packages/frontend/src/ui/deck.vue | 2 +- packages/frontend/src/ui/universal.vue | 2 +- 9 files changed, 222 insertions(+), 29 deletions(-) diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts index 759f270393..550e08d7f7 100644 --- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts @@ -180,7 +180,7 @@ import './photoswipe-!~{003}~.js'; const _hoisted_1 = createBaseVNode("i", { class: "ti ti-photo" }, null, -1); -const _sfc_main = defineComponent({ +const index_photos = defineComponent({ __name: "index.photos", props: { user: {} @@ -261,7 +261,6 @@ const style0 = { const cssModules = { "$style": style0 }; -const index_photos = _sfc_main; export {index_photos as default}; `.slice(1)); }); diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts index 18c817e0f5..68cdc0bc78 100644 --- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts @@ -13,13 +13,13 @@ function isFalsyIdentifier(identifier: estree.Identifier): boolean { return identifier.name === 'undefined' || identifier.name === 'NaN'; } -function normalizeClassWalker(tree: estree.Node): string | null { +function normalizeClassWalker(tree: estree.Node, stack: string | undefined): string | null { if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null; if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : ''; if (tree.type === 'BinaryExpression') { if (tree.operator !== '+') return null; - const left = normalizeClassWalker(tree.left); - const right = normalizeClassWalker(tree.right); + const left = normalizeClassWalker(tree.left, stack); + const right = normalizeClassWalker(tree.right, stack); if (left === null || right === null) return null; return `${left}${right}`; } @@ -33,15 +33,15 @@ function normalizeClassWalker(tree: estree.Node): string | null { if (tree.type === 'ArrayExpression') { const values = tree.elements.map((treeNode) => { if (treeNode === null) return ''; - if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); - return normalizeClassWalker(treeNode); + if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument, stack); + return normalizeClassWalker(treeNode, stack); }); if (values.some((x) => x === null)) return null; return values.join(' '); } if (tree.type === 'ObjectExpression') { const values = tree.properties.map((treeNode) => { - if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); + if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument, stack); let x = treeNode.value; let inveted = false; while (x.type === 'UnaryExpression' && x.operator === '!') { @@ -67,18 +67,26 @@ function normalizeClassWalker(tree: estree.Node): string | null { if (values.some((x) => x === null)) return null; return values.join(' '); } - console.error(`Unexpected node type: ${tree.type}`); + if ( + tree.type !== 'CallExpression' && + tree.type !== 'ChainExpression' && + tree.type !== 'ConditionalExpression' && + tree.type !== 'LogicalExpression' && + tree.type !== 'MemberExpression') { + console.error(stack ? `Unexpected node type: ${tree.type} (in ${stack})` : `Unexpected node type: ${tree.type}`); + } return null; } -export function normalizeClass(tree: estree.Node): string | null { - const walked = normalizeClassWalker(tree); +export function normalizeClass(tree: estree.Node, stack?: string): string | null { + const walked = normalizeClassWalker(tree, stack); return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, ''); } export function unwindCssModuleClassName(ast: estree.Node): void { (walk as typeof estreeWalker.walk)(ast, { enter(node, parent): void { + //#region if (parent?.type !== 'Program') return; if (node.type !== 'VariableDeclaration') return; if (node.declarations.length !== 1) return; @@ -102,6 +110,14 @@ export function unwindCssModuleClassName(ast: estree.Node): void { return true; }); if (!~__cssModulesIndex) return; + /* This region assumeed that the entered node looks like the following code. + * + * ```ts + * const SomeComponent = _export_sfc(_sfc_main, [["foo", bar], ["__cssModules", cssModules]]); + * ``` + */ + //#endregion + //#region const cssModuleForestName = ((node.declarations[0].init.arguments[1].elements[__cssModulesIndex] as estree.ArrayExpression).elements[1] as estree.Identifier).name; const cssModuleForestNode = parent.body.find((x) => { if (x.type !== 'VariableDeclaration') return false; @@ -117,6 +133,16 @@ export function unwindCssModuleClassName(ast: estree.Node): void { if (property.value.type !== 'Identifier') return []; return [[property.key.value as string, property.value.name as string]]; })); + /* This region collected a VariableDeclaration node in the module that looks like the following code. + * + * ```ts + * const cssModules = { + * "$style": style0, + * }; + * ``` + */ + //#endregion + //#region const sfcMain = parent.body.find((x) => { if (x.type !== 'VariableDeclaration') return false; if (x.declarations.length !== 1) return false; @@ -146,7 +172,22 @@ export function unwindCssModuleClassName(ast: estree.Node): void { if (ctx.type !== 'Identifier') return; if (ctx.name !== '_ctx') return; if (render.argument.body.type !== 'BlockStatement') return; + /* This region assumed that `sfcMain` looks like the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * }; + * }, + * }); + * ``` + */ + //#endregion for (const [key, value] of moduleForest) { + //#region const cssModuleTreeNode = parent.body.find((x) => { if (x.type !== 'VariableDeclaration') return false; if (x.declarations.length !== 1) return false; @@ -172,6 +213,19 @@ export function unwindCssModuleClassName(ast: estree.Node): void { if (actualValue.declarations[0].init?.type !== 'Literal') return []; return [[actualKey, actualValue.declarations[0].init.value as string]]; })); + /* This region collected VariableDeclaration nodes in the module that looks like the following code. + * + * ```ts + * const foo = "bar"; + * const baz = "qux"; + * const style0 = { + * foo: foo, + * baz: baz, + * }; + * ``` + */ + //#endregion + //#region (walk as typeof estreeWalker.walk)(render.argument.body, { enter(childNode) { if (childNode.type !== 'MemberExpression') return; @@ -189,6 +243,39 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }); }, }); + /* This region inlined the reference identifier of the class name in the render function into the actual literal, as in the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass(_ctx.$style.foo), + * }, null); + * }; + * }, + * }); + * ``` + * + * ↓ + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass("bar"), + * }, null); + * }; + * }, + * }); + */ + //#endregion + //#region (walk as typeof estreeWalker.walk)(render.argument.body, { enter(childNode) { if (childNode.type !== 'MemberExpression') return; @@ -205,13 +292,47 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }); }, }); + /* This region replaced the reference identifier of missing class names in the render function with `undefined`, as in the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass(_ctx.$style.hoge), + * }, null); + * }; + * }, + * }); + * ``` + * + * ↓ + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass(undefined), + * }, null); + * }; + * }, + * }); + * ``` + */ + //#endregion + //#region (walk as typeof estreeWalker.walk)(render.argument.body, { enter(childNode) { if (childNode.type !== 'CallExpression') return; if (childNode.callee.type !== 'Identifier') return; if (childNode.callee.name !== 'normalizeClass') return; if (childNode.arguments.length !== 1) return; - const normalized = normalizeClass(childNode.arguments[0]); + const normalized = normalizeClass(childNode.arguments[0], name); if (normalized === null) return; this.replace({ type: 'Literal', @@ -219,8 +340,60 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }); }, }); + /* This region compiled the `normalizeClass` call into a pseudo-AOT compilation, as in the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass("bar"), + * }, null); + * }; + * }, + * }); + * ``` + * + * ↓ + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: "bar", + * }, null); + * }; + * }, + * }); + * ``` + */ + //#endregion } + //#region if (node.declarations[0].init.arguments[1].elements.length === 1) { + (walk as typeof estreeWalker.walk)(ast, { + enter(childNode) { + if (childNode.type !== 'Identifier') return; + if (childNode.name !== ident) return; + this.replace({ + type: 'Identifier', + name: node.declarations[0].id.name, + }); + }, + }); + this.remove(); + /* NOTE: The above logic is valid as long as the following two conditions are met. + * + * - the uniqueness of `ident` is kept throughout the module + * - `_export_sfc` is noop when the second argument is an empty array + * + * Otherwise, the below logic should be used instead. + this.replace({ type: 'VariableDeclaration', declarations: [{ @@ -236,6 +409,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }], kind: 'const', }); + */ } else { this.replace({ type: 'VariableDeclaration', @@ -263,6 +437,35 @@ export function unwindCssModuleClassName(ast: estree.Node): void { kind: 'const', }); } + /* This region removed the `__cssModules` reference from the second argument of `_export_sfc`, as in the following code. + * + * ```ts + * const SomeComponent = _export_sfc(_sfc_main, [["foo", bar], ["__cssModules", cssModules]]); + * ``` + * + * ↓ + * + * ```ts + * const SomeComponent = _export_sfc(_sfc_main, [["foo", bar]]); + * ``` + * + * When the declaration becomes noop, it is removed as follows. + * + * ```ts + * const _sfc_main = defineComponent({ + * ... + * }); + * const SomeComponent = _export_sfc(_sfc_main, []); + * ``` + * + * ↓ + * + * ```ts + * const SomeComponent = defineComponent({ + * ... + * }); + */ + //#endregion }, }); } diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index 03788af21e..60f16f285f 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> @@ -194,4 +213,12 @@ onMounted(() => { .save { margin: 8px 0 0 0; } + +.mfmPreview { + padding: 12px; + border-radius: var(--radius); + box-sizing: border-box; + min-height: 130px; + pointer-events: none; +} diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index fe599dcead..28293b287c 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -37,7 +37,7 @@ type MfmProps = { isNote?: boolean; emojiUrls?: string[]; rootScale?: number; - nyaize: boolean | 'respect'; + nyaize?: boolean | 'respect'; parsedNodes?: mfm.MfmNode[] | null; enableEmojiMenu?: boolean; enableEmojiMenuReaction?: boolean; diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index 92070dc6c6..e4bbe15955 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -75,7 +75,6 @@ import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; -import MkTextarea from '@/components/MkTextarea.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -83,6 +82,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkFolder from '@/components/MkFolder.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; const announcements = ref([]); diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index af382bb137..f16b8709f3 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -70,7 +70,6 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue new file mode 100644 index 0000000000..f3f974a96f --- /dev/null +++ b/packages/frontend/src/pages/settings/emoji-picker.vue @@ -0,0 +1,274 @@ + + + + + + + diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 633ee894a9..e533f4420b 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -74,9 +74,9 @@ const menuDef = computed(() => [{ active: currentPage.value?.route.name === 'privacy', }, { icon: 'ti ti-mood-happy', - text: i18n.ts.reaction, - to: '/settings/reaction', - active: currentPage.value?.route.name === 'reaction', + text: i18n.ts.emojiPicker, + to: '/settings/emoji-picker', + active: currentPage.value?.route.name === 'emojiPicker', }, { icon: 'ti ti-cloud', text: i18n.ts.drive, @@ -236,7 +236,7 @@ provideMetadataReceiver((info) => { childInfo.value = null; } else { childInfo.value = info; - INFO.value.needWideArea = info.value?.needWideArea ?? undefined; + INFO.value.needWideArea = info.value.needWideArea ?? undefined; } }); diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index 66c549930b..cc6223218f 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -83,10 +83,10 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'useReactionPickerForContextMenu', 'showGapBetweenNotesInTimeline', 'instanceTicker', - 'reactionPickerSize', - 'reactionPickerWidth', - 'reactionPickerHeight', - 'reactionPickerUseDrawerForMobile', + 'emojiPickerScale', + 'emojiPickerWidth', + 'emojiPickerHeight', + 'emojiPickerUseDrawerForMobile', 'defaultSideView', 'menuDisplay', 'reportError', diff --git a/packages/frontend/src/pages/settings/reaction.vue b/packages/frontend/src/pages/settings/reaction.vue deleted file mode 100644 index fe5d9fc443..0000000000 --- a/packages/frontend/src/pages/settings/reaction.vue +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index b81811d2e7..a7a53e97e6 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -63,9 +63,9 @@ export const routes = [{ name: 'privacy', component: page(() => import('./pages/settings/privacy.vue')), }, { - path: '/reaction', - name: 'reaction', - component: page(() => import('./pages/settings/reaction.vue')), + path: '/emoji-picker', + name: 'emojiPicker', + component: page(() => import('./pages/settings/emoji-picker.vue')), }, { path: '/drive', name: 'drive', diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/scripts/emoji-picker.ts index d6d6bf1245..3cf653ea1b 100644 --- a/packages/frontend/src/scripts/emoji-picker.ts +++ b/packages/frontend/src/scripts/emoji-picker.ts @@ -3,8 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { defineAsyncComponent, Ref, ref } from 'vue'; +import { defineAsyncComponent, Ref, ref, computed, ComputedRef } from 'vue'; import { popup } from '@/os.js'; +import { defaultStore } from '@/store.js'; /** * 絵文字ピッカーを表示する。 @@ -23,8 +24,10 @@ class EmojiPicker { } public async init() { + const emojisRef = defaultStore.reactiveState.pinnedEmojis; await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { src: this.src, + pinnedEmojis: emojisRef, asReactionPicker: false, manualShowing: this.manualShowing, choseAndClose: false, @@ -44,8 +47,8 @@ class EmojiPicker { public show( src: HTMLElement, - onChosen: EmojiPicker['onChosen'], - onClosed: EmojiPicker['onClosed'], + onChosen?: EmojiPicker['onChosen'], + onClosed?: EmojiPicker['onClosed'], ) { this.src.value = src; this.manualShowing.value = true; diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts index 19e1bfba2c..9b13e794f5 100644 --- a/packages/frontend/src/scripts/reaction-picker.ts +++ b/packages/frontend/src/scripts/reaction-picker.ts @@ -5,6 +5,7 @@ import { defineAsyncComponent, Ref, ref } from 'vue'; import { popup } from '@/os.js'; +import { defaultStore } from '@/store.js'; class ReactionPicker { private src: Ref = ref(null); @@ -17,25 +18,27 @@ class ReactionPicker { } public async init() { + const reactionsRef = defaultStore.reactiveState.reactions; await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { src: this.src, + pinnedEmojis: reactionsRef, asReactionPicker: true, manualShowing: this.manualShowing, }, { done: reaction => { - this.onChosen!(reaction); + if (this.onChosen) this.onChosen(reaction); }, close: () => { this.manualShowing.value = false; }, closed: () => { this.src.value = null; - this.onClosed!(); + if (this.onClosed) this.onClosed(); }, }); } - public show(src: HTMLElement, onChosen: ReactionPicker['onChosen'], onClosed: ReactionPicker['onClosed']) { + public show(src: HTMLElement, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) { this.src.value = src; this.manualShowing.value = true; this.onChosen = onChosen; diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 8459a5721a..c7e501aa84 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -119,6 +119,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], }, + pinnedEmojis: { + where: 'account', + default: [], + }, reactionAcceptance: { where: 'account', default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null, @@ -271,19 +275,19 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'remote' as 'none' | 'remote' | 'always', }, - reactionPickerSize: { + emojiPickerScale: { where: 'device', default: 1, }, - reactionPickerWidth: { + emojiPickerWidth: { where: 'device', default: 1, }, - reactionPickerHeight: { + emojiPickerHeight: { where: 'device', default: 2, }, - reactionPickerUseDrawerForMobile: { + emojiPickerUseDrawerForMobile: { where: 'device', default: true, }, From 8ff87176f843e4e7ff3e1432c1e090867c8c2535 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 14 Dec 2023 14:23:18 +0900 Subject: [PATCH 022/136] tweak profile.avatar-decoration.dialog.vue --- .../src/pages/settings/profile.avatar-decoration.dialog.vue | 3 ++- .../frontend/src/pages/settings/profile.avatar-decoration.vue | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue index a27b46aa3e..26cacf3c37 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.update }} {{ i18n.ts.detach }} - {{ i18n.ts.attach }} + {{ i18n.ts.attach }}
@@ -73,6 +73,7 @@ const emit = defineEmits<{ }>(); const dialog = shallowRef>(); +const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0); const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0); const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false); diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.vue index 90c2b75a4d..bfef6e0325 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only