From 764a158cd7e112186cbf54cb77599bcd22ea7d69 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 28 Feb 2021 01:09:59 +0900
Subject: [PATCH] Resolve #7270

---
 src/client/components/emoji-picker-dialog.vue |  9 ++++-
 src/client/components/emoji-picker.vue        | 35 ++++++++----------
 src/client/components/note-detailed.vue       | 22 ++++-------
 src/client/components/note.vue                | 22 ++++-------
 src/client/components/ui/modal.vue            | 26 +++++++++----
 src/client/os.ts                              | 37 +++++++++++++++++++
 src/client/ui/chat/note.vue                   | 25 ++++---------
 7 files changed, 101 insertions(+), 75 deletions(-)

diff --git a/src/client/components/emoji-picker-dialog.vue b/src/client/components/emoji-picker-dialog.vue
index 177b5db44..3450d219c 100644
--- a/src/client/components/emoji-picker-dialog.vue
+++ b/src/client/components/emoji-picker-dialog.vue
@@ -1,6 +1,6 @@
 <template>
-<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
-	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
+<MkModal ref="modal" :manual-showing="manualShowing" :src="src" @click="$refs.modal.close()" @opening="$refs.picker.focus()" @close="$emit('close')" @closed="$emit('closed')">
+	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen" ref="picker"/>
 </MkModal>
 </template>
 
