mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-03 23:14:13 +00:00 
			
		
		
		
	feat(client): 通知のリアクションアイコンをホバーで拡大できるように
This commit is contained in:
		
							parent
							
								
									1afe46b19a
								
							
						
					
					
						commit
						3b8b03d8b4
					
				
					 3 changed files with 173 additions and 53 deletions
				
			
		| 
						 | 
				
			
			@ -12,6 +12,7 @@
 | 
			
		|||
### Improvements
 | 
			
		||||
- フォロー/フォロワーを非公開にできるように
 | 
			
		||||
- インスタンスプロフィールレンダリング ready
 | 
			
		||||
- 通知のリアクションアイコンをホバーで拡大できるように
 | 
			
		||||
- メールアドレスのバリデーションを強化
 | 
			
		||||
 | 
			
		||||
### Bugfixes
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="qglefbjs" :class="notification.type" v-size="{ max: [500, 600] }">
 | 
			
		||||
<div class="qglefbjs" :class="notification.type" v-size="{ max: [500, 600] }" ref="elRef">
 | 
			
		||||
	<div class="head">
 | 
			
		||||
		<MkAvatar v-if="notification.user" class="icon" :user="notification.user"/>
 | 
			
		||||
		<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,16 @@
 | 
			
		|||
			<i v-else-if="notification.type === 'quote'" class="fas fa-quote-left"></i>
 | 
			
		||||
			<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
 | 
			
		||||
			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
 | 
			
		||||
			<XReactionIcon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" :custom-emojis="notification.note.emojis" :no-style="true"/>
 | 
			
		||||
			<XReactionIcon v-else-if="notification.type === 'reaction'"
 | 
			
		||||
				:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
 | 
			
		||||
				:custom-emojis="notification.note.emojis"
 | 
			
		||||
				:no-style="true"
 | 
			
		||||
				@touchstart.passive="onReactionMouseover"
 | 
			
		||||
				@mouseover="onReactionMouseover"
 | 
			
		||||
				@mouseleave="onReactionMouseleave"
 | 
			
		||||
				@touchend="onReactionMouseleave"
 | 
			
		||||
				ref="reactionRef"
 | 
			
		||||
			/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="tail">
 | 
			
		||||
| 
						 | 
				
			
			@ -59,10 +68,11 @@
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import { getNoteSummary } from '@/misc/get-note-summary';
 | 
			
		||||
import XReactionIcon from './reaction-icon.vue';
 | 
			
		||||
import MkFollowButton from './follow-button.vue';
 | 
			
		||||
import XReactionTooltip from './reaction-tooltip.vue';
 | 
			
		||||
import notePage from '@client/filters/note';
 | 
			
		||||
import { userPage } from '@client/filters/user';
 | 
			
		||||
