From b0301dd2fbd48ada9c48be398e9b41865e6fef1f Mon Sep 17 00:00:00 2001 From: YAVIIGI <118232419+YAVIIGI@users.noreply.github.com> Date: Wed, 27 Dec 2023 20:57:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E6=8A=95=E7=A8=BF=E3=82=A6?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=89=E3=82=A6=E3=81=ABMFM=E8=A6=81?= =?UTF-8?q?=E7=B4=A0=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B=E3=83=9C?= =?UTF-8?q?=E3=82=BF=E3=83=B3=E3=81=AE=E8=BF=BD=E5=8A=A0=20(#12788)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * functionPicker ã®è¿½åŠ * Update CHANGELOG.md * fix lint errors * Add addMfmFunction * add enableQuickAddMfmFunction setting * Update CHANGELOG.md issue 番å·ã‚’è¿½åŠ * Update index.d.ts * change 'functionPicker' to 'mfmFunctionPicker' * Change indent from 4 space to 1 tab --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> --- CHANGELOG.md | 3 +- locales/index.d.ts | 2 + locales/ja-JP.yml | 2 + .../frontend/src/components/MkPostForm.vue | 12 ++++ .../frontend/src/pages/settings/general.vue | 2 + .../src/scripts/mfm-function-picker.ts | 61 +++++++++++++++++++ packages/frontend/src/store.ts | 4 ++ 7 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/src/scripts/mfm-function-picker.ts 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 157b8f44d5..dd2f34a69a 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1220,6 +1220,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 7cb678b5f3..b632fbad63 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1217,6 +1217,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 8838da15a9..aa37cef6c2 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 <button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ph-hash ph-bold ph-lg"></i></button> <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ph-plug ph-bold ph-lg"></i></button> <button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ph-smiley ph-bold ph-lg"></i></button> + <button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ph-palette ph-bold ph-lg"></i></button> </div> <div :class="$style.footerRight"> <button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ph-eye ph-bold ph-lg"></i></button> @@ -127,6 +128,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'); @@ -184,6 +186,8 @@ const poll = ref<{ const useCw = ref<boolean>(!!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<string | null>(props.initialCw ?? null); const localOnly = ref<boolean>(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]); @@ -870,6 +874,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 0839a65ebb..8eacdd32e6 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -52,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="expandLongNote">Always expand long notes</MkSwitch> <MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch> <MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch> + <MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch> <MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch> <MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch> <MkSwitch v-model="showTickerOnReplies">Show instance ticker on replies</MkSwitch> @@ -296,6 +297,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<string>) { + return new Promise((res, rej) => { + os.popupMenu([{ + text: i18n.ts.addMfmFunction, + type: 'label', + }, ...getFunctionList(textArea, textRef)], src); + }); +} + +function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) : 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<string>, 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 c86c3d01ad..18cfad2102 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -239,6 +239,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, + enableQuickAddMfmFunction: { + where: 'device', + default: false, + }, loadRawImages: { where: 'device', default: false, -- GitLab