diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f65f62788..199a420f7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,9 @@ - Fix: 自分のdirect noteがuser list timelineに追加されない ### Client -- Fix: 一部のモデログ(logYellowでの表示対象)について、表示の色が変わらない問題を修正 - Feat: AiScript専用のMFM構文`$[clickable.ev=EVENTNAME ...]`を追加。`Mk:C:mfm`のオプション`onClickEv`に関数を渡すと、クリック時に`EVENTNAME`を引数にして呼び出す +- Enhance: MFM入力補助ボタンを投稿フォームに表示できるように #12787 +- Fix: 一部のモデログ(logYellowでの表示対象)について、表示の色が変わらない問題を修正 - Fix: `fg`/`bg`MFMに長い単語を指定すると、オーバーフローされずはみ出る問題を修正 ### Server diff --git a/locales/index.d.ts b/locales/index.d.ts index 89bdddbdcf..60e88a2192 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1184,6 +1184,8 @@ export interface Locale { "overwriteContentConfirm": string; "seasonalScreenEffect": string; "decorate": string; + "addMfmFunction": string; + "enableQuickAddMfmFunction": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7bc5889297..3d31dcef49 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1181,6 +1181,8 @@ remainingN: "残り: {n}" overwriteContentConfirm: "現在の内容に上書きされますがよろしいですか?" seasonalScreenEffect: "季節に応じた画面の演出" decorate: "デコる" +addMfmFunction: "装飾を追加" +enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 5e7ca5539e..3aacf4c2da 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -86,6 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only +
@@ -126,6 +127,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; +import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; const modal = inject('modal'); @@ -182,6 +184,8 @@ const poll = ref<{ const useCw = ref(!!props.initialCw); const showPreview = ref(defaultStore.state.showPreview); watch(showPreview, () => defaultStore.set('showPreview', showPreview.value)); +const showAddMfmFunction = ref(defaultStore.state.enableQuickAddMfmFunction); +watch(showAddMfmFunction, () => defaultStore.set('enableQuickAddMfmFunction', showAddMfmFunction.value)); const cw = ref(props.initialCw ?? null); const localOnly = ref(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]); @@ -863,6 +867,14 @@ async function insertEmoji(ev: MouseEvent) { ); } +async function insertMfmFunction(ev: MouseEvent) { + mfmFunctionPicker( + ev.currentTarget ?? ev.target, + textareaEl.value, + text, + ); +} + function showActions(ev) { os.popupMenu(postFormActions.map(action => ({ text: action.title, diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 826ede17e5..3e5f5cb8c8 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -48,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.collapseRenotes }} {{ i18n.ts.enableAdvancedMfm }} {{ i18n.ts.enableAnimatedMfm }} + {{ i18n.ts.enableQuickAddMfmFunction }} {{ i18n.ts.showGapBetweenNotesInTimeline }} {{ i18n.ts.loadRawImages }} @@ -268,6 +269,7 @@ const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect')); const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline')); const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm')); const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm')); +const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction')); const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle')); const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer')); const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages')); diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts new file mode 100644 index 0000000000..465926fe04 --- /dev/null +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Ref, nextTick } from 'vue'; +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; +import { MFM_TAGS } from '@/const.js'; + +/** + * MFMの装飾のリストを表示する + */ +export function mfmFunctionPicker(src: any, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref) { + return new Promise((res, rej) => { + os.popupMenu([{ + text: i18n.ts.addMfmFunction, + type: 'label', + }, ...getFunctionList(textArea, textRef)], src); + }); +} + +function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref) : object[] { + const ret: object[] = []; + MFM_TAGS.forEach(tag => { + ret.push({ + text: tag, + icon: 'ti ti-icons', + action: () => add(textArea, textRef, tag), + }); + }); + return ret; +} + +function add(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref, type: string) { + const caretStart: number = textArea.selectionStart as number; + const caretEnd: number = textArea.selectionEnd as number; + + MFM_TAGS.forEach(tag => { + if (type === tag) { + if (caretStart === caretEnd) { + // 単純にFunctionを追加 + const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ]${textRef.value.substring(caretEnd)}`; + textRef.value = trimmedText; + } else { + // 選択範囲を囲むようにFunctionを追加 + const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ${textRef.value.substring(caretStart, caretEnd)}]${textRef.value.substring(caretEnd)}`; + textRef.value = trimmedText; + } + } + }); + + const nextCaretStart: number = caretStart + 3 + type.length; + const nextCaretEnd: number = caretEnd + 3 + type.length; + + // キャレットを戻す + nextTick(() => { + textArea.focus(); + textArea.setSelectionRange(nextCaretStart, nextCaretEnd); + }); +} diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 3f8a5f5a6f..46634af96b 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -219,6 +219,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, + enableQuickAddMfmFunction: { + where: 'device', + default: false, + }, loadRawImages: { where: 'device', default: false,