import { i18n } from '@client/i18n';
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +82,7 @@ export default defineComponent({
 | 
			
		|||
	components: {
 | 
			
		||||
		XReactionIcon, MkFollowButton
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		notification: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
| 
						 | 
				
			
			@ -88,60 +99,117 @@ export default defineComponent({
 | 
			
		|||
			default: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
 | 
			
		||||
	setup(props) {
 | 
			
		||||
		const elRef = ref<HTMLElement>(null);
 | 
			
		||||
		const reactionRef = ref(null);
 | 
			
		||||
 | 
			
		||||
		onMounted(() => {
 | 
			
		||||
			let readObserver: IntersectionObserver = null;
 | 
			
		||||
			let connection = null;
 | 
			
		||||
 | 
			
		||||
			if (!props.notification.isRead) {
 | 
			
		||||
				readObserver = new IntersectionObserver((entries, observer) => {
 | 
			
		||||
					if (!entries.some(entry => entry.isIntersecting)) return;
 | 
			
		||||
					os.stream.send('readNotification', {
 | 
			
		||||
						id: props.notification.id
 | 
			
		||||
					});
 | 
			
		||||
					entries.map(({ target }) => observer.unobserve(target));
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				readObserver.observe(elRef.value);
 | 
			
		||||
 | 
			
		||||
				connection = os.stream.useChannel('main');
 | 
			
		||||
				connection.on('readAllNotifications', () => readObserver.unobserve(elRef.value));
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			onUnmounted(() => {
 | 
			
		||||
				if (readObserver) readObserver.unobserve(elRef.value);
 | 
			
		||||
				if (connection) connection.dispose();
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const followRequestDone = ref(false);
 | 
			
		||||
		const groupInviteDone = ref(false);
 | 
			
		||||
 | 
			
		||||
		const acceptFollowRequest = () => {
 | 
			
		||||
			followRequestDone.value = true;
 | 
			
		||||
			os.api('following/requests/accept', { userId: props.notification.user.id });
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const rejectFollowRequest = () => {
 | 
			
		||||
			followRequestDone.value = true;
 | 
			
		||||
			os.api('following/requests/reject', { userId: props.notification.user.id });
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const acceptGroupInvitation = () => {
 | 
			
		||||
			groupInviteDone.value = true;
 | 
			
		||||
			os.apiWithDialog('users/groups/invitations/accept', { invitationId: props.notification.invitation.id });
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const rejectGroupInvitation = () => {
 | 
			
		||||
			groupInviteDone.value = true;
 | 
			
		||||
			os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id });
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		let isReactionHovering = false;
 | 
			
		||||
		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;
 | 
			
		||||
 | 
			
		||||
		const openReactionTooltip = () => {
 | 
			
		||||
			closeReactionTooltip();
 | 
			
		||||
			if (!isReactionHovering) return;
 | 
			
		||||
 | 
			
		||||
			const showing = ref(true);
 | 
			
		||||
			os.popup(XReactionTooltip, {
 | 
			
		||||
				showing,
 | 
			
		||||
				reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
 | 
			
		||||
				emojis: props.notification.note.emojis,
 | 
			
		||||
				source: reactionRef.value.$el,
 | 
			
		||||
			}, {}, 'closed');
 | 
			
		||||
 | 
			
		||||
			changeReactionTooltipShowingState = () => {
 | 
			
		||||
				showing.value = false;
 | 
			
		||||
			};
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const closeReactionTooltip = () => {
 | 
			
		||||
			if (changeReactionTooltipShowingState != null) {
 | 
			
		||||
				changeReactionTooltipShowingState();
 | 
			
		||||
				changeReactionTooltipShowingState = null;
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			getNoteSummary: (text: string) => getNoteSummary(text, i18n.locale),
 | 
			
		||||
			followRequestDone: false,
 | 
			
		||||
			groupInviteDone: false,
 | 
			
		||||
			connection: null,
 | 
			
		||||
			readObserver: null,
 | 
			
		||||
			followRequestDone,
 | 
			
		||||
			groupInviteDone,
 | 
			
		||||
			notePage,
 | 
			
		||||
			userPage,
 | 
			
		||||
			acceptFollowRequest,
 | 
			
		||||
			rejectFollowRequest,
 | 
			
		||||
			acceptGroupInvitation,
 | 
			
		||||
			rejectGroupInvitation,
 | 
			
		||||
			onReactionMouseover,
 | 
			
		||||
			onReactionMouseleave,
 | 
			
		||||
			elRef,
 | 
			
		||||
			reactionRef,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		if (!this.notification.isRead) {
 | 
			
		||||
			this.readObserver = new IntersectionObserver((entries, observer) => {
 | 
			
		||||
				if (!entries.some(entry => entry.isIntersecting)) return;
 | 
			
		||||
				os.stream.send('readNotification', {
 | 
			
		||||
					id: this.notification.id
 | 
			
		||||
				});
 | 
			
		||||
				entries.map(({ target }) => observer.unobserve(target));
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.readObserver.observe(this.$el);
 | 
			
		||||
 | 
			
		||||
			this.connection = markRaw(os.stream.useChannel('main'));
 | 
			
		||||
			this.connection.on('readAllNotifications', () => this.readObserver.unobserve(this.$el));
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		if (!this.notification.isRead) {
 | 
			
		||||
			this.readObserver.unobserve(this.$el);
 | 
			
		||||
			this.connection.dispose();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		acceptFollowRequest() {
 | 
			
		||||
			this.followRequestDone = true;
 | 
			
		||||
			os.api('following/requests/accept', { userId: this.notification.user.id });
 | 
			
		||||
		},
 | 
			
		||||
		rejectFollowRequest() {
 | 
			
		||||
			this.followRequestDone = true;
 | 
			
		||||
			os.api('following/requests/reject', { userId: this.notification.user.id });
 | 
			
		||||
		},
 | 
			
		||||
		acceptGroupInvitation() {
 | 
			
		||||
			this.groupInviteDone = true;
 | 
			
		||||
			os.apiWithDialog('users/groups/invitations/accept', { invitationId: this.notification.invitation.id });
 | 
			
		||||
		},
 | 
			
		||||
		rejectGroupInvitation() {
 | 
			
		||||
			this.groupInviteDone = true;
 | 
			
		||||
			os.api('users/groups/invitations/reject', { invitationId: this.notification.invitation.id });
 | 
			
		||||
		},
 | 
			
		||||
		notePage,
 | 
			
		||||
		userPage
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										51
									
								
								src/client/components/reaction-tooltip.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/client/components/reaction-tooltip.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
<template>
 | 
			
		||||
<MkTooltip :source="source" ref="tooltip" @closed="$emit('closed')" :max-width="340">
 | 
			
		||||
	<div class="beeadbfb">
 | 
			
		||||
		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
 | 
			
		||||
		<div class="name">{{ reaction.replace('@.', '') }}</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</MkTooltip>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import MkTooltip from './ui/tooltip.vue';
 | 
			
		||||
import XReactionIcon from './reaction-icon.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkTooltip,
 | 
			
		||||
		XReactionIcon,
 | 
			
		||||
	},
 | 
			
		||||
	props: {
 | 
			
		||||
		reaction: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: true,
 | 
			
		||||
		},
 | 
			
		||||
		emojis: {
 | 
			
		||||
			type: Array,
 | 
			
		||||
			required: true,
 | 
			
		||||
		},
 | 
			
		||||
		source: {
 | 
			
		||||
			required: true,
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	emits: ['closed'],
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.beeadbfb {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
 | 
			
		||||
	> .icon {
 | 
			
		||||
		display: block;
 | 
			
		||||
		width: 60px;
 | 
			
		||||
		margin: 0 auto;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .name {
 | 
			
		||||
		font-size: 0.9em;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue