This commit is contained in:
かっこかり 2025-05-04 14:31:00 +09:00 committed by GitHub
commit 3689964640
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 219 additions and 52 deletions

View File

@ -42,6 +42,8 @@
- Enhance: テーマでページヘッダーの色を変更できるように - Enhance: テーマでページヘッダーの色を変更できるように
- Enhance: スワイプでのタブ切り替えを強化 - Enhance: スワイプでのタブ切り替えを強化
- Enhance: デザインのブラッシュアップ - Enhance: デザインのブラッシュアップ
- Enhance: 投稿フォームの絵文字ピッカーに独立したウィンドウを使用できるように
- リアクションピッカーと絵文字ピッカーで表示スタイルの設定が分離しました。絵文字ピッカーでのみウィンドウスタイルを使用可能です。
- Fix: ログアウトした際に処理が終了しない問題を修正 - Fix: ログアウトした際に処理が終了しない問題を修正
- Fix: 自動バックアップが設定されている環境でログアウト直前に設定をバックアップするように - Fix: 自動バックアップが設定されている環境でログアウト直前に設定をバックアップするように
- Fix: フォルダを開いた状態でメニューからアップロードしてもルートフォルダにアップロードされる問題を修正 #15836 - Fix: フォルダを開いた状態でメニューからアップロードしてもルートフォルダにアップロードされる問題を修正 #15836

12
locales/index.d.ts vendored
View File

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

View File

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

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="modal" ref="modal"
v-slot="{ type, maxHeight }" v-slot="{ type, maxHeight }"
:zPriority="'middle'" :zPriority="'middle'"
:preferType="prefer.s.emojiPickerStyle" :preferType="asReactionPicker ? prefer.s.reactionPickerStyle : prefer.s.emojiPickerStyle === 'window' ? 'auto' : prefer.s.emojiPickerStyle"
:hasInteractionWithOtherFocusTrappedEls="true" :hasInteractionWithOtherFocusTrappedEls="true"
:transparentBg="true" :transparentBg="true"
:manualShowing="manualShowing" :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,68 @@
<!--
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 close() {
windowEl.value?.close();
}
defineExpose({
close,
});
</script>
<style lang="scss" module>
.picker {
height: 100%;
}
</style>

View File

@ -99,13 +99,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef } from 'vue'; import { inject, watch, nextTick, onMounted, onBeforeUnmount, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef } from 'vue';
import type { ShallowRef } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor'; import insertTextAtCursor from 'insert-text-at-cursor';
import { toASCII } from 'punycode.js'; import { toASCII } from 'punycode.js';
import { host, url } from '@@/js/config.js'; import { host, url } from '@@/js/config.js';
import type { ShallowRef } from 'vue';
import type { PostFormProps } from '@/types/post-form.js'; import type { PostFormProps } from '@/types/post-form.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
@ -137,6 +137,7 @@ import { mfmFunctionPicker } from '@/utility/mfm-function-picker.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js'; import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js'; import { DI } from '@/di.js';
import { globalEvents } from '@/events.js';
const $i = ensureSignin(); const $i = ensureSignin();
@ -974,20 +975,20 @@ async function insertEmoji(ev: MouseEvent) {
let pos = textareaEl.value?.selectionStart ?? 0; let pos = textareaEl.value?.selectionStart ?? 0;
let posEnd = textareaEl.value?.selectionEnd ?? text.value.length; let posEnd = textareaEl.value?.selectionEnd ?? text.value.length;
emojiPicker.show( emojiPicker.show({
target as HTMLElement, src: target as HTMLElement,
emoji => { onChosen: emoji => {
const textBefore = text.value.substring(0, pos); const textBefore = text.value.substring(0, pos);
const textAfter = text.value.substring(posEnd); const textAfter = text.value.substring(posEnd);
text.value = textBefore + emoji + textAfter; text.value = textBefore + emoji + textAfter;
pos += emoji.length; pos += emoji.length;
posEnd += emoji.length; posEnd += emoji.length;
}, },
() => { onClosed: () => {
textAreaReadOnly.value = false; textAreaReadOnly.value = false;
nextTick(() => focus()); nextTick(() => focus());
}, },
); });
} }
async function insertMfmFunction(ev: MouseEvent) { async function insertMfmFunction(ev: MouseEvent) {
@ -1102,6 +1103,10 @@ onMounted(() => {
}); });
}); });
onBeforeUnmount(() => {
emojiPicker.closeWindow();
});
defineExpose({ defineExpose({
clear, clear,
}); });

