First draft of arrows + circles on board. Fix multi-connect detection
[vchess.git] / client / src / views / Game.vue
index 3171855..b77f6b0 100644 (file)
@@ -96,7 +96,7 @@ main
       )
         img(src="/images/icons/rematch.svg")
       #playersInfo
-        p
+        p(v-if="largeScreen")
           span.name(:class="{connected: isConnected(0)}")
             | {{ game.players[0].name || "@nonymous" }}
           span.time(
@@ -118,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"
@@ -131,6 +154,7 @@ import Chat from "@/components/Chat.vue";
 import { store } from "@/store";
 import { GameStorage } from "@/utils/gameStorage";
 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";
@@ -154,6 +178,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
@@ -184,8 +209,7 @@ export default {
       retrySendmove: null,
       clockUpdate: null,
       // Related to (killing of) self multi-connects:
-      newConnect: {},
-      killed: {}
+      newConnect: {}
     };
   },
   watch: {
@@ -229,6 +253,8 @@ export default {
     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);
@@ -238,11 +264,25 @@ export default {
     },
     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");
     },
     participateInChat: function(p) {
       return Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus) && !!p.name;
@@ -256,6 +296,8 @@ export default {
     },
     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)
@@ -297,7 +339,6 @@ export default {
       this.retrySendmove = null;
       this.clockUpdate = null;
       this.newConnect = {};
-      this.killed = {};
       // 1] Initialize connection
       this.connexionString =
         params.socketUrl +
@@ -312,7 +353,8 @@ export default {
       this.socketCloseListener = setInterval(
         () => {
           if (this.conn.readyState == 3) {
-            this.conn.removeEventListener("message", this.socketMessageListener);
+            this.conn.removeEventListener(
+              "message", this.socketMessageListener);
             this.conn = new WebSocket(this.connexionString);
             this.conn.addEventListener("message", this.socketMessageListener);
           }
@@ -334,7 +376,7 @@ 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"); });
       });
@@ -351,7 +393,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) {
@@ -408,16 +450,22 @@ export default {
       // 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 });
+      }
     },
     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", []);
       }
@@ -474,10 +522,12 @@ export default {
           // TODO: shuffling and random filtering on server,
           // if the room is really crowded.
           Object.keys(data.sockIds).forEach(sid => {
-            // TODO: test sid != user.sid was already done on server
             if (sid != this.st.user.sid) {
-              this.people[sid] = { tmpIds: data.sockIds[sid] };
               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;
@@ -493,7 +543,8 @@ export default {
                 }
               }
             );
-            this.newConnect[data.from] = true; //for self multi-connects tests
+            // 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 };
@@ -523,13 +574,6 @@ export default {
           }
           break;
         }
-        case "killed":
-          // I logged in elsewhere:
-          this.conn.removeEventListener("message", this.socketMessageListener);
-          this.conn.removeEventListener("close", this.socketCloseListener);
-          this.conn = null;
-          alert(this.st.tr["New connexion detected: tab now offline"]);
-          break;
         case "askidentity": {
           // Request for identification
           const me = {
@@ -550,46 +594,44 @@ export default {
           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.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
-              );
-            }
+          // 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;
         }
@@ -627,13 +669,18 @@ 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: since socket init after game load, the game is supposedly ready
+          // 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;
@@ -667,9 +714,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 = "";
@@ -682,8 +745,8 @@ export default {
                   GameStorage.update(this.gameRef, { drawOffer: "" });
                 }
               }
-              this.$refs["basegame"].play(movePlus.move, "received", null, true);
-              const moveColIdx = ["w", "b"].indexOf(movePlus.color);
+              this.$refs["basegame"].play(
+                movePlus.move, "received", null, true);
               this.game.clocks[moveColIdx] = movePlus.clock;
               this.processMove(
                 movePlus.move,
@@ -754,11 +817,17 @@ 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();
+            GameStorage.update(this.gameRef, { chat: chat });
+          }
           if (!document.getElementById("modalChat").checked)
             document.getElementById("chatBtn").classList.add("somethingnew");
           break;
+        }
       }
     },
     updateCorrGame: function(obj, callback) {
@@ -931,7 +1000,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");
     },
@@ -950,8 +1020,12 @@ export default {
       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];
+      // 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") {
         // NOTE: clocks in seconds
         game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of
@@ -964,10 +1038,6 @@ export default {
               (Date.now() - game.moves[L-1].played) / 1000;
           }
         }
-        // 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;
@@ -989,6 +1059,12 @@ export default {
         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");
+        }
         if (game.clocks[0] < 0) {
           // Game is unstarted. clock is ignored until move 2
           game.clocks = [tc.mainTime, tc.mainTime];
@@ -1271,7 +1347,7 @@ export default {
               });
             };
             // 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());
           }
@@ -1281,7 +1357,8 @@ 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 == ""
           };
@@ -1402,10 +1479,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(