#20: Persist chat messages
authorRaphaël Jakse <raphael.trivabble@jakse.fr>
Sun, 26 Oct 2025 10:54:38 +0000 (11:54 +0100)
committerRaphaël Jakse <raphael.trivabble@jakse.fr>
Sun, 26 Oct 2025 10:54:38 +0000 (11:54 +0100)
public/trivabble.js
server/trivabble-server.js

index 764e2106997d7dce73c8c61887b13a0b98fb84ea..25e71f7afb360a706bd9e1c0668d44123c0c386f 100644 (file)
         element.classList.add("blink");
     }
 
-    function chatMessage(sender, content) {
+    function chatMessage(sender, content, date) {
+        const msgId = "";
+        if (sender && date) {
+            msgId = "chatmsg-" + date + "--" + sender;
+            if (document.getElementById(msgId)) {
+                // we already have that message
+                return;
+            }
+        }
+
         chatMessages.style.width = chatTextarea.offsetWidth + "px"; // HACK to prevent the chat box from getting too big
         const msgDom = document.createElement("div");
+        if (msgId) {
+            msgDom.id = msgId;
+        }
 
         msgDom.className = "msg";
 
         return content;
     }
 
-    function handleChatMessage(msg) {
+    function handleHistoricalChatMessage(msg) {
+        handleChatMessage(true, msg);
+    }
+
+    function handleChatMessage(fromHistory, msg) {
         if (msg.specialMsg) {
             switch (msg.specialMsg.type) {
                 case "rack": {
                 }
 
                 case "highlightCell": {
-                    localHighlightCell(msg.specialMsg.cell);
-                    infoMessage(msg.msg);
+                    if (!fromHistory) {
+                        localHighlightCell(msg.specialMsg.cell);
+                        infoMessage(msg.msg);
+                    }
                     break;
                 }
             }
         }
 
         if (msg.content) {
-            chatMessage(msg.sender, msg.content);
+            chatMessage(msg.sender, msg.content, msg.date);
         }
     }
 
         }
 
         if (data.msg) {
-            handleChatMessage(data.msg);
+            handleChatMessage(false, data.msg);
         }
 
         if (typeof data.currentPlayer === "string") {
             tileCount.textContent = data.remainingLetters;
         }
 
+        if (data.chatHistory) {
+            data.chatHistory.forEach(handleHistoricalChatMessage);
+        }
+
+        if (data.chatMessageMaxSize && data.chatMessageMaxSize > 0) {
+            chatTextarea.maxLength = data.chatMessageMaxSize;
+        }
+
         if (data.rack) {
             setRack(data.rack);
         }
index 06f1cd325325eab12844bfeadceb541019e2fd09..ec1abae81b45e0d9eefdb3f86aaf436e967ba8d6 100644 (file)
@@ -40,6 +40,8 @@ const DEFAULT_BOARD_LABEL = process.env.TRIVABBLE_DEFAULT_BOARD_LABEL || "15x15-
 const DEFAULT_BAG_FACTOR = 1;
 const CHECK_FIRST_WORD = 1; // 1: check, 0: not set, -1: no check
 const FREEZE_PLAYED_TILES = -1; // 1: freeze, 0: not set, -1: no freeze
+const CHAT_HISTORY_SIZE_LIMIT = parseInt(process.env.TRIVABBLE_CHAT_HISTORY_SIZE_LIMIT || "50");
+const CHAT_MESSAGE_MAX_SIZE = parseInt(process.env.TRIVABBLE_CHAT_MESSAGE_MAX_SIZE || "1000");
 
 const VERSION = 202508050000;
 
@@ -249,7 +251,7 @@ function newBoard(label) {
 // if you add something there that stores the player name, please make sure to
 // update changePlayerName
 // eslint-disable-next-line max-params
-Game.prototype.init = function (lang, label, factor, checkFirstWord, freezePlayedTiles) {
+Game.prototype.init = function (lang, label, factor, checkFirstWord, freezePlayedTiles, chatHistory) {
     this.label = label || DEFAULT_BOARD_LABEL;
     this.factor = factor || DEFAULT_BAG_FACTOR;
     this.board = newBoard(this.label);
@@ -263,6 +265,7 @@ Game.prototype.init = function (lang, label, factor, checkFirstWord, freezePlaye
     this.freezePlayedTiles = freezePlayedTiles || FREEZE_PLAYED_TILES;
     this.lastUpdated = new Date();
     this.currentPlayer = "";
+    this.chatHistory = chatHistory || [];
 
     this.bag = expandAndShuffleBag(bag, this.factor);
 };
@@ -281,6 +284,7 @@ Game.prototype.toJSON = function () {
         checkFirstWord:   this.checkFirstWord,
         freezePlayedTiles: this.freezePlayedTiles,
         lastUpdated:      this.lastUpdated.toISOString(),
+        chatHistory:      this.chatHistory,
         currentPlayer:    this.currentPlayer
     };
 };
@@ -300,6 +304,7 @@ Game.fromJSON = function (obj) {
     game.freezePlayedTiles = obj.freezePlayedTiles || FREEZE_PLAYED_TILES;
     game.lastUpdated      = obj.lastUpdated ? new Date(obj.lastUpdated) : new Date();
     game.currentPlayer    = obj.currentPlayer || "";
+    game.chatHistory      = obj.chatHistory || [];
     return game;
 };
 
@@ -493,7 +498,7 @@ Game.prototype.bagPushLetter = function (letter, player) {
 };
 
 Game.prototype.reset = function (player) {
-    this.init(this.lang, this.label, this.factor, this.checkFirstWord, this.freezePlayedTiles);
+    this.init(this.lang, this.label, this.factor, this.checkFirstWord, this.freezePlayedTiles, this.chatHistory);
     this.pendingEvents.push({
         player: player,
         action: "reset",
@@ -548,10 +553,26 @@ Game.prototype.changePlayerName = function (playerName, newName) {
             newName: n
         }]
     });
+
+    // We don't currently update the chat history with the new name.
+    // This is on purpose, but maybe we should update the name there too?
+
     this.commit();
     return n;
 };
 
+Game.prototype.storeAndDispatchChatMessage = function (msg) {
+    if (CHAT_HISTORY_SIZE_LIMIT !== -1) {
+        this.chatHistory.push(msg);
+    }
+
+    while (this.chatHistory.length > CHAT_HISTORY_SIZE_LIMIT) {
+        this.chatHistory.shift();
+    }
+
+    this.pendingEvents.push({msg});
+};
+
 function newGameId() {
     let number;
 
@@ -643,6 +664,9 @@ function handleCommand(cmdNumber, message, response) {
                 tileOwner: game.tileOwner,
                 remainingLetters: game.bag.length,
                 letterValues: game.letterValues,
+                chatHistory: game.chatHistory,
+                chatHistoryLimit: CHAT_HISTORY_SIZE_LIMIT,
+                chatMessageMaxSize: CHAT_MESSAGE_MAX_SIZE,
                 version: VERSION
             });
             break;
@@ -906,13 +930,18 @@ function handleCommand(cmdNumber, message, response) {
 
 
         case "msg": {
-            game.pendingEvents.push({
-                msg: {
-                    sender: playerName,
-                    content: cmd.msg,
-                    specialMsg: cmd.specialMsg
-                }
-            });
+            const msg = {
+                date: new Date(),
+                sender: playerName,
+                content: cmd.msg,
+                specialMsg: cmd.specialMsg
+            };
+
+            if (CHAT_MESSAGE_MAX_SIZE !== -1 && msg.content.length > CHAT_MESSAGE_MAX_SIZE) {
+                msg.content = msg.content.substr(0, CHAT_MESSAGE_MAX_SIZE);
+            }
+
+            game.storeAndDispatchChatMessage(msg);
 
             reply(message, response, cmdNumber, {error: 0});
             break;
@@ -931,6 +960,7 @@ function handleCommands(message, responseAndType) {
     if (!message.cmds || !message.cmds.length) {
         const {gameNumber, game} = joinGame(message.gameNumber, message);
 
+        // FIXME: deduplicate with joinGame?
         writeMessage(responseAndType,
             JSON.stringify({
                 playerName:          message.playerName,
@@ -947,6 +977,9 @@ function handleCommands(message, responseAndType) {
                 remainingLetters:    game.bag.length,
                 checkFirstWord:      game.checkFirstWord,
                 freezePlayedTiles:   game.freezePlayedTiles,
+                chatHistory:         game.chatHistory,
+                chatHistoryLimit:    CHAT_HISTORY_SIZE_LIMIT,
+                chatMessageMaxSize:  CHAT_MESSAGE_MAX_SIZE,
                 version:             VERSION
             })
         );