Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance(frontend): 投稿フォームの絵文字ピッカーに独立したウィンドウを使用できるように #15291

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
### Client
- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように
- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992`
- Enhance: 投稿フォームの絵文字ピッカーに独立したウィンドウを使用できるように

### Server
-
Expand Down
8 changes: 4 additions & 4 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ export interface Locale extends ILocale {
* ウィンドウで開く
*/
"openInWindow": string;
/**
* ウィンドウ
*/
"window": string;
/**
* プロフィール
*/
Expand Down Expand Up @@ -3186,10 +3190,6 @@ export interface Locale extends ILocale {
* 設定はページリロード後に反映されます。
*/
"reloadToApplySetting": string;
/**
* 反映には再起動が必要です。
*/
"needReloadToApply": string;
/**
* タイトルバーを表示する
*/
Expand Down
2 changes: 1 addition & 1 deletion locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ notificationSettings: "通知の設定"
basicSettings: "基本設定"
otherSettings: "その他の設定"
openInWindow: "ウィンドウで開く"
window: "ウィンドウ"
profile: "プロフィール"
timeline: "タイムライン"
noAccountDescription: "自己紹介はありません"
Expand Down Expand Up @@ -792,7 +793,6 @@ center: "中央"
wide: "広い"
narrow: "狭い"
reloadToApplySetting: "設定はページリロード後に反映されます。"
needReloadToApply: "反映には再起動が必要です。"
showTitlebar: "タイトルバーを表示する"
clearCache: "キャッシュをクリア"
onlineUsersCount: "{n}人がオンライン"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import MkEmojiPickerWindow from './MkEmojiPickerWindow.vue';
void MkEmojiPickerWindow;
72 changes: 72 additions & 0 deletions packages/frontend/src/components/MkEmojiPickerWindow.vue
Original file line number Diff line number Diff line change
@@ -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>
19 changes: 13 additions & 6 deletions packages/frontend/src/components/MkPostForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1047,6 +1048,12 @@ onMounted(() => {
});
});

onBeforeUnmount(() => {
// MkPostFormDialogでも発火しているが、Dialogではない場合は呼ばれないためこちらでも呼ぶ必要がある
// なのでDialogの場合は2回発火されるが、ウィンドウを閉じる指示のため悪影響はない
globalEvents.emit('requestCloseEmojiPickerWindow');
});

defineExpose({
clear,
});
Expand Down
10 changes: 9 additions & 1 deletion packages/frontend/src/components/MkPostFormDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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 & {
Expand All @@ -36,6 +37,13 @@ function onPosted() {
});
}

function onModalClose() {
// MkPostFormでもonBeforeUnmountで発火しているが、Dialogの場合は閉じるまでのトランジションがあるので
// 閉じるボタンが押された瞬間に先に発火する
// なのでDialogの場合は2回発火されるが、ウィンドウを閉じる指示のため悪影響はない
globalEvents.emit('requestCloseEmojiPickerWindow');
}

function onModalClosed() {
emit('closed');
}
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const globalEvents = new EventEmitter<{
themeChanged: () => void;
clientNotification: (notification: Misskey.entities.Notification) => void;
requestClearPageCache: () => void;
requestCloseEmojiPickerWindow: () => void;
}>();
11 changes: 9 additions & 2 deletions packages/frontend/src/pages/settings/emoji-picker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ SPDX-License-Identifier: AGPL-3.0-only

<MkSelect v-model="emojiPickerStyle">
<template #label>{{ i18n.ts.style }}</template>
<template #caption>{{ i18n.ts.needReloadToApply }}</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>
</div>
</FormSection>
Expand All @@ -140,6 +140,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';
Expand All @@ -165,7 +166,9 @@ function previewReaction(ev: MouseEvent) {
}

function previewEmoji(ev: MouseEvent) {
emojiPicker.show(getHTMLElement(ev));
emojiPicker.show({
src: getHTMLElement(ev),
});
}

async function overwriteFromPinnedEmojis() {
Expand Down Expand Up @@ -241,6 +244,10 @@ watch(pinnedEmojis, () => {
deep: true,
});

watch(emojiPickerStyle, async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});

definePageMetadata(() => ({
title: i18n.ts.emojiPicker,
icon: 'ti ti-mood-happy',
Expand Down
81 changes: 56 additions & 25 deletions packages/frontend/src/scripts/emoji-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
pinnedEmojis: {
where: 'account',
default: [],
default: [] as string[],
},
reactionAcceptance: {
where: 'account',
Expand Down Expand Up @@ -313,7 +313,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
emojiPickerStyle: {
where: 'device',
default: 'auto' as 'auto' | 'popup' | 'drawer',
default: 'auto' as 'auto' | 'popup' | 'drawer' | 'window',
},
recentlyUsedEmojis: {
where: 'device',
Expand Down
Loading