diff --git a/frontend/src/lib/types/message.ts b/frontend/src/lib/types/message.ts index 1c1cc25310078a37829cc99c5139d5404a53ae42..7604b4889b89a2aa5eb301513630295e1434ff29 100644 --- a/frontend/src/lib/types/message.ts +++ b/frontend/src/lib/types/message.ts @@ -17,6 +17,7 @@ export default class Message { private _versions = writable([] as { content: string; date: Date }[]); private _feedbacks = writable([] as Feedback[]); private _replyTo: string; + private _reactions = writable<{ userId: string; emoji: string }[]>([]); public constructor( id: number, @@ -89,6 +90,26 @@ export default class Message { ) as Message | undefined; } + get reactions(): Writable<{ userId: string; emoji: string }[]> { + return this._reactions; + } + + addReaction(userId: string, emoji: string) { + this._reactions.update(reactions => { + const existing = reactions.find(r => r.userId === userId); + if (existing) { + existing.emoji = emoji; + } else { + reactions.push({ userId, emoji }); + } + return reactions; + }); + } + + removeReaction(userId: string) { + this._reactions.update(reactions => reactions.filter(r => r.userId !== userId)); + } + async update(content: string, metadata: { message: string; date: number }[]): Promise<boolean> { const response = await updateMessageAPI( fetch, @@ -109,20 +130,18 @@ export default class Message { async getMessageById(id: number): Promise<Message | null> { try { - const response = await getMessagesAPI(fetch, this._session.id); // Fetch all messages for the session + const response = await getMessagesAPI(fetch, this._session.id); if (!response) { toastAlert('Failed to retrieve messages from the server.'); return null; } - // Locate the message by ID in the response const messageData = response.find((msg: any) => msg.id === id); if (!messageData) { toastAlert(`Message with ID ${id} not found.`); return null; } - // Parse the message object const parsedMessage = Message.parse(messageData, this._user, this._session); if (!parsedMessage) { toastAlert(`Failed to parse message with ID ${id}`); diff --git a/frontend/src/routes/sessions/[id]/Message.svelte b/frontend/src/routes/sessions/[id]/Message.svelte index 943515d2dee818b3418757db16fb8127b2bc38e6..b8f5a67e36ab60cf822e1a033077c141de195dd5 100644 --- a/frontend/src/routes/sessions/[id]/Message.svelte +++ b/frontend/src/routes/sessions/[id]/Message.svelte @@ -12,6 +12,7 @@ import Message from '$lib/types/message'; import type User from '$lib/types/user'; import { get } from 'svelte/store'; + import { writable } from 'svelte/store'; let { user, @@ -34,6 +35,9 @@ let historyModal: HTMLDialogElement; let messageVersions = $state(message.versions); + let showReactions = writable(false); + const emojiList = ["ðŸ‘", "â¤ï¸", "😂", "😮", "😢", "👎"]; + function startEdit() { isEdit = true; setTimeout(() => { @@ -185,6 +189,20 @@ await message.deleteFeedback(feedback); } + + function reactToMessage(emoji: string) { + let reactions = get(message.reactions); + + let currentReaction = reactions.find(r => r.userId === String(user.id)); + + if (currentReaction && currentReaction.emoji === emoji) { + message.removeReaction(String(user.id)); + } else { + message.addReaction(String(user.id), emoji); + } + + showReactions.set(false); +} </script> <div @@ -201,7 +219,16 @@ /> </div> - <div class="chat-bubble text-black" class:bg-blue-50={isSender} class:bg-gray-300={!isSender}> + <div class="relative group chat-bubble text-black" + class:bg-blue-50={isSender} + class:bg-gray-300={!isSender} + onmouseover={() => showReactions.set(true)} + onmouseleave={() => showReactions.set(false)} + onfocus={() => showReactions.set(true)} + onblur={() => showReactions.set(false)} + role="button" + tabindex="0" +> {#if replyToMessage} <a href={`#${replyToMessage.uuid}`} @@ -290,6 +317,29 @@ <Icon src={ArrowUturnLeft} class="w-5 h-full text-gray-500 hover:text-gray-800" /> </button> {/if} + + {#if get(message.reactions).length > 0} + <div class="flex items-center space-x-2 mt-2"> + {#each get(message.reactions) as reaction} + <span class="text-lg cursor-pointer" title={`Reacted by ${reaction.userId}`}> + {reaction.emoji} + </span> + {/each} + </div> + {/if} + + {#if $showReactions} + <div class="absolute bottom-0 left-0 flex space-x-2 p-2 bg-white border rounded-lg shadow-lg"> + {#each emojiList as emoji} + <button type="button" class="cursor-pointer text-lg hover:bg-gray-300 p-1 rounded-lg" + onclick={() => reactToMessage(emoji)} + onkeydown={(e) => e.key === 'Enter' && reactToMessage(emoji)} + aria-label={`React with ${emoji}`}> + {emoji} + </button> + {/each} + </div> + {/if} </div> <div class="chat-footer opacity-50"> <Icon src={Check} class="w-4 inline" /> @@ -300,6 +350,7 @@ </button> {/if} </div> + </div> <div class="absolute invisible rounded-xl border border-gray-400 bg-white divide-x"