From ad978807dc5cabb3b196f41468531d6fbc6826d3 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Tue, 7 Apr 2026 18:39:55 +0000 Subject: [PATCH] add frontend interface to send reactions --- dist/index.js | 180 +++++++++++++++--- src/Model/Chat/chatModel.ts | 14 +- src/View/Components/messageReactionButton.tsx | 92 +++++++++ src/View/Modals/chatMessageInfoModal.tsx | 27 +++ src/View/translations.ts | 24 +++ src/ViewModel/Chat/chatMessageViewModel.ts | 47 +++-- src/ViewModel/Pages/messagePageViewModel.ts | 4 +- 7 files changed, 349 insertions(+), 39 deletions(-) create mode 100644 src/View/Components/messageReactionButton.tsx diff --git a/dist/index.js b/dist/index.js index 328bb2a4..06cb349f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1258,7 +1258,8 @@ this.setReadStatus(true); }; this.handleReaction = (reaction) => { - if (!checkMatchesObjectStructure(reaction, ChatMessageReactionReference)) return; + if (!checkMatchesObjectStructure(reaction, ChatMessageReactionReference)) + return; this.addReaction(reaction); }; this.handleMessageSent = (chatMessage) => { @@ -2010,6 +2011,26 @@ this.reactionsAttention = new MapState(); this.reactionsDoubleAttention = new MapState(); this.reactionsQuestion = new MapState(); + this.reactionsThumbsUpCount = createProxyState( + [this.reactionsThumbsUp], + () => this.reactionsThumbsUp.value.size + ); + this.reactionsCheckCount = createProxyState( + [this.reactionsCheck], + () => this.reactionsCheck.value.size + ); + this.reactionsAttentionCount = createProxyState( + [this.reactionsAttention], + () => this.reactionsAttention.value.size + ); + this.reactionsDoubleAttentionCount = createProxyState( + [this.reactionsDoubleAttention], + () => this.reactionsDoubleAttention.value.size + ); + this.reactionsQuestionCount = createProxyState( + [this.reactionsQuestion], + () => this.reactionsQuestion.value.size + ); // state this.isPresentingInfoModal = new State(false); // methods @@ -2032,18 +2053,24 @@ // reactions this.addReaction = (reaction) => { switch (reaction.content) { - case "\u{1F44D}": - return this.reactionsThumbsUp.set(reaction.fileId, reaction); - case "\u2705": - return this.reactionsCheck.set(reaction.fileId, reaction); - case "\u2757\uFE0F": - return this.reactionsAttention.set(reaction.fileId, reaction); - case "\u203C\uFE0F": - return this.reactionsDoubleAttention.set(reaction.fileId, reaction); - case "\u2753": - return this.reactionsQuestion.set(reaction.fileId, reaction); + case "\u{1F44D}" /* ThumbsUp */: + return this.reactionsThumbsUp.set(reaction.sender, reaction); + case "\u2705" /* Check */: + return this.reactionsCheck.set(reaction.sender, reaction); + case "\u2757\uFE0F" /* Attention */: + return this.reactionsAttention.set(reaction.sender, reaction); + case "\u203C\uFE0F" /* DoubleAttention */: + return this.reactionsDoubleAttention.set( + reaction.sender, + reaction + ); + case "\u2753" /* Question */: + return this.reactionsQuestion.set(reaction.sender, reaction); } }; + this.sendReaction = (content) => { + this.messagePageViewModel.sendReaction(this.chatMessage.id, content); + }; // load this.loadData = () => { this.channel = this.chatMessage.channel; @@ -2746,7 +2773,13 @@ copyMessageButton: "Copy message", resendMessageButton: "Resend message", decryptMessageButton: "Decrypt message", - deleteMessageButton: "Delete message" + deleteMessageButton: "Delete message", + // + thumbsUpReaction: "Reaction: thumbs up", + checkReaction: "Reaction: check", + attentionReaction: "Reaction: exclamation mark", + doubleAttentionReaction: "Reaction: double exclamation mark", + questionReaction: "Reaction: question mark" }, task: { newBoardNamePlaceholder: "Create a board", @@ -2927,7 +2960,13 @@ copyMessageButton: "Nachricht kopieren", resendMessageButton: "Nachricht erneut senden", decryptMessageButton: "Nachricht entschl\xFCsseln", - deleteMessageButton: "Nachricht l\xF6schen" + deleteMessageButton: "Nachricht l\xF6schen", + // + thumbsUpReaction: "Reaktion: Daumen hoch", + checkReaction: "Reaktion: Haken", + attentionReaction: "Reaktion: Ausrufezeichen", + doubleAttentionReaction: "Reaktion: doppeltes Ausrufezeichen", + questionReaction: "Reaktion: Fragezeichen" }, task: { newBoardNamePlaceholder: "Board erstellen", @@ -3098,7 +3137,13 @@ copyMessageButton: "Copiar mensaje", resendMessageButton: "Reenviar mensaje", decryptMessageButton: "Desencriptar mensaje", - deleteMessageButton: "Eliminar mensaje" + deleteMessageButton: "Eliminar mensaje", + // + thumbsUpReaction: "Reacci\xF3n: pulgar hacia arriba", + checkReaction: "Reacci\xF3n: marca de verificaci\xF3n", + attentionReaction: "Reaccion: signo de atenci\xF3n", + doubleAttentionReaction: "Reaccion: signo de atenci\xF3n doble", + questionReaction: "Reaccion: signo de interrogaci\xF3n" }, task: { newBoardNamePlaceholder: "Crear un tablero", @@ -3262,9 +3307,11 @@ this.notificationViewModel.showNotification(chatMessage); } ); - chatModel.reactionHandlerManager.addHandler((reaction) => { - this.messagePageViewModel.showReaction(reaction); - }); + chatModel.reactionHandlerManager.addHandler( + (reaction) => { + this.messagePageViewModel.showReaction(reaction); + } + ); this.loadPageSelection(); this.resetColor(); this.loadInfo(); @@ -3885,6 +3932,80 @@ return RibbonButton(label, icon, isSelected, select, isHighlighted); } + // src/View/Components/messageReactionButton.tsx + function MessageReactionButton(chatMessageViewModel, content) { + function sendReaction() { + chatMessageViewModel.sendReaction(content); + } + function getUsername() { + return chatMessageViewModel.messagePageViewModel.chatViewModel.settingsViewModel.username.value; + } + function checkIsHighlighted(mapState) { + return mapState.value.has(getUsername()); + } + let audioLabel; + let count; + let isActive; + switch (content) { + case "\u{1F44D}" /* ThumbsUp */: { + audioLabel = translations.chatPage.message.thumbsUpReaction; + count = chatMessageViewModel.reactionsThumbsUpCount; + isActive = createProxyState( + [chatMessageViewModel.reactionsThumbsUp], + () => checkIsHighlighted(chatMessageViewModel.reactionsThumbsUp) + ); + break; + } + case "\u2705" /* Check */: { + audioLabel = translations.chatPage.message.checkReaction; + count = chatMessageViewModel.reactionsCheckCount; + isActive = createProxyState( + [chatMessageViewModel.reactionsCheck], + () => checkIsHighlighted(chatMessageViewModel.reactionsCheck) + ); + break; + } + case "\u2757\uFE0F" /* Attention */: { + audioLabel = translations.chatPage.message.attentionReaction; + count = chatMessageViewModel.reactionsAttentionCount; + isActive = createProxyState( + [chatMessageViewModel.reactionsAttention], + () => checkIsHighlighted(chatMessageViewModel.reactionsAttention) + ); + break; + } + case "\u203C\uFE0F" /* DoubleAttention */: { + audioLabel = translations.chatPage.message.doubleAttentionReaction; + count = chatMessageViewModel.reactionsDoubleAttentionCount; + isActive = createProxyState( + [chatMessageViewModel.reactionsDoubleAttention], + () => checkIsHighlighted(chatMessageViewModel.reactionsDoubleAttention) + ); + break; + } + case "\u2753" /* Question */: { + audioLabel = translations.chatPage.message.questionReaction; + count = chatMessageViewModel.reactionsQuestionCount; + isActive = createProxyState( + [chatMessageViewModel.reactionsQuestion], + () => checkIsHighlighted(chatMessageViewModel.reactionsQuestion) + ); + break; + } + } + return /* @__PURE__ */ createElement( + "button", + { + class: "flex", + "on:click": sendReaction, + "aria-label": audioLabel, + "toggle:selected": isActive + }, + content, + /* @__PURE__ */ createElement("span", { "subscribe:innerText": count }) + ); + } + // src/View/Modals/chatMessageInfoModal.tsx function ChatMessageInfoModal(chatMessageViewModel) { return /* @__PURE__ */ createElement( @@ -3899,12 +4020,27 @@ class: "break-word", "subscribe:innerText": chatMessageViewModel.body } - )))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.copyMessage }, translations.chatPage.message.copyMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "content_copy")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.resendMessage }, translations.chatPage.message.resendMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "redo")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.decryptMessage }, translations.chatPage.message.decryptMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "key")))), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.hideInfoModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))) + )))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.copyMessage }, translations.chatPage.message.copyMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "content_copy")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.resendMessage }, translations.chatPage.message.resendMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "redo")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.decryptMessage }, translations.chatPage.message.decryptMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-row gap" }, MessageReactionButton( + chatMessageViewModel, + "\u{1F44D}" /* ThumbsUp */ + ), MessageReactionButton( + chatMessageViewModel, + "\u2705" /* Check */ + ), MessageReactionButton( + chatMessageViewModel, + "\u2757\uFE0F" /* Attention */ + ), MessageReactionButton( + chatMessageViewModel, + "\u203C\uFE0F" /* DoubleAttention */ + ), MessageReactionButton( + chatMessageViewModel, + "\u2753" /* Question */ + ))), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.hideInfoModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))) ); } // src/View/Components/chatMessage.tsx - function ChatMessage3(chatMessageViewModel) { + function ChatMessage4(chatMessageViewModel) { const statusIcon = createProxyState( [chatMessageViewModel.status], () => { @@ -3950,7 +4086,7 @@ ); } var ChatMessageViewModelToView = (chatMessageViewModel) => { - return ChatMessage3(chatMessageViewModel); + return ChatMessage4(chatMessageViewModel); }; // src/View/ChatPages/messagePage.tsx @@ -5039,21 +5175,21 @@ this.storeAddress(this.address); this.sendSubscriptionRequest(); this.sendMessagesInOutbox(); - this.shouldAttemptReconnect = true; }; // connection this.connect = (address) => { console.log("connecting...", address); + this.shouldAttemptReconnect = true; this.udn.connect(address); }; this.disconnect = () => { + this.shouldAttemptReconnect = false; this.udn.disconnect(); const reconnectAddressPath = StorageModel.getPath( "connection" /* ConnectionModel */, filePaths.connectionModel.reconnectAddress ); this.storageModel.remove(reconnectAddressPath); - this.shouldAttemptReconnect = false; }; this.reconnect = () => { const reconnectAddressPath = this.getReconnectAddressPath(); diff --git a/src/Model/Chat/chatModel.ts b/src/Model/Chat/chatModel.ts index db27a55e..ccbc5486 100644 --- a/src/Model/Chat/chatModel.ts +++ b/src/Model/Chat/chatModel.ts @@ -214,7 +214,7 @@ export default class ChatModel { sendReaction = async ( messageId: string, - content: ReactionSymbol, + content: ReactionSymbols, ): Promise => { const nameAndChannel = this.getNameAndChannel(); if (nameAndChannel == false) return; @@ -407,7 +407,7 @@ export default class ChatModel { static createMessageReaction = ( messageId: string, sender: string, - content: ReactionSymbol, + content: ReactionSymbols, ): ChatMessageReaction => { const fileContent = FileModel.createFileContent(v4(), "reaction"); const reaction: ChatMessageReaction = { @@ -422,7 +422,13 @@ export default class ChatModel { } // types -export type ReactionSymbol = "👍" | "✅" | "❗️" | "‼️" | "❓"; +export enum ReactionSymbols { + ThumbsUp = "👍", + Check = "✅", + Attention = "❗️", + DoubleAttention = "‼️", + Question = "❓", +} export interface ChatInfo extends ValidObject { primaryChannel: string; @@ -456,7 +462,7 @@ export interface ChatMessageReaction extends ValidObject, FileContent<"reaction"> { messageId: string; sender: string; - content: ReactionSymbol | string; + content: ReactionSymbols | string; } // references diff --git a/src/View/Components/messageReactionButton.tsx b/src/View/Components/messageReactionButton.tsx new file mode 100644 index 00000000..ddecaa47 --- /dev/null +++ b/src/View/Components/messageReactionButton.tsx @@ -0,0 +1,92 @@ +import * as React from "bloatless-react"; +import { ReactionSymbols } from "../../Model/Chat/chatModel"; +import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel"; +import { translations } from "../translations"; + +export function MessageReactionButton( + chatMessageViewModel: ChatMessageViewModel, + content: ReactionSymbols, +) { + function sendReaction() { + chatMessageViewModel.sendReaction(content); + } + + function getUsername(): string { + return chatMessageViewModel.messagePageViewModel.chatViewModel + .settingsViewModel.username.value; + } + + function checkIsHighlighted(mapState: React.MapState): boolean { + return mapState.value.has(getUsername()); + } + + let audioLabel: string; + let count: React.State; + let isActive: React.State; + + switch (content) { + case ReactionSymbols.ThumbsUp: { + audioLabel = translations.chatPage.message.thumbsUpReaction; + count = chatMessageViewModel.reactionsThumbsUpCount; + isActive = React.createProxyState( + [chatMessageViewModel.reactionsThumbsUp], + () => + checkIsHighlighted(chatMessageViewModel.reactionsThumbsUp), + ); + break; + } + case ReactionSymbols.Check: { + audioLabel = translations.chatPage.message.checkReaction; + count = chatMessageViewModel.reactionsCheckCount; + isActive = React.createProxyState( + [chatMessageViewModel.reactionsCheck], + () => checkIsHighlighted(chatMessageViewModel.reactionsCheck), + ); + break; + } + case ReactionSymbols.Attention: { + audioLabel = translations.chatPage.message.attentionReaction; + count = chatMessageViewModel.reactionsAttentionCount; + isActive = React.createProxyState( + [chatMessageViewModel.reactionsAttention], + () => + checkIsHighlighted(chatMessageViewModel.reactionsAttention), + ); + break; + } + case ReactionSymbols.DoubleAttention: { + audioLabel = translations.chatPage.message.doubleAttentionReaction; + count = chatMessageViewModel.reactionsDoubleAttentionCount; + isActive = React.createProxyState( + [chatMessageViewModel.reactionsDoubleAttention], + () => + checkIsHighlighted( + chatMessageViewModel.reactionsDoubleAttention, + ), + ); + break; + } + case ReactionSymbols.Question: { + audioLabel = translations.chatPage.message.questionReaction; + count = chatMessageViewModel.reactionsQuestionCount; + isActive = React.createProxyState( + [chatMessageViewModel.reactionsQuestion], + () => + checkIsHighlighted(chatMessageViewModel.reactionsQuestion), + ); + break; + } + } + + return ( + + ); +} diff --git a/src/View/Modals/chatMessageInfoModal.tsx b/src/View/Modals/chatMessageInfoModal.tsx index 6d2c9722..10d24e14 100644 --- a/src/View/Modals/chatMessageInfoModal.tsx +++ b/src/View/Modals/chatMessageInfoModal.tsx @@ -2,6 +2,8 @@ import * as React from "bloatless-react"; import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel"; import { translations } from "../translations"; +import { ReactionSymbols } from "../../Model/Chat/chatModel"; +import { MessageReactionButton } from "../Components/messageReactionButton"; export function ChatMessageInfoModal( chatMessageViewModel: ChatMessageViewModel, @@ -87,6 +89,31 @@ export function ChatMessageInfoModal( key + +
+ +
+ {MessageReactionButton( + chatMessageViewModel, + ReactionSymbols.ThumbsUp, + )} + {MessageReactionButton( + chatMessageViewModel, + ReactionSymbols.Check, + )} + {MessageReactionButton( + chatMessageViewModel, + ReactionSymbols.Attention, + )} + {MessageReactionButton( + chatMessageViewModel, + ReactionSymbols.DoubleAttention, + )} + {MessageReactionButton( + chatMessageViewModel, + ReactionSymbols.Question, + )} +