From 1ef65040168ab7d55ce921abc9d63644a937d689 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 30 Mar 2020 21:05:46 +0200
Subject: [PATCH] Experimental game upload added

---
 client/src/components/BaseGame.vue    |  36 ++++---
 client/src/components/Board.vue       |  16 ++--
 client/src/components/MoveList.vue    |   6 +-
 client/src/components/UploadGame.vue  |  90 +++++++++++++++++-
 client/src/main.js                    |   5 +-
 client/src/translations/en.js         |   3 +-
 client/src/translations/es.js         |   3 +-
 client/src/translations/fr.js         |   3 +-
 client/src/utils/gameStorage.js       |   1 +
 client/src/utils/importgameStorage.js |   2 +-
 client/src/utils/modalClick.js        |   5 +-
 client/src/views/Game.vue             | 130 +++++++++++++++-----------
 client/src/views/MyGames.vue          |  27 ++++--
 server/db/create.sql                  |   2 +
 server/models/Game.js                 |   9 ++
 15 files changed, 238 insertions(+), 100 deletions(-)

diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue
index c3387e82..fd26d872 100644
--- a/client/src/components/BaseGame.vue
+++ b/client/src/components/BaseGame.vue
@@ -111,7 +111,7 @@ export default {
           : ""
       );
     },