View File

@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="modal" ref="modal"
:preferType="'dialog'" :preferType="'dialog'"
@click="modal?.close()" @click="modal?.close()"
@close="onModalClose()"
@closed="onModalClosed()" @closed="onModalClosed()"
@esc="modal?.close()" @esc="modal?.close()"
> >
@ -26,9 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { useTemplateRef } from 'vue'; import { useTemplateRef } from 'vue';
import type { PostFormProps } from '@/types/post-form.js';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkPostForm from '@/components/MkPostForm.vue'; import MkPostForm from '@/components/MkPostForm.vue';
import { emojiPicker } from '@/utility/emoji-picker.js';
import type { PostFormProps } from '@/types/post-form.js';
const props = withDefaults(defineProps<PostFormProps & { const props = withDefaults(defineProps<PostFormProps & {
instant?: boolean; instant?: boolean;
@ -50,6 +52,10 @@ function onPosted() {
}); });
} }
function onModalClose() {
emojiPicker.closeWindow();
}
function onModalClosed() { function onModalClosed() {
emit('closed'); emit('closed');
} }

View File

@ -5,10 +5,10 @@
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue'; import { markRaw, ref, shallowRef, defineAsyncComponent, nextTick } from 'vue';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import type { Component, Ref } from 'vue'; import type { Component, Ref, ShallowRef } from 'vue';
import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js'; import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
@ -141,6 +141,7 @@ let popupIdCount = 0;
export const popups = ref<{ export const popups = ref<{
id: number; id: number;
component: Component; component: Component;
componentRef: ShallowRef<Component | null>;
props: Record<string, any>; props: Record<string, any>;
events: Record<string, any>; events: Record<string, any>;
}[]>([]); }[]>([]);
@ -178,13 +179,21 @@ type EmitsExtractor<T> = {
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K]; [K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
}; };
export function popup<T extends Component>( export function popup<
T extends Component,
TI extends T extends new (...args: unknown[]) => infer I ? I : T,
>(
component: T, component: T,
props: ComponentProps<T>, props: ComponentProps<T>,
events: Partial<ComponentEmit<T>> = {}, events: Partial<ComponentEmit<T>> = {},
): { dispose: () => void } { ): {
dispose: () => void;
componentRef: ShallowRef<TI | null>;
} {
markRaw(component); markRaw(component);
const componentRef = shallowRef<TI | null>(null);
const id = ++popupIdCount; const id = ++popupIdCount;
const dispose = () => { const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ // このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ
@ -194,6 +203,7 @@ export function popup<T extends Component>(
}; };
const state = { const state = {
component, component,
componentRef,
props, props,
events, events,
id, id,
@ -203,6 +213,7 @@ export function popup<T extends Component>(
return { return {
dispose, dispose,
componentRef,
}; };
} }

View File

@ -100,8 +100,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['emoji', 'picker', 'style']"> <SearchMarker :keywords="['emoji', 'picker', 'style']">
<MkPreferenceContainer k="emojiPickerStyle"> <MkPreferenceContainer k="emojiPickerStyle">
<MkSelect v-model="emojiPickerStyle"> <MkSelect v-model="emojiPickerStyle">
<template #label><SearchLabel>{{ i18n.ts.style }}</SearchLabel></template> <template #label><SearchLabel>{{ i18n.ts.style }} ({{ i18n.ts.emojiPicker }})</SearchLabel></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>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['reaction', 'emoji', 'picker', 'style']">
<MkPreferenceContainer k="emojiPickerStyle">
<MkSelect v-model="emojiPickerStyle">
<template #label><SearchLabel>{{ i18n.ts.style }} ({{ i18n.ts.reactionPicker }})</SearchLabel></template>
<option value="auto">{{ i18n.ts.auto }}</option> <option value="auto">{{ i18n.ts.auto }}</option>
<option value="popup">{{ i18n.ts.popup }}</option> <option value="popup">{{ i18n.ts.popup }}</option>
<option value="drawer">{{ i18n.ts.drawer }}</option> <option value="drawer">{{ i18n.ts.drawer }}</option>
@ -133,13 +144,14 @@ import { prefer } from '@/preferences.js';
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue'; import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import { emojiPicker } from '@/utility/emoji-picker.js'; import { emojiPicker } from '@/utility/emoji-picker.js';
import { reloadAsk } from '@/utility/reload-ask.js';
const emojiPaletteForReaction = prefer.model('emojiPaletteForReaction'); const emojiPaletteForReaction = prefer.model('emojiPaletteForReaction');
const emojiPaletteForMain = prefer.model('emojiPaletteForMain'); const emojiPaletteForMain = prefer.model('emojiPaletteForMain');
const emojiPickerScale = prefer.model('emojiPickerScale'); const emojiPickerScale = prefer.model('emojiPickerScale');
const emojiPickerWidth = prefer.model('emojiPickerWidth'); const emojiPickerWidth = prefer.model('emojiPickerWidth');
const emojiPickerHeight = prefer.model('emojiPickerHeight'); const emojiPickerHeight = prefer.model('emojiPickerHeight');
const emojiPickerStyle = prefer.model('emojiPickerStyle'); const emojiPickerStyle = prefer.model('emojiPickerStyle');
const reactionPickerStyle = prefer.model('reactionPickerStyle');
const palettesSyncEnabled = ref(prefer.isSyncEnabled('emojiPalettes')); const palettesSyncEnabled = ref(prefer.isSyncEnabled('emojiPalettes'));
@ -214,6 +226,13 @@ function previewPicker(ev: MouseEvent) {
emojiPicker.show(getHTMLElement(ev)); emojiPicker.show(getHTMLElement(ev));
} }
watch([
emojiPickerStyle,
reactionPickerStyle,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
definePage(() => ({ definePage(() => ({
title: i18n.ts.emojiPalette, title: i18n.ts.emojiPalette,
icon: 'ti ti-mood-happy', icon: 'ti ti-mood-happy',

View File

@ -230,6 +230,9 @@ export const PREF_DEF = {
default: 3, default: 3,
}, },
emojiPickerStyle: { emojiPickerStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer' | 'window',
},
reactionPickerStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer', default: 'auto' as 'auto' | 'popup' | 'drawer',
}, },
squareAvatars: { squareAvatars: {

View File

@ -312,7 +312,7 @@ export const store = markRaw(new Pizzax('base', {
}, },
emojiPickerStyle: { emojiPickerStyle: {
where: 'device', where: 'device',
default: 'auto' as 'auto' | 'popup' | 'drawer', default: 'auto' as 'auto' | 'popup' | 'drawer' | 'window',
}, },
reportError: { reportError: {
where: 'device', where: 'device',

View File

@ -61,6 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:is="popup.component" :is="popup.component"
v-for="popup in popups" v-for="popup in popups"
:key="popup.id" :key="popup.id"
:ref="(el: Component | null) => popup.componentRef = el"
v-bind="popup.props" v-bind="popup.props"
v-on="popup.events" v-on="popup.events"
/> />

View File

@ -3,8 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { defineAsyncComponent, ref, watch } from 'vue'; import { defineAsyncComponent, ref, shallowRef, watch } from 'vue';
import type { Ref } from 'vue'; import type { Ref, ShallowRef } from 'vue';
import type MkEmojiPickerWindow_TypeOnly from '@/components/MkEmojiPickerWindow.vue';
import { popup } from '@/os.js'; import { popup } from '@/os.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
@ -16,7 +17,15 @@ import { prefer } from '@/preferences.js';
*/ */
class EmojiPicker { class EmojiPicker {
private src: Ref<HTMLElement | null> = ref(null); private src: Ref<HTMLElement | null> = ref(null);
private manualShowing = ref(false);
private isWindow: boolean = false;
private windowComponentEl: ShallowRef<InstanceType<typeof MkEmojiPickerWindow_TypeOnly> | null> = shallowRef(null);
private windowShowing: boolean = false;
private dialogShowing = ref(false);
private emojisRef = ref<string[]>([]);
private onChosen?: (emoji: string) => void; private onChosen?: (emoji: string) => void;
private onClosed?: () => void; private onClosed?: () => void;
@ -25,26 +34,30 @@ class EmojiPicker {
} }
public async init() { public async init() {
const emojisRef = ref<string[]>([]);
watch([prefer.r.emojiPaletteForMain, prefer.r.emojiPalettes], () => { watch([prefer.r.emojiPaletteForMain, prefer.r.emojiPalettes], () => {
emojisRef.value = prefer.s.emojiPaletteForMain == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForMain)?.emojis ?? []; this.emojisRef.value = prefer.s.emojiPaletteForMain == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForMain)?.emojis ?? [];
}, { }, {
immediate: true, immediate: true,
}); });
if (prefer.s.emojiPickerStyle === 'window') {
// init後にemojiPickerStyleが変わった場合、drawer/popup用の初期化をスキップするため、
// 正常に絵文字ピッカーが表示されない。
// なので一度initされたらwindow表示で固定する設定を変更したら要リロード
this.isWindow = true;
} else {
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src, src: this.src,
pinnedEmojis: emojisRef, pinnedEmojis: this.emojisRef,
asReactionPicker: false, asReactionPicker: false,
manualShowing: this.manualShowing, manualShowing: this.dialogShowing,
choseAndClose: false, choseAndClose: false,
}, { }, {
done: emoji => { done: emoji => {
if (this.onChosen) this.onChosen(emoji); if (this.onChosen) this.onChosen(emoji);
}, },
close: () => { close: () => {
this.manualShowing.value = false; this.dialogShowing.value = false;
}, },
closed: () => { closed: () => {
this.src.value = null; this.src.value = null;
@ -52,16 +65,43 @@ class EmojiPicker {
}, },
}); });
} }
}
public show( public show(opts: {
src: HTMLElement, src: HTMLElement,
onChosen?: EmojiPicker['onChosen'], onChosen?: EmojiPicker['onChosen'],
onClosed?: EmojiPicker['onClosed'], onClosed?: EmojiPicker['onClosed'],
) { }) {
this.src.value = src; if (this.isWindow) {
this.manualShowing.value = true; if (this.windowShowing) return;
this.onChosen = onChosen; this.windowShowing = true;
this.onClosed = onClosed; const { dispose, componentRef } = popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerWindow.vue')), {
src: opts.src,
pinnedEmojis: this.emojisRef,
asReactionPicker: false,
}, {
chosen: (emoji) => {
if (opts.onChosen) opts.onChosen(emoji);
},
closed: () => {
if (opts.onClosed) opts.onClosed();
this.windowShowing = false;
dispose();
},
});
this.windowComponentEl.value = componentRef.value;
} else {
this.src.value = opts.src;
this.dialogShowing.value = true;
this.onChosen = opts.onChosen;
this.onClosed = opts.onClosed;
}
}
public closeWindow() {
if (this.isWindow && this.windowComponentEl.value) {
this.windowComponentEl.value.close();
}
} }
} }