diff options
| author | おさむのひと <46447427+samunohito@users.noreply.github.com> | 2023-12-14 14:11:20 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-12-14 14:11:20 +0900 |
| commit | a92795d90f9e55c7b7726725dceea979fd8940a3 (patch) | |
| tree | 48a0b7e34775e0ca796cdc702c5ff153a98f43ee | |
| parent | 2023.12.0-beta.4 (diff) | |
| download | misskey-a92795d90f9e55c7b7726725dceea979fd8940a3.tar.gz misskey-a92795d90f9e55c7b7726725dceea979fd8940a3.tar.bz2 misskey-a92795d90f9e55c7b7726725dceea979fd8940a3.zip | |
feat(frontend): 絵文字ピッカーの実装 (#12617)
* 絵文字デッキの作成
* 細かい不備を修正
* fix lint
* fix
* fix CHANGELOG.md
* fix setTimeout -> nextTick
* fix https://github.com/misskey-dev/misskey/pull/12617#issuecomment-1848952862
* fix bug
* fix CHANGELOG.md
* fix CHANGELOG.md
* wip
* Update CHANGELOG.md
* Update CHANGELOG.md
* wip
---------
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
| -rw-r--r-- | CHANGELOG.md | 14 | ||||
| -rw-r--r-- | locales/index.d.ts | 7 | ||||
| -rw-r--r-- | locales/ja-JP.yml | 7 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkEmojiPicker.vue | 23 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkEmojiPickerDialog.vue | 5 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkPostForm.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/form/section.vue | 9 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/emoji-picker.vue | 274 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/index.vue | 8 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/preferences-backups.vue | 8 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/reaction.vue | 159 | ||||
| -rw-r--r-- | packages/frontend/src/router.ts | 6 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/emoji-picker.ts | 9 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/reaction-picker.ts | 9 | ||||
| -rw-r--r-- | packages/frontend/src/store.ts | 12 |
15 files changed, 354 insertions, 198 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 972c876518..7fbc1e06de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,17 @@ ## 2023.x.x (unreleased) +### Note +- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。 + + **影響:** + それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された投稿用のピン留め絵文字が使われるため)。 + 投稿用のピン留め絵文字をアップデート前の状態にするには、以下の手順で操作します。 + + 1. 「設定」メニューに移動し、「絵文字ピッカー」タブを選択します。 + 2. 「ピン留 (全般)」のタブを選択します。 + 3. 「リアクション設定からコピーする」ボタンを押すことで、アップデート前の状態に戻すことができます。 + ### General - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) @@ -25,7 +36,8 @@ ### Client - Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加 - Feat: データセーバーでコードハイライトの読み込みを削減できるように -- Enhance: 投稿フォームの絵文字ピッカーをリアクション時に使用するものと同じのを使用するように #12336 +- Enhance: 投稿フォームの絵文字ピッカーをリアクション時に使用するものと同じのを使用するように #12336 #12560 +- Enhance: リアクション用ピン留め絵文字と投稿時の絵文字入力用ピン留め絵文字を分けて設定できるように #12560 - Enhance: 絵文字のオートコンプリート機能強化 #12364 - Enhance: ユーザーのRawデータを表示するページが復活 - Enhance: リアクション選択時に音を鳴らせるように diff --git a/locales/index.d.ts b/locales/index.d.ts index d32023f5ac..40837b05a2 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -124,7 +124,12 @@ export interface Locale { "add": string; "reaction": string; "reactions": string; - "reactionSetting": string; + "emojiPicker": string; + "pinnedEmojisForReactionSettingDescription": string; + "pinnedEmojisSettingDescription": string; + "emojiPickerDisplay": string; + "copyFromPinnedEmojisForReaction": string; + "copyFromPinnedEmojis": string; "reactionSettingDescription2": string; "rememberNoteVisibility": string; "attachCancel": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2ac57fd311..3ad27910ef 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -121,7 +121,12 @@ sensitive: "センシティブ" add: "追加" reaction: "リアクション" reactions: "リアクション" -reactionSetting: "ピッカーに表示するリアクション" +emojiPicker: "絵文字ピッカー" +pinnedEmojisForReactionSettingDescription: "リアクション時にピン留め表示する絵文字を設定できます" +pinnedEmojisSettingDescription: "絵文字入力時にピン留め表示する絵文字を設定できます" +emojiPickerDisplay: "ピッカーの表示" +copyFromPinnedEmojisForReaction: "リアクション設定からコピーする" +copyFromPinnedEmojis: "絵文字設定からコピーする" reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。" rememberNoteVisibility: "公開範囲を記憶する" attachCancel: "添付取り消し" diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index d84d298e23..f36d46506f 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -121,10 +121,11 @@ import { $i } from '@/account.js'; const props = withDefaults(defineProps<{ showPinned?: boolean; - asReactionPicker?: boolean; + pinnedEmojis?: string[]; maxHeight?: number; asDrawer?: boolean; asWindow?: boolean; + asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう }>(), { showPinned: true, }); @@ -137,24 +138,22 @@ const searchEl = shallowRef<HTMLInputElement>(); const emojisEl = shallowRef<HTMLDivElement>(); const { - reactions: pinnedReactions, - reactionPickerSize, - reactionPickerWidth, - reactionPickerHeight, - disableShowingAnimatedImages, + emojiPickerScale, + emojiPickerWidth, + emojiPickerHeight, recentlyUsedEmojis, } = defaultStore.reactiveState; -const pinned = computed(() => props.asReactionPicker ? pinnedReactions.value : []); // TODO: 非リアクションの絵文字ピッカー用のpinned絵文字を設定可能にする? -const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1); -const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3); -const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2); +const pinned = computed(() => props.pinnedEmojis); +const size = computed(() => emojiPickerScale.value); +const width = computed(() => emojiPickerWidth.value); +const height = computed(() => emojiPickerHeight.value); const q = ref<string>(''); const searchResultCustom = ref<Misskey.entities.EmojiSimple[]>([]); const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); -const customEmojiFolderRoot: CustomEmojiFolderTree = { value: "", category: "", children: [] }; +const customEmojiFolderRoot: CustomEmojiFolderTree = { value: '', category: '', children: [] }; function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree { const parts = input.split('/').map(p => p.trim()); @@ -368,7 +367,7 @@ function chosen(emoji: any, ev?: MouseEvent) { emit('chosen', key); // 最近使った絵文字更新 - if (!pinned.value.includes(key)) { + if (!pinned.value?.includes(key)) { let recents = defaultStore.state.recentlyUsedEmojis; recents = recents.filter((emoji: any) => emoji !== key); recents.unshift(key); diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 2cce1f5520..6660dcf1ed 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="modal" v-slot="{ type, maxHeight }" :zPriority="'middle'" - :preferType="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" + :preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :transparentBg="true" :manualShowing="manualShowing" :src="src" @@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only class="_popup _shadow" :class="{ [$style.drawer]: type === 'drawer' }" :showPinned="showPinned" + :pinnedEmojis="pinnedEmojis" :asReactionPicker="asReactionPicker" :asDrawer="type === 'drawer'" :max-height="maxHeight" @@ -40,11 +41,13 @@ const props = withDefaults(defineProps<{ manualShowing?: boolean | null; src?: HTMLElement; showPinned?: boolean; + pinnedEmojis?: string[], asReactionPicker?: boolean; choseAndClose?: boolean; }>(), { manualShowing: null, showPinned: true, + pinnedEmojis: undefined, asReactionPicker: false, choseAndClose: true, }); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index e6d55ae982..4a1930ac0b 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -857,7 +857,7 @@ async function insertEmoji(ev: MouseEvent) { }, () => { textAreaReadOnly.value = false; - focus(); + nextTick(() => focus()); }, ); } diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue index 095b24604a..6af63d1ec6 100644 --- a/packages/frontend/src/components/form/section.vue +++ b/packages/frontend/src/components/form/section.vue @@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="[$style.root, { [$style.rootFirst]: first }]"> <div :class="[$style.label, { [$style.labelFirst]: first }]"><slot name="label"></slot></div> + <div :class="[$style.description]"><slot name="description"></slot></div> <div :class="$style.main"> <slot></slot> </div> @@ -31,7 +32,7 @@ defineProps<{ .label { font-weight: bold; padding: 1.5em 0 0 0; - margin: 0 0 16px 0; + margin: 0 0 8px 0; &:empty { display: none; @@ -45,4 +46,10 @@ defineProps<{ .main { margin: 1.5em 0 0 0; } + +.description { + font-size: 0.85em; + color: var(--fgTransparentWeak); + margin: 0 0 8px 0; +} </style> 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 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div class="_gaps_m"> + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-pin"></i></template> + <template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template> + <template #caption>{{ i18n.ts.pinnedEmojisForReactionSettingDescription }}</template> + + <div class="_gaps"> + <div> + <div v-panel style="border-radius: 6px;"> + <Sortable + v-model="pinnedEmojisForReaction" + :class="$style.emojis" + :itemKey="item => item" + :animation="150" + :delay="100" + :delayOnTouchOnly="true" + > + <template #item="{element}"> + <button class="_button" :class="$style.emojisItem" @click="removeReaction(element, $event)"> + <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/> + <MkEmoji v-else :emoji="element" :normal="true"/> + </button> + </template> + <template #footer> + <button class="_button" :class="$style.emojisAdd" @click="chooseReaction"> + <i class="ti ti-plus"></i> + </button> + </template> + </Sortable> + </div> + <div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div> + </div> + + <div class="_buttons"> + <MkButton inline @click="previewReaction"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> + <MkButton inline danger @click="setDefaultReaction"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton inline danger @click="copyFromPinnedEmojis"><i class="ti ti-copy"></i> {{ i18n.ts.copyFromPinnedEmojis }}</MkButton> + </div> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-pin"></i></template> + <template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.general }})</template> + <template #caption>{{ i18n.ts.pinnedEmojisSettingDescription }}</template> + + <div class="_gaps"> + <div> + <div v-panel style="border-radius: 6px;"> + <Sortable + v-model="pinnedEmojis" + :class="$style.emojis" + :itemKey="item => item" + :animation="150" + :delay="100" + :delayOnTouchOnly="true" + > + <template #item="{element}"> + <button class="_button" :class="$style.emojisItem" @click="removeEmoji(element, $event)"> + <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/> + <MkEmoji v-else :emoji="element" :normal="true"/> + </button> + </template> + <template #footer> + <button class="_button" :class="$style.emojisAdd" @click="chooseEmoji"> + <i class="ti ti-plus"></i> + </button> + </template> + </Sortable> + </div> + <div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div> + </div> + + <div class="_buttons"> + <MkButton inline @click="previewEmoji"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> + <MkButton inline danger @click="setDefaultEmoji"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton inline danger @click="copyFromPinnedEmojisForReaction"><i class="ti ti-copy"></i> {{ i18n.ts.copyFromPinnedEmojisForReaction }}</MkButton> + </div> + </div> + </MkFolder> + + <FormSection> + <template #label>{{ i18n.ts.emojiPickerDisplay }}</template> + + <div class="_gaps_m"> + <MkRadios v-model="emojiPickerScale"> + <template #label>{{ i18n.ts.size }}</template> + <option :value="1">{{ i18n.ts.small }}</option> + <option :value="2">{{ i18n.ts.medium }}</option> + <option :value="3">{{ i18n.ts.large }}</option> + </MkRadios> + + <MkRadios v-model="emojiPickerWidth"> + <template #label>{{ i18n.ts.numberOfColumn }}</template> + <option :value="1">5</option> + <option :value="2">6</option> + <option :value="3">7</option> + <option :value="4">8</option> + <option :value="5">9</option> + </MkRadios> + + <MkRadios v-model="emojiPickerHeight"> + <template #label>{{ i18n.ts.height }}</template> + <option :value="1">{{ i18n.ts.small }}</option> + <option :value="2">{{ i18n.ts.medium }}</option> + <option :value="3">{{ i18n.ts.large }}</option> + <option :value="4">{{ i18n.ts.large }}+</option> + </MkRadios> + + <MkSwitch v-model="emojiPickerUseDrawerForMobile"> + {{ i18n.ts.useDrawerReactionPickerForMobile }} + <template #caption>{{ i18n.ts.needReloadToApply }}</template> + </MkSwitch> + </div> + </FormSection> +</div> +</template> + +<script lang="ts" setup> +import { computed, ref, Ref, watch } from 'vue'; +import Sortable from 'vuedraggable'; +import MkRadios from '@/components/MkRadios.vue'; +import MkButton from '@/components/MkButton.vue'; +import FormSection from '@/components/form/section.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import * as os from '@/os.js'; +import { defaultStore } from '@/store.js'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { deepClone } from '@/scripts/clone.js'; +import { reactionPicker } from '@/scripts/reaction-picker.js'; +import { emojiPicker } from '@/scripts/emoji-picker.js'; +import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue'; +import MkEmoji from '@/components/global/MkEmoji.vue'; +import MkFolder from '@/components/MkFolder.vue'; + +const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(defaultStore.state.reactions)); +const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis)); + +const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale')); +const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth')); +const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight')); +const emojiPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('emojiPickerUseDrawerForMobile')); + +const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev); +const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev); +const setDefaultReaction = () => setDefault(pinnedEmojisForReaction); + +const removeEmoji = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis, reaction, ev); +const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev); +const setDefaultEmoji = () => setDefault(pinnedEmojis); + +function previewReaction(ev: MouseEvent) { + reactionPicker.show(getHTMLElement(ev)); +} + +function previewEmoji(ev: MouseEvent) { + emojiPicker.show(getHTMLElement(ev)); +} + +async function copyFromPinnedEmojis() { + const { canceled } = await os.confirm({ + type: 'warning', + text: 'a', + }); + + if (canceled) { + return; + } + + pinnedEmojisForReaction.value = [...pinnedEmojis.value]; +} + +async function copyFromPinnedEmojisForReaction() { + const { canceled } = await os.confirm({ + type: 'warning', + text: 'a', + }); + + if (canceled) { + return; + } + + pinnedEmojis.value = [...pinnedEmojisForReaction.value]; +} + +function remove(itemsRef: Ref<string[]>, reaction: string, ev: MouseEvent) { + os.popupMenu([{ + text: i18n.ts.remove, + action: () => { + itemsRef.value = itemsRef.value.filter(x => x !== reaction); + }, + }], getHTMLElement(ev)); +} + +async function setDefault(itemsRef: Ref<string[]>) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.resetAreYouSure, + }); + if (canceled) return; + + itemsRef.value = deepClone(defaultStore.def.reactions.default); +} + +async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) { + os.pickEmoji(getHTMLElement(ev), { + showPinned: false, + }).then(it => { + const emoji = it as string; + if (!itemsRef.value.includes(emoji)) { + itemsRef.value.push(emoji); + } + }); +} + +function getHTMLElement(ev: MouseEvent): HTMLElement { + const target = ev.currentTarget ?? ev.target; + return target as HTMLElement; +} + +watch(pinnedEmojisForReaction, () => { + defaultStore.set('reactions', pinnedEmojisForReaction.value); +}, { + deep: true, +}); + +watch(pinnedEmojis, () => { + defaultStore.set('pinnedEmojis', pinnedEmojis.value); +}, { + deep: true, +}); + +definePageMetadata({ + title: i18n.ts.emojiPicker, + icon: 'ti ti-mood-happy', +}); +</script> + +<style lang="scss" module> +.tab { + margin: calc(var(--margin) / 2) 0; + padding: calc(var(--margin) / 2) 0; + background: var(--bg); +} + +.emojis { + padding: 12px; + font-size: 1.1em; +} + +.emojisItem { + display: inline-block; + padding: 8px; + cursor: move; +} + +.emojisAdd { + display: inline-block; + padding: 8px; +} + +.editorCaption { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); +} +</style> 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 @@ -<!-- -SPDX-FileCopyrightText: syuilo and other misskey contributors -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<div class="_gaps_m"> - <FromSlot> - <template #label>{{ i18n.ts.reactionSettingDescription }}</template> - <div v-panel style="border-radius: 6px;"> - <Sortable v-model="reactions" :class="$style.reactions" :itemKey="item => item" :animation="150" :delay="100" :delayOnTouchOnly="true"> - <template #item="{element}"> - <button class="_button" :class="$style.reactionsItem" @click="remove(element, $event)"> - <MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true"/> - <MkEmoji v-else :emoji="element" :normal="true"/> - </button> - </template> - <template #footer> - <button class="_button" :class="$style.reactionsAdd" @click="chooseEmoji"><i class="ti ti-plus"></i></button> - </template> - </Sortable> - </div> - <template #caption>{{ i18n.ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ i18n.ts.preview }}</button></template> - </FromSlot> - - <MkRadios v-model="reactionPickerSize"> - <template #label>{{ i18n.ts.size }}</template> - <option :value="1">{{ i18n.ts.small }}</option> - <option :value="2">{{ i18n.ts.medium }}</option> - <option :value="3">{{ i18n.ts.large }}</option> - </MkRadios> - <MkRadios v-model="reactionPickerWidth"> - <template #label>{{ i18n.ts.numberOfColumn }}</template> - <option :value="1">5</option> - <option :value="2">6</option> - <option :value="3">7</option> - <option :value="4">8</option> - <option :value="5">9</option> - </MkRadios> - <MkRadios v-model="reactionPickerHeight"> - <template #label>{{ i18n.ts.height }}</template> - <option :value="1">{{ i18n.ts.small }}</option> - <option :value="2">{{ i18n.ts.medium }}</option> - <option :value="3">{{ i18n.ts.large }}</option> - <option :value="4">{{ i18n.ts.large }}+</option> - </MkRadios> - - <MkSwitch v-model="reactionPickerUseDrawerForMobile"> - {{ i18n.ts.useDrawerReactionPickerForMobile }} - <template #caption>{{ i18n.ts.needReloadToApply }}</template> - </MkSwitch> - - <FormSection> - <div class="_buttons"> - <MkButton inline @click="preview"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> - <MkButton inline danger @click="setDefault"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> - </div> - </FormSection> -</div> -</template> - -<script lang="ts" setup> -import { defineAsyncComponent, watch, ref, computed } from 'vue'; -import Sortable from 'vuedraggable'; -import MkRadios from '@/components/MkRadios.vue'; -import FromSlot from '@/components/form/slot.vue'; -import MkButton from '@/components/MkButton.vue'; -import FormSection from '@/components/form/section.vue'; -import MkSwitch from '@/components/MkSwitch.vue'; -import * as os from '@/os.js'; -import { defaultStore } from '@/store.js'; -import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { deepClone } from '@/scripts/clone.js'; - -const reactions = ref(deepClone(defaultStore.state.reactions)); - -const reactionPickerSize = computed(defaultStore.makeGetterSetter('reactionPickerSize')); -const reactionPickerWidth = computed(defaultStore.makeGetterSetter('reactionPickerWidth')); -const reactionPickerHeight = computed(defaultStore.makeGetterSetter('reactionPickerHeight')); -const reactionPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile')); - -function save() { - defaultStore.set('reactions', reactions.value); -} - -function remove(reaction, ev: MouseEvent) { - os.popupMenu([{ - text: i18n.ts.remove, - action: () => { - reactions.value = reactions.value.filter(x => x !== reaction); - }, - }], ev.currentTarget ?? ev.target); -} - -function preview(ev: MouseEvent) { - os.popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { - asReactionPicker: true, - src: ev.currentTarget ?? ev.target, - }, {}, 'closed'); -} - -async function setDefault() { - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.resetAreYouSure, - }); - if (canceled) return; - - reactions.value = deepClone(defaultStore.def.reactions.default); -} - -function chooseEmoji(ev: MouseEvent) { - os.pickEmoji(ev.currentTarget ?? ev.target, { - showPinned: false, - }).then(emoji => { - if (!reactions.value.includes(emoji)) { - reactions.value.push(emoji); - } - }); -} - -watch(reactions, () => { - save(); -}, { - deep: true, -}); - -const headerActions = computed(() => []); - -const headerTabs = computed(() => []); - -definePageMetadata({ - title: i18n.ts.reaction, - icon: 'ti ti-mood-happy', - action: { - icon: 'ti ti-eye', - handler: preview, - }, -}); -</script> - -<style lang="scss" module> -.reactions { - padding: 12px; - font-size: 1.1em; -} - -.reactionsItem { - display: inline-block; - padding: 8px; - cursor: move; -} - -.reactionsAdd { - display: inline-block; - padding: 8px; -} -</style> 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<HTMLElement | null> = 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, }, |