add backend for message reactions

This commit is contained in:
marlon-erler
2026-04-06 20:47:49 +00:00
parent 7ccb237eac
commit bb0a71a01f
7 changed files with 306 additions and 11 deletions
+124 -7
View File
@@ -1,11 +1,16 @@
// this file is responsible for managing chats.
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
import {
checkMatchesObjectStructure,
DATA_VERSION,
ValidObject,
} from "../Utility/typeSafety";
import FileModel, { FileContent } from "../Files/fileModel";
import {
HandlerManager,
createTimestamp,
localeCompare,
parse,
parseValidObject,
stringify,
} from "../Utility/utility";
@@ -36,6 +41,8 @@ export default class ChatModel {
chatMessageHandlerManager: HandlerManager<ChatMessage> =
new HandlerManager();
reactionHandlerManager: HandlerManager<ChatMessageReaction> =
new HandlerManager();
get secondaryChannels(): string[] {
return this.info.secondaryChannels.sort(localeCompare);
@@ -74,6 +81,17 @@ export default class ChatModel {
return [...this.getMessageDirPath(), id];
};
getReactionDirPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.reactions(this.id),
);
};
getReactionPath = (id: string): string[] => {
return [...this.getReactionDirPath(), id];
};
// handlers
handleMessage = (body: string): void => {
const chatMessage: ChatMessage | null = parseValidObject(
@@ -83,11 +101,19 @@ export default class ChatModel {
if (chatMessage == null) return;
chatMessage.status = ChatMessageStatus.Received;
this.addMessage(chatMessage);
this.addMessage(chatMessage);
this.setReadStatus(true);
};
handleReaction = (reaction: ChatMessageReaction | any): void => {
if (
!checkMatchesObjectStructure(reaction, ChatMessageReactionReference)
)
return;
this.addReaction(reaction);
};
handleMessageSent = (chatMessage: ChatMessage): void => {
chatMessage.status = ChatMessageStatus.Sent;
this.addMessage(chatMessage);
@@ -132,10 +158,13 @@ export default class ChatModel {
);
};
sendMessage = async (
body: string,
fileContent?: FileContent<string>,
): Promise<boolean> => {
addReaction = async (reaction: ChatMessageReaction): Promise<void> => {
const reactionPath: string[] = this.getReactionPath(reaction.fileId);
this.storageModel.writeStringifiable(reactionPath, reaction);
this.reactionHandlerManager.trigger(reaction);
};
getNameAndChannel = (): [string, string] | false => {
const senderName = this.settingsModel.username;
if (senderName == "") return false;
@@ -146,6 +175,17 @@ export default class ChatModel {
const combinedChannel: string = allChannels.join("/");
return [senderName, combinedChannel];
};
sendMessage = async (
body: string,
fileContent?: FileContent<string>,
): Promise<boolean> => {
const nameAndChannel = this.getNameAndChannel();
if (nameAndChannel == false) return;
const [senderName, combinedChannel] = nameAndChannel;
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
combinedChannel,
senderName,
@@ -172,6 +212,23 @@ export default class ChatModel {
chatMessage.stringifiedFile = decryptedFile;
};
sendReaction = async (
messageId: string,
content: ReactionSymbol,
): Promise<void> => {
const nameAndChannel = this.getNameAndChannel();
if (nameAndChannel == false) return;
const [senderName, combinedChannel] = nameAndChannel;
const reaction = ChatModel.createMessageReaction(
messageId,
senderName,
content,
);
this.sendMessage("", reaction);
this.addReaction(reaction);
};
subscribe = (): void => {
this.connectionModel.addChannel(this.info.primaryChannel);
};
@@ -231,11 +288,12 @@ export default class ChatModel {
const chatMessages: ChatMessage[] = [];
for (const messageId of messageIds) {
const messagePath: string[] = this.getMessagePath(messageId);
const chatMessage: ChatMessage | any =
const chatMessage: ChatMessage | null =
this.storageModel.readStringifiable(
messagePath,
ChatMessageReference,
);
if (chatMessage == null) continue;
chatMessages.push(chatMessage);
}
@@ -245,6 +303,27 @@ export default class ChatModel {
return sorted;
}
get reactions(): ChatMessageReaction[] {
const reactionIds: string[] = this.storageModel.list(
this.getReactionDirPath(),
);
if (!Array.isArray(reactionIds)) return [];
const reactions: ChatMessageReaction[] = [];
for (const reactionId of reactionIds) {
const reactionPath: string[] = this.getReactionPath(reactionId);
const reaction: ChatMessageReaction | null =
this.storageModel.readStringifiable(
reactionPath,
ChatMessageReactionReference,
);
if (reaction == null) continue;
reactions.push(reaction);
}
return reactions;
}
// init
constructor(
storageModel: StorageModel,
@@ -324,9 +403,27 @@ export default class ChatModel {
return chatMessage;
};
static createMessageReaction = (
messageId: string,
sender: string,
content: ReactionSymbol,
): ChatMessageReaction => {
const fileContent = FileModel.createFileContent(v4(), "reaction");
const reaction: ChatMessageReaction = {
...fileContent,
messageId,
sender,
content,
};
return reaction;
};
}
// types
export type ReactionSymbol = "👍" | "✅" | "❗️" | "‼️" | "❓";
export interface ChatInfo extends ValidObject {
primaryChannel: string;
secondaryChannels: string[];
@@ -355,6 +452,13 @@ export interface ChatMessage extends ValidObject {
stringifiedFile: string;
}
export interface ChatMessageReaction
extends ValidObject, FileContent<"reaction"> {
messageId: string;
sender: string;
content: ReactionSymbol | string;
}
// references
export const ChatInfoReference: ChatInfo = {
dataVersion: DATA_VERSION,
@@ -380,3 +484,16 @@ export const ChatMessageReference: ChatMessage = {
stringifiedFile: "",
};
export const ChatMessageReactionReference: ChatMessageReaction = {
dataVersion: DATA_VERSION,
fileId: "",
fileContentId: "",
creationDate: "",
type: "reaction",
messageId: "",
sender: "",
content: "",
};
+1
View File
@@ -65,6 +65,7 @@ export default class FileModel {
if (didStore == false) return;
this.boardsAndTasksModel.handleFileContent(fileContent);
this.chatModel.handleReaction(fileContent);
};
// methods
+4
View File
@@ -247,6 +247,10 @@ export const filePaths = {
info: (id: string) => [...filePaths.chat.chatBase(id), "info"],
color: (id: string) => [...filePaths.chat.chatBase(id), "color"],
messages: (id: string) => [...filePaths.chat.chatBase(id), "messages"],
reactions: (id: string) => [
...filePaths.chat.chatBase(id),
"reactions",
],
lastUsedPage: (id: string) => [
...filePaths.chat.chatBase(id),
"last-used-page",
+32 -1
View File
@@ -1,6 +1,12 @@
import * as React from "bloatless-react";
import { ChatMessage, ChatMessageStatus } from "../../Model/Chat/chatModel";
import {
ChatMessage,
ChatMessageReaction,
ChatMessageReactionReference,
ChatMessageStatus,
ReactionSymbol,
} from "../../Model/Chat/chatModel";
import CoreViewModel from "../Global/coreViewModel";
import MessagePageViewModel from "../Pages/messagePageViewModel";
@@ -19,6 +25,12 @@ export default class ChatMessageViewModel {
);
sentByUser: boolean;
reactionsThumbsUp = new React.MapState<ChatMessageReaction>();
reactionsCheck = new React.MapState<ChatMessageReaction>();
reactionsAttention = new React.MapState<ChatMessageReaction>();
reactionsDoubleAttention = new React.MapState<ChatMessageReaction>();
reactionsQuestion = new React.MapState<ChatMessageReaction>();
// state
isPresentingInfoModal: React.State<boolean> = new React.State(false);
@@ -42,6 +54,25 @@ export default class ChatMessageViewModel {
this.isPresentingInfoModal.value = false;
};
// reactions
addReaction = (reaction: ChatMessageReaction): void => {
switch (reaction.content) {
case "👍":
return this.reactionsThumbsUp.set(reaction.fileId, reaction);
case "✅":
return this.reactionsCheck.set(reaction.fileId, reaction);
case "❗️":
return this.reactionsAttention.set(reaction.fileId, reaction);
case "‼️":
return this.reactionsDoubleAttention.set(
reaction.fileId,
reaction,
);
case "❓":
return this.reactionsQuestion.set(reaction.fileId, reaction);
}
};
// load
loadData = (): void => {
this.channel = this.chatMessage.channel;
+9 -1
View File
@@ -3,7 +3,10 @@ import * as React from "bloatless-react";
import CalendarPageViewModel, {
CALENDAR_EVENT_BOARD_ID,
} from "../Pages/calendarPageViewModel";
import ChatModel, { ChatMessage } from "../../Model/Chat/chatModel";
import ChatModel, {
ChatMessage,
ChatMessageReaction,
} from "../../Model/Chat/chatModel";
import StorageModel, {
StorageModelSubPath,
filePaths,
@@ -176,6 +179,11 @@ export default class ChatViewModel {
this.notificationViewModel.showNotification(chatMessage);
},
);
chatModel.reactionHandlerManager.addHandler(
(reaction: ChatMessageReaction) => {
this.messagePageViewModel.showReaction(reaction);
},
);
// load
this.loadPageSelection();
+19 -1
View File
@@ -1,6 +1,10 @@
import * as React from "bloatless-react";
import { ChatMessage } from "../../Model/Chat/chatModel";
import {
ChatMessage,
ChatMessageReaction,
ReactionSymbol,
} from "../../Model/Chat/chatModel";
import ChatMessageViewModel from "../Chat/chatMessageViewModel";
import ChatViewModel from "../Chat/chatViewModel";
import CoreViewModel from "../Global/coreViewModel";
@@ -37,6 +41,10 @@ export default class MessagePageViewModel {
messageViewModel.loadData();
};
sendReaction = (messageId: string, content: ReactionSymbol): void => {
this.chatViewModel.chatModel.sendReaction(messageId, content);
};
// view
showChatMessage = (chatMessage: ChatMessage): void => {
const chatMessageViewModel = new ChatMessageViewModel(
@@ -60,12 +68,22 @@ export default class MessagePageViewModel {
}
};
showReaction = (reaction: ChatMessageReaction): void => {
const messageViewModel: ChatMessageViewModel | undefined =
this.chatMessageViewModels.value.get(reaction.messageId);
if (messageViewModel == undefined) return;
messageViewModel.addReaction(reaction);
};
// load
loadData = (): void => {
this.chatMessageViewModels.clear();
for (const chatMessage of this.chatViewModel.chatModel.messages) {
this.showChatMessage(chatMessage);
}
for (const reaction of this.chatViewModel.chatModel.reactions) {
this.showReaction(reaction);
}
};
// init