Improve smartphone menu open/close buttons
[vchess.git] / client / src / views / Hall.vue
index 6cc58e9..2141cf9 100644 (file)
@@ -8,17 +8,34 @@ main
     .card.text-center
       label.modal-close(for="modalInfo")
       p(v-html="infoMessage")
-  input#modalNewgame.modal(type="checkbox")
+  input#modalAccept.modal(type="checkbox")
+  div#acceptDiv(role="dialog")
+    .card.text-center
+      p
+        span.variantName {{ curChallToAccept.vname }} 
+        span {{ curChallToAccept.cadence }} 
+        span {{ st.tr["with"] + " " + curChallToAccept.from.name }}
+      .diagram(v-html="tchallDiag")
+      .button-group#buttonsTchall
+        button.acceptBtn(@click="decisionChallenge(true)") {{ st.tr["Accept challenge?"] }}
+        button.refuseBtn(@click="decisionChallenge(false)") {{ st.tr["Refuse"] }}
+  input#modalNewgame.modal(
+    type="checkbox"
+    @change="cadenceFocusIfOpened($event)"
+  )
   div#newgameDiv(
     role="dialog"
     data-checkbox="modalNewgame"
   )
     .card
       label#closeNewgame.modal-close(for="modalNewgame")
-      form(@submit.prevent="newChallenge()" @keyup.enter="newChallenge()")
+      div(@keyup.enter="newChallenge()")
         fieldset
           label(for="selectVariant") {{ st.tr["Variant"] }} *
-          select#selectVariant(v-model="newchallenge.vid")
+          select#selectVariant(
+            @change="loadNewchallVariant(trySetNewchallDiag)"
+            v-model="newchallenge.vid"
+          )
             option(
               v-for="v in st.variants"
               :value="v.id"
@@ -28,14 +45,21 @@ main
         fieldset
           label(for="cadence") {{ st.tr["Cadence"] }} *
           div#predefinedCadences
-            button(type="button") 3+2
-            button(type="button") 5+3
             button(type="button") 15+5
+            button(type="button") 45+30
+            button(type="button") 3d
+            button(type="button") 7d
           input#cadence(
             type="text"
             v-model="newchallenge.cadence"
-            placeholder="5+0, 1h+30s, 7d+1d ..."
+            placeholder="5+0, 1h+30s, 5d ..."
           )
+        fieldset
+          label(for="selectRandomLevel") {{ st.tr["Randomness"] }} *
+          select#selectRandomLevel(v-model="newchallenge.randomness")
+            option(value="0") {{ st.tr["Deterministic"] }}
+            option(value="1") {{ st.tr["Symmetric random"] }}
+            option(value="2") {{ st.tr["Asymmetric random"] }}
         fieldset(v-if="st.user.id > 0")
           label(for="selectPlayers") {{ st.tr["Play with?"] }}
           input#selectPlayers(
@@ -43,15 +67,17 @@ main
             v-model="newchallenge.to"
           )
         fieldset(v-if="st.user.id > 0 && newchallenge.to.length > 0")
-          label(for="inputFen") FEN
           input#inputFen(
+            placeholder="FEN"
+            @input="trySetNewchallDiag()"
             type="text"
             v-model="newchallenge.fen"
           )
+        .diagram(v-html="newchallenge.diag")
       button(@click="newChallenge()") {{ st.tr["Send challenge"] }}
   input#modalPeople.modal(
     type="checkbox"
-    @click="resetChatColor()"
+    @click="resetSocialColor()"
   )
   div#peopleWrap(
     role="dialog"
@@ -63,14 +89,19 @@ main
         #players
           p(
             v-for="sid in Object.keys(people)"
-            v-if="!!people[sid].name"
+            v-if="people[sid].name"
           )
             span {{ people[sid].name }}
             button.player-action(
-              v-if="isGamer(sid) || (st.user.id > 0 && sid!=st.user.sid)"
-              @click="challOrWatch(sid)"
+              v-if="isGamer(sid)"
+              @click="watchGame(sid)"
+            )
+              | {{ st.tr["Observe"] }}
+            button.player-action(
+              v-else-if="st.user.id > 0 && sid!=st.user.sid"
+              @click="challenge(sid)"
             )
