This commit is contained in:
2026-08-22 18:42:52 +02:00
parent 429d82e64c
commit 120b6cf4d5
11 changed files with 378 additions and 10 deletions
+74 -1
View File
@@ -7,6 +7,9 @@ import {useServerStore} from '@/stores/server';
import {useUserStore} from "@/stores/user.ts";
import {useMarkdown} from '@/composables/useMarkdown'
import {onReloadAll} from '@/plugins/events.ts'
import EmojiPicker from '@/components/EmojiPicker.vue'
import {useEmojiStore, type Emoji} from '@/stores/emoji.ts'
import {useAuthStore} from '@/stores/auth.ts'
const props = defineProps<{
serverId: string
@@ -17,6 +20,8 @@ const channelId = computed(() => props.channelId);
const messageStore = useMessageStore();
const serverStore = useServerStore();
const userStore = useUserStore();
const emojiStore = useEmojiStore();
const authStore = useAuthStore();
const {renderMarkdown} = useMarkdown()
const messageContainer = ref<HTMLElement | null>(null);
@@ -29,6 +34,7 @@ const paginationLock = ref<'before' | 'after' | null>(null);
const paginationLockScrollTop = ref(0);
const lastScrollTop = ref(0);
const markedMessageByChannel = new Map<string, string>();
const reactionPending = ref(new Set<string>());
const markCurrentChannelRead = async (targetChannelId: string) => {
if (messageStore.activeChannelId !== targetChannelId || !newestId.value) return;
@@ -180,6 +186,34 @@ const sendMessage = async () => {
}
};
const reactionKey = (messageId: string, emojiId: string, skinTone: number | null) =>
`${messageId}:${emojiId}:${skinTone ?? 0}`;
const toggleReaction = async (messageId: string, emojiId: string, skinTone: number | null, active: boolean) => {
const key = reactionKey(messageId, emojiId, skinTone);
if (reactionPending.value.has(key)) return;
reactionPending.value.add(key);
try {
if (active) await messageStore.removeReaction(messageId, emojiId, skinTone);
else await messageStore.addReaction(messageId, emojiId);
} catch (error) {
console.error('Erreur lors de la modification de la réaction:', error);
} finally {
reactionPending.value.delete(key);
}
};
const addReaction = (messageId: string, emoji: Emoji) =>
toggleReaction(messageId, emoji.id, null, false);
const emojiText = (emojiId: string, sequence: string | null) => {
const emoji = emojiStore.emojisById[emojiId];
if (emoji?.emoji_type === 'custom') return null;
return sequence ?? emoji?.unicode_sequence ?? '';
};
const reactionIsActive = (userIds: string[]) => Boolean(authStore.currentUser?.id && userIds.includes(authStore.currentUser.id));
const returnToRecentMessages = async () => {
paginationLock.value = null;
await messageStore.fetchMessages(channelId.value);
@@ -213,11 +247,16 @@ watch(isAtBottom, async (atBottom) => {
watch(channelId, async (newChannelId) => {
if (newChannelId) {
await emojiStore.fetchEmojis(props.serverId);
await messageStore.fetchMessages(newChannelId);
await scrollToBottom();
await markCurrentChannelRead(newChannelId);
}
}, {immediate: true})
watch(() => props.serverId, (newServerId) => {
if (newServerId) void emojiStore.fetchEmojis(newServerId);
}, {immediate: true});
</script>
<template>
@@ -257,6 +296,29 @@ watch(channelId, async (newChannelId) => {
<div class="text-body-1 text-high-emphasis opacity-100 mt-1">
<div class="markdown-content" v-html="renderMarkdown(msg.content)"></div>
</div>
<div class="d-flex flex-wrap align-center ga-1 mt-2">
<v-chip
v-for="reaction in msg.reactions"
:key="`${reaction.emoji_id}:${reaction.skin_tone ?? 0}`"
:color="reactionIsActive(reaction.user_ids) ? 'primary' : undefined"
:variant="reactionIsActive(reaction.user_ids) ? 'flat' : 'outlined'"
class="reaction-chip"
size="small"
@click="toggleReaction(msg.id, reaction.emoji_id, reaction.skin_tone, reactionIsActive(reaction.user_ids))"
>
<img v-if="emojiStore.emojisById[reaction.emoji_id]?.emoji_type === 'custom' && reaction.asset_url" :alt="reaction.name" :src="reaction.asset_url" />
<span v-else>{{ emojiText(reaction.emoji_id, reaction.unicode_sequence) }}</span>
<span class="ml-1">{{ reaction.count }}</span>
</v-chip>
<v-menu :close-on-content-click="false">
<template #activator="{props: menuProps}">
<v-btn v-bind="menuProps" density="compact" icon="mdi-plus" size="small" variant="text" />
</template>
<EmojiPicker :emojis="emojiStore.emojis" @select="(emoji) => addReaction(msg.id, emoji)" />
</v-menu>
</div>
</v-list-item>
</v-list>
@@ -300,7 +362,12 @@ watch(channelId, async (newChannelId) => {
<template v-slot:append-inner>
<v-btn color="grey-darken-1" density="compact" icon="mdi-gift" variant="text"></v-btn>
<v-btn color="grey-darken-1" density="compact" icon="mdi-sticker-emoji" variant="text"></v-btn>
<v-btn color="grey-darken-1" density="compact" icon="mdi-emoticon" variant="text"></v-btn>
<v-menu :close-on-content-click="false">
<template #activator="{props: menuProps}">
<v-btn v-bind="menuProps" color="grey-darken-1" density="compact" icon="mdi-emoticon" variant="text" />
</template>
<EmojiPicker :emojis="emojiStore.emojis" @select="(emoji) => { newMessage += emoji.emoji_type === 'custom' ? `:${emoji.name}:` : (emoji.unicode_sequence ?? '') }" />
</v-menu>
</template>
</v-textarea>
</v-sheet>
@@ -342,4 +409,10 @@ watch(channelId, async (newChannelId) => {
margin: 8px 0;
overflow-x: auto;
}
.reaction-chip img {
width: 18px;
height: 18px;
object-fit: contain;
}
</style>