From 725da57f8e2983d744629b524f9084516a43cbac Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 20 Feb 2020 18:21:47 +0100
Subject: [PATCH] Modal to accept/refuse challenge + diagrams preview when
 creating challenge

---
 client/src/components/ContactForm.vue |  17 ++-
 client/src/components/Language.vue    |  13 +--
 client/src/components/UpsertUser.vue  |   2 +-
 client/src/main.js                    |   2 +-
 client/src/translations/en.js         |   3 +
 client/src/translations/es.js         |   3 +
 client/src/translations/fr.js         |   3 +
 client/src/views/Analyse.vue          |  12 +-
 client/src/views/Game.vue             |  52 +++++----
 client/src/views/Hall.vue             | 157 +++++++++++++++++++++-----
 10 files changed, 189 insertions(+), 75 deletions(-)

diff --git a/client/src/components/ContactForm.vue b/client/src/components/ContactForm.vue
index 9046e0a7..cadbf73d 100644
--- a/client/src/components/ContactForm.vue
+++ b/client/src/components/ContactForm.vue
@@ -10,15 +10,14 @@ div
   )
     .card
       label.modal-close(for="modalContact")
-      form(@submit.prevent="trySendMessage()" @keyup.enter="trySendMessage()")
-        fieldset
-          label(for="userEmail") {{ st.tr["Email"] }}
-          input#userEmail(type="email")
-        fieldset
-          label(for="mailSubject") {{ st.tr["Subject"] }}
-          input#mailSubject(type="text")
-        fieldset
-          textarea#mailContent(:placeholder="st.tr['Your message']")
+      fieldset
+        label(for="userEmail") {{ st.tr["Email"] }}
+        input#userEmail(type="email")
+      fieldset
+        label(for="mailSubject") {{ st.tr["Subject"] }}
+        input#mailSubject(type="text")
+      fieldset
+        textarea#mailContent(:placeholder="st.tr['Your message']")
       button(@click="trySendMessage()") {{ st.tr["Send"] }}
       #dialog.text-center {{ st.tr[infoMsg] }}
 </template>
diff --git a/client/src/components/Language.vue b/client/src/components/Language.vue
index 633f89dd..f49a592e 100644
--- a/client/src/components/Language.vue
+++ b/client/src/components/Language.vue
@@ -13,13 +13,12 @@ div
   )
     .card
       label.modal-close(for="modalLang")
-      form(@change="setLanguage($event)")
-        fieldset
-          label(for="langSelect") {{ st.tr["Language"] }}
-          select#langSelect
-            each language,langCode in langName
-              option(value=langCode)
-                =language
+      fieldset(@change="setLanguage($event)")
+        label(for="langSelect") {{ st.tr["Language"] }}
+        select#langSelect
+          each language,langCode in langName
+            option(value=langCode)
+              =language
 </template>
 
 <script>
diff --git a/client/src/components/UpsertUser.vue b/client/src/components/UpsertUser.vue
index eb8adffc..b5c6fbe4 100644
--- a/client/src/components/UpsertUser.vue
+++ b/client/src/components/UpsertUser.vue
@@ -11,7 +11,7 @@ div
     .card
       label.modal-close(for="modalUser")
       h3.section {{ st.tr[stage] }}
-      form(@submit.prevent="onSubmit()" @keyup.enter="onSubmit()")
+      div(@keyup.enter="onSubmit()")
         div(v-show="stage!='Login'")
           fieldset
             label(for="username") {{ st.tr["User name"] }}
