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
+117 -1
View File
@@ -603,6 +603,10 @@
info: (id) => [...filePaths.chat.chatBase(id), "info"],
color: (id) => [...filePaths.chat.chatBase(id), "color"],
messages: (id) => [...filePaths.chat.chatBase(id), "messages"],
reactions: (id) => [
...filePaths.chat.chatBase(id),
"reactions"
],
lastUsedPage: (id) => [
...filePaths.chat.chatBase(id),
"last-used-page"
@@ -1037,6 +1041,7 @@
const didStore = this.storeFileContent(fileContent);
if (didStore == false) return;
this.boardsAndTasksModel.handleFileContent(fileContent);
this.chatModel.handleReaction(fileContent);
};
// methods
this.addFileContentAndSend = (fileContent) => {
@@ -1203,6 +1208,7 @@
// init
constructor(storageModel2, connectionModel2, settingsModel2, chatListModel2, chatId) {
this.chatMessageHandlerManager = new HandlerManager();
this.reactionHandlerManager = new HandlerManager();
// paths
this.getBasePath = () => {
return StorageModel.getPath(
@@ -1231,6 +1237,15 @@
this.getMessagePath = (id) => {
return [...this.getMessageDirPath(), id];
};
this.getReactionDirPath = () => {
return StorageModel.getPath(
"chat" /* Chat */,
filePaths.chat.reactions(this.id)
);
};
this.getReactionPath = (id) => {
return [...this.getReactionDirPath(), id];
};
// handlers
this.handleMessage = (body) => {
const chatMessage = parseValidObject(
@@ -1242,6 +1257,10 @@
this.addMessage(chatMessage);
this.setReadStatus(true);
};
this.handleReaction = (reaction) => {
if (!checkMatchesObjectStructure(reaction, ChatMessageReactionReference)) return;
this.addReaction(reaction);
};
this.handleMessageSent = (chatMessage) => {
chatMessage.status = "sent" /* Sent */;
this.addMessage(chatMessage);
@@ -1276,7 +1295,12 @@
chatMessage.stringifiedFile
);
};
this.sendMessage = async (body, fileContent) => {
this.addReaction = async (reaction) => {
const reactionPath = this.getReactionPath(reaction.fileId);
this.storageModel.writeStringifiable(reactionPath, reaction);
this.reactionHandlerManager.trigger(reaction);
};
this.getNameAndChannel = () => {
const senderName = this.settingsModel.username;
if (senderName == "") return false;
const allChannels = [this.info.primaryChannel];
@@ -1284,6 +1308,12 @@
allChannels.push(secondaryChannel);
}
const combinedChannel = allChannels.join("/");
return [senderName, combinedChannel];
};
this.sendMessage = async (body, fileContent) => {
const nameAndChannel = this.getNameAndChannel();
if (nameAndChannel == false) return;
const [senderName, combinedChannel] = nameAndChannel;
const chatMessage = await _ChatModel.createChatMessage(
combinedChannel,
senderName,
@@ -1307,6 +1337,18 @@
chatMessage.body = decryptedBody;
chatMessage.stringifiedFile = decryptedFile;
};
this.sendReaction = async (messageId, content) => {
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);
};
this.subscribe = () => {
this.connectionModel.addChannel(this.info.primaryChannel);
};
@@ -1376,6 +1418,7 @@
messagePath,
ChatMessageReference
);
if (chatMessage == null) continue;
chatMessages.push(chatMessage);
}
const sorted = chatMessages.sort(
@@ -1383,6 +1426,23 @@
);
return sorted;
}
get reactions() {
const reactionIds = this.storageModel.list(
this.getReactionDirPath()
);
if (!Array.isArray(reactionIds)) return [];
const reactions = [];
for (const reactionId of reactionIds) {
const reactionPath = this.getReactionPath(reactionId);
const reaction = this.storageModel.readStringifiable(
reactionPath,
ChatMessageReactionReference
);
if (reaction == null) continue;
reactions.push(reaction);
}
return reactions;
}
// utility
static splitChannel(channelString) {
return channelString.split("/");
@@ -1427,6 +1487,18 @@
return chatMessage;
};
}
static {
this.createMessageReaction = (messageId, sender, content) => {
const fileContent = FileModel2.createFileContent(v4_default(), "reaction");
const reaction = {
...fileContent,
messageId,
sender,
content
};
return reaction;
};
}
};
var ChatInfoReference = {
dataVersion: DATA_VERSION,
@@ -1445,6 +1517,16 @@
status: "",
stringifiedFile: ""
};
var ChatMessageReactionReference = {
dataVersion: DATA_VERSION,
fileId: "",
fileContentId: "",
creationDate: "",
type: "reaction",
messageId: "",
sender: "",
content: ""
};
// src/Model/Chat/chatListModel.ts
var ChatListModel = class {
@@ -1923,6 +2005,11 @@
this.status = new State(
void 0
);
this.reactionsThumbsUp = new MapState();
this.reactionsCheck = new MapState();
this.reactionsAttention = new MapState();
this.reactionsDoubleAttention = new MapState();
this.reactionsQuestion = new MapState();
// state
this.isPresentingInfoModal = new State(false);
// methods
@@ -1942,6 +2029,21 @@
this.hideInfoModal = () => {
this.isPresentingInfoModal.value = false;
};
// 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);
}
};
// load
this.loadData = () => {
this.channel = this.chatMessage.channel;
@@ -1980,6 +2082,9 @@
this.chatViewModel.chatModel.addMessage(chatMessage);
messageViewModel.loadData();
};
this.sendReaction = (messageId, content) => {
this.chatViewModel.chatModel.sendReaction(messageId, content);
};
// view
this.showChatMessage = (chatMessage) => {
const chatMessageViewModel = new ChatMessageViewModel(
@@ -1999,12 +2104,20 @@
);
}
};
this.showReaction = (reaction) => {
const messageViewModel = this.chatMessageViewModels.value.get(reaction.messageId);
if (messageViewModel == void 0) return;
messageViewModel.addReaction(reaction);
};
// load
this.loadData = () => {
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);
}
};
this.chatViewModel = chatViewModel;
this.cannotSendMessage = createProxyState(
@@ -3149,6 +3262,9 @@
this.notificationViewModel.showNotification(chatMessage);
}
);
chatModel.reactionHandlerManager.addHandler((reaction) => {
this.messagePageViewModel.showReaction(reaction);
});
this.loadPageSelection();
this.resetColor();
this.loadInfo();
+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