mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-03 23:14:13 +00:00 
			
		
		
		
	refactor(client): use composition api for tooltip logic
This commit is contained in:
		
							parent
							
								
									0e3213ff6d
								
							
						
					
					
						commit
						4b7b51d5cc
					
				
					 6 changed files with 187 additions and 219 deletions
				
			
		packages/client/src
| 
						 | 
					@ -94,7 +94,7 @@
 | 
				
			||||||
					<template v-else><i class="fas fa-reply"></i></template>
 | 
										<template v-else><i class="fas fa-reply"></i></template>
 | 
				
			||||||
					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
 | 
										<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
 | 
				
			||||||
				</button>
 | 
									</button>
 | 
				
			||||||
				<XRenoteButton :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/>
 | 
									<XRenoteButton class="button" :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/>
 | 
				
			||||||
				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
 | 
									<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
 | 
				
			||||||
					<i class="fas fa-plus"></i>
 | 
										<i class="fas fa-plus"></i>
 | 
				
			||||||
				</button>
 | 
									</button>
 | 
				
			||||||
| 
						 | 
					@ -132,16 +132,16 @@ import XMediaList from './media-list.vue';
 | 
				
			||||||
import XCwButton from './cw-button.vue';
 | 
					import XCwButton from './cw-button.vue';
 | 
				
			||||||
import XPoll from './poll.vue';
 | 
					import XPoll from './poll.vue';
 | 
				
			||||||
import XRenoteButton from './renote-button.vue';
 | 
					import XRenoteButton from './renote-button.vue';
 | 
				
			||||||
import { pleaseLogin } from '@client/scripts/please-login';
 | 
					import { pleaseLogin } from '@/scripts/please-login';
 | 
				
			||||||
import { focusPrev, focusNext } from '@client/scripts/focus';
 | 
					import { focusPrev, focusNext } from '@/scripts/focus';
 | 
				
			||||||
import { url } from '@client/config';
 | 
					import { url } from '@/config';
 | 
				
			||||||
import copyToClipboard from '@client/scripts/copy-to-clipboard';
 | 
					import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
				
			||||||
import { checkWordMute } from '@client/scripts/check-word-mute';
 | 
					import { checkWordMute } from '@/scripts/check-word-mute';
 | 
				
			||||||
import { userPage } from '@client/filters/user';
 | 
					import { userPage } from '@/filters/user';
 | 
				
			||||||
import * as os from '@client/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import { noteActions, noteViewInterruptors } from '@client/store';
 | 
					import { noteActions, noteViewInterruptors } from '@/store';
 | 
				
			||||||
import { reactionPicker } from '@client/scripts/reaction-picker';
 | 
					import { reactionPicker } from '@/scripts/reaction-picker';
 | 
				
			||||||