diff --git a/client/src/main.js b/client/src/main.js
index db70c3e1..70b7d78a 100644
--- a/client/src/main.js
+++ b/client/src/main.js
@@ -18,7 +18,7 @@ new Vue({
       if (e.code === "Escape") {
         let modalBoxes = document.querySelectorAll("[id^='modal']");
         modalBoxes.forEach(m => {
-          if (m.checked && m.id != "modalWelcome") m.checked = false;
+          if (m.checked && m.id != "modalAccept") m.checked = false;
         });
       }
     });
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 70f8969d..ef9f291e 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -2,6 +2,7 @@ export const translations = {
   Abort: "Abort",
   About: "About",
   "Accept draw?": "Accept draw?",
+  "Accept challenge?": "Accept challenge?",
   All: "All",
   Analyse: "Analyse",
   "Any player": "Any player",
@@ -81,6 +82,7 @@ export const translations = {
   "Processing... Please wait": "Processing... Please wait",
   Problems: "Problems",
   "participant(s):": "participant(s):",
+  Refuse: "Refuse",
   Register: "Register",
   "Registration complete! Please check your emails": "Registration complete! Please check your emails",
   "Remove game?": "Remove game?",
@@ -112,6 +114,7 @@ export const translations = {
   "White win": "White win",
   "Who's there?": "Who's there?",
   With: "With",
+  with: "with",
   "Write news": "Write news",
   "Wrong time control": "Wrong time control",
   "Your message": "Your message",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 3c79b660..e51720a4 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -2,6 +2,7 @@ export const translations = {
   Abort: "Terminar",
   About: "Acerca de",
   "Accept draw?": "¿Acceptar tablas?",
+  "Accept challenge?": "¿Acceptar el desafío?",
   All: "Todos",
   Analyse: "Analizar",
   "Any player": "Cualquier jugador",
@@ -81,6 +82,7 @@ export const translations = {
   "Processing... Please wait": "Procesando... por favor espere",
   Problems: "Problemas",
   "participant(s):": "participante(s):",
+  Refuse: "Rechazar",
   Register: "Registrarse",
   "Registration complete! Please check your emails": "¡Registro completo! Por favor revise sus correos electrónicos",
   "Remove game?": "¿Eliminar la partida?",
@@ -112,6 +114,7 @@ export const translations = {
   "White win": "Las blancas gagnan",
   "Who's there?": "¿Quién está ahí?",
   With: "Con",
+  with: "con",
   "Write news": "Escribir una news",
   "Wrong time control": "Cadencia errónea",
   "Your message": "Tu mensaje",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 33503d30..d3533fcb 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -2,6 +2,7 @@ export const translations = {
   Abort: "Arrêter",
   About: "À propos",
   "Accept draw?": "Accepter la nulle ?",
+  "Accept challenge?": "Accepter le défi ?",
   All: "Tous",
   Analyse: "Analyser",
   "Any player": "N'importe qui",
@@ -81,6 +82,7 @@ export const translations = {
   "Processing... Please wait": "Traitement en cours... Attendez SVP",
   Problems: "Problèmes",
   "participant(s):": "participant(s) :",
+  Refuse: "Refuser",
   Register: "S'enregistrer",
   "Registration complete! Please check your emails": "Enregistrement terminé ! Allez voir vos emails",
   "Remove game?": "Supprimer la partie ?",
@@ -112,6 +114,7 @@ export const translations = {
   "White win": "Les blancs gagnent",
   "Who's there?": "Qui est là ?",
   With: "Avec",
+  with: "avec",
   "Write news": "Écrire une news",
   "Wrong time control": "Cadence erronée",
   "Your message": "Votre message",
diff --git a/client/src/views/Analyse.vue b/client/src/views/Analyse.vue
index d1c6d593..23515c67 100644
--- a/client/src/views/Analyse.vue
+++ b/client/src/views/Analyse.vue
@@ -5,9 +5,8 @@ main
       .text-center
         input#fen(
           v-model="curFen"
-          @input="adjustFenSize()"
+          @input="adjustFenSize(); tryGotoFen()"
         )
-        button(@click="gotoFen()") {{ st.tr["Go"] }}
   BaseGame(
     :game="game"
     :vr="vr"
@@ -73,9 +72,12 @@ export default {
       let fenInput = document.getElementById("fen");
       fenInput.style.width = this.curFen.length + "ch";
     },
-    gotoFen: function() {
-      this.gameRef.fen = this.curFen;
-      this.loadGame();
+    tryGotoFen: function() {
+      if (V.IsGoodFen(this.curFen))
+      {
+        this.gameRef.fen = this.curFen;
+        this.loadGame();
+      }
     }
   }
 };
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index 90a9a9fa..1294d50d 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -513,6 +513,29 @@ export default {
             }
           }
         }
+        this.repeat = {}; //reset: scan past moves' FEN:
+        let repIdx = 0;
+        // NOTE: vr_tmp to obtain FEN strings is redundant with BaseGame
+        let vr_tmp = new V(game.fenStart);
+        let movesCount = -1;
+        let curTurn = "n";
+        game.moves.forEach(m => {
+          if (vr_tmp.turn != curTurn)
+          {
+            movesCount++;
+            curTurn = vr_tmp.turn;
+          }
+          vr_tmp.play(m);
+          const fenObj = V.ParseFen(vr_tmp.getFen());
+          repIdx = fenObj.position + "_" + fenObj.turn;
+          if (fenObj.flags) repIdx += "_" + fenObj.flags;
+          this.repeat[repIdx] = this.repeat[repIdx]
+            ? this.repeat[repIdx] + 1
+            : 1;
+        });
+        if (vr_tmp.turn != curTurn)
+          movesCount++;
+        if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep";
         this.game = Object.assign(
           {},
           game,
@@ -524,7 +547,8 @@ export default {
             // opponent sid not strictly required (or available), but easier
             // at least oppsid or oppid is available anyway:
             oppsid: myIdx < 0 ? undefined : game.players[1 - myIdx].sid,
-            oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].uid
+            oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].uid,
+            movesCount: movesCount,
           }
         );
         this.re_setClocks();
@@ -533,20 +557,6 @@ export default {
           // Did lastate arrive before game was rendered?
           if (this.lastate) this.processLastate();
         });
-        this.repeat = {}; //reset: scan past moves' FEN:
-        let repIdx = 0;
-        // NOTE: vr_tmp to obtain FEN strings is redundant with BaseGame
-        let vr_tmp = new V(game.fenStart);
-        game.moves.forEach(m => {
-          vr_tmp.play(m);
-          const fenObj = V.ParseFen(vr_tmp.getFen());
-          repIdx = fenObj.position + "_" + fenObj.turn;
-          if (fenObj.flags) repIdx += "_" + fenObj.flags;
-          this.repeat[repIdx] = this.repeat[repIdx]
-            ? this.repeat[repIdx] + 1
-            : 1;
-        });
-        if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep";
         if (callback) callback();
       };
       if (game) {
@@ -563,7 +573,7 @@ export default {
       }
     },
     re_setClocks: function() {
-      if (this.game.moves.length < 2 || this.game.score != "*") {
+      if (this.game.movesCount < 2 || this.game.score != "*") {
         // 1st move not completed yet, or game over: freeze time
         this.virtualClocks = this.game.clocks.map(s => ppt(s));
         return;
@@ -632,10 +642,9 @@ export default {
       let addTime = 0;
       if (move.color == this.game.mycolor) {
         if (this.drawOffer == "received")
-          //I refuse draw
+          // I refuse draw
           this.drawOffer = "";
-        if (this.game.moves.length >= 2) {
-          //after first move
+        if (this.game.movesCount >= 2) {
           const elapsed = Date.now() - this.game.initime[colorIdx];
           // elapsed time is measured in milliseconds
           addTime = this.game.increment - elapsed / 1000;
@@ -649,13 +658,14 @@ export default {
         move.addTime = addTime;
       } else addTime = move.addTime; //supposed transmitted
       // Update current game object:
+      if (nextIdx != colorIdx)
+        this.game.movesCount++;
       this.game.moves.push(move);
       this.game.fen = move.fen;
       this.game.clocks[colorIdx] += addTime;
       // move.initime is set only when I receive a "lastate" move from opponent
       this.game.initime[nextIdx] = move.initime || Date.now();
-      //if (colorIdx != nextIdx)
-        this.re_setClocks();
+      this.re_setClocks();
       // If repetition detected, consider that a draw offer was received:
       const fenObj = V.ParseFen(move.fen);
       let repIdx = fenObj.position + "_" + fenObj.turn;
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index 82bd769b..a39cd548 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -8,6 +8,17 @@ main
     .card.text-center
       label.modal-close(for="modalInfo")
       p(v-html="infoMessage")
+  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")
   div#newgameDiv(
     role="dialog"
@@ -15,10 +26,13 @@ main
   )
     .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"
@@ -43,11 +57,13 @@ 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"
@@ -131,6 +147,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 +171,17 @@ 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") || "",
+        // 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,6 +197,8 @@ 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: {
@@ -185,6 +211,8 @@ export default {
     }
   },
   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)
@@ -630,6 +658,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
@@ -653,9 +708,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);
@@ -684,7 +737,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
@@ -692,7 +745,7 @@ 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);
@@ -709,7 +762,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
@@ -719,25 +793,30 @@ 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);
+            this.tchallDiag = getDiagram({
+              position: parsedFen.position,
+              orientation: V.GetOppCol(parsedFen.turn)
+            });
+            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 });
         }
@@ -747,9 +826,7 @@ 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(),
@@ -822,7 +899,7 @@ export default {
   padding: 15px 0
   max-width: 430px
 
-#newgameDiv > .card
+#newgameDiv > .card, #acceptDiv > .card
   max-width: 767px
   max-height: 100%
 
@@ -875,6 +952,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)
-- 
2.44.0