-              | {{ getActionLabel(sid) }}
+              | {{ st.tr["Challenge"] }}
           p.anonymous @nonymous ({{ anonymousCount }})
         #chat
           Chat(
@@ -131,6 +162,7 @@ import { ArrayFun } from "@/utils/array";
 import { ajax } from "@/utils/ajax";
 import params from "@/parameters";
 import { getRandString, shuffle } from "@/utils/alea";
+import { getDiagram } from "@/utils/printDiagram";
 import Chat from "@/components/Chat.vue";
 import GameList from "@/components/GameList.vue";
 import ChallengeList from "@/components/ChallengeList.vue";
@@ -154,10 +186,18 @@ export default {
       infoMessage: "",
       newchallenge: {
         fen: "",
-        vid: localStorage.getItem("vid") || "",
+        vid: parseInt(localStorage.getItem("vid")) || 0,
         to: "", //name of challenged player (if any)
-        cadence: localStorage.getItem("cadence") || ""
+        cadence: localStorage.getItem("cadence") || "",
+        randomness: parseInt(localStorage.getItem("randomness")) || 2,
+        // VariantRules object, stored to not interfere with
+        // diagrams of targetted challenges:
+        V: null,
+        vname: "",
+        diag: "" //visualizing FEN
       },
+      tchallDiag: "",
+      curChallToAccept: {from: {}},
       newChat: "",
       conn: null,
       connexionString: "",
@@ -173,18 +213,23 @@ export default {
       this.challenges.concat(this.games).forEach(o => {
         if (o.vname == "") o.vname = this.getVname(o.vid);
       });
+      if (!this.newchallenge.V && this.newchallenge.vid > 0)
+        this.loadNewchallVariant();
     }
   },
   computed: {
     anonymousCount: function() {
       let count = 0;
       Object.values(this.people).forEach(p => {
-        count += !p.name ? 1 : 0;
+        // Do not cound people who did not send their identity yet:
+        count += (!p.name && p.id === 0) ? 1 : 0;
       });
       return count;
     }
   },
   created: function() {
+    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: ["/"] });
     // Ask server for current corr games (all but mines)
