From cafe016679ee9c14bf7bf0a37104ade7f74aff89 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 9 Mar 2020 18:24:49 +0100
Subject: [PATCH] MyGames page is now dynamic (experimental, not much tested)

---
 client/src/components/Board.vue    | 17 ++++---
 client/src/components/GameList.vue |  2 +-
 client/src/views/Game.vue          | 63 ++++++++++++-----------
 client/src/views/Hall.vue          | 12 +++++
 client/src/views/MyGames.vue       | 82 ++++++++++++++++++++----------
 server/sockets.js                  | 58 +++++++++++++--------
 6 files changed, 147 insertions(+), 87 deletions(-)

diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue
index 86c2acca..cf3d3e68 100644
--- a/client/src/components/Board.vue
+++ b/client/src/components/Board.vue
@@ -19,6 +19,7 @@ export default {
   ],
   data: function() {
     return {
+      mobileBrowser: ("ontouchstart" in window),
       possibleMoves: [], //filled after each valid click/dragstart
       choices: [], //promotion pieces, or checkered captures... (as moves)
       selectedPiece: null, //moving piece (or clicked piece)
@@ -292,7 +293,7 @@ export default {
     }
     let onEvents = {};
     // NOTE: click = mousedown + mouseup
-    if ("ontouchstart" in window) {
+    if (this.mobileBrowser) {
       onEvents = {
         on: {
           touchstart: this.mousedown,
@@ -343,9 +344,10 @@ export default {
     mousemove: function(e) {
       if (!this.selectedPiece) return;
       // There is an active element: move it around
-      const [offsetX, offsetY] = e.clientX
-        ? [e.clientX, e.clientY] //desktop browser
-        : [e.changedTouches[0].pageX, e.changedTouches[0].pageY]; //smartphone
+      const [offsetX, offsetY] =
+        this.mobileBrowser
+          ? [e.changedTouches[0].pageX, e.changedTouches[0].pageY]
+          : [e.clientX, e.clientY];
       this.selectedPiece.style.left = offsetX - this.start.x + "px";
       this.selectedPiece.style.top = offsetY - this.start.y + "px";
     },
@@ -353,9 +355,10 @@ export default {
       if (!this.selectedPiece) return;
       // There is an active element: obtain the move from start and end squares
       this.selectedPiece.style.zIndex = -3000; //HACK to find square from final coords
-      const [offsetX, offsetY] = e.clientX
-        ? [e.clientX, e.clientY]
-        : [e.changedTouches[0].pageX, e.changedTouches[0].pageY];
+      const [offsetX, offsetY] =
+        this.mobileBrowser
+          ? [e.changedTouches[0].pageX, e.changedTouches[0].pageY]
+          : [e.clientX, e.clientY];
       let landing = document.elementFromPoint(offsetX, offsetY);
       this.selectedPiece.style.zIndex = 3000;
       // Next condition: classList.contains(piece) fails because of marks
diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue
index 451d1bac..183d6463 100644
--- a/client/src/components/GameList.vue
+++ b/client/src/components/GameList.vue
@@ -82,7 +82,7 @@ export default {
                 : "b";
             if (g.score == "*") {
               priority++;
-              if (isMyTurn(g, myColor)) priority++;
+              if (g.turn == myColor || isMyTurn(g, myColor)) priority++;
             }
           }
           if (g.created < minCreated) minCreated = g.created;
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index 024847ce..e909f70e 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -165,7 +165,6 @@ export default {
       rematchOffer: "",
       lastateAsked: false,
       people: {}, //players + observers
-      onMygames: [], //opponents (or me) on "MyGames" page
       lastate: undefined, //used if opponent send lastate before game is ready
       repeat: {}, //detect position repetition
       curDiag: "", //for corr moves confirmation
@@ -271,7 +270,6 @@ export default {
       this.drawOffer = "";
       this.lastateAsked = false;
       this.rematchOffer = "";
-      this.onMygames = [];
       this.lastate = undefined;
       this.newChat = "";
       this.roomInitialized = false;
@@ -290,6 +288,8 @@ export default {
         params.socketUrl +
         "/?sid=" +
         this.st.user.sid +
+        "&id=" +
+        this.st.user.id +
         "&tmpId=" +
         getRandString() +
         "&page=" +
@@ -401,17 +401,17 @@ export default {
     getGameType: function(game) {
       return game.cadence.indexOf("d") >= 0 ? "corr" : "live";
     },
-    // Notify turn after a new move (to opponent and me on MyGames page)
-    notifyTurn: function(sid) {
-      const player = this.people[sid];
-      const colorIdx = this.game.players.findIndex(
-        p => p.sid == sid || p.uid == player.id);
-      const color = ["w","b"][colorIdx];
-      const movesCount = this.game.moves.length;
-      const yourTurn =
-        (color == "w" && movesCount % 2 == 0) ||
-        (color == "b" && movesCount % 2 == 1);
-      this.send("turnchange", { target: sid, yourTurn: yourTurn });
+    // Notify something after a new move (to opponent and me on MyGames page)
+    notifyMyGames: function(thing, data) {
+      this.send(
+        "notify" + thing,
+        {
+          data: data,
+          targets: this.game.players.map(p => {
+            return { sid: p.sid, uid: p.uid };
+          })
+        }
+      );
     },
     showNextGame: function() {
       // Did I play in current game? If not, add it to nextIds list
@@ -462,22 +462,6 @@ export default {
         case "disconnect":
           this.$delete(this.people, data.from);
           break;
-        case "mconnect": {
-          // TODO: from MyGames page : send mconnect message with the list of gid (live and corr)
-          // Either me (another tab) or opponent
-          const sid = data.from;
-          if (!this.onMygames.some(s => s == sid))
-          {
-            this.onMygames.push(sid);
-            this.notifyTurn(sid); //TODO: this may require server ID (so, notify after receiving identity)
-          }
-          break;
-          if (!this.people[sid])
-            this.send("askidentity", { target: sid });
-        }
-        case "mdisconnect":
-          ArrayFun.remove(this.onMygames, sid => sid == data.from);
-          break;
         case "getfocus": {
           let player = this.people[data.from];
           if (!!player) {
@@ -867,6 +851,8 @@ export default {
         const notifyNewGame = () => {
           let oppsid = this.getOppsid(); //may be null
           this.send("rnewgame", { data: gameInfo, oppsid: oppsid });
+          // Also to MyGames page:
+          this.notifyMyGames("newgame", gameInfo);
         };
         if (this.game.type == "live")
           this.addAndGotoLiveGame(gameInfo, notifyNewGame);
@@ -1185,7 +1171,6 @@ export default {
           const score = this.vr.getCurrentScore();
           if (score != "*") this.gameOver(score);
         }
-// TODO: notifyTurn: "changeturn" message
         this.game.moves.push(move);
         this.game.fen = this.vr.getFen();
         if (this.game.type == "live") {
@@ -1216,6 +1201,16 @@ export default {
           // NOTE: 'var' to see that variable outside this block
           var filtered_move = getFilteredMove(move);
         }
+        if (moveCol == this.game.mycolor && !data.receiveMyMove) {
+          // Notify turn on MyGames page:
+          this.notifyMyGames(
+            "turn",
+            {
+              gid: this.gameRef.id,
+              turn: this.vr.turn
+            }
+          );
+        }
         // Since corr games are stored at only one location, update should be
         // done only by one player for each move:
         if (
@@ -1394,6 +1389,14 @@ export default {
         else this.updateCorrGame(scoreObj, callback);
         // 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(
+          "score",
+          {
+            gid: this.gameRef.id,
+            score: score
+          }
+        );
       }
       else if (!!callback) callback();
     }
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index ac2e26ec..5217c34d 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -324,6 +324,8 @@ export default {
       params.socketUrl +
       "/?sid=" +
       this.st.user.sid +
+      "&id=" +
+      this.st.user.id +
       "&tmpId=" +
       getRandString() +
       "&page=" +
@@ -977,6 +979,16 @@ export default {
         // Send game info (only if live) to everyone except me and opponent
         // TODO: this double message send could be avoided.
         this.send("newgame", { data: gameInfo, oppsid: oppsid });
+        // Also to MyGames page:
+        this.send(
+          "notifynewgame",
+          {
+            data: gameInfo,
+            targets: gameInfo.players.map(p => {
+              return { sid: p.sid, uid: p.uid };
+            })
+          }
+        );
       };
       if (c.type == "live") {
         notifyNewgame();
diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue
index 5148c3cf..5d862073 100644
--- a/client/src/views/MyGames.vue
+++ b/client/src/views/MyGames.vue
@@ -74,6 +74,8 @@ export default {
       params.socketUrl +
       "/?sid=" +
       this.st.user.sid +
+      "&id=" +
+      this.st.user.id +
       "&tmpId=" +
       getRandString() +
       "&page=" +
@@ -100,9 +102,59 @@ export default {
         elt.previousElementSibling.classList.remove("active");
       else elt.nextElementSibling.classList.remove("active");
     },
-    // TODO: classifyObject is redundant (see Hall.vue)
-    classifyObject: function(o) {
-      return o.cadence.indexOf("d") === -1 ? "live" : "corr";
+    tryShowNewsIndicator: function(type) {
+      if (
+        (type == "live" && this.display == "corr") ||
+        (type == "corr" && this.display == "live")
+      ) {
+        document
+          .getElementById(type + "Games")
+          .classList.add("somethingnew");
+      }
+    },
+    socketMessageListener: function(msg) {
+      const data = JSON.parse(msg.data);
+      switch (data.code) {
+        // NOTE: no need to increment movesCount: unused if turn is provided
+        case "notifyturn":
+        case "notifyscore": {
+          const info = data.data;
+          let games =
+            !!parseInt(info.gid)
+              ? this.corrGames
+              : this.liveGames;
+          let g = games.find(g => g.id == info.gid);
+          // "notifything" --> "thing":
+          const thing = data.code.substr(6);
+          this.$set(g, thing, info[thing]);
+          this.tryShowNewsIndicator(g.type);
+          break;
+        }
+        case "notifynewgame": {
+          const gameInfo = data.data;
+          // st.variants might be uninitialized,
+          // if unlucky and newgame right after connect:
+          const v = this.st.variants.find(v => v.id == gameInfo.vid);
+          const vname = !!v ? v.name : "";
+          const type = gameInfo.cadence.indexOf('d') >= 0 ? "corr": "live";
+          const game = Object.assign(
+            {
+              vname: vname,
+              type: type,
+              score: "*"
+            },
+            gameInfo
+          );
+          this[type + "Games"].push(game);
+          this.tryShowNewsIndicator(type);
+          break;
+        }
+      }
+    },
+    socketCloseListener: function() {
+      this.conn = new WebSocket(this.connexionString);
+      this.conn.addEventListener("message", this.socketMessageListener);
+      this.conn.addEventListener("close", this.socketCloseListener);
     },
     showGame: function(game) {
       // TODO: "isMyTurn" is duplicated (see GameList component). myColor also
@@ -170,30 +222,6 @@ export default {
           }
         );
       }
-    },
-    socketMessageListener: function(msg) {
-      const data = JSON.parse(msg.data);
-      if (data.code == "changeturn") {
-        let games = !!parseInt(data.gid)
-          ? this.corrGames
-          : this.liveGames;
-        // NOTE: new move itself is not received, because it wouldn't be used.
-        let g = games.find(g => g.id == data.gid);
-        this.$set(g, "movesCount", g.movesCount + 1);
-        if (
-          (g.type == "live" && this.display == "corr") ||
-          (g.type == "corr" && this.display == "live")
-        ) {
-          document
-            .getElementById(g.type + "Games")
-            .classList.add("somethingnew");
-        }
-      }
-    },
-    socketCloseListener: function() {
-      this.conn = new WebSocket(this.connexionString);
-      this.conn.addEventListener("message", this.socketMessageListener);
-      this.conn.addEventListener("close", this.socketCloseListener);
     }
   }
 };
diff --git a/server/sockets.js b/server/sockets.js
index d1489f71..d96b6967 100644
--- a/server/sockets.js
+++ b/server/sockets.js
@@ -24,9 +24,12 @@ module.exports = function(wss) {
   // or "/mygames" for Mygames page (simpler: no 'people' array).
   // tmpId is required if a same user (browser) has different tabs
   let clients = {};
+  let sidToPages = {};
+  let idToSid = {};
   wss.on("connection", (socket, req) => {
     const query = getJsonFromUrl(req.url);
     const sid = query["sid"];
+    const id = query["id"];
     const tmpId = query["tmpId"];
     const page = query["page"];
     const notifyRoom = (page,code,obj={}) => {
@@ -60,14 +63,22 @@ module.exports = function(wss) {
       delete clients[page][sid][tmpId];
       if (Object.keys(clients[page][sid]).length == 0) {
         delete clients[page][sid];
+        const pgIndex = sidToPages[sid].findIndex(pg => pg == page);
+        sidToPages[sid].splice(pgIndex, 1);
         if (Object.keys(clients[page]).length == 0)
           delete clients[page];
+        // Am I totally offline?
+        if (sidToPages[sid].length == 0) {
+          delete sidToPages[sid];
+          delete idToSid[id];
+        }
       }
     };
 
     const doDisconnect = () => {
       deleteConnexion();
-      if (!clients[page] || !clients[page][sid]) {
+      // Nothing to notify when disconnecting from MyGames page:
+      if (page != "/mygames" && (!clients[page] || !clients[page][sid])) {
         // I effectively disconnected from this page:
         notifyRoom(page, "disconnect");
         if (page.indexOf("/game/") >= 0)
@@ -139,7 +150,7 @@ module.exports = function(wss) {
           });
           // NOTE: a "gamer" could also just be an observer
           Object.keys(clients).forEach(p => {
-            if (p != "/") {
+            if (p.indexOf("/game/") >= 0) {
               Object.keys(clients[p]).forEach(k => {
                 // 'page' indicator is needed for gamers
                 if (k != sid) sockIds.push({ sid:k, page:p });
@@ -243,26 +254,6 @@ module.exports = function(wss) {
           notifyRoom("/", "result", { gid: obj.gid, score: obj.score });
           break;
 
-        case "mconnect":
-          // Special case: notify some game rooms that
-          // I'm watching game state from MyGames
-          // TODO: this code is ignored for now
-          obj.gids.forEach(gid => {
-            const pg = "/game/" + gid;
-            Object.keys(clients[pg]).forEach(s => {
-              Object.keys(clients[pg][s]).forEach(x => {
-                send(
-                  clients[pg][s][x].socket,
-                  { code: "mconnect", from: sid }
-                );
-              });
-            });
-          });
-          break;
-        case "mdisconnect":
-          // TODO
-          // Also TODO: pass newgame to MyGames, and gameover (result)
-          break;
         case "mabort": {
           const gamePg = "/game/" + obj.gid;
           if (!!clients[gamePg] && !!clients[gamePg][obj.target]) {
@@ -276,6 +267,24 @@ module.exports = function(wss) {
           break;
         }
 
+        case "notifyscore":
+        case "notifyturn":
+        case "notifynewgame":
+          if (!!clients["/mygames"]) {
+            obj.targets.forEach(t => {
+              const k = t.sid || idToSid[t.uid];
+              if (!!clients["/mygames"][k]) {
+                Object.keys(clients["/mygames"][k]).forEach(x => {
+                  send(
+                    clients["/mygames"][k][x].socket,
+                    { code: obj.code, data: obj.data }
+                  );
+                });
+              }
+            });
+          }
+          break;
+
         case "getfocus":
         case "losefocus":
           if (page == "/") notifyAllBut("/", obj.code, { page: "/" }, [sid]);
@@ -319,6 +328,11 @@ module.exports = function(wss) {
       clients[page][sid] = { [tmpId]: newElt };
     else
       clients[page][sid][tmpId] = newElt;
+    // Also update helper correspondances
+    if (!idToSid[id]) idToSid[id] = sid;
+    if (!sidToPages[sid]) sidToPages[sid] = [];
+    const pgIndex = sidToPages[sid].findIndex(pg => pg == page);
+    if (pgIndex === -1) sidToPages[sid].push(page);
     socket.on("message", messageListener);
     socket.on("close", closeListener);
   });
-- 
2.44.0