2024-04-24 15:33:56 +02:00
|
|
|
<template>
|
|
|
|
<div
|
|
|
|
ref="elRef"
|
|
|
|
v-size="{ max: [500, 450] }"
|
|
|
|
class="qglefbjs notification"
|
|
|
|
:class="notification.type"
|
|
|
|
>
|
|
|
|
<div class="meta">
|
|
|
|
<span class="info">
|
|
|
|
<span class="sub-icon" :class="notification.type">
|
|
|
|
<i
|
|
|
|
v-if="notification.type === 'renote'"
|
|
|
|
:class="icon('ph-rocket-launch', false)"
|
|
|
|
></i>
|
|
|
|
<XReactionIcon
|
|
|
|
v-else-if="
|
|
|
|
showEmojiReactions && notification.type === 'reaction'
|
|
|
|
"
|
|
|
|
ref="reactionRef"
|
|
|
|
:reaction="
|
|
|
|
notification.reaction
|
|
|
|
? notification.reaction.replace(
|
|
|
|
/^:(\w+):$/,
|
|
|
|
':$1@.:',
|
|
|
|
)
|
|
|
|
: notification.reaction
|
|
|
|
"
|
|
|
|
:custom-emojis="notification.note.emojis"
|
|
|
|
/>
|
|
|
|
<XReactionIcon
|
|
|
|
v-else-if="
|
|
|
|
!showEmojiReactions && notification.type === 'reaction'
|
|
|
|
"
|
|
|
|
:reaction="defaultReaction"
|
|
|
|
:no-style="true"
|
|
|
|
/>
|
|
|
|
</span>
|
|
|
|
<span class="avatars">
|
|
|
|
<MkAvatar
|
|
|
|
v-for="user in users"
|
2024-05-17 16:30:10 +02:00
|
|
|
:key="user.id"
|
2024-04-24 15:33:56 +02:00
|
|
|
class="avatar"
|
|
|
|
:user="user"
|
|
|
|
/>
|
|
|
|
</span>
|
|
|
|
<span class="text">
|
|
|
|
{{ getText() }}
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</span>
|
|
|
|
<MkTime
|
|
|
|
v-if="withTime"
|
|
|
|
:time="notification.createdAt"
|
|
|
|
class="time"
|
|
|
|
/>
|
|
|
|
</div>
|
2024-04-25 03:17:01 +02:00
|
|
|
<!-- Since the reacted user list is actually shown above, the emoji-viewer is hidden to prevent visual noise -->
|
2024-04-25 03:32:59 +02:00
|
|
|
<XNote
|
2024-04-24 15:33:56 +02:00
|
|
|
v-if="notification.type === 'renote'"
|
|
|
|
class="content"
|
2024-04-25 03:17:01 +02:00
|
|
|
:note="removeReplyTo(notification.note.renote)"
|
|
|
|
:hide-emoji-viewer="true"
|
2024-04-25 09:53:29 +02:00
|
|
|
:is-long-judger="isLongJudger"
|
2024-04-24 15:33:56 +02:00
|
|
|
/>
|
2024-04-25 03:32:59 +02:00
|
|
|
<XNote
|
2024-04-24 15:33:56 +02:00
|
|
|
v-else
|
|
|
|
class="content"
|
2024-04-25 03:17:01 +02:00
|
|
|
:note="removeReplyTo(notification.note)"
|
|
|
|
:hide-emoji-viewer="true"
|
2024-04-25 09:53:29 +02:00
|
|
|
:is-long-judger="isLongJudger"
|
2024-04-24 15:33:56 +02:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2024-04-26 16:39:58 +02:00
|
|
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
2024-04-24 15:33:56 +02:00
|
|
|
import type { Connection } from "firefish-js/src/streaming";
|
|
|
|
import type { Channels } from "firefish-js/src/streaming.types";
|
2024-04-29 04:56:31 +02:00
|
|
|
import type { entities } from "firefish-js";
|
2024-04-24 15:33:56 +02:00
|
|
|
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
|
|
|
import XReactionTooltip from "@/components/MkReactionTooltip.vue";
|
|
|
|
import { i18n } from "@/i18n";
|
|
|
|
import * as os from "@/os";
|
|
|
|
import { useStream } from "@/stream";
|
|
|
|
import { useTooltip } from "@/scripts/use-tooltip";
|
|
|
|
import { defaultStore } from "@/store";
|
|
|
|
import { instance } from "@/instance";
|
|
|
|
import icon from "@/scripts/icon";
|
|
|
|
import type {
|
|
|
|
NotificationFolded,
|
|
|
|
ReactionNotificationFolded,
|
|
|
|
} from "@/types/notification";
|
2024-04-25 03:32:59 +02:00
|
|
|
import XNote from "@/components/MkNote.vue";
|
2024-04-24 15:33:56 +02:00
|
|
|
|
|
|
|
const props = withDefaults(
|
|
|
|
defineProps<{
|
|
|
|
notification: NotificationFolded;
|
|
|
|
withTime?: boolean;
|
|
|
|
full?: boolean;
|
|
|
|
}>(),
|
|
|
|
{
|
|
|
|
withTime: false,
|
|
|
|
full: false,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
const stream = useStream();
|
|
|
|
|
|
|
|
const elRef = ref<HTMLElement | null>(null);
|
|
|
|
const reactionRef = ref<InstanceType<typeof XReactionIcon> | null>(null);
|
|
|
|
|
|
|
|
const showEmojiReactions =
|
|
|
|
defaultStore.state.enableEmojiReactions ||
|
|
|
|
defaultStore.state.showEmojisInReactionNotifications;
|
|
|
|
const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReaction)
|
|
|
|
? instance.defaultReaction
|
|
|
|
: "⭐";
|
|
|
|
|
2024-04-26 16:39:58 +02:00
|
|
|
const users = computed(() => props.notification.users.slice(0, 5));
|
|
|
|
const userleft = computed(
|
|
|
|
() => props.notification.users.length - users.value.length,
|
|
|
|
);
|
2024-04-24 15:33:56 +02:00
|
|
|
|
|
|
|
let readObserver: IntersectionObserver | undefined;
|
|
|
|
let connection: Connection<Channels["main"]> | null = null;
|
|
|
|
|
2024-04-25 09:53:29 +02:00
|
|
|
function isLongJudger(note: entities.Note) {
|
|
|
|
return (
|
|
|
|
note.text != null &&
|
|
|
|
(note.text.split("\n").length > 5 ||
|
|
|
|
note.text.length > 300 ||
|
|
|
|
note.files.length > 4)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-04-24 15:33:56 +02:00
|
|
|
function getText() {
|
|
|
|
let res = "";
|
|
|
|
switch (props.notification.type) {
|
|
|
|
case "renote":
|
|
|
|
res = i18n.ts._notification.renoted;
|
|
|
|
break;
|
|
|
|
case "reaction":
|
|
|
|
res = i18n.ts._notification.reacted;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (userleft.value > 0) {
|
|
|
|
res = i18n.t("_notification.andCountUsers", {
|
|
|
|
count: userleft.value,
|
|
|
|
acted: res,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2024-04-25 03:17:01 +02:00
|
|
|
/**
|
|
|
|
* Delete reply-related properties that are not needed for notifications
|
|
|
|
*/
|
|
|
|
function removeReplyTo(note: entities.Note): entities.Note {
|
|
|
|
return Object.assign(note, {
|
|
|
|
replyId: null,
|
|
|
|
reply: undefined,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-04-24 15:33:56 +02:00
|
|
|
useTooltip(reactionRef, (showing) => {
|
|
|
|
const n = props.notification as ReactionNotificationFolded;
|
|
|
|
os.popup(
|
|
|
|
XReactionTooltip,
|
|
|
|
{
|
|
|
|
showing,
|
|
|
|
reaction: n.reaction
|
|
|
|
? n.reaction.replace(/^:(\w+):$/, ":$1@.:")
|
|
|
|
: n.reaction,
|
|
|
|
emojis: n.note.emojis,
|
|
|
|
targetElement: reactionRef.value!.$el,
|
|
|
|
},
|
|
|
|
{},
|
|
|
|
"closed",
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
const unreadNotifications = props.notification.notifications.filter(
|
|
|
|
(n) => !n.isRead,
|
|
|
|
);
|
|
|
|
|
|
|
|
readObserver = new IntersectionObserver((entries, observer) => {
|
|
|
|
if (!entries.some((entry) => entry.isIntersecting)) return;
|
|
|
|
for (const u of unreadNotifications) {
|
|
|
|
stream.send("readNotification", {
|
|
|
|
id: u.id,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
observer.disconnect();
|
|
|
|
});
|
|
|
|
|
|
|
|
readObserver.observe(elRef.value!);
|
|
|
|
|
|
|
|
connection = stream.useChannel("main");
|
|
|
|
connection.on("readAllNotifications", () => readObserver!.disconnect());
|
|
|
|
});
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
if (readObserver) readObserver.disconnect();
|
|
|
|
if (connection) connection.dispose();
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
.qglefbjs {
|
|
|
|
position: relative;
|
|
|
|
box-sizing: border-box;
|
|
|
|
font-size: 0.9em;
|
|
|
|
overflow-wrap: break-word;
|
|
|
|
contain: content;
|
|
|
|
|
|
|
|
&.max-width_500px > .meta{
|
|
|
|
padding-block: 16px;
|
|
|
|
font-size: 0.9em;
|
|
|
|
}
|
|
|
|
&.max-width_450px > .meta {
|
2024-04-25 03:42:03 +02:00
|
|
|
padding: 12px 16px 0 16px;
|
2024-04-24 15:33:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
> .meta {
|
|
|
|
margin-top: 1px; // Otherwise it will cover the line
|
|
|
|
padding: 24px 32px 0 32px;
|
|
|
|
display: flex;
|
|
|
|
align-items: baseline;
|
|
|
|
white-space: nowrap;
|
|
|
|
> .info {
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
white-space: nowrap;
|
|
|
|
min-width: 0;
|
|
|
|
overflow: hidden;
|
|
|
|
// flex-grow: 1;
|
|
|
|
// display: inline-flex;
|
|
|
|
> .sub-icon {
|
|
|
|
margin-right: 3px;
|
|
|
|
font-size: 14px;
|
|
|
|
}
|
|
|
|
> .avatars > .avatar {
|
|
|
|
width: 20px;
|
|
|
|
height: 20px;
|
|
|
|
margin-right: 5px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
> .time {
|
|
|
|
margin-left: auto;
|
|
|
|
// flex-grow: 0;
|
|
|
|
// flex-shrink: 0;
|
|
|
|
white-space: nowrap;
|
|
|
|
font-size: 0.9em;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|