Experimental game upload added
authorBenjamin Auder <benjamin.auder@somewhere>
Mon, 30 Mar 2020 19:05:46 +0000 (21:05 +0200)
committerBenjamin Auder <benjamin.auder@somewhere>
Mon, 30 Mar 2020 19:05:46 +0000 (21:05 +0200)
15 files changed:
client/src/components/BaseGame.vue
client/src/components/Board.vue
client/src/components/MoveList.vue
client/src/components/UploadGame.vue
client/src/main.js
client/src/translations/en.js
client/src/translations/es.js
client/src/translations/fr.js
client/src/utils/gameStorage.js
client/src/utils/importgameStorage.js
client/src/utils/modalClick.js
client/src/views/Game.vue
client/src/views/MyGames.vue
server/db/create.sql
server/models/Game.js

index c3387e8..fd26d87 100644 (file)
@@ -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 != "*"
     // 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) => {
         // 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);
           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({
       if (firstMoveColor == "b") {
         // 'start' & 'end' is required for Board component
         this.moves.unshift({
+          index: parsedFen.movesCount,
           notation: "...",
           unambiguous: "...",
           start: { x: -1, y: -1 },
           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';
       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';
       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 += '\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) {
         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) {
         if (i+1 < this.moves.length) {
-          pgn += moveNumber + "." + (i+1) + " " +
+          pgn += moveNumber + ".b " +
             getFullNotation(this.moves[i+1], "unambiguous") + "\n";
         }
       }
             getFullNotation(this.moves[i+1], "unambiguous") + "\n";
         }
       }
@@ -323,8 +331,14 @@ export default {
         this.autoplayLoop = null;
       } else {
         this.autoplay = true;
         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
       }
     },
     // Animate an elementary move
index 639938b..56f3488 100644 (file)
@@ -519,12 +519,11 @@ export default {
       };
     },
     mousedown: function(e) {
       };
     },
     mousedown: function(e) {
-      if (!([1, 3].includes(e.which))) return;
       e.preventDefault();
       e.preventDefault();
-      if (e.which != 3)
+      if (!this.mobileBrowser && e.which != 3)
         // Cancel current drawing and circles, if any
         this.cancelResetArrows();
         // 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
         // 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 {
           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;
         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) {
       }
     },
     mouseup: function(e) {
-      if (!([1, 3].includes(e.which))) return;
       e.preventDefault();
       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);
         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);
       }
         this.movingArrow = { x: -1, y: -1 };
         this.processArrowAttempt(e);
       }
index d2ae3f3..4c351dd 100644 (file)
@@ -101,10 +101,8 @@ export default {
     window.addEventListener("resize", () => {
       if (!timeoutLaunched) {
         timeoutLaunched = true;
     window.addEventListener("resize", () => {
       if (!timeoutLaunched) {
         timeoutLaunched = true;
-        setTimeout(() => {
-          this.adjustBoard();
-          timeoutLaunched = false;
-        }, 500);
+        this.adjustBoard();
+        setTimeout(() => { timeoutLaunched = false; }, 500);
       }
     });
   },
       }
     });
   },
index 369333c..a94c39b 100644 (file)
@@ -9,6 +9,7 @@ div
 </template>
 
 <script>
 </template>
 
 <script>
+import { getRandString } from "@/utils/alea";
 export default {
   name: "my-upload-game",
   methods: {
 export default {
   name: "my-upload-game",
   methods: {
@@ -23,11 +24,90 @@ export default {
                        };
                        reader.readAsText(file);
                },
                        };
                        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);
+      });
     }
   }
 };
     }
   }
 };
index f2e38f1..3577b15 100644 (file)
@@ -15,14 +15,15 @@ new Vue({
     window.doClick = elemId => {
       document.getElementById(elemId).click();
     };
     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 &&
     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;
           }
           ) {
             m.checked = false;
           }
