From db1f1f9adb920605c7a16b060a7737e54636ee08 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sat, 29 Feb 2020 13:42:46 +0100
Subject: [PATCH] Improvements - untested

---
 client/public/sounds/SOURCE        |  3 +--
 client/public/sounds/move.mp3      |  1 -
 client/public/sounds/newgame.mp3   |  1 -
 client/public/sounds/newgame.wav   |  1 +
 client/public/sounds/undo.mp3      |  1 -
 client/src/components/BaseGame.vue |  4 ---
 client/src/components/Chat.vue     |  6 +++++
 client/src/components/GameList.vue | 13 ++++++----
 client/src/components/Settings.vue | 14 +++++------
 client/src/store.js                | 11 ++++++---
 client/src/translations/en.js      |  5 ++--
 client/src/translations/es.js      |  5 ++--
 client/src/translations/fr.js      |  5 ++--
 client/src/utils/gameStorage.js    | 13 ++++++++--
 client/src/views/Game.vue          | 39 +++++++++++++++++++++---------
 client/src/views/Hall.vue          | 11 +++++++--
 client/src/views/MyGames.vue       | 35 +++++++++++++++++++++++++--
 server/models/Game.js              | 26 +++++++++++++++-----
 server/routes/games.js             | 16 +++++++++++-
 server/sockets.js                  | 22 +++++++++++++++++
 20 files changed, 175 insertions(+), 57 deletions(-)
 delete mode 100644 client/public/sounds/move.mp3
 delete mode 100644 client/public/sounds/newgame.mp3
 create mode 100644 client/public/sounds/newgame.wav
 delete mode 100644 client/public/sounds/undo.mp3

