Experimental focus/blur detection
[vchess.git] / client / src / views / Hall.vue
index 009a1ab..271c0bc 100644 (file)
@@ -100,7 +100,7 @@ main
             )
               | {{ st.tr["Observe"] }}
             button.player-action(
-              v-else-if="st.user.id > 0 && sid != st.user.sid"
+              v-else-if="isFocusedOnHall(sid)"
               @click="challenge(sid)"
             )
               | {{ st.tr["Challenge"] }}
@@ -233,66 +233,85 @@ export default {
     if (this.st.variants.length > 0 && this.newchallenge.vid > 0)
       this.loadNewchallVariant();
     const my = this.st.user;
-    this.$set(this.people, my.sid, { id: my.id, name: my.name, pages: ["/"] });
+    this.$set(
+      this.people,
+      my.sid,
+      {
+        id: my.id,
+        name: my.name,
+        pages: [{ path: "/", focus: true }]
+      }
+    );
     // Ask server for current corr games (all but mines)
     ajax(
       "/games",
       "GET",
-      { uid: this.st.user.id, excluded: true },
-      response => {
-        this.games = this.games.concat(
-          response.games.map(g => {
-            const type = this.classifyObject(g);
-            const vname = this.getVname(g.vid);
-            return Object.assign({}, g, { type: type, vname: vname });
-          })
-        );
+      {
+        data: { uid: this.st.user.id, excluded: true },
+        success: (response) => {
+          this.games = this.games.concat(
+            response.games.map(g => {
+              const type = this.classifyObject(g);
+              const vname = this.getVname(g.vid);
+              return Object.assign({}, g, { type: type, vname: vname });
+            })
+          );
+        }
       }
     );
     // Also ask for corr challenges (open + sent by/to me)
-    ajax("/challenges", "GET", { uid: this.st.user.id }, response => {
-      // Gather all senders names, and then retrieve full identity:
-      // (TODO [perf]: some might be online...)
-      let names = {};
-      response.challenges.forEach(c => {
-        if (c.uid != this.st.user.id) names[c.uid] = "";
-        else if (!!c.target && c.target != this.st.user.id)
-          names[c.target] = "";
-      });
-      const addChallenges = () => {
-        names[this.st.user.id] = this.st.user.name; //in case of
-        this.challenges = this.challenges.concat(
-          response.challenges.map(c => {
-            const from = { name: names[c.uid], id: c.uid }; //or just name
-            const type = this.classifyObject(c);
-            const vname = this.getVname(c.vid);
-            return Object.assign(
-              {},
+    ajax(
+      "/challenges",
+      "GET",
+      {
+        data: { uid: this.st.user.id },
+        success: (response) => {
+          // Gather all senders names, and then retrieve full identity:
+          // (TODO [perf]: some might be online...)
+          let names = {};
+          response.challenges.forEach(c => {
+            if (c.uid != this.st.user.id) names[c.uid] = "";
+            else if (!!c.target && c.target != this.st.user.id)
+              names[c.target] = "";
+          });
+          const addChallenges = () => {
+            names[this.st.user.id] = this.st.user.name; //in case of
+            this.challenges = this.challenges.concat(
+              response.challenges.map(c => {
+                const from = { name: names[c.uid], id: c.uid }; //or just name
+                const type = this.classifyObject(c);
+                const vname = this.getVname(c.vid);
+                return Object.assign(
+                  {},
+                  {
+                    type: type,
+                    vname: vname,
+                    from: from,
+                    to: c.target ? names[c.target] : ""
+                  },
+                  c
+                );
+              })
+            );
+          };
+          if (Object.keys(names).length > 0) {
+            ajax(
+              "/users",
+              "GET",
               {
-                type: type,
-                vname: vname,
-                from: from,
-                to: c.target ? names[c.target] : ""
-              },
-              c
+                data: { ids: Object.keys(names).join(",") },
+                success: (response2) => {
+                  response2.users.forEach(u => {
+                    names[u.id] = u.name;
+                  });
+                  addChallenges();
+                }
+              }
             );
-          })
-        );
-      };
-      if (Object.keys(names).length > 0) {
-        ajax(
-          "/users",
-          "GET",
-          { ids: Object.keys(names).join(",") },
-          response2 => {
-            response2.users.forEach(u => {
-              names[u.id] = u.name;
-            });
-            addChallenges();
-          }
-        );
-      } else addChallenges();
-    });
+          } else addChallenges();
+        }
+      }
+    );
     const connectAndPoll = () => {
       this.send("connect");
       this.send("pollclientsandgamers");
@@ -313,6 +332,7 @@ export default {
     this.conn.onclose = this.socketCloseListener;
   },
   mounted: function() {
+    document.addEventListener('visibilitychange', this.visibilityChange);
     ["peopleWrap", "infoDiv", "newgameDiv"].forEach(eltName => {
       let elt = document.getElementById(eltName);
       elt.addEventListener("click", processModalClick);
@@ -331,9 +351,18 @@ export default {
     this.setDisplay("g", showGtype);
   },
   beforeDestroy: function() {
+    document.removeEventListener('visibilitychange', this.visibilityChange);
     this.send("disconnect");
   },
   methods: {
+    visibilityChange: function() {
+      // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27
+      this.send(
+        document.visibilityState == "visible"
+          ? "getfocus"
+          : "losefocus"
+      );
+    },
     // Helpers:
     cadenceFocusIfOpened: function() {
       if (event.target.checked)
@@ -375,7 +404,16 @@ export default {
       else elt.nextElementSibling.classList.remove("active");
     },
     isGamer: function(sid) {
-      return this.people[sid].pages.some(p => p.indexOf("/game/") >= 0);
+      return this.people[sid].pages
+        .some(p => p.focus && p.path.indexOf("/game/") >= 0);
+    },
+    isFocusedOnHall: function(sid) {
+      return (
+        // This is meant to challenge people, thus the next 2 conditions:
+        this.st.user.id > 0 &&
+        sid != this.st.user.sid &&
+        this.people[sid].pages.some(p => p.path == "/" && p.focus)
+      );
     },
     challenge: function(sid) {
       // Available, in Hall (only)
@@ -387,8 +425,10 @@ export default {
       // In some game, maybe playing maybe not: show a random one
       let gids = [];
       this.people[sid].pages.forEach(p => {
-        const matchGid = p.match(/[a-zA-Z0-9]+$/);
-        if (!!matchGid) gids.push(matchGid[0]);
+        if (p.focus) {
+          const matchGid = p.path.match(/[a-zA-Z0-9]+$/);
+          if (!!matchGid) gids.push(matchGid[0]);
+        }
       });
       const gid = gids[Math.floor(Math.random() * gids.length)];
       const game = this.games.find(g => g.id == gid);
@@ -434,11 +474,12 @@ export default {
               this.send("askidentity", { target: s.sid, page: page });
               identityAsked[s.sid] = true;
             }
-            if (!this.people[s.sid])
+            if (!this.people[s.sid]) {
               // Do not set name or id: identity unknown yet
-              this.$set(this.people, s.sid, { pages: [page] });
-            else if (this.people[s.sid].pages.indexOf(page) < 0)
-              this.people[s.sid].pages.push(page);
+              this.people[s.sid] = { pages: [{path: page, focus: true}] };
+            }
+            else if (!(this.people[s.sid].pages.find(p => p.path == page)))
+              this.people[s.sid].pages.push({ path: page, focus: true });
             if (!s.page)
               // Peer is in Hall
               this.send("askchallenge", { target: s.sid });
@@ -450,18 +491,16 @@ export default {
         case "connect":
         case "gconnect": {
           const page = data.page || "/";
-          // NOTE: player could have been polled earlier, but might have logged in then
-          // So it's a good idea to ask identity if he was anonymous.
-          // But only ask game / challenge if currently disconnected.
+          // Only ask game / challenge if first connexion:
           if (!this.people[data.from]) {
-            this.$set(this.people, data.from, { pages: [page] });
+            this.people[data.from] = { pages: [{ path: page, focus: true }] };
             if (data.code == "connect")
               this.send("askchallenge", { target: data.from });
             else this.send("askgame", { target: data.from, page: page });
           } else {
             // Append page if not already in list
-            if (this.people[data.from].pages.indexOf(page) < 0)
-              this.people[data.from].pages.push(page);
+            if (!(this.people[data.from].pages.find(p => p.path == page)))
+              this.people[data.from].pages.push({ path: page, focus: true });
           }
           if (!this.people[data.from].name && this.people[data.from].id !== 0) {
             // Identity not known yet
@@ -499,11 +538,26 @@ export default {
             }
           }
           const page = data.page || "/";
-          ArrayFun.remove(this.people[data.from].pages, p => p == page);
+          ArrayFun.remove(this.people[data.from].pages, p => p.path == page);
           if (this.people[data.from].pages.length == 0)
             this.$delete(this.people, data.from);
           break;
         }
+        case "getfocus":
+          // If user reload a page, focus may arrive earlier than connect
+          if (!!this.people[data.from]) {
+            this.people[data.from].pages
+              .find(p => p.path == data.page).focus = true;
+            this.$forceUpdate(); //TODO: shouldn't be required
+          }
+          break;
+        case "losefocus":
+          if (!!this.people[data.from]) {
+            this.people[data.from].pages
+              .find(p => p.path == data.page).focus = false;
+            this.$forceUpdate(); //TODO: shouldn't be required
+          }
+          break;
         case "killed":
           // I logged in elsewhere:
           this.conn = null;
@@ -522,11 +576,13 @@ export default {
         }
         case "identity": {
           const user = data.data;
-          this.$set(this.people, user.sid, {
-            id: user.id,
-            name: user.name,
-            pages: this.people[user.sid].pages
-          });
+          let player = this.people[user.sid];
+          // player.pages is already set
+          player.id = user.id;
+          player.name = user.name;
+          // TODO: this.$set(people, ...) fails. So forceUpdate.
+          //       But this shouldn't be like that!
+          this.$forceUpdate();
           // If I multi-connect, kill current connexion if no mark (I'm older)
           if (this.newConnect[user.sid]) {
             if (
@@ -751,7 +807,11 @@ export default {
           // Delete current challenge (will be replaced now)
           this.send("deletechallenge", { data: this.challenges[cIdx].id });
           if (ctype == "corr") {
-            ajax("/challenges", "DELETE", { id: this.challenges[cIdx].id });
+            ajax(
+              "/challenges",
+              "DELETE",
+              { data: { id: this.challenges[cIdx].id } }
+            );
           }
           this.challenges.splice(cIdx, 1);
         }
@@ -788,9 +848,16 @@ export default {
         finishAddChallenge(null);
       } else {
         // Correspondance game: send challenge to server
-        ajax("/challenges", "POST", { chall: chall }, response => {
-          finishAddChallenge(response.cid);
-        });
+        ajax(
+          "/challenges",
+          "POST",
+          {
+            data: { chall: chall },
+            success: (response) => {
+              finishAddChallenge(response.cid);
+            }
+          }
+        );
       }
     },
     // Callback function after a diagram was showed to accept
@@ -813,8 +880,13 @@ export default {
         const oppsid = this.getOppsid(c);
         if (!!oppsid)
           this.send("refusechallenge", { data: c.id, target: oppsid });
-        if (c.type == "corr")
-          ajax("/challenges", "DELETE", { id: c.id });
+        if (c.type == "corr") {
+          ajax(
+            "/challenges",
+            "DELETE",
+            { data: { id: c.id } }
+          );
+        }
       }
       this.send("deletechallenge", { data: c.id });
     },
@@ -853,8 +925,13 @@ export default {
       }
       else {
         // My challenge
-        if (c.type == "corr")
-          ajax("/challenges", "DELETE", { id: c.id });
+        if (c.type == "corr") {
+          ajax(
+            "/challenges",
+            "DELETE",
+            { data: { id: c.id } }
+          );
+        }
         this.send("deletechallenge", { data: c.id });
       }
       // In all cases, the challenge is consumed:
@@ -889,11 +966,14 @@ export default {
         ajax(
           "/games",
           "POST",
-          { gameInfo: gameInfo, cid: c.id }, //cid useful to delete challenge
-          response => {
-            gameInfo.id = response.gameId;
-            notifyNewgame();
-            this.$router.push("/game/" + response.gameId);
+          {
+            // cid is useful to delete the challenge:
+            data: { gameInfo: gameInfo, cid: c.id },
+            success: (response) => {
+              gameInfo.id = response.gameId;
+              notifyNewgame();
+              this.$router.push("/game/" + response.gameId);
+            }
           }
         );
       }