index e1d6189..e8c1e5c 100644 (file)
@@ -4,6 +4,7 @@ export const translations = {
   About: "About",
   "Accept draw?": "Accept draw?",
   "Accept challenge?": "Accept challenge?",
   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",
   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!",
   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",
   "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",
   "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",
   "Three repetitions": "Three repetitions",
   Time: "Time",
   "Undetermined result": "Undetermined result",
index d14f460..85b458d 100644 (file)
@@ -4,6 +4,7 @@ export const translations = {
   About: "Acerca de",
   "Accept draw?": "¿Acceptar tablas?",
   "Accept challenge?": "¿Acceptar el desafío?",
   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",
   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!",
   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",
   "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",
   "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",
   "Three repetitions": "Tres repeticiones",
   Time: "Tiempo",
   "Undetermined result": "Resultado indeterminado",
index 2d7b535..493b5b5 100644 (file)
@@ -4,6 +4,7 @@ export const translations = {
   About: "À propos",
   "Accept draw?": "Accepter la nulle ?",
   "Accept challenge?": "Accepter le défi ?",
   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",
   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 !",
   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",
   "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",
   "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é",
   "Three repetitions": "Triple répétition",
   Time: "Temps",
   "Undetermined result": "Résultat indéterminé",
index 109c825..f69de2e 100644 (file)
@@ -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]);
           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];
           });
             else if (k == "delchat") game.chats = [];
             else game[k] = obj[k];
           });
index e5ad6e2..46a60bb 100644 (file)
@@ -47,7 +47,7 @@ export const ImportgameStorage = {
       };
       transaction.onerror = function(err) {
         // Duplicate key error (most likely)
       };
       transaction.onerror = function(err) {
         // Duplicate key error (most likely)
-        callback(err);
+        callback(err.target.error);
       };
       transaction.objectStore("importgames").add(game);
     });
       };
       transaction.objectStore("importgames").add(game);
     });
index 9693578..f4687fd 100644 (file)
@@ -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;
   // 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();
 }
 }
index 2a565f6..5145e58 100644 (file)
@@ -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 {{ 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"
       span.variant-name {{ game.vname }}
       span#nextGame(
         v-if="nextIds.length > 0"
@@ -236,10 +236,14 @@ export default {
     this.atCreation();
   },
   mounted: function() {
     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 => {
     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;
     },
       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();
         // 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?)
     },
     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() {
       }
     },
     clearChat: function() {
@@ -475,6 +490,7 @@ export default {
       }
     },
     getGameType: function(game) {
       }
     },
     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)
       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":
           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 (
           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,
             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,
               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";
         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) }
             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" : "";
         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) }
             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();
           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");
           }
           if (!document.getElementById("modalChat").checked)
             document.getElementById("chatBtn").classList.add("somethingnew");
@@ -893,7 +912,7 @@ export default {
       }
     },
     clickDraw: function() {
       }
     },
     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 =
       if (["received", "threerep"].includes(this.drawOffer)) {
         if (!confirm(this.st.tr["Accept draw?"])) return;
         const message =
@@ -945,7 +964,7 @@ export default {
       });
     },
     clickRematch: function() {
       });
     },
     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 = {
       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.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];
       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 (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];
         // 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;
           }
         }
               (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);
       }
         // 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];
         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;
         }
       }
             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")
       // 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;
       }
       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 => {
       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;
       });
         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
       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) {
     //  - 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(
       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
         // 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) {
     },
     // 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);
       if (!data) data = {};
       const moveCol = this.vr.turn;
       const colorIdx = ["w", "b"].indexOf(moveCol);
index 890d939..52e6dc3 100644 (file)
@@ -153,12 +153,19 @@ export default {
               // Now ask completed games (partial list)
               this.loadMore(
                 "live",
               // 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() {
     });
   },
   beforeDestroy: function() {
@@ -184,11 +191,17 @@ export default {
       }
     },
     addGameImport(game) {
       }
     },
     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 (
     },
     tryShowNewsIndicator: function(type) {
       if (
index 7c361b4..56ae4f1 100644 (file)
@@ -68,6 +68,8 @@ create table Games (
   rematchOffer character default '',
   deletedByWhite boolean,
   deletedByBlack boolean,
   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)
   foreign key (vid) references Variants(id),
   foreign key (white) references Users(id),
   foreign key (black) references Users(id)
index aec80a0..ee381d4 100644 (file)
@@ -18,6 +18,8 @@ const UserModel = require("./User");
  *   randomness: integer
  *   deletedByWhite: boolean
  *   deletedByBlack: boolean
  *   randomness: integer
  *   deletedByWhite: boolean
  *   deletedByBlack: boolean
+ *   chatReadWhite: datetime
+ *   chatReadBlack: datetime
  *
  * Structure table Moves:
  *   gid: ref game id
  *
  * 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, " +
         "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 " +
           "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.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 ]+$/))
       ) && (
       ) && (
         !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,";
       }
         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 + "',";
       if (!!obj.score) {
         modifs += "score = '" + obj.score + "'," +
                   "scoreMsg = '" + obj.scoreMsg + "',";