This commit is contained in:
かっこかり 2025-02-13 11:44:49 +09:00 committed by GitHub
commit b7887bc6e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 196 additions and 42 deletions

View File

@ -7,6 +7,7 @@
- Feat: 投稿フォームで画像をプレビュー可能に
- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように
- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992`
- Enhance: 投稿フォームの絵文字ピッカーに独立したウィンドウを使用できるように
- Enhance: クライアントエラー画面の多言語対応
- Enhance: 開発者モードでメニューからファイルIDをコピー出来るように `#15441'
- Fix: コンディショナルロールを手動で割り当てできる導線を削除 `#13529`

12
locales/index.d.ts vendored
View File

@ -130,6 +130,10 @@ export interface Locale extends ILocale {
*
*/
"openInWindow": string;
/**
*
*/
"window": string;
/**
*
*/
@ -542,6 +546,10 @@ export interface Locale extends ILocale {
*
*/
"emojiPicker": string;
/**
*
*/
"reactionPicker": string;
/**
*
*/
@ -3186,10 +3194,6 @@ export interface Locale extends ILocale {
*
*/
"reloadToApplySetting": string;
/**
*
*/
"needReloadToApply": string;
/**
*
*/

View File

@ -28,6 +28,7 @@ notificationSettings: "通知の設定"
basicSettings: "基本設定"
otherSettings: "その他の設定"
openInWindow: "ウィンドウで開く"
window: "ウィンドウ"
profile: "プロフィール"
timeline: "タイムライン"
noAccountDescription: "自己紹介はありません"
@ -131,6 +132,7 @@ add: "追加"
reaction: "リアクション"
reactions: "リアクション"
emojiPicker: "絵文字ピッカー"
reactionPicker: "リアクションピッカー"
pinnedEmojisForReactionSettingDescription: "リアクション時にピン留め表示する絵文字を設定できます"
pinnedEmojisSettingDescription: "絵文字入力時にピン留め表示する絵文字を設定できます"
emojiPickerDisplay: "ピッカーの表示"
@ -792,7 +794,6 @@ center: "中央"
wide: "広い"
narrow: "狭い"
reloadToApplySetting: "設定はページリロード後に反映されます。"
needReloadToApply: "反映には再起動が必要です。"
showTitlebar: "タイトルバーを表示する"
clearCache: "キャッシュをクリア"
onlineUsersCount: "{n}人がオンライン"

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="modal"
v-slot="{ type, maxHeight }"
:zPriority="'middle'"
:preferType="defaultStore.state.emojiPickerStyle"
:preferType="asReactionPicker ? defaultStore.state.reactionPickerStyle : defaultStore.state.emojiPickerStyle === 'window' ? 'auto' : defaultStore.state.emojiPickerStyle"
:hasInteractionWithOtherFocusTrappedEls="true"
:transparentBg="true"
:manualShowing="manualShowing"

View File

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MkEmojiPickerWindow from './MkEmojiPickerWindow.vue';
void MkEmojiPickerWindow;

View File

@ -0,0 +1,72 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkWindow
ref="window"
:initialWidth="300"
:initialHeight="290"
:canResize="true"
:mini="true"
:front="true"
@closed="emit('closed')"
>
<MkEmojiPicker
:showPinned="showPinned"
:asReactionPicker="asReactionPicker"
:targetNote="targetNote"
asWindow
:class="$style.picker"
@chosen="chosen"
/>
</MkWindow>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import { globalEvents } from '@/events.js';
import MkWindow from '@/components/MkWindow.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
withDefaults(defineProps<{
src?: HTMLElement;
showPinned?: boolean;
pinnedEmojis?: string[];
asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note;
}>(), {
showPinned: true,
});
const emit = defineEmits<{
(ev: 'chosen', v: string): void;
(ev: 'closed'): void;
}>();
function chosen(emoji: string) {
emit('chosen', emoji);
}
const windowEl = useTemplateRef('window');
function onCloseRequested() {
windowEl.value?.close();
}
onMounted(() => {
globalEvents.on('requestCloseEmojiPickerWindow', onCloseRequested);
});
onBeforeUnmount(() => {
globalEvents.off('requestCloseEmojiPickerWindow', onCloseRequested);
});
</script>
<style lang="scss" module>
.picker {
height: 100%;
}
</style>

View File

@ -100,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue';
import { inject, watch, nextTick, onMounted, onBeforeUnmount, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue';
import type { ShallowRef } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
@ -129,6 +129,7 @@ import { uploadFile } from '@/scripts/upload.js';
import { deepClone } from '@/scripts/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
import { globalEvents } from '@/events.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
@ -919,20 +920,20 @@ async function insertEmoji(ev: MouseEvent) {
let pos = textareaEl.value?.selectionStart ?? 0;
let posEnd = textareaEl.value?.selectionEnd ?? text.value.length;
emojiPicker.show(
target as HTMLElement,
emoji => {
emojiPicker.show({
src: target as HTMLElement,
onChosen: emoji => {
const textBefore = text.value.substring(0, pos);
const textAfter = text.value.substring(posEnd);
text.value = textBefore + emoji + textAfter;
pos += emoji.length;
posEnd += emoji.length;
},
() => {
onClosed: () => {
textAreaReadOnly.value = false;
nextTick(() => focus());
},
);
});
}
async function insertMfmFunction(ev: MouseEvent) {
@ -1047,6 +1048,12 @@ onMounted(() => {
});
});
onBeforeUnmount(() => {
// MkPostFormDialogDialog
// Dialog2
globalEvents.emit('requestCloseEmojiPickerWindow');
});
defineExpose({
clear,
});

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()" @esc="modal?.close()">
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @close="onModalClose()" @closed="onModalClosed()" @esc="modal?.close()">
<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
</MkModal>
</template>
@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import { globalEvents } from '@/events.js';
import type { PostFormProps } from '@/types/post-form.js';
const props = withDefaults(defineProps<PostFormProps & {
@ -36,6 +37,13 @@ function onPosted() {
});
}
function onModalClose() {
// MkPostFormonBeforeUnmountDialog
//
// Dialog2
globalEvents.emit('requestCloseEmojiPickerWindow');
}
function onModalClosed() {
emit('closed');
}

View File

@ -10,4 +10,5 @@ export const globalEvents = new EventEmitter<{
themeChanged: () => void;
clientNotification: (notification: Misskey.entities.Notification) => void;
requestClearPageCache: () => void;
requestCloseEmojiPickerWindow: () => void;
}>();

View File

@ -114,8 +114,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRadios>
<MkSelect v-model="emojiPickerStyle">
<template #label>{{ i18n.ts.style }}</template>
<template #caption>{{ i18n.ts.needReloadToApply }}</template>
<template #label>{{ i18n.ts.style }} ({{ i18n.ts.emojiPicker }})</template>
<option value="auto">{{ i18n.ts.auto }}</option>
<option value="popup">{{ i18n.ts.popup }}</option>
<option value="drawer">{{ i18n.ts.drawer }}</option>
<option value="window">{{ i18n.ts.window }}</option>
</MkSelect>
<MkSelect v-model="reactionPickerStyle">
<template #label>{{ i18n.ts.style }} ({{ i18n.ts.reactionPicker }})</template>
<option value="auto">{{ i18n.ts.auto }}</option>
<option value="popup">{{ i18n.ts.popup }}</option>
<option value="drawer">{{ i18n.ts.drawer }}</option>
@ -140,6 +147,7 @@ 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 { reloadAsk } from '@/scripts/reload-ask.js';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkFolder from '@/components/MkFolder.vue';
@ -151,6 +159,7 @@ const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScal
const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth'));
const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight'));
const emojiPickerStyle = computed(defaultStore.makeGetterSetter('emojiPickerStyle'));
const reactionPickerStyle = computed(defaultStore.makeGetterSetter('reactionPickerStyle'));
const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev);
const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev);
@ -165,7 +174,9 @@ function previewReaction(ev: MouseEvent) {
}
function previewEmoji(ev: MouseEvent) {
emojiPicker.show(getHTMLElement(ev));
emojiPicker.show({
src: getHTMLElement(ev),
});
}
async function overwriteFromPinnedEmojis() {
@ -241,6 +252,13 @@ watch(pinnedEmojis, () => {
deep: true,
});
watch([
emojiPickerStyle,
reactionPickerStyle,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
definePageMetadata(() => ({
title: i18n.ts.emojiPicker,
icon: 'ti ti-mood-happy',

View File

@ -16,7 +16,12 @@ import { defaultStore } from '@/store.js';
*/
class EmojiPicker {
private src: Ref<HTMLElement | null> = ref(null);
private manualShowing = ref(false);
private isWindow: boolean = false;
private windowShowing: boolean = false;
private dialogShowing = ref(false);
private onChosen?: (emoji: string) => void;
private onClosed?: () => void;
@ -26,35 +31,61 @@ 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,
}, {
done: emoji => {
if (this.onChosen) this.onChosen(emoji);
},
close: () => {
this.manualShowing.value = false;
},
closed: () => {
this.src.value = null;
if (this.onClosed) this.onClosed();
},
});
if (defaultStore.state.emojiPickerStyle === 'window') {
// init後にemojiPickerStyleが変わった場合、drawer/popup用の初期化をスキップするため、
// 正常に絵文字ピッカーが表示されない。
// なので一度initされたらwindow表示で固定する設定を変更したら要リロード
this.isWindow = true;
} else {
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
pinnedEmojis: emojisRef,
asReactionPicker: false,
manualShowing: this.dialogShowing,
choseAndClose: false,
}, {
done: emoji => {
if (this.onChosen) this.onChosen(emoji);
},
close: () => {
this.dialogShowing.value = false;
},
closed: () => {
this.src.value = null;
if (this.onClosed) this.onClosed();
},
});
}
}
public show(
public show(opts: {
src: HTMLElement,
onChosen?: EmojiPicker['onChosen'],
onClosed?: EmojiPicker['onClosed'],
) {
this.src.value = src;
this.manualShowing.value = true;
this.onChosen = onChosen;
this.onClosed = onClosed;
}) {
if (this.isWindow) {
if (this.windowShowing) return;
this.windowShowing = true;
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerWindow.vue')), {
src: opts.src,
pinnedEmojis: defaultStore.reactiveState.pinnedEmojis,
asReactionPicker: false,
}, {
chosen: (emoji) => {
if (opts.onChosen) opts.onChosen(emoji);
},
closed: () => {
if (opts.onClosed) opts.onClosed();
this.windowShowing = false;
dispose();
},
});
} else {
this.src.value = opts.src;
this.dialogShowing.value = true;
this.onChosen = opts.onChosen;
this.onClosed = opts.onClosed;
}
}
}

View File

@ -131,7 +131,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
pinnedEmojis: {
where: 'account',
default: [],
default: [] as string[],
},
reactionAcceptance: {
where: 'account',
@ -312,6 +312,10 @@ export const defaultStore = markRaw(new Storage('base', {
default: 2,
},
emojiPickerStyle: {
where: 'device',
default: 'auto' as 'auto' | 'popup' | 'drawer' | 'window',
},
reactionPickerStyle: {
where: 'device',
default: 'auto' as 'auto' | 'popup' | 'drawer',
},