import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
 | 
					import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: note.vueとほぼ同じなので共通化したい
 | 
					// TODO: note.vueとほぼ同じなので共通化したい
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
| 
						 | 
					@ -154,8 +154,8 @@ export default defineComponent({
 | 
				
			||||||
		XCwButton,
 | 
							XCwButton,
 | 
				
			||||||
		XPoll,
 | 
							XPoll,
 | 
				
			||||||
		XRenoteButton,
 | 
							XRenoteButton,
 | 
				
			||||||
		MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')),
 | 
							MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
 | 
				
			||||||
		MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')),
 | 
							MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	inject: {
 | 
						inject: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,7 +78,7 @@
 | 
				
			||||||
					<template v-else><i class="fas fa-reply"></i></template>
 | 
										<template v-else><i class="fas fa-reply"></i></template>
 | 
				
			||||||
					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
 | 
										<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
 | 
				
			||||||
				</button>
 | 
									</button>
 | 
				
			||||||
				<XRenoteButton :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/>
 | 
									<XRenoteButton class="button" :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/>
 | 
				
			||||||
				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
 | 
									<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
 | 
				
			||||||
					<i class="fas fa-plus"></i>
 | 
										<i class="fas fa-plus"></i>
 | 
				
			||||||
				</button>
 | 
									</button>
 | 
				
			||||||
| 
						 | 
					@ -115,16 +115,16 @@ import XMediaList from './media-list.vue';
 | 
				
			||||||
import XCwButton from './cw-button.vue';
 | 
					import XCwButton from './cw-button.vue';
 | 
				
			||||||
import XPoll from './poll.vue';
 | 
					import XPoll from './poll.vue';
 | 
				
			||||||
import XRenoteButton from './renote-button.vue';
 | 
					import XRenoteButton from './renote-button.vue';
 | 
				
			||||||
import { pleaseLogin } from '@client/scripts/please-login';
 | 
					import { pleaseLogin } from '@/scripts/please-login';
 | 
				
			||||||
import { focusPrev, focusNext } from '@client/scripts/focus';
 | 
					import { focusPrev, focusNext } from '@/scripts/focus';
 | 
				
			||||||
import { url } from '@client/config';
 | 
					import { url } from '@/config';
 | 
				
			||||||
import copyToClipboard from '@client/scripts/copy-to-clipboard';
 | 
					import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
				
			||||||
import { checkWordMute } from '@client/scripts/check-word-mute';
 | 
					import { checkWordMute } from '@/scripts/check-word-mute';
 | 
				
			||||||
import { userPage } from '@client/filters/user';
 | 
					import { userPage } from '@/filters/user';
 | 
				
			||||||
import * as os from '@client/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import { noteActions, noteViewInterruptors } from '@client/store';
 | 
					import { noteActions, noteViewInterruptors } from '@/store';
 | 
				
			||||||
import { reactionPicker } from '@client/scripts/reaction-picker';
 | 
					import { reactionPicker } from '@/scripts/reaction-picker';
 | 
				
			||||||
import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
 | 
					import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
	components: {
 | 
						components: {
 | 
				
			||||||
| 
						 | 
					@ -136,8 +136,8 @@ export default defineComponent({
 | 
				
			||||||
		XCwButton,
 | 
							XCwButton,
 | 
				
			||||||
		XPoll,
 | 
							XPoll,
 | 
				
			||||||
		XRenoteButton,
 | 
							XRenoteButton,
 | 
				
			||||||
		MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')),
 | 
							MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
 | 
				
			||||||
		MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')),
 | 
							MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	inject: {
 | 
						inject: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,6 +78,7 @@ import notePage from '@/filters/note';
 | 
				
			||||||
import { userPage } from '@/filters/user';
 | 
					import { userPage } from '@/filters/user';
 | 
				
			||||||
import { i18n } from '@/i18n';
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
 | 
					import { useTooltip } from '@/scripts/use-tooltip';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
	components: {
 | 
						components: {
 | 
				
			||||||
| 
						 | 
					@ -153,47 +154,14 @@ export default defineComponent({
 | 
				
			||||||
			os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id });
 | 
								os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id });
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		let isReactionHovering = false;
 | 
							const { onMouseover: onReactionMouseover, onMouseleave: onReactionMouseleave } = useTooltip((showing) => {
 | 
				
			||||||
		let reactionTooltipTimeoutId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const onReactionMouseover = () => {
 | 
					 | 
				
			||||||
			if (isReactionHovering) return;
 | 
					 | 
				
			||||||
			isReactionHovering = true;
 | 
					 | 
				
			||||||
			reactionTooltipTimeoutId = setTimeout(openReactionTooltip, 300);
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const onReactionMouseleave = () => {
 | 
					 | 
				
			||||||
			if (!isReactionHovering) return;
 | 
					 | 
				
			||||||
			isReactionHovering = false;
 | 
					 | 
				
			||||||
			clearTimeout(reactionTooltipTimeoutId);
 | 
					 | 
				
			||||||
			closeReactionTooltip();
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		let changeReactionTooltipShowingState: (() => void) | null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const openReactionTooltip = () => {
 | 
					 | 
				
			||||||
			closeReactionTooltip();
 | 
					 | 
				
			||||||
			if (!isReactionHovering) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const showing = ref(true);
 | 
					 | 
				
			||||||
			os.popup(XReactionTooltip, {
 | 
								os.popup(XReactionTooltip, {
 | 
				
			||||||
				showing,
 | 
									showing,
 | 
				
			||||||
				reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
 | 
									reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
 | 
				
			||||||
				emojis: props.notification.note.emojis,
 | 
									emojis: props.notification.note.emojis,
 | 
				
			||||||
				source: reactionRef.value.$el,
 | 
									source: reactionRef.value.$el,
 | 
				
			||||||
			}, {}, 'closed');
 | 
								}, {}, 'closed');
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
			changeReactionTooltipShowingState = () => {
 | 
					 | 
				
			||||||
				showing.value = false;
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const closeReactionTooltip = () => {
 | 
					 | 
				
			||||||
			if (changeReactionTooltipShowingState != null) {
 | 
					 | 
				
			||||||
				changeReactionTooltipShowingState();
 | 
					 | 
				
			||||||
				changeReactionTooltipShowingState = null;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			getNoteSummary: (note: misskey.entities.Note) => getNoteSummary(note),
 | 
								getNoteSummary: (note: misskey.entities.Note) => getNoteSummary(note),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,13 +2,13 @@
 | 
				
			||||||
<button
 | 
					<button
 | 
				
			||||||
	class="hkzvhatu _button"
 | 
						class="hkzvhatu _button"
 | 
				
			||||||
	:class="{ reacted: note.myReaction == reaction, canToggle }"
 | 
						:class="{ reacted: note.myReaction == reaction, canToggle }"
 | 
				
			||||||
	@click="toggleReaction(reaction)"
 | 
						@click="toggleReaction()"
 | 
				
			||||||
	v-if="count > 0"
 | 
						v-if="count > 0"
 | 
				
			||||||
	@touchstart.passive="onMouseover"
 | 
						@touchstart.passive="onMouseover"
 | 
				
			||||||
	@mouseover="onMouseover"
 | 
						@mouseover="onMouseover"
 | 
				
			||||||
	@mouseleave="onMouseleave"
 | 
						@mouseleave="onMouseleave"
 | 
				
			||||||
	@touchend="onMouseleave"
 | 
						@touchend="onMouseleave"
 | 
				
			||||||
	ref="reaction"
 | 
						ref="buttonRef"
 | 
				
			||||||
	v-particle="canToggle"
 | 
						v-particle="canToggle"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
 | 
						<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
 | 
				
			||||||
| 
						 | 
					@ -17,15 +17,18 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { defineComponent, ref } from 'vue';
 | 
					import { computed, defineComponent, onMounted, ref, watch } from 'vue';
 | 
				
			||||||
import XDetails from '@/components/reactions-viewer.details.vue';
 | 
					import XDetails from '@/components/reactions-viewer.details.vue';
 | 
				
			||||||
import XReactionIcon from '@/components/reaction-icon.vue';
 | 
					import XReactionIcon from '@/components/reaction-icon.vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
 | 
					import { useTooltip } from '@/scripts/use-tooltip';
 | 
				
			||||||
 | 
					import { $i } from '@/account';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
	components: {
 | 
						components: {
 | 
				
			||||||
		XReactionIcon
 | 
							XReactionIcon
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	props: {
 | 
						props: {
 | 
				
			||||||
		reaction: {
 | 
							reaction: {
 | 
				
			||||||
			type: String,
 | 
								type: String,
 | 
				
			||||||
| 
						 | 
					@ -44,101 +47,78 @@ export default defineComponent({
 | 
				
			||||||
			required: true,
 | 
								required: true,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			close: null,
 | 
					 | 
				
			||||||
			detailsTimeoutId: null,
 | 
					 | 
				
			||||||
			isHovering: false
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	computed: {
 | 
					 | 
				
			||||||
		canToggle(): boolean {
 | 
					 | 
				
			||||||
			return !this.reaction.match(/@\w/) && this.$i;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	watch: {
 | 
					 | 
				
			||||||
		count(newCount, oldCount) {
 | 
					 | 
				
			||||||
			if (oldCount < newCount) this.anime();
 | 
					 | 
				
			||||||
			if (this.close != null) this.openDetails();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		if (!this.isInitial) this.anime();
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		toggleReaction() {
 | 
					 | 
				
			||||||
			if (!this.canToggle) return;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const oldReaction = this.note.myReaction;
 | 
						setup(props) {
 | 
				
			||||||
 | 
							const buttonRef = ref<HTMLElement>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const toggleReaction = () => {
 | 
				
			||||||
 | 
								if (!canToggle.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const oldReaction = props.note.myReaction;
 | 
				
			||||||
			if (oldReaction) {
 | 
								if (oldReaction) {
 | 
				
			||||||
				os.api('notes/reactions/delete', {
 | 
									os.api('notes/reactions/delete', {
 | 
				
			||||||
					noteId: this.note.id
 | 
										noteId: props.note.id
 | 
				
			||||||
				}).then(() => {
 | 
									}).then(() => {
 | 
				
			||||||
					if (oldReaction !== this.reaction) {
 | 
										if (oldReaction !== props.reaction) {
 | 
				
			||||||
						os.api('notes/reactions/create', {
 | 
											os.api('notes/reactions/create', {
 | 
				
			||||||
							noteId: this.note.id,
 | 
												noteId: props.note.id,
 | 
				
			||||||
							reaction: this.reaction
 | 
												reaction: props.reaction
 | 
				
			||||||
						});
 | 
											});
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				os.api('notes/reactions/create', {
 | 
									os.api('notes/reactions/create', {
 | 
				
			||||||
					noteId: this.note.id,
 | 
										noteId: props.note.id,
 | 
				
			||||||
					reaction: this.reaction
 | 
										reaction: props.reaction
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							};
 | 
				
			||||||
		onMouseover() {
 | 
					 | 
				
			||||||
			if (this.isHovering) return;
 | 
					 | 
				
			||||||
			this.isHovering = true;
 | 
					 | 
				
			||||||
			this.detailsTimeoutId = setTimeout(this.openDetails, 300);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		onMouseleave() {
 | 
					 | 
				
			||||||
			if (!this.isHovering) return;
 | 
					 | 
				
			||||||
			this.isHovering = false;
 | 
					 | 
				
			||||||
			clearTimeout(this.detailsTimeoutId);
 | 
					 | 
				
			||||||
			this.closeDetails();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		openDetails() {
 | 
					 | 
				
			||||||
			os.api('notes/reactions', {
 | 
					 | 
				
			||||||
				noteId: this.note.id,
 | 
					 | 
				
			||||||
				type: this.reaction,
 | 
					 | 
				
			||||||
				limit: 11
 | 
					 | 
				
			||||||
			}).then((reactions: any[]) => {
 | 
					 | 
				
			||||||
				const users = reactions
 | 
					 | 
				
			||||||
					.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
 | 
					 | 
				
			||||||
					.map(x => x.user);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
				this.closeDetails();
 | 
							const anime = () => {
 | 
				
			||||||
				if (!this.isHovering) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				const showing = ref(true);
 | 
					 | 
				
			||||||
				os.popup(XDetails, {
 | 
					 | 
				
			||||||
					showing,
 | 
					 | 
				
			||||||
					reaction: this.reaction,
 | 
					 | 
				
			||||||
					emojis: this.note.emojis,
 | 
					 | 
				
			||||||
					users,
 | 
					 | 
				
			||||||
					count: this.count,
 | 
					 | 
				
			||||||
					source: this.$refs.reaction
 | 
					 | 
				
			||||||
				}, {}, 'closed');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				this.close = () => {
 | 
					 | 
				
			||||||
					showing.value = false;
 | 
					 | 
				
			||||||
				};
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		closeDetails() {
 | 
					 | 
				
			||||||
			if (this.close != null) {
 | 
					 | 
				
			||||||
				this.close();
 | 
					 | 
				
			||||||
				this.close = null;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		anime() {
 | 
					 | 
				
			||||||
			if (document.hidden) return;
 | 
								if (document.hidden) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// TODO
 | 
								// TODO: 新しくリアクションが付いたことが視覚的に分かりやすいアニメーション
 | 
				
			||||||
		},
 | 
							};
 | 
				
			||||||
	}
 | 
					
 | 
				
			||||||
 | 
							watch(() => props.count, (newCount, oldCount) => {
 | 
				
			||||||
 | 
								if (oldCount < newCount) anime();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							onMounted(() => {
 | 
				
			||||||
 | 
								if (!props.isInitial) anime();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const { onMouseover, onMouseleave } = useTooltip(async (showing) => {
 | 
				
			||||||
 | 
								const reactions = await os.api('notes/reactions', {
 | 
				
			||||||
 | 
									noteId: props.note.id,
 | 
				
			||||||
 | 
									type: props.reaction,
 | 
				
			||||||
 | 
									limit: 11
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const users = reactions
 | 
				
			||||||
 | 
									.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
 | 
				
			||||||
 | 
									.map(x => x.user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								os.popup(XDetails, {
 | 
				
			||||||
 | 
									showing,
 | 
				
			||||||
 | 
									reaction: props.reaction,
 | 
				
			||||||
 | 
									emojis: props.note.emojis,
 | 
				
			||||||
 | 
									users,
 | 
				
			||||||
 | 
									count: props.count,
 | 
				
			||||||
 | 
									source: buttonRef.value
 | 
				
			||||||
 | 
								}, {}, 'closed');
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								buttonRef,
 | 
				
			||||||
 | 
								canToggle,
 | 
				
			||||||
 | 
								toggleReaction,
 | 
				
			||||||
 | 
								onMouseover,
 | 
				
			||||||
 | 
								onMouseleave,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,13 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<button
 | 
					<button
 | 
				
			||||||
	class="button _button canRenote"
 | 
						class="eddddedb _button canRenote"
 | 
				
			||||||
	@click="renote()"
 | 
						@click="renote()"
 | 
				
			||||||
	v-if="canRenote"
 | 
						v-if="canRenote"
 | 
				
			||||||
	@touchstart.passive="onMouseover"
 | 
						@touchstart.passive="onMouseover"
 | 
				
			||||||
	@mouseover="onMouseover"
 | 
						@mouseover="onMouseover"
 | 
				
			||||||
	@mouseleave="onMouseleave"
 | 
						@mouseleave="onMouseleave"
 | 
				
			||||||
	@touchend="onMouseleave"
 | 
						@touchend="onMouseleave"
 | 
				
			||||||
	ref="renoteButton"
 | 
						ref="buttonRef"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<i class="fas fa-retweet"></i>
 | 
						<i class="fas fa-retweet"></i>
 | 
				
			||||||
	<p class="count" v-if="count > 0">{{ count }}</p>
 | 
						<p class="count" v-if="count > 0">{{ count }}</p>
 | 
				
			||||||
| 
						 | 
					@ -21,10 +21,13 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { defineComponent, ref } from 'vue';
 | 
					import { computed, defineComponent, ref } from 'vue';
 | 
				
			||||||
import XDetails from '@client/components/renote.details.vue';
 | 
					import XDetails from '@/components/renote.details.vue';
 | 
				
			||||||
import { pleaseLogin } from '@client/scripts/please-login';
 | 
					import { pleaseLogin } from '@/scripts/please-login';
 | 
				
			||||||
import * as os from '@client/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
 | 
					import { $i } from '@/account';
 | 
				
			||||||
 | 
					import { useTooltip } from '@/scripts/use-tooltip';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
	props: {
 | 
						props: {
 | 
				
			||||||
| 
						 | 
					@ -37,95 +40,68 @@ export default defineComponent({
 | 
				
			||||||
			required: true,
 | 
								required: true,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	data() {
 | 
					
 | 
				
			||||||
		return {
 | 
						setup(props) {
 | 
				
			||||||
			close: null,
 | 
							const buttonRef = ref<HTMLElement>();
 | 
				
			||||||
			detailsTimeoutId: null,
 | 
					
 | 
				
			||||||
			isHovering: false
 | 
							const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
 | 
				
			||||||
		};
 | 
					
 | 
				
			||||||
	},
 | 
							const { onMouseover, onMouseleave } = useTooltip(async (showing) => {
 | 
				
			||||||
	computed: {
 | 
								const renotes = await os.api('notes/renotes', {
 | 
				
			||||||
		canRenote(): boolean {
 | 
									noteId: props.note.id,
 | 
				
			||||||
			return ['public', 'home'].includes(this.note.visibility) || this.note.userId === this.$i.id;
 | 
									limit: 11
 | 
				
			||||||
		},
 | 
								});
 | 
				
			||||||
	},
 | 
					
 | 
				
			||||||
	watch: {
 | 
								const users = renotes
 | 
				
			||||||
		count(newCount, oldCount) {
 | 
									.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
 | 
				
			||||||
			if (oldCount < newCount) this.anime();
 | 
									.map(x => x.user);
 | 
				
			||||||
			if (this.close != null) this.openDetails();
 | 
					
 | 
				
			||||||
		},
 | 
								if (users.length < 1) return;
 | 
				
			||||||
	},
 | 
					
 | 
				
			||||||
	methods: {
 | 
								os.popup(XDetails, {
 | 
				
			||||||
		renote(viaKeyboard = false) {
 | 
									showing,
 | 
				
			||||||
 | 
									users,
 | 
				
			||||||
 | 
									count: props.count,
 | 
				
			||||||
 | 
									source: buttonRef.value
 | 
				
			||||||
 | 
								}, {}, 'closed');
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const renote = (viaKeyboard = false) => {
 | 
				
			||||||
			pleaseLogin();
 | 
								pleaseLogin();
 | 
				
			||||||
			os.popupMenu([{
 | 
								os.popupMenu([{
 | 
				
			||||||
				text: this.$ts.renote,
 | 
									text: i18n.locale.renote,
 | 
				
			||||||
				icon: 'fas fa-retweet',
 | 
									icon: 'fas fa-retweet',
 | 
				
			||||||
				action: () => {
 | 
									action: () => {
 | 
				
			||||||
					os.api('notes/create', {
 | 
										os.api('notes/create', {
 | 
				
			||||||
						renoteId: this.note.id
 | 
											renoteId: props.note.id
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}, {
 | 
								}, {
 | 
				
			||||||
				text: this.$ts.quote,
 | 
									text: i18n.locale.quote,
 | 
				
			||||||
				icon: 'fas fa-quote-right',
 | 
									icon: 'fas fa-quote-right',
 | 
				
			||||||
				action: () => {
 | 
									action: () => {
 | 
				
			||||||
					os.post({
 | 
										os.post({
 | 
				
			||||||
						renote: this.note,
 | 
											renote: props.note,
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}], this.$refs.renoteButton, {
 | 
								}], buttonRef.value, {
 | 
				
			||||||
				viaKeyboard
 | 
									viaKeyboard
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		},
 | 
							};
 | 
				
			||||||
		onMouseover() {
 | 
					 | 
				
			||||||
			if (this.isHovering) return;
 | 
					 | 
				
			||||||
			this.isHovering = true;
 | 
					 | 
				
			||||||
			this.detailsTimeoutId = setTimeout(this.openDetails, 300);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		onMouseleave() {
 | 
					 | 
				
			||||||
			if (!this.isHovering) return;
 | 
					 | 
				
			||||||
			this.isHovering = false;
 | 
					 | 
				
			||||||
			clearTimeout(this.detailsTimeoutId);
 | 
					 | 
				
			||||||
			this.closeDetails();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		openDetails() {
 | 
					 | 
				
			||||||
			os.api('notes/renotes', {
 | 
					 | 
				
			||||||
				noteId: this.note.id,
 | 
					 | 
				
			||||||
				limit: 11
 | 
					 | 
				
			||||||
			}).then((renotes: any[]) => {
 | 
					 | 
				
			||||||
				const users = renotes
 | 
					 | 
				
			||||||
					.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
 | 
					 | 
				
			||||||
					.map(x => x.user);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
				this.closeDetails();
 | 
							return {
 | 
				
			||||||
				if (!this.isHovering || users.length < 1) return;
 | 
								buttonRef,
 | 
				
			||||||
 | 
								canRenote,
 | 
				
			||||||
				const showing = ref(true);
 | 
								renote,
 | 
				
			||||||
				os.popup(XDetails, {
 | 
								onMouseover,
 | 
				
			||||||
					showing,
 | 
								onMouseleave,
 | 
				
			||||||
					users,
 | 
							};
 | 
				
			||||||
					count: this.count,
 | 
						},
 | 
				
			||||||
					source: this.$refs.renoteButton
 | 
					 | 
				
			||||||
				}, {}, 'closed');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				this.close = () => {
 | 
					 | 
				
			||||||
					showing.value = false;
 | 
					 | 
				
			||||||
				};
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		closeDetails() {
 | 
					 | 
				
			||||||
			if (this.close != null) {
 | 
					 | 
				
			||||||
				this.close();
 | 
					 | 
				
			||||||
				this.close = null;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
.button {
 | 
					.eddddedb {
 | 
				
			||||||
	display: inline-block;
 | 
						display: inline-block;
 | 
				
			||||||
	height: 32px;
 | 
						height: 32px;
 | 
				
			||||||
	margin: 2px;
 | 
						margin: 2px;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										44
									
								
								packages/client/src/scripts/use-tooltip.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								packages/client/src/scripts/use-tooltip.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					import { Ref, ref } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
 | 
				
			||||||
 | 
						let isHovering = false;
 | 
				
			||||||
 | 
						let timeoutId: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let changeShowingState: (() => void) | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const open = () => {
 | 
				
			||||||
 | 
							close();
 | 
				
			||||||
 | 
							if (!isHovering) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const showing = ref(true);
 | 
				
			||||||
 | 
							onShow(showing);
 | 
				
			||||||
 | 
							changeShowingState = () => {
 | 
				
			||||||
 | 
								showing.value = false;
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const close = () => {
 | 
				
			||||||
 | 
							if (changeShowingState != null) {
 | 
				
			||||||
 | 
								changeShowingState();
 | 
				
			||||||
 | 
								changeShowingState = null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onMouseover = () => {
 | 
				
			||||||
 | 
							if (isHovering) return;
 | 
				
			||||||
 | 
							isHovering = true;
 | 
				
			||||||
 | 
							timeoutId = window.setTimeout(open, 300);
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onMouseleave = () => {
 | 
				
			||||||
 | 
							if (!isHovering) return;
 | 
				
			||||||
 | 
							isHovering = false;
 | 
				
			||||||
 | 
							window.clearTimeout(timeoutId);
 | 
				
			||||||
 | 
							close();
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							onMouseover,
 | 
				
			||||||
 | 
							onMouseleave,
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue