Experimental game upload added
[vchess.git] / client / src / views / Game.vue
index 1a86d9b..5145e58 100644 (file)
@@ -26,13 +26,10 @@ main
         span {{ st.tr["Participant(s):"] }} 
         span(
           v-for="p in Object.values(people)"
-          v-if="p.focus && !!p.name"
+          v-if="participateInChat(p)"
         )
           | {{ p.name }} 
-        span.anonymous(
-          v-if="Object.values(people).some(p => p.focus && !p.name)"
-        )
-          | + @nonymous
+        span.anonymous(v-if="someAnonymousPresent()") + @nonymous
       Chat(
         ref="chatcomp"
         :players="game.players"
@@ -60,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"
@@ -99,7 +96,7 @@ main
       )
         img(src="/images/icons/rematch.svg")
       #playersInfo
-        p
+        p(v-if="isLargeScreen()")
           span.name(:class="{connected: isConnected(0)}")
             | {{ game.players[0].name || "@nonymous" }}
           span.time(
@@ -121,6 +118,29 @@ main
             span.time-separator(v-if="!!virtualClocks[1][1]") :
             span.time-right(v-if="!!virtualClocks[1][1]")
               | {{ virtualClocks[1][1] }}
+        p(v-else)
+          span.name(:class="{connected: isConnected(0)}")
+            | {{ game.players[0].name || "@nonymous" }}
+          span.split-names -
+          span.name(:class="{connected: isConnected(1)}")
+            | {{ game.players[1].name || "@nonymous" }}
+          br
+          span.time(
+            v-if="game.score=='*'"
+            :class="{yourturn: !!vr && vr.turn == 'w'}"
+          )
+            span.time-left {{ virtualClocks[0][0] }}
+            span.time-separator(v-if="!!virtualClocks[0][1]") :
+            span.time-right(v-if="!!virtualClocks[0][1]")
+              | {{ virtualClocks[0][1] }}
+          span.time(
+            v-if="game.score=='*'"
+            :class="{yourturn: !!vr && vr.turn == 'b'}"
+          )
+            span.time-left {{ virtualClocks[1][0] }}
+            span.time-separator(v-if="!!virtualClocks[1][1]") :
+            span.time-right(v-if="!!virtualClocks[1][1]")
+              | {{ virtualClocks[1][1] }}
   BaseGame(
     ref="basegame"
     :game="game"
@@ -133,7 +153,9 @@ import BaseGame from "@/components/BaseGame.vue";
 import Chat from "@/components/Chat.vue";
 import { store } from "@/store";
 import { GameStorage } from "@/utils/gameStorage";
+import { ImportgameStorage } from "@/utils/importgameStorage";
 import { ppt } from "@/utils/datetime";
+import { notify } from "@/utils/notifications";
 import { ajax } from "@/utils/ajax";
 import { extractTime } from "@/utils/timeControl";
 import { getRandString } from "@/utils/alea";
@@ -157,6 +179,7 @@ export default {
       gameRef: "",
       nextIds: [],
       game: {}, //passed to BaseGame
+      focus: !document.hidden, //will not always work... TODO
       // virtualClocks will be initialized from true game.clocks
       virtualClocks: [],
       vr: null, //"variant rules" object initialized from FEN
@@ -179,6 +202,7 @@ export default {
       // If newmove got no pingback, send again:
       opponentGotMove: false,
       connexionString: "",
+      socketCloseListener: 0,
       // Incomplete info games: show move played
       moveNotation: "",
       // Intervals from setInterval():
@@ -186,13 +210,15 @@ export default {
       retrySendmove: null,
       clockUpdate: null,
       // Related to (killing of) self multi-connects:
-      newConnect: {},
-      killed: {}
+      newConnect: {}
     };
   },
   watch: {
     $route: function(to, from) {
-      if (from.params["id"] != to.params["id"]) {
+      if (to.path.length < 6 || to.path.substr(0, 6) != "/game/")
+        // Page change
+        this.cleanBeforeDestroy();
+      else if (from.params["id"] != to.params["id"]) {
         // Change everything:
         this.cleanBeforeDestroy();
         let boardDiv = document.querySelector(".game");
@@ -200,11 +226,9 @@ export default {
           // In case of incomplete information variant:
           boardDiv.style.visibility = "hidden";
         this.atCreation();
-      } else {
+      } else
         // Same game ID
         this.nextIds = JSON.parse(this.$route.query["next"] || "[]");
-        this.loadGame(this.game);
-      }
     }
   },
   // NOTE: some redundant code with Hall.vue (mostly related to people array)
@@ -212,11 +236,14 @@ export default {
     this.atCreation();
   },
   mounted: function() {
-    document.addEventListener('visibilitychange', this.visibilityChange);
-    ["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 => {
@@ -225,32 +252,76 @@ export default {
     }
   },
   beforeDestroy: function() {
-    document.removeEventListener('visibilitychange', this.visibilityChange);
     this.cleanBeforeDestroy();
   },
   methods: {
+    cleanBeforeDestroy: function() {
+      clearInterval(this.socketCloseListener);
+      document.removeEventListener('visibilitychange', this.visibilityChange);
+      window.removeEventListener('focus', this.onFocus);
+      window.removeEventListener('blur', this.onBlur);
+      if (!!this.askLastate) clearInterval(this.askLastate);
+      if (!!this.retrySendmove) clearInterval(this.retrySendmove);
+      if (!!this.clockUpdate) clearInterval(this.clockUpdate);
+      this.conn.removeEventListener("message", this.socketMessageListener);
+      this.send("disconnect");
+      this.conn = null;
+    },
     visibilityChange: function() {
       // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27
-      this.send(
-        document.visibilityState == "visible"
-          ? "getfocus"
-          : "losefocus"
+      this.focus = (document.visibilityState == "visible");
+      if (!this.focus && !!this.rematchOffer) {
+        this.rematchOffer = "";
+        this.send("rematchoffer", { data: false });
+        // Do not remove rematch offer from (local) storage
+      }
+      this.send(this.focus ? "getfocus" : "losefocus");
+    },
+    onFocus: function() {
+      this.focus = true;
+      this.send("getfocus");
+    },
+    onBlur: function() {
+      this.focus = false;
+      if (!!this.rematchOffer) {
+        this.rematchOffer = "";
+        this.send("rematchoffer", { data: false });
+      }
+      this.send("losefocus");
+    },
+    isLargeScreen: function() {
+      return window.innerWidth >= 500;
+    },
+    participateInChat: function(p) {
+      return Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus) && !!p.name;
+    },
+    someAnonymousPresent: function() {
+      return (
+        Object.values(this.people).some(p =>
+          !p.name && Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus)
+        )
       );
     },
     atCreation: function() {
+      document.addEventListener('visibilitychange', this.visibilityChange);
+      window.addEventListener('focus', this.onFocus);
+      window.addEventListener('blur', this.onBlur);
       // 0] (Re)Set variables
       this.gameRef = this.$route.params["id"];
       // next = next corr games IDs to navigate faster (if applicable)
       this.nextIds = JSON.parse(this.$route.query["next"] || "[]");
       // Always add myself to players' list
       const my = this.st.user;
+      const tmpId = getRandString();
       this.$set(
         this.people,
         my.sid,
         {
           id: my.id,
           name: my.name,
-          focus: true
+          tmpIds: {
+            tmpId: { focus: true }
+          }
         }
       );
       this.game = {
@@ -276,25 +347,31 @@ export default {
       this.retrySendmove = null;
       this.clockUpdate = null;
       this.newConnect = {};
-      this.killed = {};
       // 1] Initialize connection
       this.connexionString =
         params.socketUrl +
-        "/?sid=" +
-        this.st.user.sid +
-        "&id=" +
-        this.st.user.id +
-        "&tmpId=" +
-        getRandString() +
+        "/?sid=" + this.st.user.sid +
+        "&id=" + this.st.user.id +
+        "&tmpId=" + tmpId +
         "&page=" +
         // Discard potential "/?next=[...]" for page indication:
         encodeURIComponent(this.$route.path.match(/\/game\/[a-zA-Z0-9]+/)[0]);
       this.conn = new WebSocket(this.connexionString);
-      this.conn.onmessage = this.socketMessageListener;
-      this.conn.onclose = this.socketCloseListener;
+      this.conn.addEventListener("message", this.socketMessageListener);
+      this.socketCloseListener = setInterval(
+        () => {
+          if (this.conn.readyState == 3) {
+            this.conn.removeEventListener(
+              "message", this.socketMessageListener);
+            this.conn = new WebSocket(this.connexionString);
+            this.conn.addEventListener("message", this.socketMessageListener);
+          }
+        },
+        1000
+      );
       // Socket init required before loading remote game:
       const socketInit = callback => {
-        if (!!this.conn && this.conn.readyState == 1)
+        if (this.conn.readyState == 1)
           // 1 == OPEN state
           callback();
         else
@@ -307,20 +384,11 @@ export default {
           this.loadVariantThenGame(game, () => socketInit(this.roomInit));
         else
           // Live game stored remotely: need socket to retrieve it
-          // NOTE: the callback "roomInit" will be lost, so we don't provide it.
+          // NOTE: the callback "roomInit" will be lost, so it's not provided.
           // --> It will be given when receiving "fullgame" socket event.
           socketInit(() => { this.send("askfullgame"); });
       });
     },
-    cleanBeforeDestroy: function() {
-      if (!!this.askLastate)
-        clearInterval(this.askLastate);
-      if (!!this.retrySendmove)
-        clearInterval(this.retrySendmove);
-      if (!!this.clockUpdate)
-        clearInterval(this.clockUpdate);
-      this.send("disconnect");
-    },
     roomInit: function() {
       if (!this.roomInitialized) {
         // Notify the room only now that I connected, because
@@ -333,7 +401,7 @@ export default {
       }
     },
     send: function(code, obj) {
-      if (!!this.conn)
+      if (!!this.conn && this.conn.readyState == 1)
         this.conn.send(JSON.stringify(Object.assign({ code: code }, obj)));
     },
     isConnected: function(index) {
@@ -347,14 +415,22 @@ export default {
       return (
         (
           !!player.sid &&
-          Object.keys(this.people).some(sid =>
-            sid == player.sid && this.people[sid].focus)
+          Object.keys(this.people).some(sid => {
+            return (
+              sid == player.sid &&
+              Object.values(this.people[sid].tmpIds).some(v => v.focus)
+            );
+          })
         )
         ||
         (
           !!player.id &&
-          Object.values(this.people).some(p =>
-            p.id == player.id && p.focus)
+          Object.values(this.people).some(p => {
+            return (
+              p.id == player.id &&
+              Object.values(p.tmpIds).some(v => v.focus)
+            );
+          })
         )
       );
     },
@@ -369,34 +445,52 @@ 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 });
+      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() {
-      // Nothing more to do if game is live (chats not recorded)
-      if (this.game.type == "corr") {
-        if (!!this.game.mycolor) {
+      if (!!this.game.mycolor) {
+        if (this.game.type == "corr") {
           ajax(
             "/chats",
             "DELETE",
             { data: { gid: this.game.id } }
           );
+        } else {
+          // Live game
+          GameStorage.update(this.gameRef, { delchat: true });
         }
         this.$set(this.game, "chats", []);
       }
     },
     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)
@@ -445,46 +539,61 @@ export default {
       const data = JSON.parse(msg.data);
       switch (data.code) {
         case "pollclients":
-          // TODO: shuffling and random filtering on server, if
-          // the room is really crowded.
-          data.sockIds.forEach(sid => {
+          // TODO: shuffling and random filtering on server,
+          // if the room is really crowded.
+          Object.keys(data.sockIds).forEach(sid => {
             if (sid != this.st.user.sid) {
-              this.people[sid] = { focus: true };
               this.send("askidentity", { target: sid });
+              this.people[sid] = { tmpIds: data.sockIds[sid] };
+            } else {
+              // Complete my tmpIds:
+              Object.assign(this.people[sid].tmpIds, data.sockIds[sid]);
             }
           });
           break;
         case "connect":
-          if (!this.people[data.from]) {
-            this.people[data.from] = { focus: true };
-            this.newConnect[data.from] = true; //for self multi-connects tests
-            this.send("askidentity", { target: data.from });
+          if (!this.people[data.from[0]]) {
+            // focus depends on the tmpId (e.g. tab)
+            this.$set(
+              this.people,
+              data.from[0],
+              {
+                tmpIds: {
+                  [data.from[1]]: { focus: true }
+                }
+              }
+            );
+            // For self multi-connects tests:
+            this.newConnect[data.from[0]] = true;
+            this.send("askidentity", { target: data.from[0] });
+          } else {
+            this.people[data.from[0]].tmpIds[data.from[1]] = { focus: true };
+            this.$forceUpdate(); //TODO: shouldn't be required
           }
           break;
         case "disconnect":
-          this.$delete(this.people, data.from);
+          if (!this.people[data.from[0]]) return;
+          delete this.people[data.from[0]].tmpIds[data.from[1]];
+          if (Object.keys(this.people[data.from[0]].tmpIds).length == 0)
+            this.$delete(this.people, data.from[0]);
+          else this.$forceUpdate(); //TODO: shouldn't be required
           break;
         case "getfocus": {
-          let player = this.people[data.from];
+          let player = this.people[data.from[0]];
           if (!!player) {
-            player.focus = true;
+            player.tmpIds[data.from[1]].focus = true;
             this.$forceUpdate(); //TODO: shouldn't be required
           }
           break;
         }
         case "losefocus": {
-          let player = this.people[data.from];
+          let player = this.people[data.from[0]];
           if (!!player) {
-            player.focus = false;
+            player.tmpIds[data.from[1]].focus = false;
             this.$forceUpdate(); //TODO: shouldn't be required
           }
           break;
         }
-        case "killed":
-          // I logged in elsewhere:
-          this.conn = null;
-          alert(this.st.tr["New connexion detected: tab now offline"]);
-          break;
         case "askidentity": {
           // Request for identification
           const me = {
@@ -499,62 +608,63 @@ export default {
         case "identity": {
           const user = data.data;
           let player = this.people[user.sid];
-          // player.focus is already set
+          // player.tmpIds is already set
           player.name = user.name;
           player.id = user.id;
           this.$forceUpdate(); //TODO: shouldn't be required
           // If I multi-connect, kill current connexion if no mark (I'm older)
           if (this.newConnect[user.sid]) {
+            delete this.newConnect[user.sid];
             if (
               user.id > 0 &&
               user.id == this.st.user.id &&
-              user.sid != this.st.user.sid &&
-              !this.killed[this.st.user.sid]
+              user.sid != this.st.user.sid
             ) {
-                this.send("killme", { sid: this.st.user.sid });
-                this.killed[this.st.user.sid] = true;
+              this.cleanBeforeDestroy();
+              alert(this.st.tr["New connexion detected: tab now offline"]);
+              break;
             }
-            delete this.newConnect[user.sid];
           }
-          if (!this.killed[this.st.user.sid]) {
-            // Ask potentially missed last state, if opponent and I play
-            if (
-              !!this.game.mycolor &&
-              this.game.type == "live" &&
-              this.game.score == "*" &&
-              this.game.players.some(p => p.sid == user.sid)
-            ) {
-              this.send("asklastate", { target: user.sid });
-              let counter = 1;
-              this.askLastate = setInterval(
-                () => {
-                  // Ask at most 3 times:
-                  // if no reply after that there should be a network issue.
-                  if (
-                    counter < 3 &&
-                    !this.gotLastate &&
-                    !!this.people[user.sid]
-                  ) {
-                    this.send("asklastate", { target: user.sid });
-                    counter++;
-                  } else {
-                    clearInterval(this.askLastate);
-                  }
-                },
-                1500
-              );
-            }
+          // Ask potentially missed last state, if opponent and I play
+          if (
+            !this.gotLastate &&
+            !!this.game.mycolor &&
+            this.game.type == "live" &&
+            this.game.score == "*" &&
+            this.game.players.some(p => p.sid == user.sid)
+          ) {
+            this.send("asklastate", { target: user.sid });
+            let counter = 1;
+            this.askLastate = setInterval(
+              () => {
+                // Ask at most 3 times:
+                // if no reply after that there should be a network issue.
+                if (
+                  counter < 3 &&
+                  !this.gotLastate &&
+                  !!this.people[user.sid]
+                ) {
+                  this.send("asklastate", { target: user.sid });
+                  counter++;
+                } else {
+                  clearInterval(this.askLastate);
+                }
+              },
+              1500
+            );
           }
           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,
@@ -569,7 +679,7 @@ export default {
             .filter(k =>
               [
                 "id","fen","players","vid","cadence","fenStart","vname",
-                "moves","clocks","initime","score","drawOffer","rematchOffer"
+                "moves","clocks","score","drawOffer","rematchOffer"
               ].includes(k))
             .reduce(
               (obj, k) => {
@@ -581,25 +691,29 @@ export default {
           this.send("fullgame", { data: gameToSend, target: data.from });
           break;
         case "fullgame":
-          // Callback "roomInit" to poll clients only after game is loaded
-          this.loadVariantThenGame(data.data, this.roomInit);
+          if (!!data.data.empty) {
+            alert(this.st.tr["The game should be in another tab"]);
+            this.$router.go(-1);
+          }
+          else
+            // Callback "roomInit" to poll clients only after game is loaded
+            this.loadVariantThenGame(data.data, this.roomInit);
           break;
         case "asklastate":
           // Sending informative last state if I played a move or score != "*"
           // If the game or moves aren't loaded yet, delay the sending:
+          // TODO: socket init after game load, so the game is supposedly ready
           if (!this.game || !this.game.moves) this.lastateAsked = true;
           else this.sendLastate(data.from);
           break;
         case "lastate": {
           // Got opponent infos about last move
           this.gotLastate = true;
-          if (!data.data.nothing) {
-            this.lastate = data.data;
-            if (this.game.rendered)
-              // Game is rendered (Board component)
-              this.processLastate();
-            // Else: will be processed when game is ready
-          }
+          this.lastate = data.data;
+          if (this.game.rendered)
+            // Game is rendered (Board component)
+            this.processLastate();
+          // Else: will be processed when game is ready
           break;
         }
         case "newmove": {
@@ -622,9 +736,25 @@ export default {
             } else {
               this.gotMoveIdx = movePlus.index;
               const receiveMyMove = (movePlus.color == this.game.mycolor);
-              if (!receiveMyMove && !!this.game.mycolor)
+              const moveColIdx = ["w", "b"].indexOf(movePlus.color);
+              if (!receiveMyMove && !!this.game.mycolor) {
                 // Notify opponent that I got the move:
-                this.send("gotmove", {data: movePlus.index, target: data.from});
+                this.send(
+                  "gotmove",
+                  { data: movePlus.index, target: data.from }
+                );
+                // And myself if I'm elsewhere:
+                if (!this.focus) {
+                  notify(
+                    "New move",
+                    {
+                      body:
+                        (this.game.players[moveColIdx].name || "@nonymous") +
+                        " just played."
+                    }
+                  );
+                }
+              }
               if (movePlus.cancelDrawOffer) {
                 // Opponent refuses draw
                 this.drawOffer = "";
@@ -637,13 +767,12 @@ export default {
                   GameStorage.update(this.gameRef, { drawOffer: "" });
                 }
               }
-              this.$refs["basegame"].play(movePlus.move, "received", null, true);
+              this.$refs["basegame"].play(
+                movePlus.move, "received", null, true);
+              this.game.clocks[moveColIdx] = movePlus.clock;
               this.processMove(
                 movePlus.move,
-                {
-                  clock: movePlus.clock,
-                  receiveMyMove: receiveMyMove
-                }
+                { receiveMyMove: receiveMyMove }
               );
             }
           }
@@ -651,9 +780,11 @@ export default {
         }
         case "gotmove": {
           this.opponentGotMove = true;
-          // Now his clock starts running:
+          // Now his clock starts running on my side:
           const oppIdx = ['w','b'].indexOf(this.vr.turn);
-          this.game.initime[oppIdx] = Date.now();
+          // NOTE: next line to avoid multi-resetClocks when several tabs
+          // on same game, resulting in a faster countdown.
+          if (!!this.clockUpdate) clearInterval(this.clockUpdate);
           this.re_setClocks();
           break;
         }
@@ -671,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) }
@@ -681,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) }
@@ -708,18 +839,20 @@ export default {
           }
           break;
         }
-        case "newchat":
-          this.$refs["chatcomp"].newChat(data.data);
+        case "newchat": {
+          let chat = data.data;
+          this.$refs["chatcomp"].newChat(chat);
+          if (this.game.type == "live") {
+            chat.added = Date.now();
+            if (!!this.game.mycolor)
+              GameStorage.update(this.gameRef, { chat: chat });
+          }
           if (!document.getElementById("modalChat").checked)
             document.getElementById("chatBtn").classList.add("somethingnew");
           break;
+        }
       }
     },
-    socketCloseListener: function() {
-      this.conn = new WebSocket(this.connexionString);
-      this.conn.addEventListener("message", this.socketMessageListener);
-      this.conn.addEventListener("close", this.socketCloseListener);
-    },
     updateCorrGame: function(obj, callback) {
       ajax(
         "/games",
@@ -736,52 +869,50 @@ export default {
       );
     },
     sendLastate: function(target) {
-      if (
-        (this.game.moves.length > 0 && this.vr.turn != this.game.mycolor) ||
-        this.game.score != "*" ||
-        this.drawOffer == "sent" ||
-        this.rematchOffer == "sent"
-      ) {
-        // Send our "last state" informations to opponent
-        const L = this.game.moves.length;
-        const myIdx = ["w", "b"].indexOf(this.game.mycolor);
-        const myLastate = {
-          lastMove: L > 0 ? this.game.moves[L - 1] : undefined,
-          clock: this.game.clocks[myIdx],
-          // Since we played a move (or abort or resign),
-          // only drawOffer=="sent" is possible
-          drawSent: this.drawOffer == "sent",
-          rematchSent: this.rematchOffer == "sent",
-          score: this.game.score,
-          scoreMsg: this.game.scoreMsg,
-          movesCount: L,
-          initime: this.game.initime[1 - myIdx] //relevant only if I played
-        };
-        this.send("lastate", { data: myLastate, target: target });
-      } else {
-        this.send("lastate", { data: {nothing: true}, target: target });
-      }
+      // Send our "last state" informations to opponent
+      const L = this.game.moves.length;
+      const myIdx = ["w", "b"].indexOf(this.game.mycolor);
+      const myLastate = {
+        lastMove:
+          (L > 0 && this.vr.turn != this.game.mycolor)
+            ? this.game.moves[L - 1]
+            : undefined,
+        clock: this.game.clocks[myIdx],
+        // Since we played a move (or abort or resign),
+        // only drawOffer=="sent" is possible
+        drawSent: this.drawOffer == "sent",
+        rematchSent: this.rematchOffer == "sent",
+        score: this.game.score != "*" ? this.game.score : undefined,
+        scoreMsg: this.game.score != "*" ? this.game.scoreMsg : undefined,
+        movesCount: L
+      };
+      this.send("lastate", { data: myLastate, target: target });
     },
     // lastate was received, but maybe game wasn't ready yet:
     processLastate: function() {
       const data = this.lastate;
       this.lastate = undefined; //security...
       const L = this.game.moves.length;
+      const oppIdx = 1 - ["w", "b"].indexOf(this.game.mycolor);
+      this.game.clocks[oppIdx] = data.clock;
       if (data.movesCount > L) {
         // Just got last move from him
         this.$refs["basegame"].play(data.lastMove, "received", null, true);
-        this.processMove(data.lastMove, { clock: data.clock });
+        this.processMove(data.lastMove);
+      } else {
+        if (!!this.clockUpdate) clearInterval(this.clockUpdate);
+        this.re_setClocks();
       }
       if (data.drawSent) this.drawOffer = "received";
       if (data.rematchSent) this.rematchOffer = "received";
-      if (data.score != "*") {
+      if (!!data.score) {
         this.drawOffer = "";
         if (this.game.score == "*")
           this.gameOver(data.score, data.scoreMsg);
       }
     },
     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 =
@@ -819,7 +950,6 @@ export default {
           // Game state (including FEN): will be updated
           moves: [],
           clocks: [-1, -1], //-1 = unstarted
-          initime: [0, 0], //initialized later
           score: "*"
         }
       );
@@ -834,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 = {
@@ -849,7 +979,7 @@ export default {
           this.send("rnewgame", { data: gameInfo, oppsid: oppsid });
           // To main Hall if corr game:
           if (this.game.type == "corr")
-            this.send("newgame", { data: gameInfo });
+            this.send("newgame", { data: gameInfo, page: "/" });
           // Also to MyGames page:
           this.notifyMyGames("newgame", gameInfo);
         };
@@ -893,7 +1023,8 @@ export default {
       }
     },
     abortGame: function() {
-      if (!this.game.mycolor || !confirm(this.st.tr["Terminate game?"])) return;
+      if (!this.game.mycolor || !confirm(this.st.tr["Terminate game?"]))
+        return;
       this.gameOver("?", "Stop");
       this.send("abort");
     },
@@ -906,62 +1037,61 @@ 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;
       });
-      const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers
-      if (!game.chats) game.chats = []; //live games don't have chat history
+      // "mycolor" is undefined for observers
+      const mycolor = [undefined, "w", "b"][myIdx + 1];
       if (gtype == "corr") {
-        // NOTE: clocks in seconds, initime in milliseconds
+        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];
         const L = game.moves.length;
         if (game.score == "*") {
-          // Set clocks + initime
-          game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
-          if (L >= 1) game.initime[L % 2] = game.moves[L-1].played;
-          // NOTE: game.clocks shouldn't be computed right now:
-          // job will be done in re_setClocks() called soon below.
-        }
-        // Sort chat messages from newest to oldest
-        game.chats.sort((c1, c2) => {
-          return c2.added - c1.added;
-        });
-        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;
-            }
+          // Adjust clocks
+          if (L >= 2) {
+            game.clocks[L % 2] -=
+              (Date.now() - game.moves[L-1].played) / 1000;
           }
-          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" && game.clocks[0] < 0) {
-        // Game is unstarted. clocks and initime are ignored until move 2
-        game.clocks = [tc.mainTime, tc.mainTime];
-        game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
-        if (myIdx >= 0) {
-          // I play in this live game
-          GameStorage.update(game.id, {
-            clocks: game.clocks,
-            initime: game.initime
-          });
+      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 (myIdx >= 0) {
+            // I play in this live game
+            GameStorage.update(game.id, {
+              clocks: game.clocks
+            });
+          }
+        } else {
+          if (!!game.initime)
+            // It's my turn: clocks not updated yet
+            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")
@@ -995,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
@@ -1057,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(
@@ -1072,73 +1209,82 @@ export default {
             }
           }
         );
-      } else
-        // Local game (or live remote)
+      }
+      else if (!!this.gameRef.match(/^i/))
+        // Game import (maybe remote)
+        ImportgameStorage.get(this.gameRef, callback);
+      else
+        // Local live game (or remote)
         GameStorage.get(this.gameRef, callback);
     },
     re_setClocks: function() {
+      this.virtualClocks = this.game.clocks.map(s => ppt(s).split(':'));
       if (this.game.moves.length < 2 || this.game.score != "*") {
         // 1st move not completed yet, or game over: freeze time
-        this.virtualClocks = this.game.clocks.map(s => ppt(s).split(':'));
         return;
       }
       const currentTurn = this.vr.turn;
       const currentMovesCount = this.game.moves.length;
       const colorIdx = ["w", "b"].indexOf(currentTurn);
-      let countdown =
-        this.game.clocks[colorIdx] -
-        (Date.now() - this.game.initime[colorIdx]) / 1000;
-      this.virtualClocks = [0, 1].map(i => {
-        const removeTime =
-          i == colorIdx ? (Date.now() - this.game.initime[colorIdx]) / 1000 : 0;
-        return ppt(this.game.clocks[i] - removeTime).split(':');
-      });
       this.clockUpdate = setInterval(
         () => {
           if (
-            countdown < 0 ||
+            this.game.clocks[colorIdx] < 0 ||
             this.game.moves.length > currentMovesCount ||
             this.game.score != "*"
           ) {
             clearInterval(this.clockUpdate);
-            if (countdown < 0)
+            this.clockUpdate = null;
+            if (this.game.clocks[colorIdx] < 0)
               this.gameOver(
                 currentTurn == "w" ? "0-1" : "1-0",
                 "Time"
               );
-          } else
+          } else {
             this.$set(
               this.virtualClocks,
               colorIdx,
-              ppt(Math.max(0, --countdown)).split(':')
+              ppt(Math.max(0, --this.game.clocks[colorIdx])).split(':')
             );
+          }
         },
         1000
       );
     },
     // 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);
+      const nextIdx = 1 - colorIdx;
       const doProcessMove = () => {
-        const colorIdx = ["w", "b"].indexOf(moveCol);
-        const nextIdx = 1 - colorIdx;
         const origMovescount = this.game.moves.length;
-        let addTime = 0; //for live games
+        // The move is (about to be) played: stop clock
+        clearInterval(this.clockUpdate);
+        this.clockUpdate = null;
         if (moveCol == this.game.mycolor && !data.receiveMyMove) {
           if (this.drawOffer == "received")
             // I refuse draw
             this.drawOffer = "";
           if (this.game.type == "live" && origMovescount >= 2) {
-            const elapsed = Date.now() - this.game.initime[colorIdx];
-            // elapsed time is measured in milliseconds
-            addTime = this.game.increment - elapsed / 1000;
+            this.game.clocks[colorIdx] += this.game.increment;
+            // For a correct display in casqe of disconnected opponent:
+            this.$set(
+              this.virtualClocks,
+              colorIdx,
+              ppt(this.game.clocks[colorIdx]).split(':')
+            );
+            GameStorage.update(this.gameRef, {
+              // It's not my turn anymore:
+              initime: null
+            });
           }
         }
         // Update current game object:
         playMove(move, this.vr);
-        // The move is played: stop clock
-        clearInterval(this.clockUpdate);
         if (!data.score)
           // Received move, score is computed in BaseGame, but maybe not yet.
           // ==> Compute it here, although this is redundant (TODO)
@@ -1146,21 +1292,10 @@ export default {
         if (data.score != "*") this.gameOver(data.score);
         this.game.moves.push(move);
         this.game.fen = this.vr.getFen();
-        if (this.game.type == "live") {
-          if (!!data.clock) this.game.clocks[colorIdx] = data.clock;
-          else this.game.clocks[colorIdx] += addTime;
-        } else {
+        if (this.game.type == "corr") {
           // In corr games, just reset clock to mainTime:
           this.game.clocks[colorIdx] = extractTime(this.game.cadence).mainTime;
         }
-        // NOTE: opponent's initime is reset after "gotmove" is received
-        if (
-          !this.game.mycolor ||
-          moveCol != this.game.mycolor ||
-          !!data.receiveMyMove
-        ) {
-          this.game.initime[nextIdx] = Date.now();
-        }
         // If repetition detected, consider that a draw offer was received:
         const fenObj = this.vr.getFenForRepeat();
         this.repeat[fenObj] =
@@ -1185,6 +1320,19 @@ export default {
         }
         // Since corr games are stored at only one location, update should be
         // done only by one player for each move:
+        if (
+          this.game.type == "live" &&
+          !!this.game.mycolor &&
+          moveCol != this.game.mycolor &&
+          this.game.moves.length >= 2
+        ) {
+          // Receive a move: update initime
+          this.game.initime = Date.now();
+          GameStorage.update(this.gameRef, {
+            // It's my turn now!
+            initime: this.game.initime
+          });
+        }
         if (
           !!this.game.mycolor &&
           !data.receiveMyMove &&
@@ -1221,12 +1369,11 @@ export default {
                 move: filtered_move,
                 moveIdx: origMovescount,
                 clocks: this.game.clocks,
-                initime: this.game.initime,
                 drawOffer: drawCode
               });
             };
             // The active tab can update storage immediately
-            if (!document.hidden) updateStorage();
+            if (this.focus) updateStorage();
             // Small random delay otherwise
             else setTimeout(updateStorage, 500 + 1000 * Math.random());
           }
@@ -1236,12 +1383,14 @@ export default {
           let sendMove = {
             move: filtered_move,
             index: origMovescount,
-            // color is required to check if this is my move (if several tabs opened)
+            // color is required to check if this is my move
+            // (if several tabs opened)
             color: moveCol,
             cancelDrawOffer: this.drawOffer == ""
           };
           if (this.game.type == "live")
             sendMove["clock"] = this.game.clocks[colorIdx];
+          // (Live) Clocks will re-start when the opponent pingback arrive
           this.opponentGotMove = false;
           this.send("newmove", {data: sendMove});
           // If the opponent doesn't reply gotmove soon enough, re-send move:
@@ -1289,6 +1438,7 @@ export default {
             // The board might have been hidden:
             if (boardDiv.style.visibility == "hidden")
               boardDiv.style.visibility = "visible";
+            if (data.score == "*") this.re_setClocks();
           }
         };
         let el = document.querySelector("#buttonsConfirm > .acceptBtn");
@@ -1355,10 +1505,18 @@ export default {
         };
         if (this.game.type == "live") {
           GameStorage.update(this.gameRef, scoreObj);
+          // Notify myself locally if I'm elsewhere:
+          if (!this.focus) {
+            notify(
+              "Game over",
+              { body: score + " : " + scoreMsg }
+            );
+          }
           if (!!callback) callback();
         }
         else this.updateCorrGame(scoreObj, callback);
-        // Notify the score to main Hall. TODO: only one player (currently double send)
+        // Notify the score to main Hall.
+        // TODO: only one player (currently double send)
         this.send("result", { gid: this.game.id, score: score });
         // Also to MyGames page (TODO: doubled as well...)
         this.notifyMyGames(
@@ -1409,7 +1567,7 @@ button
   margin: 0
   display: inline-flex
   img
-    height: 24px
+    height: 22px
     display: flex
     @media screen and (max-width: 767px)
       height: 18px