summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-11-08 03:38:48 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-11-08 03:38:48 +0900
commit3b8b03d8b486ae9f6b0bd0eca4b12756acb19135 (patch)
tree49af6fa760491505d74ceea80934e0495235343a /src
parent:art: (diff)
downloadmisskey-3b8b03d8b486ae9f6b0bd0eca4b12756acb19135.tar.gz
misskey-3b8b03d8b486ae9f6b0bd0eca4b12756acb19135.tar.bz2
misskey-3b8b03d8b486ae9f6b0bd0eca4b12756acb19135.zip
feat(client): 通知のリアクションアイコンをホバーで拡大できるように
Diffstat (limited to 'src')
-rw-r--r--src/client/components/notification.vue168
-rw-r--r--src/client/components/reaction-tooltip.vue51
2 files changed, 169 insertions, 50 deletions
diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue
index a2e714b4e2..ce1fa5b160 100644
--- a/src/client/components/notification.vue
+++ b/src/client/components/notification.vue
@@ -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() {
- return {
- getNoteSummary: (text: string) => getNoteSummary(text, i18n.locale),
- followRequestDone: false,
- groupInviteDone: false,
- connection: null,
- readObserver: null,
- };
- },
- 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
+ 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));
});
- 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();
});
+ });
- this.readObserver.observe(this.$el);
+ const followRequestDone = ref(false);
+ const groupInviteDone = ref(false);
- this.connection = markRaw(os.stream.useChannel('main'));
- this.connection.on('readAllNotifications', () => this.readObserver.unobserve(this.$el));
- }
- },
+ const acceptFollowRequest = () => {
+ followRequestDone.value = true;
+ os.api('following/requests/accept', { userId: props.notification.user.id });
+ };
- beforeUnmount() {
- if (!this.notification.isRead) {
- this.readObserver.unobserve(this.$el);
- this.connection.dispose();
- }
- },
+ const rejectFollowRequest = () => {
+ followRequestDone.value = true;
+ os.api('following/requests/reject', { userId: props.notification.user.id });
+ };
- 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
- }
+ 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,
+ groupInviteDone,
+ notePage,
+ userPage,
+ acceptFollowRequest,
+ rejectFollowRequest,
+ acceptGroupInvitation,
+ rejectGroupInvitation,
+ onReactionMouseover,
+ onReactionMouseleave,
+ elRef,
+ reactionRef,
+ };
+ },
});
</script>
diff --git a/src/client/components/reaction-tooltip.vue b/src/client/components/reaction-tooltip.vue
new file mode 100644
index 0000000000..93143cbe81
--- /dev/null
+++ b/src/client/components/reaction-tooltip.vue
@@ -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>