From 16eccad49271b81282cdbd482d29e7a8bb048f4c Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:03:07 +0900 Subject: enhance(frontend): シンタックスハイライトにテーマを適用できるように (#13175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(frontend): シンタックスハイライトにテーマを適用できるように * Update Changelog * こっちも * テーマの値がディープマージされるように * 常にテーマ設定に準じるように * テーマ更新時に新しいshikiテーマを読み込むように --- packages/frontend/src/scripts/clone.ts | 4 +- packages/frontend/src/scripts/code-highlighter.ts | 68 ++++++++++++++++++++++- packages/frontend/src/scripts/merge.ts | 31 +++++++++++ packages/frontend/src/scripts/theme.ts | 10 +++- 4 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 packages/frontend/src/scripts/merge.ts (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts index ac38faefaa..6d3a1c8c79 100644 --- a/packages/frontend/src/scripts/clone.ts +++ b/packages/frontend/src/scripts/clone.ts @@ -8,13 +8,13 @@ // あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった // https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045 -type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[]; +export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[]; export function deepClone(x: T): T { if (typeof x === 'object') { if (x === null) return x; if (Array.isArray(x)) return x.map(deepClone) as T; - const obj = {} as Record; + const obj = {} as Record; for (const [k, v] of Object.entries(x)) { obj[k] = v === undefined ? undefined : deepClone(v); } diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index bc05ec94d5..b11dfed41a 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -1,9 +1,51 @@ +import { bundledThemesInfo } from 'shiki'; import { getHighlighterCore, loadWasm } from 'shiki/core'; import darkPlus from 'shiki/themes/dark-plus.mjs'; -import type { Highlighter, LanguageRegistration } from 'shiki'; +import { unique } from './array.js'; +import { deepClone } from './clone.js'; +import { deepMerge } from './merge.js'; +import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki'; +import { ColdDeviceStorage } from '@/store.js'; +import lightTheme from '@/themes/_light.json5'; +import darkTheme from '@/themes/_dark.json5'; let _highlighter: Highlighter | null = null; +export async function getTheme(mode: 'light' | 'dark', getName: true): Promise; +export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise; +export async function getTheme(mode: 'light' | 'dark', getName = false): Promise { + const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme')); + + if (theme.base) { + const base = [lightTheme, darkTheme].find(x => x.id === theme.base); + if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter); + } + + if (theme.codeHighlighter) { + let _res: ThemeRegistration = {}; + if (theme.codeHighlighter.base === '_none_') { + _res = deepClone(theme.codeHighlighter.overrides); + } else { + const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus; + _res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base); + } + if (_res.name == null) { + _res.name = theme.id; + } + _res.type = mode; + + if (getName) { + return _res.name; + } + return _res; + } + + if (getName) { + return 'dark-plus'; + } + return darkPlus; +} + export async function getHighlighter(): Promise { if (!_highlighter) { return await initHighlighter(); @@ -13,11 +55,17 @@ export async function getHighlighter(): Promise { export async function initHighlighter() { const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json'); - + await loadWasm(import('shiki/onig.wasm?init')); + // テーマの重複を消す + const themes = unique([ + darkPlus, + ...(await Promise.all([getTheme('light'), getTheme('dark')])), + ]); + const highlighter = await getHighlighterCore({ - themes: [darkPlus], + themes, langs: [ import('shiki/langs/javascript.mjs'), { @@ -27,6 +75,20 @@ export async function initHighlighter() { ], }); + ColdDeviceStorage.watch('lightTheme', async () => { + const newTheme = await getTheme('light'); + if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { + highlighter.loadTheme(newTheme); + } + }); + + ColdDeviceStorage.watch('darkTheme', async () => { + const newTheme = await getTheme('dark'); + if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { + highlighter.loadTheme(newTheme); + } + }); + _highlighter = highlighter; return highlighter; diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts new file mode 100644 index 0000000000..60097051fa --- /dev/null +++ b/packages/frontend/src/scripts/merge.ts @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { deepClone } from './clone.js'; +import type { Cloneable } from './clone.js'; + +function isPureObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** + * valueにないキーをdefからもらう(再帰的)\ + * nullはそのまま、undefinedはdefの値 + **/ +export function deepMerge>(value: X, def: X): X { + if (isPureObject(value) && isPureObject(def)) { + const result = deepClone(value as Cloneable) as X; + for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { + if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) { + result[k] = v; + } else if (isPureObject(v) && isPureObject(result[k])) { + const child = deepClone(result[k] as Cloneable) as X[keyof X] & Record; + result[k] = deepMerge(child, v); + } + } + return result; + } + return value; +} diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index 21ef85fe7a..d3bd9ba4bc 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -6,6 +6,7 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; import { deepClone } from './clone.js'; +import type { BuiltinTheme } from 'shiki'; import { globalEvents } from '@/events.js'; import lightTheme from '@/themes/_light.json5'; import darkTheme from '@/themes/_dark.json5'; @@ -18,6 +19,13 @@ export type Theme = { desc?: string; base?: 'dark' | 'light'; props: Record; + codeHighlighter?: { + base: BuiltinTheme; + overrides?: Record; + } | { + base: '_none_'; + overrides: Record; + }; }; export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); @@ -53,7 +61,7 @@ export const getBuiltinThemesRef = () => { return builtinThemes; }; -let timeout = null; +let timeout: number | null = null; export function applyTheme(theme: Theme, persist = true) { if (timeout) window.clearTimeout(timeout); -- cgit v1.2.3-freya From 74245df3829622a3cc0c880ea710b5c1c4f5c584 Mon Sep 17 00:00:00 2001 From: 1Step621 <86859447+1STEP621@users.noreply.github.com> Date: Tue, 6 Feb 2024 16:45:21 +0900 Subject: Enhance(frontend): フロント側でもリアクション権限のチェックをするように (#13134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * フロント側でもリアクション権限のチェックをするように * update CHANGELOG.md * lint fixes * remove unrelated diffs * deny -> reject denyは「(信用しないことを理由に)拒否する」という意味らしい * allow -> accept * EmojiSimpleにlocalOnlyを含めるように * リアクション権限のない絵文字は打てないように(ダイアログを出すのではなく) * regenerate type definitions * lint fix * remove unused locales * remove unnecessary async --- CHANGELOG.md | 4 ++++ .../backend/src/core/entities/EmojiEntityService.ts | 1 + packages/backend/src/models/json-schema/emoji.ts | 4 ++++ packages/frontend/src/components/MkEmojiPicker.vue | 4 +++- .../frontend/src/components/MkEmojiPickerDialog.vue | 3 +++ .../frontend/src/components/MkEmojiPickerWindow.vue | 4 +++- packages/frontend/src/components/MkNote.vue | 2 +- packages/frontend/src/components/MkNoteDetailed.vue | 2 +- .../src/components/MkReactionsViewer.reaction.vue | 18 +++++++++++++----- packages/frontend/src/pages/settings/emoji-picker.vue | 2 +- .../frontend/src/scripts/check-reaction-permissions.ts | 8 ++++++++ packages/frontend/src/scripts/reaction-picker.ts | 6 +++++- packages/misskey-js/src/autogen/apiClientJSDoc.ts | 2 +- packages/misskey-js/src/autogen/endpoint.ts | 2 +- packages/misskey-js/src/autogen/entities.ts | 2 +- packages/misskey-js/src/autogen/models.ts | 2 +- packages/misskey-js/src/autogen/types.ts | 3 ++- 17 files changed, 53 insertions(+), 16 deletions(-) create mode 100644 packages/frontend/src/scripts/check-reaction-permissions.ts (limited to 'packages/frontend/src/scripts') diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fc99cf5b8..17c06a90e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,10 @@ - Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように - Enhance: リモートのユーザーはメニューから直接リモートで表示できるように - Enhance: コードのシンタックスハイライトにテーマを適用できるように +- Enhance: リアクション権限がない場合、ハートにフォールバックするのではなく、権限がないことをダイアログで表示するように + - リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合 + - センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合 + - ロールが必要な絵文字をリアクションしようとした場合 - Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 5b97cfad5e..655c4c5ada 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -31,6 +31,7 @@ export class EmojiEntityService { category: emoji.category, // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) url: emoji.publicUrl || emoji.originalUrl, + localOnly: emoji.localOnly ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, }; diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 99a58f8773..954eb98d57 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -27,6 +27,10 @@ export const packedEmojiSimpleSchema = { type: 'string', optional: false, nullable: false, }, + localOnly: { + type: 'boolean', + optional: true, nullable: false, + }, isSensitive: { type: 'boolean', optional: true, nullable: false, diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 58160cdf5b..f5ab7a2e29 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -118,6 +118,7 @@ import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; import { $i } from '@/account.js'; +import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; const props = withDefaults(defineProps<{ showPinned?: boolean; @@ -126,6 +127,7 @@ const props = withDefaults(defineProps<{ asDrawer?: boolean; asWindow?: boolean; asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう + targetNote?: Misskey.entities.Note; }>(), { showPinned: true, }); @@ -340,7 +342,7 @@ watch(q, () => { }); function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean { - return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false; + return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji); } function focus() { diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 6660dcf1ed..444e8a4cec 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only :showPinned="showPinned" :pinnedEmojis="pinnedEmojis" :asReactionPicker="asReactionPicker" + :targetNote="targetNote" :asDrawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen" @@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only