diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f65f6278822c089a88116ee22f9cf85e34b153d..199a420f7bfff2b9f834ec6071281f74122289e9 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 89bdddbdcf93a3630ee9246e570ec75ea3ec60f0..60e88a219240b22aa0bbc36cedcb32213a7e374a 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 7bc5889297bbd5f35fdea2ce93b4fffc5e70353f..3d31dcef4954f7161d6d1ace15da8146f42dd290 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 5e7ca5539ec95b0f224ac6c57684d5814462497e..3aacf4c2da01ba4e322192f9ce73db083d754050 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="ti ti-hash"></i></button> <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button> <button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> + <button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></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="ti ti-eye"></i></button> @@ -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<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]); @@ -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 826ede17e5c7ff4aab8c88859a7d7bd28201e04c..3e5f5cb8c8f029663f669585862531e5dc0850be 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 <MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</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> <MkRadios v-model="reactionsDisplaySize"> @@ -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 0000000000000000000000000000000000000000..465926fe044522db315c239edc96d581f44b5c7e --- /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 3f8a5f5a6f3942c4f726f8a92d3905599dc5fae5..46634af96b0a334dd7964cac834b42e873f68427 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,