@@ -209,7 +254,6 @@ export default {
       let names = {};
       response.challenges.forEach(c => {
         if (c.uid != this.st.user.id) names[c.uid] = "";
-        //unknwon for now
         else if (!!c.target && c.target != this.st.user.id)
           names[c.target] = "";
       });
@@ -288,6 +332,10 @@ export default {
   },
   methods: {
     // Helpers:
+    cadenceFocusIfOpened: function() {
+      if (event.target.checked)
+        document.getElementById("cadence").focus();
+    },
     send: function(code, obj) {
       if (this.conn) {
         this.conn.send(JSON.stringify(Object.assign({ code: code }, obj)));
@@ -326,29 +374,23 @@ export default {
     isGamer: function(sid) {
       return this.people[sid].pages.some(p => p.indexOf("/game/") >= 0);
     },
-    getActionLabel: function(sid) {
-      return this.people[sid].pages.some(p => p == "/")
-        ? "Challenge"
-        : "Observe";
+    challenge: function(sid) {
+      // Available, in Hall (only)
+      this.newchallenge.to = this.people[sid].name;
+      document.getElementById("modalPeople").checked = false;
+      window.doClick("modalNewgame");
     },
-    challOrWatch: function(sid) {
-      if (this.people[sid].pages.some(p => p == "/")) {
-        // Available, in Hall
-        this.newchallenge.to = this.people[sid].name;
-        document.getElementById("modalPeople").checked = false;
-        window.doClick("modalNewgame");
-      } else {
-        // 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]);
-        });
-        const gid = gids[Math.floor(Math.random() * gids.length)];
-        const game = this.games.find(g => g.id == gid);
-        if (game) this.showGame(game);
-        else this.$router.push("/game/" + gid); //game vs. me
-      }
+    watchGame: function(sid) {
+      // 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]);
+      });
+      const gid = gids[Math.floor(Math.random() * gids.length)];
+      const game = this.games.find(g => g.id == gid);
+      if (game) this.showGame(game);
+      else this.$router.push("/game/" + gid); //game vs. me
     },
     showGame: function(g) {
       // NOTE: we are an observer, since only games I don't play are shown here
@@ -358,7 +400,7 @@ export default {
         url += "?rid=" + g.rids[Math.floor(Math.random() * g.rids.length)];
       this.$router.push(url);
     },
-    resetChatColor: function() {
+    resetSocialColor: function() {
       // TODO: this is called twice, once on opening an once on closing
       document.getElementById("peopleBtn").classList.remove("somethingnew");
     },
@@ -377,17 +419,18 @@ export default {
           data.sockIds.forEach(s => {
             const page = s.page || "/";
             if (s.sid != this.st.user.sid && !identityAsked[s.sid]) {
-              identityAsked[s.sid] = true;
               this.send("askidentity", { target: s.sid, page: page });
+              identityAsked[s.sid] = true;
             }
             if (!this.people[s.sid])
-              this.$set(this.people, s.sid, { id: 0, name: "", pages: [page] });
+              // 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);
             if (!s.page)
-              //peer is in Hall
+              // Peer is in Hall
               this.send("askchallenge", { target: s.sid });
-            //peer is in Game
+            // Peer is in Game
             else this.send("askgame", { target: s.sid, page: page });
           });
           break;
@@ -399,20 +442,17 @@ export default {
           // So it's a good idea to ask identity if he was anonymous.
           // But only ask game / challenge if currently disconnected.
           if (!this.people[data.from]) {
-            this.$set(this.people, data.from, {
-              name: "",
-              id: 0,
-              pages: [page]
-            });
+            this.$set(this.people, data.from, { pages: [page] });
             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
+            // 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].id == 0) {
+          if (!this.people[data.from].name && this.people[data.from].id !== 0) {
+            // Identity not known yet
             this.newConnect[data.from] = true; //for self multi-connects tests
             this.send("askidentity", { target: data.from, page: page });
           }
@@ -451,12 +491,8 @@ export default {
         }
         case "killed":
           // I logged in elsewhere:
-          alert(this.st.tr["New connexion detected: tab now offline"]);
-          // TODO: this fails. See https://github.com/websockets/ws/issues/489
-          //this.conn.removeEventListener("message", this.socketMessageListener);
-          //this.conn.removeEventListener("close", this.socketCloseListener);
-          //this.conn.close();
           this.conn = null;
+          alert(this.st.tr["New connexion detected: tab now offline"]);
           break;
         case "askidentity": {
           // Request for identification (TODO: anonymous shouldn't need to reply)
@@ -471,6 +507,11 @@ 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
+          });
           if (user.name) {
             // If I multi-connect, kill current connexion if no mark (I'm older)
             if (
@@ -484,14 +525,6 @@ export default {
                 this.killed[this.st.user.sid] = true;
               }
             }
-            if (user.sid != this.st.user.sid) {
-              //I already know my identity...
-              this.$set(this.people, user.sid, {
-                id: user.id,
-                name: user.name,
-                pages: this.people[user.sid].pages
-              });
-            }
           }
           delete this.newConnect[user.sid];
           break;
@@ -510,6 +543,7 @@ export default {
               id: c.id,
               from: this.st.user.sid,
               to: c.to,
+              randomness: c.randomness,
               fen: c.fen,
               vid: c.vid,
               cadence: c.cadence,
@@ -530,6 +564,7 @@ export default {
           ) {
             let newChall = Object.assign({}, chall);
             newChall.type = this.classifyObject(chall);
+            newChall.randomness = chall.randomness;
             newChall.added = Date.now();
             let fromValues = Object.assign({}, this.people[chall.from]);
             delete fromValues["pages"]; //irrelevant in this context
@@ -563,28 +598,33 @@ export default {
         case "newgame": {
           // NOTE: it may be live or correspondance
           const game = data.data;
-          let locGame = this.games.find(g => g.id == game.id);
-          if (!locGame) {
-            let newGame = game;
-            newGame.type = this.classifyObject(game);
-            newGame.vname = this.getVname(game.vid);
-            if (!game.score)
-              //if new game from Hall
-              newGame.score = "*";
-            newGame.rids = [game.rid];
-            delete newGame["rid"];
-            this.games.push(newGame);
-            if (
-              (newGame.type == "live" && this.gdisplay == "corr") ||
-              (newGame.type == "corr" && this.gdisplay == "live")
-            ) {
-              document
-                .getElementById("btnG" + newGame.type)
-                .classList.add("somethingnew");
+          // Ignore games where I play (corr games)
+          if (game.players.every(p =>
+            p.sid != this.st.user.sid || p.id != this.st.user.id))
+          {
+            let locGame = this.games.find(g => g.id == game.id);
+            if (!locGame) {
+              let newGame = game;
+              newGame.type = this.classifyObject(game);
+              newGame.vname = this.getVname(game.vid);
+              if (!game.score)
+                //if new game from Hall
+                newGame.score = "*";
+              newGame.rids = [game.rid];
+              delete newGame["rid"];
+              this.games.push(newGame);
+              if (
+                (newGame.type == "live" && this.gdisplay == "corr") ||
+                (newGame.type == "corr" && this.gdisplay == "live")
+              ) {
+                document
+                  .getElementById("btnG" + newGame.type)
+                  .classList.add("somethingnew");
+              }
+            } else {
+              // Append rid (if not already in list)
+              if (!locGame.rids.includes(game.rid)) locGame.rids.push(game.rid);
             }
-          } else {
-            // Append rid (if not already in list)
-            if (!locGame.rids.includes(game.rid)) locGame.rids.push(game.rid);
           }
           break;
         }
@@ -626,6 +666,33 @@ export default {
       this.conn.addEventListener("close", this.socketCloseListener);
     },
     // Challenge lifecycle:
+    loadNewchallVariant: async function(cb) {
+      const vname = this.getVname(this.newchallenge.vid);
+      const vModule = await import("@/variants/" + vname + ".js");
+      this.newchallenge.V = vModule.VariantRules;
+      this.newchallenge.vname = vname;
+      if (cb)
+        cb();
+    },
+    trySetNewchallDiag: function() {
+      if (!this.newchallenge.fen) {
+        this.newchallenge.diag = "";
+        return;
+      }
+      // If vid > 0 then the variant is loaded (function above):
+      window.V = this.newchallenge.V;
+      if (
+        this.newchallenge.vid > 0 &&
+        this.newchallenge.fen &&
+        V.IsGoodFen(this.newchallenge.fen)
+      ) {
+        const parsedFen = V.ParseFen(this.newchallenge.fen);
+        this.newchallenge.diag = getDiagram({
+          position: parsedFen.position,
+          orientation: V.GetOppCol(parsedFen.turn)
+        });
+      }
+    },
     newChallenge: async function() {
       if (this.newchallenge.cadence.match(/^[0-9]+$/))
         this.newchallenge.cadence += "+0"; //assume minutes, no increment
@@ -649,9 +716,7 @@ export default {
         alert(error);
         return;
       }
-      const vname = this.getVname(this.newchallenge.vid);
-      const vModule = await import("@/variants/" + vname + ".js");
-      window.V = vModule.VariantRules;
+      window.V = this.newchallenge.V;
       error = checkChallenge(this.newchallenge);
       if (error) {
         alert(error);
@@ -659,6 +724,8 @@ export default {
       }
       // NOTE: "from" information is not required here
       let chall = Object.assign({}, this.newchallenge);
+      delete chall["V"];
+      delete chall["diag"];
       const finishAddChallenge = cid => {
         chall.id = cid || "c" + getRandString();
         // Remove old challenge if any (only one at a time of a given type):
@@ -680,7 +747,7 @@ export default {
         });
         // Add new challenge:
         chall.from = {
-          //decompose to avoid revealing email
+          // Decompose to avoid revealing email
           sid: this.st.user.sid,
           id: this.st.user.id,
           name: this.st.user.name
@@ -688,12 +755,20 @@ export default {
         chall.added = Date.now();
         // NOTE: vname and type are redundant (can be deduced from cadence + vid)
         chall.type = ctype;
-        chall.vname = vname;
+        chall.vname = this.newchallenge.vname;
         this.challenges.push(chall);
         // Remember cadence  + vid for quicker further challenges:
         localStorage.setItem("cadence", chall.cadence);
         localStorage.setItem("vid", chall.vid);
+        localStorage.setItem("randomness", chall.randomness);
         document.getElementById("modalNewgame").checked = false;
+        // Show the challenge if not on current display
+        if (
+          (ctype == "live" && this.cdisplay == "corr") ||
+          (ctype == "corr" && this.cdisplay == "live")
+        ) {
+          this.setDisplay('c', ctype);
+        }
       };
       if (ctype == "live") {
         // Live challenges have a random ID
@@ -705,7 +780,28 @@ export default {
         });
       }
     },
-    clickChallenge: function(c) {
+    // Callback function after a diagram was showed to accept
+    // or refuse targetted challenge:
+    decisionChallenge: function(accepted) {
+      this.curChallToAccept.accepted = accepted;
+      this.finishProcessingChallenge(this.curChallToAccept);
+      document.getElementById("modalAccept").checked = false;
+    },
+    finishProcessingChallenge: function(c) {
+      if (c.accepted) {
+        c.seat = {
+          // Again, avoid c.seat = st.user to not reveal email
+          sid: this.st.user.sid,
+          id: this.st.user.id,
+          name: this.st.user.name
+        };
+        this.launchGame(c);
+      } else {
+        this.send("refusechallenge", { data: c.id, target: c.from.sid });
+      }
+      this.send("deletechallenge", { data: c.id });
+    },
+    clickChallenge: async function(c) {
       const myChallenge =
         c.from.sid == this.st.user.sid || //live
         (this.st.user.id > 0 && c.from.id == this.st.user.id); //corr
@@ -715,25 +811,31 @@ export default {
           return;
         }
         c.accepted = true;
+        const vModule = await import("@/variants/" + c.vname + ".js");
+        window.V = vModule.VariantRules;
         if (c.to) {
-          //c.to == this.st.user.name (connected)
-          // TODO: if special FEN, show diagram after loading variant
-          c.accepted = confirm("Accept challenge?");
-        }
-        if (c.accepted) {
-          c.seat = {
-            //again, avoid c.seat = st.user to not reveal email
-            sid: this.st.user.sid,
-            id: this.st.user.id,
-            name: this.st.user.name
-          };
-          this.launchGame(c);
-        } else {
-          this.send("refusechallenge", { data: c.id, target: c.from.sid });
+          // c.to == this.st.user.name (connected)
+          if (c.fen) {
+            const parsedFen = V.ParseFen(c.fen);
+            c.mycolor = V.GetOppCol(parsedFen.turn);
+            this.tchallDiag = getDiagram({
+              position: parsedFen.position,
+              orientation: c.mycolor
+            });
+            this.curChallToAccept = c;
+            document.getElementById("modalAccept").checked = true;
+          }
+          else {
+            if (!confirm(this.st.tr["Accept challenge?"]))
+              c.accepted = false;
+            this.finishProcessingChallenge(c);
+          }
         }
-        this.send("deletechallenge", { data: c.id });
-      } //my challenge
+        else
+          this.finishProcessingChallenge(c);
+      }
       else {
+        // My challenge
         if (c.type == "corr") {
           ajax("/challenges", "DELETE", { id: c.id });
         }
@@ -743,14 +845,15 @@ export default {
       ArrayFun.remove(this.challenges, ch => ch.id == c.id);
     },
     // NOTE: when launching game, the challenge is already being deleted
-    launchGame: async function(c) {
-      const vModule = await import("@/variants/" + c.vname + ".js");
-      window.V = vModule.VariantRules;
+    launchGame: function(c) {
       // These game informations will be shared
       let gameInfo = {
         id: getRandString(),
-        fen: c.fen || V.GenRandInitFen(),
-        players: shuffle([c.from, c.seat]), //white then black
+        fen: c.fen || V.GenRandInitFen(c.randomness),
+        // White player index 0, black player index 1:
+        players: c.mycolor
+          ? (c.mycolor == "w" ? [c.seat, c.from] : [c.from, c.seat])
+          : shuffle([c.from, c.seat]),
         vid: c.vid,
         cadence: c.cadence
       };
@@ -800,8 +903,8 @@ export default {
       GameStorage.add(game, (err) => {
         // If an error occurred, game is not added: abort
         if (!err) {
-          if (this.st.settings.sound >= 1)
-            new Audio("/sounds/newgame.mp3").play().catch(() => {});
+          if (this.st.settings.sound)
+            new Audio("/sounds/newgame.flac").play().catch(() => {});
           this.$router.push("/game/" + gameInfo.id);
         }
       });
@@ -818,7 +921,7 @@ export default {
   padding: 15px 0
   max-width: 430px
 
-#newgameDiv > .card
+#newgameDiv > .card, #acceptDiv > .card
   max-width: 767px
   max-height: 100%
 
@@ -871,6 +974,24 @@ button.player-action
 .tabbtn
   background-color: #f9faee
 
+button.acceptBtn
+  background-color: lightgreen
+button.refuseBtn
+  background-color: red
+
+#buttonsTchall
+  margin-top: 10px
+
+.variantName
+  font-weight: bold
+
+.diagram
+  margin: 0 auto
+  max-width: 400px
+
+#inputFen
+  width: 100%
+
 #div2, #div3
   margin-top: 15px
 @media screen and (max-width: 767px)