enhance(frontend): 絵文字メニューから直接絵文字パレットに追加できるように (#17420)

* enhance(frontend): 絵文字メニューから直接絵文字パレットに追加できるように

* Update Changelog

* fix lint

* Update Changelog

* enhance: 追加し直す挙動に変更

* ✌️

* fix
This commit is contained in:
かっこかり 2026-06-04 20:50:33 +09:00 committed by GitHub
commit e2bcd9c2b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 176 additions and 14 deletions

View file

@ -9,6 +9,7 @@
### Client
- Enhance: ユーザーページのファイルタブでスクロール位置が保持されるように
- Enhance: ドライブページでスクロール位置が保持されるように
- Enhance: 絵文字のメニューから直接絵文字パレットに絵文字を追加できるように
- Fix: URLプレビューのプレイヤーをウィンドウで開いたとき、プレイヤーが読み込まれるまでの間 `Invalid URL` と表示される問題を修正
- Fix: 一部の実績が正しく表示されない問題を修正
- Fix: アクセストークン発行時のダイアログのタイトルが「確認コード」となっているのを修正

View file

@ -1415,6 +1415,11 @@ viewRenotedChannel: "リノート先のチャンネルを見る"
previewingTheme: "テーマのプレビュー中"
previewingThemeRestore: "元に戻す"
accessToken: "アクセストークン"
chooseEmojiPalette: "絵文字パレットを選択"
addToEmojiPalette: "絵文字パレットに追加"
emojiPaletteAlreadyAddedConfirm: "この絵文字はすでにこの絵文字パレットに含まれています。追加しなおしますか?"
append: "末尾に追加"
prepend: "先頭に追加"
_imageEditing:
_vars:

View file

@ -38,6 +38,7 @@ import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
import { noteEvents } from '@/composables/use-note-capture.js';
import { mute as muteEmoji, unmute as unmuteEmoji, checkMuted as isEmojiMuted } from '@/utility/emoji-mute.js';
import { addToEmojiPalette } from '@/utility/emoji-palette.js';
import { haptic } from '@/utility/haptic.js';
const props = defineProps<{
@ -206,6 +207,16 @@ async function menu(ev: PointerEvent) {
});
}
if (canToggle.value) {
menuItems.push({
text: i18n.ts.addToEmojiPalette,
icon: 'ti ti-palette',
action: () => {
addToEmojiPalette(isLocalCustomEmoji ? `:${emojiName.value}:` : props.reaction);
},
});
}
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}

View file