diff --git a/client/public/sounds/SOURCE b/client/public/sounds/SOURCE
index dd2e2865..1182b943 100644
--- a/client/public/sounds/SOURCE
+++ b/client/public/sounds/SOURCE
@@ -1,2 +1 @@
-https://audiojungle.net/item/chess-pieces-and-board/11657386
-http://freesound.org/search/?q=cancel + sword
+http://soundbible.com/1531-Temple-Bell.html
diff --git a/client/public/sounds/move.mp3 b/client/public/sounds/move.mp3
deleted file mode 100644
index 8586f77b..00000000
--- a/client/public/sounds/move.mp3
+++ /dev/null
@@ -1 +0,0 @@
-#$# git-fat d9100d524f9bca2f601823d2422bd3c21ed1329c                18889
diff --git a/client/public/sounds/newgame.mp3 b/client/public/sounds/newgame.mp3
deleted file mode 100644
index 09dac630..00000000
--- a/client/public/sounds/newgame.mp3
+++ /dev/null
@@ -1 +0,0 @@
-#$# git-fat fca619a21ac9647e2cf6888ecc78e84a3ca8011c                14194
diff --git a/client/public/sounds/newgame.wav b/client/public/sounds/newgame.wav
new file mode 100644
index 00000000..f88939a0
--- /dev/null
+++ b/client/public/sounds/newgame.wav
@@ -0,0 +1 @@
+#$# git-fat 839f8d81e4e6bf135e3828be652483420d4118b8               917140
diff --git a/client/public/sounds/undo.mp3 b/client/public/sounds/undo.mp3
deleted file mode 100644
index d7673144..00000000
--- a/client/public/sounds/undo.mp3
+++ /dev/null
@@ -1 +0,0 @@
-#$# git-fat 527d57bf4f0c83bbfdcfc18d2f9503c28b8e3cc5                 8305
diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue
index 2a54cbbe..9783c71f 100644
--- a/client/src/components/BaseGame.vue
+++ b/client/src/components/BaseGame.vue
@@ -355,8 +355,6 @@ export default {
         })();
       };
       const afterMove = (smove, initurn) => {
-        if (this.st.settings.sound == 2)
-          new Audio("/sounds/move.mp3").play().catch(() => {});
         if (this.vr.turn != initurn) {
           // Turn has changed: move is complete
           if (!smove.fen) {
@@ -440,8 +438,6 @@ export default {
         if (light) this.cursor--;
         else {
           this.positionCursorTo(this.cursor - 1);
-          if (this.st.settings.sound == 2)
-            new Audio("/sounds/undo.mp3").play().catch(() => {});
           this.incheck = this.vr.getCheckSquares(this.vr.turn);
           this.emitFenIfAnalyze();
         }
diff --git a/client/src/components/Chat.vue b/client/src/components/Chat.vue
index f1e1fad2..7f807676 100644
--- a/client/src/components/Chat.vue
+++ b/client/src/components/Chat.vue
@@ -1,5 +1,7 @@
 <template lang="pug">
 div
+  button(@click="clearHistory()")
+    | {{ st.tr["Clear chat"] }}
   input#inputChat(
     type="text"
     :placeholder="st.tr['Chat here']"
@@ -51,6 +53,10 @@ export default {
       const chat = { msg: chatTxt, name: this.st.user.name || "@nonymous" };
       this.$emit("mychat", chat);
       this.chats.unshift(chat);
+    },
+    clearHistory: function() {
+      this.chats = [];
+      this.$emit("chatcleared");
     }
   }
 };
diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue
index 6bf0e9ac..39ad9bf2 100644
--- a/client/src/components/GameList.vue
+++ b/client/src/components/GameList.vue
@@ -54,6 +54,13 @@ export default {
       // Show in order: games where it's my turn, my running games, my games, other games
       let minCreated = Number.MAX_SAFE_INTEGER;
       let maxCreated = 0;
+      const isMyTurn = (g, myColor) => {
+        const rem = g.movesCount % 2;
+        return (
+          (rem == 0 && myColor == "w") ||
+          (rem == 1 && myColor == "b")
+        );
+      };
       let augmentedGames = this.games
         .filter(g => !this.deleted[g.id])
         .map(g => {
@@ -72,11 +79,7 @@ export default {
                 : "b";
             if (g.score == "*") {
               priority++;
-              // I play in this game, so g.fen will be defined
-              // NOTE: this is a fragile way to detect turn,
-              // but since V isn't defined let's do that for now. (TODO:)
-              //if (V.ParseFen(g.fen).turn == myColor)
-              if (g.fen.match(" " + myColor + " ")) priority++;
+              if (isMyTurn(g, myColor)) priority++;
             }
           }
           if (g.created < minCreated) minCreated = g.created;
diff --git a/client/src/components/Settings.vue b/client/src/components/Settings.vue
index a6c3e658..d01f2cd4 100644
--- a/client/src/components/Settings.vue
+++ b/client/src/components/Settings.vue
@@ -46,11 +46,12 @@ div
             option(value="chesscom") {{ st.tr["green"] }}
             option(value="chesstempo") {{ st.tr["blue"] }}
         fieldset
-          label(for="setSound") {{ st.tr["Play sounds?"] }}
-          select#setSound(v-model="st.settings.sound")
-            option(value="0") {{ st.tr["None"] }}
-            option(value="1") {{ st.tr["New game"] }}
-            option(value="2") {{ st.tr["All"] }}
+          label(for="setSound")
+            | {{ st.tr["Sound on new game?"] }}
+          input#setSound(
+            type="checkbox"
+            v-model="st.settings.sound"
+          )
 </template>
 
 <script>
@@ -82,10 +83,9 @@ export default {
       const propName = event.target.id
         .substr(3)
         .replace(/^\w/, c => c.toLowerCase());
-      let value = ["bcolor", "sound"].includes(propName)
+      const value = propName == "bcolor"
         ? event.target.value
         : event.target.checked;
-      if (propName == "sound") value = parseInt(value);
       store.updateSetting(propName, value);
     }
   }
diff --git a/client/src/store.js b/client/src/store.js
index 52870e0a..b81ec552 100644
--- a/client/src/store.js
+++ b/client/src/store.js
@@ -53,11 +53,16 @@ export const store = {
       this.state.user.notify = res.notify;
     });
     // Settings initialized with values from localStorage
+    const getItemDefaultTrue = (item) => {
+      const value = localStorage.getItem(item);
+      if (!value) return true;
+      return value == "true";
+    };
     this.state.settings = {
       bcolor: localStorage.getItem("bcolor") || "lichess",
-      sound: parseInt(localStorage.getItem("sound")) || 1,
-      hints: localStorage.getItem("hints") == "true",
-      highlight: localStorage.getItem("highlight") == "true"
+      sound: getItemDefaultTrue("sound"),
+      hints: getItemDefaultTrue("hints"),
+      highlight: getItemDefaultTrue("highlight")
     };
     const supportedLangs = ["en", "es", "fr"];
     const navLanguage = navigator.language.substr(0,2);
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 4416b76a..ff178cd4 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -3,7 +3,6 @@ export const translations = {
   About: "About",
   "Accept draw?": "Accept draw?",
   "Accept challenge?": "Accept challenge?",
-  All: "All",
   Analyse: "Analyse",
   "Analysis mode": "Analysis mode",
   "Any player": "Any player",
@@ -22,6 +21,7 @@ export const translations = {
   Challenge: "Challenge",
   "Challenge declined": "Challenge declined",
   "Chat here": "Chat here",
+  "Clear history": "Clear history",
   "Connection token sent. Check your emails!": "Connection token sent. Check your emails!",
   Contact: "Contact",
   "Correspondance challenges": "Correspondance challenges",
@@ -71,13 +71,11 @@ export const translations = {
   News: "News",
   "No more problems": "No more problems",
   "No subject. Send anyway?": "No subject. Send anyway?",
-  None: "None",
   "Notifications by email": "Notifications by email",
   Number: "Number",
   Observe: "Observe",
   "Offer draw?": "Offer draw?",
   "Opponent action": "Opponent action",
-  "Play sounds?": "Play sounds?",
   "Play with?": "Play with?",
   Players: "Players",
   "Please log in to accept corr challenges": "Please log in to accept corr challenges",
@@ -105,6 +103,7 @@ export const translations = {
   "Show possible moves?": "Show possible moves?",
   "Show solution": "Show solution",
   Solution: "Solution",
+  "Sound alert when game starts?": "Sound alert when game starts?",
   Stop: "Stop",
   "Stop game": "Stop game",
   Subject: "Subject",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index da8d6730..eb08331d 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -3,7 +3,6 @@ export const translations = {
   About: "Acerca de",
   "Accept draw?": "¿Acceptar tablas?",
   "Accept challenge?": "¿Acceptar el desafío?",
-  All: "Todos",
   Analyse: "Analizar",
   "Analysis mode": "Modo análisis",
   "Any player": "Cualquier jugador",
@@ -22,6 +21,7 @@ export const translations = {
   Challenge: "Desafiar",
   "Challenge declined": "Desafío rechazado",
   "Chat here": "Chat aquí",
+  "Clear history": "Clara historia",
   "Connection token sent. Check your emails!": "Token de conexión enviado. ¡Revisa tus correos!",
   Contact: "Contacto",
   "Correspondance challenges": "Desafíos por correspondencia",
@@ -71,13 +71,11 @@ export const translations = {
   News: "Noticias",
   "No more problems": "No mas problemas",
   "No subject. Send anyway?": "Sin asunto. ¿Enviar sin embargo?",
-  None: "Ninguno",
   "Notifications by email": "Notificaciones por email",
   Number: "Número",
   "Offer draw?": "¿Ofrecer tablas?",
   Observe: "Observar",
   "Opponent action": "Acción del adversario",
-  "Play sounds?": "¿Permitir sonidos?",
   "Play with?": "¿Jugar con?",
   Players: "Jugadores",
   "Please log in to accept corr challenges": "Inicia sesión para aceptar los desafíos por correspondencia",
@@ -105,6 +103,7 @@ export const translations = {
   "Show possible moves?": "¿Mostrar posibles movimientos?",
   "Show solution": "Mostrar la solución",
   Solution: "Solución",
+  "Sound alert when game starts?": "¿Alerta audible cuando comienza una partida?",
   Stop: "Interrupción",
   "Stop game": "Terminar la partida",
   Subject: "Asunto",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index b64ecc26..6e37e605 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -3,7 +3,6 @@ export const translations = {
   About: "À propos",
   "Accept draw?": "Accepter la nulle ?",
   "Accept challenge?": "Accepter le défi ?",
-  All: "Tous",
   Analyse: "Analyser",
   "Analysis mode": "Mode analyse",
   "Any player": "N'importe qui",
@@ -22,6 +21,7 @@ export const translations = {
   Challenge: "Défier",
   "Challenge declined": "Défi refusé",
   "Chat here": "Chattez ici",
+  "Clear history": "Effacer l'historique",
   "Connection token sent. Check your emails!": "Token de connection envoyé. Allez voir vos emails !",
   Contact: "Contact",
   "Correspondance challenges": "Défis par correspondance",
@@ -71,13 +71,11 @@ export const translations = {
   News: "Nouvelles",
   "No more problems": "Plus de problèmes",
   "No subject. Send anyway?": "Pas de sujet. Envoyer quand-même ??",
-  None: "Aucun",
   "Notifications by email": "Notifications par email",
   Number: "Numéro",
   "Offer draw?": "Proposer nulle ?",
   Observe: "Observer",
   "Opponent action": "Action de l'adversaire",
-  "Play sounds?": "Jouer les sons ?",
   "Play with?": "Jouer avec ?",
   Players: "Joueurs",
   "Please log in to accept corr challenges": "Identifiez vous pour accepter des défis par correspondance",
@@ -105,6 +103,7 @@ export const translations = {
   "Show possible moves?": "Montrer les coups possibles ?",
   "Show solution": "Montrer la solution",
   Solution: "Solution",
+  "Sound alert when game starts?": "Alert sonore quand une partie démarre?",
   Stop: "Arrêt",
   "Stop game": "Arrêter la partie",
   Subject: "Sujet",
diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js
index ce609af0..4eb6bc73 100644
--- a/client/src/utils/gameStorage.js
+++ b/client/src/utils/gameStorage.js
@@ -96,7 +96,8 @@ export const GameStorage = {
   },
 
   // Retrieve all local games (running, completed, imported...)
-  getAll: function(callback) {
+  // light: do not retrieve moves or players or clocks (TODO: this is the only usage)
+  getAll: function(light, callback) {
     dbOperation((err,db) => {
       let objectStore = db.transaction("games").objectStore("games");
       let games = [];
@@ -104,7 +105,15 @@ export const GameStorage = {
         let cursor = event.target.result;
         // if there is still another cursor to go, keep running this code
         if (cursor) {
-          games.push(cursor.value);
+          let g = cursor.value;
+          if (light) {
+            g.movesCount = g.moves.length;
+            delete g.moves;
+            delete g.clocks;
+            delete g.initime;
+            delete g.players;
+          }
+          games.push(g);
           cursor.continue();
         } else callback(games);
       };
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index 2da8d9e0..23c45783 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -24,6 +24,7 @@ main
         :pastChats="game.chats"
         :newChat="newChat"
         @mychat="processChat"
+        @chatcleared="clearChat"
       )
   .row
     #aboveBoard.col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2
@@ -193,6 +194,29 @@ export default {
         Object.values(this.people).some(p => p.id == player.uid)
       );
     },
+    resetChatColor: function() {
+      // TODO: this is called twice, once on opening an once on closing
+      document.getElementById("chatBtn").classList.remove("somethingnew");
+    },
+    processChat: function(chat) {
+      this.send("newchat", { data: chat });
+      // NOTE: anonymous chats in corr games are not stored on server (TODO?)
+      if (this.game.type == "corr" && this.st.user.id > 0)
+        GameStorage.update(this.gameRef.id, { chat: chat });
+    },
+    clearChat: function() {
+      // Nothing more to do if game is live (chats not recorded)
+      if (this.game.mycolor && this.game.type == "corr") {
+        ajax(
+          "/chats",
+          "DELETE",
+          {gid: this.game.id},
+          () => {
+            this.$set(this.game, "pastChats", []);
+          }
+        );
+      }
+    },
     socketMessageListener: function(msg) {
       if (!this.conn) return;
       const data = JSON.parse(msg.data);
@@ -634,7 +658,10 @@ export default {
           const sendMove = {
             move: filtered_move,
             addTime: addTime,
-            cancelDrawOffer: this.drawOffer == ""
+            cancelDrawOffer: this.drawOffer == "",
+            // Players' SID required for /mygames page
+            // TODO: precompute and add this field to game object?
+            players: this.game.players.map(p => p.sid)
           };
           this.send("newmove", { data: sendMove });
         }
@@ -717,16 +744,6 @@ export default {
       }
       else doProcessMove();
     },
-    resetChatColor: function() {
-      // TODO: this is called twice, once on opening an once on closing
-      document.getElementById("chatBtn").classList.remove("somethingnew");
-    },
-    processChat: function(chat) {
-      this.send("newchat", { data: chat });
-      // NOTE: anonymous chats in corr games are not stored on server (TODO?)
-      if (this.game.type == "corr" && this.st.user.id > 0)
-        GameStorage.update(this.gameRef.id, { chat: chat });
-    },
     gameOver: function(score, scoreMsg) {
       this.game.score = score;
       this.$set(this.game, "scoreMsg", scoreMsg || getScoreMessage(score));
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index 92660898..7a7f911b 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -751,6 +751,13 @@ export default {
         localStorage.setItem("cadence", chall.cadence);
         localStorage.setItem("vid", chall.vid);
         document.getElementById("modalNewgame").checked = false;
+        // Show the challenge if not on current display
+        if (
+          (ctype == "live" && this.cdisplay == "corr") ||
+          (ctype == "corr" && this.cdisplay == "live")
+        ) {
+          this.setDisplay('c', ctype);
+        }
       };
       if (ctype == "live") {
         // Live challenges have a random ID
@@ -885,8 +892,8 @@ export default {
       GameStorage.add(game, (err) => {
         // If an error occurred, game is not added: abort
         if (!err) {
-          if (this.st.settings.sound >= 1)
-            new Audio("/sounds/newgame.mp3").play().catch(() => {});
+          if (this.st.settings.sound)
+            new Audio("/sounds/newgame.wav").play().catch(() => {});
           this.$router.push("/game/" + gameInfo.id);
         }
       });
diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue
index 41cced56..e0914f67 100644
--- a/client/src/views/MyGames.vue
+++ b/client/src/views/MyGames.vue
@@ -34,11 +34,13 @@ export default {
       st: store.state,
       display: "live",
       liveGames: [],
-      corrGames: []
+      corrGames: [],
+      conn: null,
+      connexionString: ""
     };
   },
   created: function() {
-    GameStorage.getAll(localGames => {
+    GameStorage.getAll(true, localGames => {
       localGames.forEach(g => (g.type = this.classifyObject(g)));
       this.liveGames = localGames;
     });
@@ -48,6 +50,18 @@ export default {
         this.corrGames = res.games;
       });
     }
+    // Initialize connection
+    this.connexionString =
+      params.socketUrl +
+      "/?sid=" +
+      this.st.user.sid +
+      "&tmpId=" +
+      getRandString() +
+      "&page=" +
+      encodeURIComponent(this.$route.path);
+    this.conn = new WebSocket(this.connexionString);
+    this.conn.onmessage = this.socketMessageListener;
+    this.conn.onclose = this.socketCloseListener;
   },
   mounted: function() {
     const showType = localStorage.getItem("type-myGames") || "live";
@@ -69,6 +83,23 @@ export default {
     },
     showGame: function(g) {
       this.$router.push("/game/" + g.id);
+    },
+    socketMessageListener: function(msg) {
+      const data = JSON.parse(msg.data);
+      // Only event is newmove, and received only:
+      if (data.code == "newmove") {
+        let games = !!parseInt(data.gid)
+          ? this.corrGames
+          : this.liveGames;
+        // NOTE: new move itself is not received, because it wouldn't be used.
+        let g = games.find(g => g.id == data.gid);
+        this.$set(g, "movesCount", g.movesCount + 1);
+      }
+    },
+    socketCloseListener: function() {
+      this.conn = new WebSocket(this.connexionString);
+      this.conn.addEventListener("message", this.socketMessageListener);
+      this.conn.addEventListener("close", this.socketCloseListener);
     }
   }
 };
diff --git a/server/models/Game.js b/server/models/Game.js
index 21ece2f1..3fde91f8 100644
--- a/server/models/Game.js
+++ b/server/models/Game.js
@@ -91,11 +91,18 @@ const GameModel =
         db.all(query, (err2,players) => {
           if (light)
           {
-            const game = Object.assign({},
-              gameInfo,
-              {players: players}
-            );
-            cb(null, game);
+            query =
+              "SELECT COUNT(*) AS nbMoves " +
+              "FROM Moves " +
+              "WHERE gid = " + id;
+            db.get(query, (err,ret) => {
+              const game = Object.assign({},
+                gameInfo,
+                {players: players},
+                {movesCount: ret.nbMoves}
+              );
+              cb(null, game);
+            });
           }
           else
           {
@@ -235,7 +242,6 @@ const GameModel =
         query += modifs + " WHERE id = " + id;
         db.run(query);
       }
-      let wrongMoveIndex = false;
       if (obj.move)
       {
         // Security: only update moves if index is right
@@ -263,6 +269,14 @@ const GameModel =
             + id + ",?,'" + obj.chat.name + "'," + Date.now() + ")";
         db.run(query, obj.chat.msg);
       }
+      else if (obj.delchat)
+      {
+        query =
+          "DELETE " +
+          "FROM Chats " +
+          "WHERE gid = " + id;
+        db.run(query, cb);
+      }
     });
   },
 
diff --git a/server/routes/games.js b/server/routes/games.js
index 1b788cb9..607ab39e 100644
--- a/server/routes/games.js
+++ b/server/routes/games.js
@@ -54,7 +54,7 @@ router.get("/games", access.ajax, (req,res) => {
   }
 });
 
-// New move + fen update + score + chats...
+// FEN update + score(Msg) + draw status / and new move + chats
 router.put("/games", access.logged, access.ajax, (req,res) => {
   const gid = req.body.gid;
   const obj = req.body.newObj;
@@ -83,4 +83,18 @@ router.put("/games", access.logged, access.ajax, (req,res) => {
   }
 });
 
+// TODO: chats deletion here, but could/should be elsewhere.
+// Moves update also could, although logical unit in a game.
+router.delete("/chats", access.logged, access.ajax, (req,res) => {
+  const gid = req.query["gid"];
+  GameModel.getPlayers(gid, (err,players) => {
+    if (players.some(p => p.uid == req.userId))
+    {
+      GameModel.update(gid, {delchat: true}, (err) => {
+        res.json(err || {});
+      });
+    }
+  });
+});
+
 module.exports = router;
diff --git a/server/sockets.js b/server/sockets.js
index 42a8840f..cf5ea1b8 100644
--- a/server/sockets.js
+++ b/server/sockets.js
@@ -190,8 +190,30 @@ module.exports = function(wss) {
         case "abort":
         case "drawoffer":
         case "draw":
+        {
           notifyRoom(page, obj.code, {data:obj.data});
+          const mygamesPg = "/mygames";
+          if (obj.code == "newmove" && clients[mygamesPg])
+          {
+            // Relay newmove info to myGames page
+            // NOTE: the move itself is not needed (for now at least)
+            const newmoveForMygames = {
+              gid: page.split("/")[2] //format is "/game/gid"
+            };
+            obj.data.players.forEach(pSid => {
+              if (clients[mygamesPg][pSid])
+              {
+                Object.keys(clients[mygamesPg][pSid]).forEach(x => {
+                  send(
+                    clients[mygamesPg][pSid][x],
+                    {code:"newmove", data:newmoveForMygames}
+                  );
+                });
+              }
+            });
+          }
           break;
+        }
 
         case "result":
           // Special case: notify all, 'transroom': Game --> Hall
-- 
2.44.0