@@ -16,6 +16,11 @@ export default defineComponent({
 	},
 
 	props: {
+		manualShowing: {
+			type: Boolean,
+			required: false,
+			default: null,
+		},
 		src: {
 			required: false
 		},
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
index b11f0a62f..41e667dd9 100644
--- a/src/client/components/emoji-picker.vue
+++ b/src/client/components/emoji-picker.vue
@@ -54,23 +54,17 @@
 				</div>
 			</section>
 		</div>
-		<div v-appear="() => showingCustomEmojis = true">
+		<div>
 			<header class="_acrylic">{{ $ts.customEmojis }}</header>
-			<template v-if="showingCustomEmojis">
-				<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
-			</template>
+			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
 		</div>
-		<div v-appear="() => showingEmojis = true">
+		<div>
 			<header class="_acrylic">{{ $ts.emoji }}</header>
-			<template v-if="showingEmojis">
-				<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
-			</template>
+			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
 		</div>
-		<div v-appear="() => showingTags = true">
+		<div>
 			<header class="_acrylic">{{ $ts.tags }}</header>
-			<template v-if="showingTags">
-				<XSection v-for="tag in emojiTags" :emojis="customEmojis.filter(e => e.aliases.includes(tag)).map(e => ':' + e.name + ':')">{{ tag }}</XSection>
-			</template>
+			<XSection v-for="tag in emojiTags" :emojis="customEmojis.filter(e => e.aliases.includes(tag)).map(e => ':' + e.name + ':')">{{ tag }}</XSection>
 		</div>
 	</div>
 	<div class="tabs">
@@ -127,9 +121,6 @@ export default defineComponent({
 			searchResultCustom: [],
 			searchResultUnicode: [],
 			tab: 'index',
-			showingCustomEmojis: false,
-			showingEmojis: false,
-			showingTags: false,
 			categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'],
 			faGlobe, faClock, faChevronDown, faAsterisk, faLaugh, faUtensils, faLeaf, faShapes, faBicycle, faHashtag,
 		};
@@ -279,14 +270,18 @@ export default defineComponent({
 	},
 
 	mounted() {
-		if (!isMobile && !isDeviceTouch) {
-			this.$refs.search.focus({
-				preventScroll: true
-			});
-		}
+		this.focus();
 	},
 
 	methods: {
+		focus() {
+			if (!isMobile && !isDeviceTouch) {
+				this.$refs.search.focus({
+					preventScroll: true
+				});
+			}
+		},
+
 		getKey(emoji: any) {
 			return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
 		},
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index e1927133a..7df87c6b0 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -523,20 +523,14 @@ export default defineComponent({
 		react(viaKeyboard = false) {
 			pleaseLogin();
 			this.blur();
-			os.popup(import('@/components/emoji-picker-dialog.vue'), {
-				src: this.$refs.reactButton,
-				asReactionPicker: true
-			}, {
-				done: reaction => {
-					if (reaction) {
-						os.api('notes/reactions/create', {
-							noteId: this.appearNote.id,
-							reaction: reaction
-						});
-					}
-					this.focus();
-				},
-			}, 'closed');
+			os.pickReaction(this.$refs.reactButton, reaction => {
+				os.api('notes/reactions/create', {
+					noteId: this.appearNote.id,
+					reaction: reaction
+				});
+			}, () => {
+				this.focus();
+			});
 		},
 
 		reactDirectly(reaction) {
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 6af0668e2..dab764376 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -498,20 +498,14 @@ export default defineComponent({
 		react(viaKeyboard = false) {
 			pleaseLogin();
 			this.blur();
-			os.popup(import('@/components/emoji-picker-dialog.vue'), {
-				src: this.$refs.reactButton,
-				asReactionPicker: true
-			}, {
-				done: reaction => {
-					if (reaction) {
-						os.api('notes/reactions/create', {
-							noteId: this.appearNote.id,
-							reaction: reaction
-						});
-					}
-					this.focus();
-				},
-			}, 'closed');
+			os.pickReaction(this.$refs.reactButton, reaction => {
+				os.api('notes/reactions/create', {
+					noteId: this.appearNote.id,
+					reaction: reaction
+				});
+			}, () => {
+				this.focus();
+			});
 		},
 
 		reactDirectly(reaction) {
diff --git a/src/client/components/ui/modal.vue b/src/client/components/ui/modal.vue
index 405fa4aaa..1c8ae6390 100644
--- a/src/client/components/ui/modal.vue
+++ b/src/client/components/ui/modal.vue
@@ -1,11 +1,13 @@
 <template>
-<div class="mk-modal" v-hotkey.global="keymap" :style="{ pointerEvents: showing ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
+<div class="mk-modal" v-hotkey.global="keymap" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 	<transition :name="$store.state.animation ? 'modal-bg' : ''" appear>
-		<div class="bg _modalBg" v-if="showing" @click="onBgClick"></div>
+		<div class="bg _modalBg" v-if="manualShowing != null ? manualShowing : showing" @click="onBgClick"></div>
 	</transition>
 	<div class="content" :class="{ popup, fixed, top: position === 'top' }" @click.self="onBgClick" ref="content">
-		<transition :name="$store.state.animation ? popup ? 'modal-popup-content' : 'modal-content' : ''" appear @after-leave="$emit('closed')" @after-enter="childRendered">
-			<slot v-if="showing"></slot>
+		<transition :name="$store.state.animation ? popup ? 'modal-popup-content' : 'modal-content' : ''" appear @after-leave="$emit('closed')" @enter="$emit('opening')" @after-enter="childRendered">
+			<div v-show="manualShowing != null ? manualShowing : showing">
+				<slot></slot>
+			</div>
 		</transition>
 	</div>
 </div>
@@ -29,6 +31,11 @@ export default defineComponent({
 		modal: true
 	},
 	props: {
+		manualShowing: {
+			type: Boolean,
+			required: false,
+			default: null,
+		},
 		srcCenter: {
 			type: Boolean,
 			required: false
@@ -40,7 +47,7 @@ export default defineComponent({
 			required: false
 		}
 	},
-	emits: ['click', 'esc', 'closed'],
+	emits: ['opening', 'click', 'esc', 'close', 'closed'],
 	data() {
 		return {
 			showing: true,
@@ -60,15 +67,17 @@ export default defineComponent({
 		}
 	},
 	mounted() {
-		this.fixed = getFixedContainer(this.src) != null;
+		this.$watch('src', () => {
+			this.fixed = getFixedContainer(this.src) != null;
+		}, { immediate: true });
 
 		this.$nextTick(() => {
-			if (!this.popup) return;
-
 			const popover = this.$refs.content as any;
 
 			// TODO: ResizeObserver無くしたい
 			new ResizeObserver((entries, observer) => {
+				if (!this.popup) return;
+
 				const rect = this.src.getBoundingClientRect();
 				
 				const width = popover.offsetWidth;
@@ -141,6 +150,7 @@ export default defineComponent({
 
 		close() {
 			this.showing = false;
+			this.$emit('close');
 		},
 
 		onBgClick() {
diff --git a/src/client/os.ts b/src/client/os.ts
index a971eebd4..9fafb6db4 100644
--- a/src/client/os.ts
+++ b/src/client/os.ts
@@ -357,6 +357,43 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
 	});
 }
 
+let reactionPicker = null;
+export async function pickReaction(src: HTMLElement, chosen, closed) {
+	if (reactionPicker) {
+		if (reactionPicker.opening) return;
+
+		reactionPicker.opening = true;
+		reactionPicker.src.value = src;
+		reactionPicker.manualShowing.value = true;
+		reactionPicker.chosen = chosen;
+		reactionPicker.closed = closed;
+	} else {
+		reactionPicker = {
+			opening: true,
+			src: ref(src),
+			manualShowing: ref(true),
+			chosen, closed
+		};
+		popup(import('@/components/emoji-picker-dialog.vue'), {
+			src: reactionPicker.src,
+			asReactionPicker: true,
+			manualShowing: reactionPicker.manualShowing
+		}, {
+			done: reaction => {
+				reactionPicker.chosen(reaction);
+			},
+			close: () => {
+				reactionPicker.manualShowing.value = false;
+			},
+			closed: () => {
+				reactionPicker.src.value = null;
+				reactionPicker.closed();
+				reactionPicker.opening = false;
+			}
+		});
+	}
+}
+
 export function modalMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
 	return new Promise((resolve, reject) => {
 		let dispose;
diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue
index d80978e18..75b92a32f 100644
--- a/src/client/ui/chat/note.vue
+++ b/src/client/ui/chat/note.vue
@@ -504,23 +504,14 @@ export default defineComponent({
 			pleaseLogin();
 			this.operating = true;
 			this.blur();
-			const { dispose } = await os.popup(import('@/components/emoji-picker-dialog.vue'), {
-				src: this.$refs.reactButton,
-				asReactionPicker: true
-			}, {
-				done: reaction => {
-					if (reaction) {
-						os.api('notes/reactions/create', {
-							noteId: this.appearNote.id,
-							reaction: reaction
-						});
-					}
-				},
-				closed: () => {
-					this.operating = false;
-					this.focus();
-					dispose();
-				}
+			os.pickReaction(this.$refs.reactButton, reaction => {
+				os.api('notes/reactions/create', {
+					noteId: this.appearNote.id,
+					reaction: reaction
+				});
+			}, () => {
+				this.operating = false;
+				this.focus();
 			});
 		},