@ -46,6 +46,7 @@ export type ItemOption<T extends OptionValue = OptionValue> = {
type?: 'option';
value: T;
label: string;
caption?: string;
};
export type ItemGroup<T extends OptionValue = OptionValue> = {
@ -177,6 +178,7 @@ function show() {
for (const option of item.items) {
menu.push({
text: option.label,
caption: option.caption,
active: computed(() => model.value === option.value),
action: () => {
model.value = option.value as ModelTChecked;
@ -186,6 +188,7 @@ function show() {
} else {
menu.push({
text: item.label,
caption: item.caption,
active: computed(() => model.value === item.value),
action: () => {
model.value = item.value as ModelTChecked;

View file

@ -50,6 +50,7 @@ import { $i } from '@/i.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
import { makeEmojiMuteKey, mute as muteEmoji, unmute as unmuteEmoji, checkMuted as checkEmojiMuted } from '@/utility/emoji-mute';
import { addToEmojiPalette } from '@/utility/emoji-palette.js';
const props = defineProps<{
name: string;
@ -167,8 +168,20 @@ function onClick(ev: PointerEvent) {
});
}
if (isLocal.value) {
menuItems.push({
text: i18n.ts.addToEmojiPalette,
icon: 'ti ti-palette',
action: () => {
addToEmojiPalette(`:${props.name}:`);
},
});
}
if (($i?.isModerator ?? $i?.isAdmin) && isLocal.value) {
menuItems.push({
type: 'divider',
}, {
text: i18n.ts.edit,
icon: 'ti ti-pencil',
action: async () => {

View file

@ -20,6 +20,7 @@ import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
import { mute as muteEmoji, unmute as unmuteEmoji, checkMuted as checkMutedEmoji } from '@/utility/emoji-mute.js';
import { addToEmojiPalette } from '@/utility/emoji-palette.js';
const props = defineProps<{
emoji: string;
@ -94,17 +95,31 @@ function onClick(ev: PointerEvent) {
menuItems.push({
type: 'divider',
}, isMuted.value ? {
text: i18n.ts.emojiUnmute,
icon: 'ti ti-mood-smile',
});
if (isMuted.value) {
menuItems.push({
text: i18n.ts.emojiUnmute,
icon: 'ti ti-mood-smile',
action: () => {
unmute();
},
});
} else {
menuItems.push({
text: i18n.ts.emojiMute,
icon: 'ti ti-mood-off',
action: () => {
mute();
},
});
}
menuItems.push({
text: i18n.ts.addToEmojiPalette,
icon: 'ti ti-palette',
action: () => {
unmute();
},
} : {
text: i18n.ts.emojiMute,
icon: 'ti ti-mood-off',
action: () => {
mute();
addToEmojiPalette(props.emoji);
},
});

View file

@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { prefer } from '@/preferences.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import type { MkSelectItem } from '@/components/MkSelect.vue';
export function chooseEmojiPalette() {
return os.select({
title: i18n.ts.chooseEmojiPalette,
default: prefer.s.emojiPaletteForMain ?? prefer.s.emojiPaletteForReaction ?? prefer.s.emojiPalettes[0]?.id,
items: prefer.s.emojiPalettes.map<MkSelectItem<string>>((palette) => {
let caption: string | undefined = undefined;
if (prefer.s.emojiPaletteForMain === palette.id) {
caption = i18n.ts._emojiPalette.paletteForMain;
} else if (prefer.s.emojiPaletteForReaction === palette.id) {
caption = i18n.ts._emojiPalette.paletteForReaction;
}
return {
label: palette.name || `(${i18n.ts.noName})`,
caption,
value: palette.id,
};
}),
});
}
export async function addToEmojiPalette(emoji: string) {
const res = await chooseEmojiPalette();
if (res.canceled || res.result == null) return;
const palette = prefer.s.emojiPalettes.find((p) => p.id === res.result);
if (!palette) return;
let emojis = [...palette.emojis];
if (!emojis.includes(emoji)) {
emojis.push(emoji);
prefer.commit('emojiPalettes', prefer.s.emojiPalettes.map((p) => {
if (p.id === palette.id) {
return {
...p,
emojis,
};
} else {
return p;
}
}));
os.success();
} else {
const res = await os.actions({
type: 'warning',
text: i18n.ts.emojiPaletteAlreadyAddedConfirm,
actions: [{
value: 'prepend',
text: i18n.ts.prepend,
}, {
value: 'append',
text: i18n.ts.append,
}, {
value: 'doNothing',
text: i18n.ts.doNothing,
}],
});
if (res.canceled || res.result === 'doNothing') return;
emojis = emojis.filter((e) => e !== emoji);
if (res.result === 'append') {
emojis.push(emoji);
} else if (res.result === 'prepend') {
emojis.unshift(emoji);
}
prefer.commit('emojiPalettes', prefer.s.emojiPalettes.map((p) => {
if (p.id === palette.id) {
return {
...p,
emojis,
};
} else {
return p;
}
}));
os.success();
}
}

View file

@ -22,8 +22,8 @@ class EmojiPicker {
}
public init() {
watch([prefer.r.emojiPaletteForMain, prefer.r.emojiPalettes], () => {
this.emojisRef.value = prefer.s.emojiPaletteForMain == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForMain)?.emojis ?? [];
watch([prefer.r.emojiPaletteForMain, prefer.r.emojiPalettes], ([newId, newPalettes]) => {
this.emojisRef.value = newId == null ? newPalettes[0].emojis : newPalettes.find(palette => palette.id === newId)?.emojis ?? [];
}, {
immediate: true,
});

View file

@ -17,8 +17,8 @@ class ReactionPicker {
}
public init() {
watch([prefer.r.emojiPaletteForReaction, prefer.r.emojiPalettes], () => {
this.reactionsRef.value = prefer.s.emojiPaletteForReaction == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForReaction)?.emojis ?? [];
watch([prefer.r.emojiPaletteForReaction, prefer.r.emojiPalettes], ([newId, newPalettes]) => {
this.reactionsRef.value = newId == null ? newPalettes[0].emojis : newPalettes.find(palette => palette.id === newId)?.emojis ?? [];
}, {
immediate: true,
});

View file

@ -5675,6 +5675,26 @@ export interface Locale extends ILocale {
*
*/
"accessToken": string;
/**
*
*/
"chooseEmojiPalette": string;
/**
*
*/
"addToEmojiPalette": string;
/**
*
*/
"emojiPaletteAlreadyAddedConfirm": string;
/**
*
*/
"append": string;
/**
*
*/
"prepend": string;
"_imageEditing": {
"_vars": {
/**