-    // TODO: is it OK to pass "computed" as propoerties?
+    // TODO: is it OK to pass "computed" as properties?
     // Also, some are seemingly not recomputed when vr is initialized.
     showMoves: function() {
       return this.game.score != "*"
@@ -211,6 +211,7 @@ export default {
         // Strategy working also for multi-moves:
         if (!Array.isArray(move)) move = [move];
         move.forEach((m,idx) => {
+          m.index = this.vr.movesCount;
           m.notation = this.vr.getNotation(m);
           m.unambiguous = V.GetUnambiguousNotation(m);
           this.vr.play(m);
@@ -221,6 +222,7 @@ export default {
       if (firstMoveColor == "b") {
         // 'start' & 'end' is required for Board component
         this.moves.unshift({
+          index: parsedFen.movesCount,
           notation: "...",
           unambiguous: "...",
           start: { x: -1, y: -1 },
@@ -271,30 +273,36 @@ export default {
       let pgn = "";
       pgn += '[Site "vchess.club"]\n';
       pgn += '[Variant "' + this.game.vname + '"]\n';
-      pgn += '[Date "' + getDate(new Date()) + '"]\n';
+      const gdt = getDate(new Date(this.game.created || Date.now()));
+      pgn += '[Date "' + gdt + '"]\n';
       pgn += '[White "' + this.game.players[0].name + '"]\n';
       pgn += '[Black "' + this.game.players[1].name + '"]\n';
       pgn += '[Fen "' + this.game.fenStart + '"]\n';
       pgn += '[Result "' + this.game.score + '"]\n';
-      if (!!this.game.id)
-        pgn += '[URL "' + params.serverUrl + '/game/' + this.game.id + '"]\n';
+      if (!!this.game.id) {
+        pgn += '[Cadence "' + this.game.cadence + '"]\n';
+        pgn += '[Url "' + params.serverUrl + '/game/' + this.game.id + '"]\n';
+      }
       pgn += '\n';
       for (let i = 0; i < this.moves.length; i += 2) {
         if (i > 0) pgn += " ";
         // Adjust dots notation for a better display:
         let fullNotation = getFullNotation(this.moves[i]);
         if (fullNotation == "...") fullNotation = "..";
-        pgn += (i/2+1) + "." + fullNotation;
+        pgn += (this.moves[i].index / 2 + 1) + "." + fullNotation;
         if (i+1 < this.moves.length)
           pgn += " " + getFullNotation(this.moves[i+1]);
       }
       pgn += "\n\n";
       for (let i = 0; i < this.moves.length; i += 2) {
-        const moveNumber = i / 2 + 1;
-        pgn += moveNumber + "." + i + " " +
-          getFullNotation(this.moves[i], "unambiguous") + "\n";
+        const moveNumber = this.moves[i].index / 2 + 1;
+        // Skip "dots move", useless for machine reading:
+        if (this.moves[i].notation != "...") {
+          pgn += moveNumber + ".w " +
+            getFullNotation(this.moves[i], "unambiguous") + "\n";
+        }
         if (i+1 < this.moves.length) {
-          pgn += moveNumber + "." + (i+1) + " " +
+          pgn += moveNumber + ".b " +
             getFullNotation(this.moves[i+1], "unambiguous") + "\n";
         }
       }
@@ -323,8 +331,14 @@ export default {
         this.autoplayLoop = null;
       } else {
         this.autoplay = true;
-        infinitePlay();
-        this.autoplayLoop = setInterval(infinitePlay, 1500);
+        setTimeout(
+          () => {
+            infinitePlay();
+            this.autoplayLoop = setInterval(infinitePlay, 1500);
+          },
+          // Small delay otherwise the first move is played too fast
+          500
+        );
       }
     },
     // Animate an elementary move
diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue
index 639938b2..56f34880 100644
--- a/client/src/components/Board.vue
+++ b/client/src/components/Board.vue
@@ -519,12 +519,11 @@ export default {
       };
     },
     mousedown: function(e) {
-      if (!([1, 3].includes(e.which))) return;
       e.preventDefault();
-      if (e.which != 3)
+      if (!this.mobileBrowser && e.which != 3)
         // Cancel current drawing and circles, if any
         this.cancelResetArrows();
-      if (e.which == 1 || this.mobileBrowser) {
+      if (this.mobileBrowser || e.which == 1) {
         // Mouse left button
         if (!this.start) {
           // NOTE: classList[0] is enough: 'piece' is the first assigned class
@@ -566,8 +565,8 @@ export default {
         } else {
           this.processMoveAttempt(e);
         }
-      } else {
-        // e.which == 3 : mouse right button
+      } else if (e.which == 3) {
+        // Mouse right button
         let elem = e.target;
         // Next loop because of potential marks
         while (elem.tagName == "IMG") elem = elem.parentNode;
@@ -612,17 +611,16 @@ export default {
       }
     },
     mouseup: function(e) {
-      if (!([1, 3].includes(e.which))) return;
       e.preventDefault();
-      if (e.which == 1) {
+      if (this.mobileBrowser || e.which == 1) {
         if (!this.selectedPiece) return;
         // Drag'n drop. Selected piece is no longer needed:
         this.selectedPiece.parentNode.removeChild(this.selectedPiece);
         delete this.selectedPiece;
         this.selectedPiece = null;
         this.processMoveAttempt(e);
-      } else {
-        // Mouse right button (e.which == 3)
+      } else if (e.which == 3) {
+        // Mouse right button
         this.movingArrow = { x: -1, y: -1 };
         this.processArrowAttempt(e);
       }
diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue
index d2ae3f35..4c351dd5 100644
--- a/client/src/components/MoveList.vue
+++ b/client/src/components/MoveList.vue
@@ -101,10 +101,8 @@ export default {
     window.addEventListener("resize", () => {
       if (!timeoutLaunched) {
         timeoutLaunched = true;
-        setTimeout(() => {
-          this.adjustBoard();
-          timeoutLaunched = false;
-        }, 500);
+        this.adjustBoard();
+        setTimeout(() => { timeoutLaunched = false; }, 500);
       }
     });
   },
diff --git a/client/src/components/UploadGame.vue b/client/src/components/UploadGame.vue
index 369333c1..a94c39b7 100644
--- a/client/src/components/UploadGame.vue
+++ b/client/src/components/UploadGame.vue
@@ -9,6 +9,7 @@ div
 </template>
 
 <script>
+import { getRandString } from "@/utils/alea";
 export default {
   name: "my-upload-game",
   methods: {
@@ -23,11 +24,90 @@ export default {
 			};
 			reader.readAsText(file);
 		},
-    parseAndEmit: function(pgn) {
-      // TODO: header gives game Info, third secton the moves
-      let game = {};
-      // mark sur ID pour dire import : I_
-      this.$emit("game-uploaded", game);
+    parseAndEmit: async function(pgn) {
+      let game = {
+        // Players potential ID and socket IDs are not searched
+        players: [
+          { id: 0, sid: "" },
+          { id: 0, sid: "" }
+        ]
+      };
+      const lines  = pgn.split('\n');
+      let idx = 0;
+      // Read header
+      while (lines[idx].length > 0) {
+        // NOTE: not using "split(' ')" because the FEN has spaces
+        const spaceIdx = lines[idx].indexOf(' ');
+        const prop = lines[idx].substr(0, spaceIdx).match(/^\[(.*)$/)[1];
+        const value = lines[idx].substr(spaceIdx + 1).match(/^"(.*)"\]$/)[1];
+        switch (prop) {
+          case "Variant":
+            game.vname = value;
+            break;
+          case "Date":
+            game.created = new Date(value).getTime();
+            break;
+          case "White":
+            game.players[0].name = value;
+            break;
+          case "Black":
+            game.players[1].name = value;
+            break;
+          case "Fen":
+            game.fenStart = value;
+            break;
+          case "Result":
+            // Allow importing unfinished games, but mark them as
+            // "unknown result" to avoid running the clocks...
+            game.result = (value != "*" ? value : "?");
+            break;
+          case "Url":
+            // Prefix "I_" to say "this is an import"
+            game.id = "i" + value.match(/\/game\/([a-zA-Z0-9]+)$/)[1];
+            break;
+          case "Cadence":
+            game.cadence = value;
+            break;
+        }
+        idx++;
+      }
+      if (!game.id) {
+        game.id = "i" + getRandString();
+        // Provide a random cadence, just to be sure nothing breaks:
+        game.cadence = "1d";
+      }
+      game.chats = []; //not stored in PGN :)
+      // Skip "human moves" section:
+      while (lines[++idx].length > 0) {}
+      // Read moves
+      game.moves = [];
+      await import("@/variants/" + game.vname + ".js")
+      .then((vModule) => {
+        window.V = vModule[game.vname + "Rules"];
+        while (++idx < lines.length && lines[idx].length > 0) {
+          const lineParts = lines[idx].split(" ");
+          const startEnd = lineParts[1].split('.');
+          let move = {};
+          if (startEnd[0] != "-") move.start = V.SquareToCoords(startEnd[0]);
+          if (startEnd[1] != "-") move.end = V.SquareToCoords(startEnd[1]);
+          const appearVanish = lineParts[2].split('/').map(lpart => {
+            if (lpart == "-") return [];
+            return lpart.split('.').map(psq => {
+              const xy = V.SquareToCoords(psq.substr(2));
+              return {
+                x: xy.x,
+                y: xy.y,
+                c: psq[0],
+                p: psq[1]
+              };
+            });
+          });
+          move.appear = appearVanish[0];
+          move.vanish = appearVanish[1];
+          game.moves.push(move);
+        }
+        this.$emit("game-uploaded", game);
+      });
     }
   }
 };
diff --git a/client/src/main.js b/client/src/main.js
index f2e38f1f..3577b155 100644
--- a/client/src/main.js
+++ b/client/src/main.js
@@ -15,14 +15,15 @@ new Vue({
     window.doClick = elemId => {
       document.getElementById(elemId).click();
     };
-    // Esc key can close modals:
+    // Esc key can close some modals:
     document.addEventListener("keydown", e => {
       if (e.code === "Escape") {
         let modalBoxes = document.querySelectorAll("[id^='modal']");
         modalBoxes.forEach(m => {
           if (
             m.checked &&
-            !["modalAccept","modalConfirm"].includes(m.id)
+            !["modalAccept", "modalConfirm", "modalChat", "modalPeople"]
+              .includes(m.id)
           ) {
             m.checked = false;
           }
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index e1d6189e..e8c1e5c1 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -4,6 +4,7 @@ export const translations = {
   About: "About",
   "Accept draw?": "Accept draw?",
   "Accept challenge?": "Accept challenge?",
+  "An error occurred. Try again!": "An error occurred. Try again!",
   Analyse: "Analyse",
   "Analysis mode": "Analysis mode",
   "Analysis disabled for this variant": "Analysis disabled for this variant",
@@ -86,7 +87,6 @@ export const translations = {
   News: "News",
   "No challenges found :( Click on 'New game'!": "No challenges found :( Click on 'New game'!",
   "No games found :( Send a challenge!": "No games found :( Send a challenge!",
-  "No identifier found: use the upload button in analysis mode": "No identifier found: use the upload button in analysis mode",
   "No more problems": "No more problems",
   "No subject. Send anyway?": "No subject. Send anyway?",
   "Notifications by email": "Notifications by email",
@@ -135,6 +135,7 @@ export const translations = {
   "Symmetric random": "Symmetric random",
   "Terminate game?": "Terminate game?",
   "The game should be in another tab": "The game should be in another tab",
+  "The game was already imported": "The game was already imported",
   "Three repetitions": "Three repetitions",
   Time: "Time",
   "Undetermined result": "Undetermined result",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index d14f4606..85b458da 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -4,6 +4,7 @@ export const translations = {
   About: "Acerca de",
   "Accept draw?": "¿Acceptar tablas?",
   "Accept challenge?": "¿Acceptar el desafío?",
+  "An error occurred. Try again!": "Se ha producido un error. ¡Intenta de nuevo!",
   Analyse: "Analizar",
   "Analysis mode": "Modo análisis",
   "Analysis disabled for this variant": "Análisis deshabilitado para esta variante",
@@ -86,7 +87,6 @@ export const translations = {
   News: "Noticias",
   "No challenges found :( Click on 'New game'!": "No se encontró ningún desafío :( ¡Haz clic en 'Nueva partida'!",
   "No games found :( Send a challenge!": "No se encontró partidas :( ¡Envía un desafío!",
-  "No identifier found: use the upload button in analysis mode": "No se encontró ningún identificador: use el botón enviar en modo de análisis",
   "No more problems": "No mas problemas",
   "No subject. Send anyway?": "Sin asunto. ¿Enviar sin embargo?",
   "Notifications by email": "Notificaciones por email",
@@ -135,6 +135,7 @@ export const translations = {
   "Symmetric random": "Aleatorio simétrico",
   "Terminate game?": "¿Terminar la partida?",
   "The game should be in another tab": "la partida debería estar en otra pestaña",
+  "The game was already imported": "La partida ya ha sido importada",
   "Three repetitions": "Tres repeticiones",
   Time: "Tiempo",
   "Undetermined result": "Resultado indeterminado",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 2d7b535b..493b5b5f 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -4,6 +4,7 @@ export const translations = {
   About: "À propos",
   "Accept draw?": "Accepter la nulle ?",
   "Accept challenge?": "Accepter le défi ?",
+  "An error occurred. Try again!": "Une erreur est survenue. Réessayez !",
   Analyse: "Analyser",
   "Analysis mode": "Mode analyse",
   "Analysis disabled for this variant": "Analyse désactivée pour cette variante",
@@ -86,7 +87,6 @@ export const translations = {
   News: "Nouvelles",
   "No challenges found :( Click on 'New game'!": "Aucun défi trouvé :( Cliquez sur 'Nouvelle partie' !",
   "No games found :( Send a challenge!": "Aucune partie trouvée :( Envoyez un défi !",
-  "No identifier found: use the upload button in analysis mode": "Pas d'identifiant trouvé : utilisez le bouton d'envoi en mode analyse",
   "No more problems": "Plus de problèmes",
   "No subject. Send anyway?": "Pas de sujet. Envoyer quand-même ??",
   "Notifications by email": "Notifications par email",
@@ -135,6 +135,7 @@ export const translations = {
   "Symmetric random": "Aléatoire symétrique",
   "Terminate game?": "Stopper la partie ?",
   "The game should be in another tab": "La partie devrait être dans un autre onglet",
+  "The game was already imported": "La partie a déjà été importée",
   "Three repetitions": "Triple répétition",
   Time: "Temps",
   "Undetermined result": "Résultat indéterminé",
diff --git a/client/src/utils/gameStorage.js b/client/src/utils/gameStorage.js
index 109c8251..f69de2e2 100644
--- a/client/src/utils/gameStorage.js
+++ b/client/src/utils/gameStorage.js
@@ -87,6 +87,7 @@ export const GameStorage = {
           Object.keys(obj).forEach(k => {
             if (k == "move") game.moves.push(obj[k]);
             else if (k == "chat") game.chats.push(obj[k]);
+            else if (k == "chatRead") game.chatRead = Date.now();
             else if (k == "delchat") game.chats = [];
             else game[k] = obj[k];
           });
diff --git a/client/src/utils/importgameStorage.js b/client/src/utils/importgameStorage.js
index e5ad6e22..46a60bbb 100644
--- a/client/src/utils/importgameStorage.js
+++ b/client/src/utils/importgameStorage.js
@@ -47,7 +47,7 @@ export const ImportgameStorage = {
       };
       transaction.onerror = function(err) {
         // Duplicate key error (most likely)
-        callback(err);
+        callback(err.target.error);
       };
       transaction.objectStore("importgames").add(game);
     });
diff --git a/client/src/utils/modalClick.js b/client/src/utils/modalClick.js
index 96935787..f4687fde 100644
--- a/client/src/utils/modalClick.js
+++ b/client/src/utils/modalClick.js
@@ -1,5 +1,6 @@
-export function processModalClick(e) {
+export function processModalClick(e, cb) {
   // Close a modal when click on it but outside focused element
   const data = e.target.dataset;
-  if (data.checkbox) document.getElementById(data.checkbox).checked = false;
+  if (!!data.checkbox) document.getElementById(data.checkbox).checked = false;
+  if (!!cb) cb();
 }
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index 2a565f68..5145e588 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -57,7 +57,7 @@ main
           span {{ st.tr["Cancel"] }}
   .row
     #aboveBoard.col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2
-      span.variant-cadence {{ game.cadence }}
+      span.variant-cadence(v-if="game.type!='import'") {{ game.cadence }}
       span.variant-name {{ game.vname }}
       span#nextGame(
         v-if="nextIds.length > 0"
@@ -236,10 +236,14 @@ export default {
     this.atCreation();
   },
   mounted: function() {
-    ["chatWrap", "infoDiv"].forEach(eltName => {
-      document.getElementById(eltName)
-        .addEventListener("click", processModalClick);
-    });
+    document.getElementById("chatWrap")
+      .addEventListener("click", (e) => {
+        processModalClick(e, () => {
+          this.toggleChat("close")
+        });
+      });
+    document.getElementById("infoDiv")
+      .addEventListener("click", processModalClick);
     if ("ontouchstart" in window) {
       // Disable tooltips on smartphones:
       document.querySelectorAll("#aboveBoard .tooltip").forEach(elt => {
@@ -441,22 +445,33 @@ export default {
       if (!!oppsid && !!this.people[oppsid]) return oppsid;
       return null;
     },
-    toggleChat: function() {
-      if (document.getElementById("modalChat").checked)
+    // NOTE: action if provided is always a closing action
+    toggleChat: function(action) {
+      if (!action && document.getElementById("modalChat").checked)
         // Entering chat
         document.getElementById("inputChat").focus();
-      // TODO: next line is only required when exiting chat,
-      // but the event for now isn't well detected.
-      document.getElementById("chatBtn").classList.remove("somethingnew");
+      else {
+        document.getElementById("chatBtn").classList.remove("somethingnew");
+        if (!!this.game.mycolor) {
+          // Update "chatRead" variable either on server or locally
+          if (this.game.type == "corr")
+            this.updateCorrGame({ chatRead: this.game.mycolor });
+          else if (this.game.type == "live")
+            GameStorage.update(this.gameRef, { chatRead: true });
+        }
+      }
     },
     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)
-        this.updateCorrGame({ chat: chat });
-      else if (this.game.type == "live") {
-        chat.added = Date.now();
-        GameStorage.update(this.gameRef, { chat: chat });
+      if (!!this.game.mycolor) {
+        if (this.game.type == "corr")
+          this.updateCorrGame({ chat: chat });
+        else {
+          // Live game
+          chat.added = Date.now();
+          GameStorage.update(this.gameRef, { chat: chat });
+        }
       }
     },
     clearChat: function() {
@@ -475,6 +490,7 @@ export default {
       }
     },
     getGameType: function(game) {
+      if (!!game.id.match(/^i/)) return "import";
       return game.cadence.indexOf("d") >= 0 ? "corr" : "live";
     },
     // Notify something after a new move (to opponent and me on MyGames page)
@@ -640,13 +656,15 @@ export default {
           break;
         }
         case "askgame":
-          // Send current (live) game if not asked by any of the players
+          // Send current (live or import) game,
+          // if not asked by any of the players
           if (
-            this.game.type == "live" &&
+            this.game.type != "corr" &&
             this.game.players.every(p => p.sid != data.from[0])
           ) {
             const myGame = {
               id: this.game.id,
+              // FEN is current position, unused for now
               fen: this.game.fen,
               players: this.game.players,
               vid: this.game.vid,
@@ -784,7 +802,7 @@ export default {
         case "drawoffer":
           // NOTE: observers don't know who offered draw
           this.drawOffer = "received";
-          if (this.game.type == "live") {
+          if (!!this.game.mycolor && this.game.type == "live") {
             GameStorage.update(
               this.gameRef,
               { drawOffer: V.GetOppCol(this.game.mycolor) }
@@ -794,7 +812,7 @@ export default {
         case "rematchoffer":
           // NOTE: observers don't know who offered rematch
           this.rematchOffer = data.data ? "received" : "";
-          if (this.game.type == "live") {
+          if (!!this.game.mycolor && this.game.type == "live") {
             GameStorage.update(
               this.gameRef,
               { rematchOffer: V.GetOppCol(this.game.mycolor) }
@@ -826,7 +844,8 @@ export default {
           this.$refs["chatcomp"].newChat(chat);
           if (this.game.type == "live") {
             chat.added = Date.now();
-            GameStorage.update(this.gameRef, { chat: chat });
+            if (!!this.game.mycolor)
+              GameStorage.update(this.gameRef, { chat: chat });
           }
           if (!document.getElementById("modalChat").checked)
             document.getElementById("chatBtn").classList.add("somethingnew");
@@ -893,7 +912,7 @@ export default {
       }
     },
     clickDraw: function() {
-      if (!this.game.mycolor) return; //I'm just spectator
+      if (!this.game.mycolor || this.game.type == "import") return;
       if (["received", "threerep"].includes(this.drawOffer)) {
         if (!confirm(this.st.tr["Accept draw?"])) return;
         const message =
@@ -945,7 +964,7 @@ export default {
       });
     },
     clickRematch: function() {
-      if (!this.game.mycolor) return; //I'm just spectator
+      if (!this.game.mycolor || this.game.type == "import") return;
       if (this.rematchOffer == "received") {
         // Start a new game!
         let gameInfo = {
@@ -1018,19 +1037,16 @@ export default {
       this.gameOver(score, side + " surrender");
     },
     loadGame: function(game, callback) {
-      this.vr = new V(game.fen);
-      const gtype = this.getGameType(game);
+      const gtype = game.type || this.getGameType(game);
       const tc = extractTime(game.cadence);
       const myIdx = game.players.findIndex(p => {
         return p.sid == this.st.user.sid || p.id == this.st.user.id;
       });
       // "mycolor" is undefined for observers
       const mycolor = [undefined, "w", "b"][myIdx + 1];
-      // Live games before 26/03/2020 don't have chat history:
-      if (!game.chats) game.chats = []; //TODO: remove line
-      // Sort chat messages from newest to oldest
-      game.chats.sort((c1, c2) => c2.added - c1.added);
       if (gtype == "corr") {
+        if (mycolor == 'w') game.chatRead = game.chatReadWhite;
+        else if (mycolor == 'b') game.chatRead = game.chatReadBlack;
         // NOTE: clocks in seconds
         game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of
         game.clocks = [tc.mainTime, tc.mainTime];
@@ -1042,33 +1058,10 @@ export default {
               (Date.now() - game.moves[L-1].played) / 1000;
           }
         }
-        if (myIdx >= 0 && game.score == "*" && game.chats.length > 0) {
-          // Did a chat message arrive after my last move?
-          let dtLastMove = 0;
-          if (L == 1 && myIdx == 0)
-            dtLastMove = game.moves[0].played;
-          else if (L >= 2) {
-            if (L % 2 == 0) {
-              // It's now white turn
-              dtLastMove = game.moves[L-1-(1-myIdx)].played;
-            } else {
-              // Black turn:
-              dtLastMove = game.moves[L-1-myIdx].played;
-            }
-          }
-          if (dtLastMove < game.chats[0].added)
-            document.getElementById("chatBtn").classList.add("somethingnew");
-        }
         // Now that we used idx and played, re-format moves as for live games
         game.moves = game.moves.map(m => m.squares);
       }
-      if (gtype == "live") {
-        if (
-          game.chats.length > 0 &&
-          (!game.initime || game.initime < game.chats[0].added)
-        ) {
-          document.getElementById("chatBtn").classList.add("somethingnew");
-        }
+      else if (gtype == "live") {
         if (game.clocks[0] < 0) {
           // Game is unstarted. clock is ignored until move 2
           game.clocks = [tc.mainTime, tc.mainTime];
@@ -1084,6 +1077,21 @@ export default {
             game.clocks[myIdx] -= (Date.now() - game.initime) / 1000;
         }
       }
+      else
+        // gtype == "import"
+        game.clocks = [tc.mainTime, tc.mainTime];
+      // Live games before 26/03/2020 don't have chat history:
+      if (!game.chats) game.chats = []; //TODO: remove line
+      // Sort chat messages from newest to oldest
+      game.chats.sort((c1, c2) => c2.added - c1.added);
+      if (
+        myIdx >= 0 &&
+        game.chats.length > 0 &&
+        (!game.chatRead || game.chatRead < game.chats[0].added)
+      ) {
+        // A chat message arrived since my last reading:
+        document.getElementById("chatBtn").classList.add("somethingnew");
+      }
       // TODO: merge next 2 "if" conditions
       if (!!game.drawOffer) {
         if (game.drawOffer == "t")
@@ -1117,15 +1125,17 @@ export default {
       }
       this.repeat = {}; //reset: scan past moves' FEN:
       let repIdx = 0;
-      let vr_tmp = new V(game.fenStart);
+      this.vr = new V(game.fenStart);
       let curTurn = "n";
       game.moves.forEach(m => {
-        playMove(m, vr_tmp);
-        const fenIdx = vr_tmp.getFen().replace(/ /g, "_");
+        playMove(m, this.vr);
+        const fenIdx = this.vr.getFenForRepeat();
         this.repeat[fenIdx] = this.repeat[fenIdx]
           ? this.repeat[fenIdx] + 1
           : 1;
       });
+      // Imported games don't have current FEN
+      if (!game.fen) game.fen = this.vr.getFen();
       if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep";
       this.game = Object.assign(
         // NOTE: assign mycolor here, since BaseGame could also be VS computer
@@ -1179,6 +1189,11 @@ export default {
     //  - from server (one correspondance game I play[ed] or not)
     //  - from remote peer (one live game I don't play, finished or not)
     fetchGame: function(callback) {
+      
+console.log("fecth");
+      console.log(this.gameRef);
+      console.log(this.gameRef.match(/^i/));
+
       if (Number.isInteger(this.gameRef) || !isNaN(parseInt(this.gameRef))) {
         // corr games identifiers are integers
         ajax(
@@ -1195,7 +1210,7 @@ export default {
           }
         );
       }
-      else if (!!this.gameRef.match(/^I_/))
+      else if (!!this.gameRef.match(/^i/))
         // Game import (maybe remote)
         ImportgameStorage.get(this.gameRef, callback);
       else
@@ -1238,6 +1253,9 @@ export default {
     },
     // Update variables and storage after a move:
     processMove: function(move, data) {
+      if (this.game.type == "import")
+        // Shouldn't receive any messages in this mode:
+        return;
       if (!data) data = {};
       const moveCol = this.vr.turn;
       const colorIdx = ["w", "b"].indexOf(moveCol);
diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue
index 890d9395..52e6dc3b 100644
--- a/client/src/views/MyGames.vue
+++ b/client/src/views/MyGames.vue
@@ -153,12 +153,19 @@ export default {
               // Now ask completed games (partial list)
               this.loadMore(
                 "live",
-                () => this.loadMore("corr", adjustAndSetDisplay)
+                () => this.loadMore("corr", () => {
+                  this.loadMore("import", adjustAndSetDisplay);
+                })
               );
             }
           }
         );
-      } else this.loadMore("live", adjustAndSetDisplay);
+      }
+      else {
+        this.loadMore("live", () => {
+          this.loadMore("import", adjustAndSetDisplay);
+        });
+      }
     });
   },
   beforeDestroy: function() {
@@ -184,11 +191,17 @@ export default {
       }
     },
     addGameImport(game) {
-      if (!game.id) {
-        alert(this.st.tr[
-          "No identifier found: use the upload button in analysis mode"]);
-      }
-      else this.importGames.push(game);
+      game.type = "import";
+      ImportgameStorage.add(game, (err) => {
+        if (!!err) {
+          if (err.message.indexOf("Key already exists") < 0) {
+            alert(this.st.tr["An error occurred. Try again!"]);
+            return;
+          }
+          else alert(this.st.tr["The game was already imported"]);
+        }
+        this.$router.push("/game/" + game.id);
+      });
     },
     tryShowNewsIndicator: function(type) {
       if (
diff --git a/server/db/create.sql b/server/db/create.sql
index 7c361b47..56ae4f19 100644
--- a/server/db/create.sql
+++ b/server/db/create.sql
@@ -68,6 +68,8 @@ create table Games (
   rematchOffer character default '',
   deletedByWhite boolean,
   deletedByBlack boolean,
+  chatReadWhite datetime,
+  chatReadBlack datetime,
   foreign key (vid) references Variants(id),
   foreign key (white) references Users(id),
   foreign key (black) references Users(id)
diff --git a/server/models/Game.js b/server/models/Game.js
index aec80a01..ee381d42 100644
--- a/server/models/Game.js
+++ b/server/models/Game.js
@@ -18,6 +18,8 @@ const UserModel = require("./User");
  *   randomness: integer
  *   deletedByWhite: boolean
  *   deletedByBlack: boolean
+ *   chatReadWhite: datetime
+ *   chatReadBlack: datetime
  *
  * Structure table Moves:
  *   gid: ref game id
@@ -74,6 +76,7 @@ const GameModel =
         "SELECT " +
           "g.id, g.fen, g.fenStart, g.cadence, g.created, " +
           "g.white, g.black, g.score, g.scoreMsg, " +
+          "g.chatReadWhite, g.chatReadBlack, " +
           "g.drawOffer, g.rematchOffer, v.name AS vname " +
         "FROM Games g " +
         "JOIN Variants v " +
@@ -309,6 +312,8 @@ const GameModel =
         !obj.fen || !!(obj.fen.match(/^[a-zA-Z0-9, /-]*$/))
       ) && (
         !obj.score || !!(obj.score.match(/^[012?*\/-]+$/))
+      ) && (
+        !obj.chatRead || !(['w','b'].includes(obj.chatRead))
       ) && (
         !obj.scoreMsg || !!(obj.scoreMsg.match(/^[a-zA-Z ]+$/))
       ) && (
@@ -343,6 +348,10 @@ const GameModel =
         const myColor = obj.deletedBy == 'w' ? "White" : "Black";
         modifs += "deletedBy" + myColor + " = true,";
       }
+      if (!!obj.chatRead) {
+        const myColor = obj.chatRead == 'w' ? "White" : "Black";
+        modifs += "chatRead" + myColor + " = " + Date.now() + ",";
+      }
       if (!!obj.score) {
         modifs += "score = '" + obj.score + "'," +
                   "scoreMsg = '" + obj.scoreMsg + "',";
-- 
2.44.0