From 49dad26138d3dee0cacbb94ad8d3d3eff12c477a Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 30 Mar 2020 01:55:54 +0200
Subject: [PATCH] First draft of arrows + circles on board. Fix multi-connect
 detection

---
 client/public/images/circle.svg    |  56 ++++++
 client/public/images/mark.svg      |  59 +++++-
 client/src/App.vue                 |  12 +-
 client/src/components/BaseGame.vue |  27 ++-
 client/src/components/Board.vue    | 300 +++++++++++++++++++++++------
 client/src/components/MoveList.vue |   3 +
 client/src/views/Game.vue          | 105 +++++-----
 client/src/views/Hall.vue          |  27 +--
 server/sockets.js                  |  38 +---
 9 files changed, 463 insertions(+), 164 deletions(-)
 create mode 100644 client/public/images/circle.svg

diff --git a/client/public/images/circle.svg b/client/public/images/circle.svg
new file mode 100644
index 00000000..970670fd
--- /dev/null
+++ b/client/public/images/circle.svg
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="200"
+   height="200"
+   viewBox="-100 -100 200 200"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="circle.svg"
+   inkscape:version="0.92.4 5da689c313, 2019-01-14">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="960"
+     inkscape:window-height="1060"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="1.18"
+     inkscape:cx="101.69492"
+     inkscape:cy="100"
+     inkscape:window-x="0"
+     inkscape:window-y="20"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg4" />
+  <circle
+     r="91.875"
+     id="circle2"
+     cx="0"
+     cy="0"
+     style="fill:none;stroke:#5f0e78;stroke-width:15.75px" />
+</svg>
diff --git a/client/public/images/mark.svg b/client/public/images/mark.svg
index 3ee8e351..b7140678 100644
--- a/client/public/images/mark.svg
+++ b/client/public/images/mark.svg
@@ -1,5 +1,56 @@
-<?xml version="1.0" standalone="yes"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Mark possible moves - vchess" viewbox="0 0 599 599" width="600" height="600">
-	<circle id="mark_circle" fill="#90c" cx="300" cy="300" r="200" />
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="Mark possible moves - vchess"
+   viewbox="0 0 599 599"
+   width="600"
+   height="600"
+   sodipodi:docname="mark.svg"
+   inkscape:version="0.92.4 5da689c313, 2019-01-14">
+  <metadata
+     id="metadata8">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs6" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="960"
+     inkscape:window-height="1060"
+     id="namedview4"
+     showgrid="false"
+     inkscape:zoom="0.39333333"
+     inkscape:cx="305.08475"
+     inkscape:cy="300"
+     inkscape:window-x="0"
+     inkscape:window-y="20"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="Mark possible moves - vchess" />
+  <circle
+     id="mark_circle"
+     cx="300"
+     cy="300"
+     r="180"
+     style="fill:#9900cc;stroke-width:0.89999998" />
 </svg>
diff --git a/client/src/App.vue b/client/src/App.vue
index 369391e7..a961d7fc 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -336,18 +336,24 @@ div.board12
 img.piece
   width: 100%
 
-img.piece, img.mark-square
+img.piece, img.mark-square, img.circle-square
   max-width: 100%
   height: auto
   display: block
 
 img.mark-square
-  opacity: 0.6
+  opacity: .7
   width: 76%
   position: absolute
   top: 12%
   left: 12%
-  opacity: .7
+
+img.circle-square
+  opacity: 0.7
+  width: 100%
+  position: absolute
+  top: 0
+  left: 0
 
 .in-shadow
   filter: brightness(50%)
diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue
index 1ec412e1..c3387e82 100644
--- a/client/src/components/BaseGame.vue
+++ b/client/src/components/BaseGame.vue
@@ -54,6 +54,7 @@ div#baseGame
         @showrules="showRules"
         @analyze="analyzePosition"
         @goto-move="gotoMove"
+        @reset-arrows="resetArrows"
       )
     .clearer
 </template>
@@ -186,6 +187,10 @@ export default {
       if (e.deltaY < 0) this.undo();
       else if (e.deltaY > 0) this.play();
     },
+    resetArrows: function() {
+      // TODO: make arrows scale with board, and remove this
+      this.$refs["board"].cancelResetArrows();
+    },
     showRules: function() {
       //this.$router.push("/variants/" + this.game.vname);
       window.open("#/variants/" + this.game.vname, "_blank"); //better
@@ -250,11 +255,8 @@ export default {
         this.game.vname +
         "/?fen=" +
         this.vr.getFen().replace(/ /g, "_");
-      if (this.game.mycolor)
-        newUrl += "&side=" + this.game.mycolor;
-      // Open in same tab in live games (against cheating)
-      if (this.game.type == "live") this.$router.push(newUrl);
-      else window.open("#" + newUrl);
+      if (!!this.game.mycolor) newUrl += "&side=" + this.game.mycolor;
+      window.open("#" + newUrl);
     },
     download: function() {
       const content = this.getPgn();
@@ -279,15 +281,22 @@ export default {
       pgn += '\n';
       for (let i = 0; i < this.moves.length; i += 2) {
         if (i > 0) pgn += " ";
-        pgn += (i/2+1) + "." + getFullNotation(this.moves[i]);
+        // Adjust dots notation for a better display:
+        let fullNotation = getFullNotation(this.moves[i]);
+        if (fullNotation == "...") fullNotation = "..";
+        pgn += (i/2+1) + "." + fullNotation;
         if (i+1 < this.moves.length)
           pgn += " " + getFullNotation(this.moves[i+1]);
       }
       pgn += "\n\n";
       for (let i = 0; i < this.moves.length; i += 2) {
-        pgn += getFullNotation(this.moves[i], "unambiguous") + "\n";
-        if (i+1 < this.moves.length)
-          pgn += getFullNotation(this.moves[i+1], "unambiguous") + "\n";
+        const moveNumber = i / 2 + 1;
+        pgn += moveNumber + "." + i + " " +
+          getFullNotation(this.moves[i], "unambiguous") + "\n";
+        if (i+1 < this.moves.length) {
+          pgn += moveNumber + "." + (i+1) + " " +
+            getFullNotation(this.moves[i+1], "unambiguous") + "\n";
+        }
       }
       return pgn;
     },
diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue
index 80a79a4a..83dc8630 100644
--- a/client/src/components/Board.vue
+++ b/client/src/components/Board.vue
@@ -24,6 +24,10 @@ export default {
       choices: [], //promotion pieces, or checkered captures... (as moves)
       selectedPiece: null, //moving piece (or clicked piece)
       start: null, //pixels coordinates + id of starting square (click or drag)
+      startArrow: null,
+      movingArrow: { x: -1, y: -1 },
+      arrows: [], //object of {start: x,y / end: x,y}
+      circles: {}, //object of squares' ID --> true (TODO: use a set?)
       click: "",
       clickTime: 0,
       settings: store.state.settings
@@ -104,6 +108,7 @@ export default {
           },
           [...Array(sizeY).keys()].map(j => {
             const cj = orientation == "w" ? j : sizeY - j - 1;
+            const squareId = "sq-" + ci + "-" + cj;
             let elems = [];
             if (showPiece(ci, cj)) {
               elems.push(
@@ -112,7 +117,7 @@ export default {
                     piece: true,
                     ghost:
                       !!this.selectedPiece &&
-                      this.selectedPiece.parentNode.id == "sq-" + ci + "-" + cj
+                      this.selectedPiece.parentNode.id == squareId
                   },
                   attrs: {
                     src:
@@ -140,6 +145,18 @@ export default {
                 })
               );
             }
+            if (!!this.circles[squareId]) {
+              elems.push(
+                h("img", {
+                  "class": {
+                    "circle-square": true
+                  },
+                  attrs: {
+                    src: "/images/circle.svg"
+                  }
+                })
+              );
+            }
             const lightSquare = (ci + cj) % 2 == lightSquareMod;
             return h(
               "div",
@@ -363,6 +380,89 @@ export default {
       );
       elementArray.unshift(choices);
     }
+    if (
+      !this.mobileBrowser &&
+      (this.arrows.length > 0 || this.movingArrow.x >= 0)
+    ) {
+      let svgArrows = [];
+      this.arrows.forEach(a => {
+        svgArrows.push(
+          h(
+            "path",
+            {
+              "class": { "svg-arrow": true },
+              attrs: {
+                d: (
+                  "M" + a.start.x + "," + a.start.y + " " +
+                  "L" + a.end.x + "," + a.end.y
+                )
+              }
+            }
+          )
+        );
+      });
+      if (this.movingArrow.x >= 0) {
+        svgArrows.push(
+          h(
+            "path",
+            {
+              "class": { "svg-arrow": true },
+              attrs: {
+                d: (
+                  "M" + this.startArrow.x + "," + this.startArrow.y + " " +
+                  "L" + this.movingArrow.x + "," + this.movingArrow.y
+                )
+              }
+            }
+          )
+        );
+      }
+      // Add SVG element for drawing arrows
+      elementArray.push(
+        h(
+          "svg",
+          {
+            attrs: {
+              id: "arrowCanvas",
+              stroke: "none"
+            }
+          },
+          [
+            h(
+              "defs",
+              {},
+              [
+                h(
+                  "marker",
+                  {
+                    attrs: {
+                      id: "arrow",
+                      markerWidth: "2",
+                      markerHeight: "2",
+                      markerUnits: "strokeWidth",
+                      refX: "0",
+                      refY: "1",
+                      orient: "auto"
+                    }
+                  },
+                  [
+                    h(
+                      "path",
+                      {
+                        attrs: {
+                          d: "M0,0 L0,2 L2,1 z",
+                          style: "fill: blue"
+                        }
+                      }
+                    )
+                  ]
+                )
+              ]
+            )
+          ].concat(svgArrows)
+        )
+      );
+    }
     let onEvents = {};
     // NOTE: click = mousedown + mouseup
     if (this.mobileBrowser) {
@@ -378,80 +478,132 @@ export default {
         on: {
           mousedown: this.mousedown,
           mousemove: this.mousemove,
-          mouseup: this.mouseup
+          mouseup: this.mouseup,
+          contextmenu: this.blockContextMenu
         }
       };
     }
     return h("div", onEvents, elementArray);
   },
   methods: {
+    blockContextMenu: function(e) {
+      e.preventDefault();
+      e.stopPropagation();
+      return false;
+    },
+    cancelResetArrows: function() {
+      this.startArrow = null;
+      this.arrows = [];
+      this.circles = {};
+    },
     mousedown: function(e) {
+      if (!([1, 3].includes(e.which))) return;
       e.preventDefault();
-      if (!this.start) {
-        // NOTE: classList[0] is enough: 'piece' is the first assigned class
-        const withPiece = e.target.classList[0] == "piece";
-        // Emit the click event which could be used by some variants
-        this.$emit(
-          "click-square",
-          getSquareFromId(withPiece ? e.target.parentNode.id : e.target.id)
-        );
-        // Start square must contain a piece.
-        if (!withPiece) return;
-        let parent = e.target.parentNode; //surrounding square
-        // Show possible moves if current player allowed to play
-        const startSquare = getSquareFromId(parent.id);
-        this.possibleMoves = [];
-        const color = this.analyze ? this.vr.turn : this.userColor;
-        if (this.vr.canIplay(color, startSquare))
-          this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare);
-        // For potential drag'n drop, remember start coordinates
-        // (to center the piece on mouse cursor)
-        let rect = parent.getBoundingClientRect();
-        this.start = {
+      if (e.which != 3)
+        // Cancel current drawing and circles, if any
+        this.cancelResetArrows();
+      if (e.which == 1 || this.mobileBrowser) {
+        // Mouse left button
+        if (!this.start) {
+          // NOTE: classList[0] is enough: 'piece' is the first assigned class
+          const withPiece = (e.target.classList[0] == "piece");
+          // Emit the click event which could be used by some variants
+          this.$emit(
+            "click-square",
+            getSquareFromId(withPiece ? e.target.parentNode.id : e.target.id)
+          );
+          // Start square must contain a piece.
+          if (!withPiece) return;
+          let parent = e.target.parentNode; //surrounding square
+          // Show possible moves if current player allowed to play
+          const startSquare = getSquareFromId(parent.id);
+          this.possibleMoves = [];
+          const color = this.analyze ? this.vr.turn : this.userColor;
+          if (this.vr.canIplay(color, startSquare))
+            this.possibleMoves = this.vr.getPossibleMovesFrom(startSquare);
+          // For potential drag'n drop, remember start coordinates
+          // (to center the piece on mouse cursor)
+          const rect = parent.getBoundingClientRect();
+          this.start = {
+            x: rect.x + rect.width / 2,
+            y: rect.y + rect.width / 2,
+            id: parent.id
+          };
+          // Add the moving piece to the board, just after current image
+          this.selectedPiece = e.target.cloneNode();
+          Object.assign(
+            this.selectedPiece.style,
+            {
+              position: "absolute",
+              top: 0,
+              display: "inline-block",
+              zIndex: 3000
+            }
+          );
+          parent.insertBefore(this.selectedPiece, e.target.nextSibling);
+        } else {
+          this.processMoveAttempt(e);
+        }
+      } else {
+        // e.which == 3 : mouse right button
+        let elem = e.target;
+        // Next loop because of potential marks
+        while (elem.tagName == "IMG") elem = elem.parentNode;
+        // To center the arrow in square:
+        const rect = elem.getBoundingClientRect();
+        this.startArrow = {
           x: rect.x + rect.width / 2,
           y: rect.y + rect.width / 2,
-          id: parent.id
+          id: elem.id
         };
-        // Add the moving piece to the board, just after current image
-        this.selectedPiece = e.target.cloneNode();
+      }
+    },
+    mousemove: function(e) {
+      if (!this.selectedPiece && !this.startArrow) return;
+      e.preventDefault();
+      if (!!this.selectedPiece) {
+        // There is an active element: move it around
+        const [offsetX, offsetY] =
+          this.mobileBrowser
+            ? [e.changedTouches[0].pageX, e.changedTouches[0].pageY]
+            : [e.clientX, e.clientY];
         Object.assign(
           this.selectedPiece.style,
           {
-            position: "absolute",
-            top: 0,
-            display: "inline-block",
-            zIndex: 3000
+            left: offsetX - this.start.x + "px",
+            top: offsetY - this.start.y + "px"
           }
         );
-        parent.insertBefore(this.selectedPiece, e.target.nextSibling);
-      } else {
-        this.processMoveAttempt(e);
       }
-    },
-    mousemove: function(e) {
-      if (!this.selectedPiece) return;
-      e.preventDefault();
-      // There is an active element: move it around
-      const [offsetX, offsetY] =
-        this.mobileBrowser
-          ? [e.changedTouches[0].pageX, e.changedTouches[0].pageY]
-          : [e.clientX, e.clientY];
-      Object.assign(
-        this.selectedPiece.style,
-        {
-          left: offsetX - this.start.x + "px",
-          top: offsetY - this.start.y + "px"
+      else {
+        let elem = e.target;
+        // Next loop because of potential marks
+        while (elem.tagName == "IMG") elem = elem.parentNode;
+        // To center the arrow in square:
+        if (elem.id != this.startArrow.id) {
+          const rect = elem.getBoundingClientRect();
+          this.movingArrow = {
+            x: rect.x + rect.width / 2,
+            y: rect.y + rect.width / 2
+          };
         }
-      );
+      }
     },
     mouseup: function(e) {
-      if (!this.selectedPiece) return;
+      if (!([1, 3].includes(e.which))) return;
       e.preventDefault();
-      // Drag'n drop. Selected piece is no longer needed:
-      this.selectedPiece.parentNode.removeChild(this.selectedPiece);
-      delete this.selectedPiece;
-      this.selectedPiece = null;
-      this.processMoveAttempt(e);
+      if (e.which == 1) {
+        if (!this.selectedPiece) return;
+        // Drag'n drop. Selected piece is no longer needed:
+        this.selectedPiece.parentNode.removeChild(this.selectedPiece);
+        delete this.selectedPiece;
+        this.selectedPiece = null;
+        this.processMoveAttempt(e);
+      } else {
+        // Mouse right button (e.which == 3)
+        this.movingArrow = { x: -1, y: -1 };
+        this.processArrowAttempt(e);
+      }
     },
     processMoveAttempt: function(e) {
       // Obtain the move from start and end squares
@@ -482,6 +634,31 @@ export default {
       } else if (moves.length == 1) this.play(moves[0]);
       // else: forbidden move attempt
     },
+    processArrowAttempt: function(e) {
+      // Obtain the arrow from start and end squares
+      const [offsetX, offsetY] = [e.clientX, e.clientY];
+      let landing = document.elementFromPoint(offsetX, offsetY);
+      // Next condition: classList.contains(piece) fails because of marks
+      while (landing.tagName == "IMG") landing = landing.parentNode;
+      if (this.startArrow.id == landing.id)
+        // Draw (or erase) a circle
+        this.$set(this.circles, landing.id, !this.circles[landing.id]);
+      else {
+        // OK: add arrow, landing is a new square
+        const rect = landing.getBoundingClientRect();
+        this.arrows.push({
+          start: {
+            x: this.startArrow.x,
+            y: this.startArrow.y
+          },
+          end: {
+            x: rect.x + rect.width / 2,
+            y: rect.y + rect.width / 2
+          }
+        });
+      }
+      this.startArrow = null;
+    },
     findMatchingMoves: function(endSquare) {
       // Run through moves list and return the matching set (if promotions...)
       return (
@@ -538,6 +715,21 @@ img.ghost
   opacity: 0.5
   top: 0
 
+#arrowCanvas
+  pointer-events: none
+  position: absolute
+  top: 0
+  left: 0
+  width: 100%
+  height: 100%
+
+.svg-arrow
+  opacity: 0.65
+  stroke: #5f0e78
+  stroke-width: 10px
+  fill: none
+  marker-end: url(#arrow)
+
 .incheck-light
   background-color: rgba(204, 51, 0, 0.7) !important
 .incheck-dark
diff --git a/client/src/components/MoveList.vue b/client/src/components/MoveList.vue
index 6eaf43ce..d2ae3f35 100644
--- a/client/src/components/MoveList.vue
+++ b/client/src/components/MoveList.vue
@@ -149,6 +149,9 @@ export default {
     adjustBoard: function() {
       const boardContainer = document.getElementById("boardContainer");
       if (!boardContainer) return; //no board on page
+      let arrows = document.getElementById("arrowCanvas");
+      // TODO: arrows on board don't scale
+      if (!!arrows) this.$emit("reset-arrows");
       const k = document.getElementById("boardSize").value;
       const movesWidth = window.innerWidth >= 768 ? 280 : 0;
       const minBoardWidth = 240; //TODO: these 240 and 280 are arbitrary...
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index 19033f50..b77f6b0f 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -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"
@@ -186,8 +209,7 @@ export default {
       retrySendmove: null,
       clockUpdate: null,
       // Related to (killing of) self multi-connects:
-      newConnect: {},
-      killed: {}
+      newConnect: {}
     };
   },
   watch: {
@@ -317,7 +339,6 @@ export default {
       this.retrySendmove = null;
       this.clockUpdate = null;
       this.newConnect = {};
-      this.killed = {};
       // 1] Initialize connection
       this.connexionString =
         params.socketUrl +
@@ -522,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 };
@@ -552,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 = {
@@ -579,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;
         }
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index 0ed355bf..067a50db 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -257,8 +257,7 @@ export default {
       connexionString: "",
       socketCloseListener: 0,
       // Related to (killing of) self multi-connects:
-      newConnect: {},
-      killed: {}
+      newConnect: {}
     };
   },
   watch: {
@@ -638,13 +637,13 @@ export default {
           const page = data.page || "/";
           if (data.code == "connect") {
             // Ask challenges only on first connexion:
-            if (!this.people[data.from])
+            if (!this.people[data.from[0]])
               this.send("askchallenges", { target: data.from[0] });
           }
           // Ask game only if live:
           else if (!page.match(/\/[0-9]+$/))
             this.send("askgame", { target: data.from[0], page: page });
-          if (!this.people[data.from]) {
+          if (!this.people[data.from[0]]) {
             this.$set(
               this.people,
               data.from[0],
@@ -654,7 +653,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], page: page });
           } else {
             this.people[data.from[0]].tmpIds[data.from[1]] =
@@ -713,13 +713,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 = {
@@ -742,16 +735,16 @@ export default {
           this.$forceUpdate();
           // 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;
+              // I logged in elsewhere:
+              this.cleanBeforeDestroy();
+              alert(this.st.tr["New connexion detected: tab now offline"]);
             }
-            delete this.newConnect[user.sid];
           }
           break;
         }
diff --git a/server/sockets.js b/server/sockets.js
index e791293e..3a776d1d 100644
--- a/server/sockets.js
+++ b/server/sockets.js
@@ -90,36 +90,6 @@ module.exports = function(wss) {
           // When page changes:
           doDisconnect();
           break;
-        case "killme": {
-          // Self multi-connect: manual removal + disconnect
-          const doKill = (pg) => {
-            Object.keys(clients[pg][obj.sid]).forEach(x => {
-              send(clients[pg][obj.sid][x].socket, { code: "killed" });
-            });
-            delete clients[pg][obj.sid];
-          };
-          const disconnectFromOtherConnexion = (pg,code,o={}) => {
-            Object.keys(clients[pg]).forEach(k => {
-              if (k != obj.sid) {
-                Object.keys(clients[pg][k]).forEach(x => {
-                  send(
-                    clients[pg][k][x].socket,
-                    Object.assign({ code: code, from: obj.sid }, o)
-                  );
-                });
-              }
-            });
-          };
-          Object.keys(clients).forEach(pg => {
-            if (clients[pg][obj.sid]) {
-              doKill(pg);
-              disconnectFromOtherConnexion(pg, "disconnect");
-              if (pg.indexOf("/game/") >= 0 && clients["/"])
-                disconnectFromOtherConnexion("/", "gdisconnect", { page: pg });
-            }
-          });
-          break;
-        }
         case "pollclients": {
           // From Game
           let sockIds = {};
@@ -319,7 +289,13 @@ module.exports = function(wss) {
 
         case "getfocus":
         case "losefocus":
-          clients[page][sid][tmpId].focus = (obj.code == "getfocus");
+          if (
+            !!clients[page] &&
+            !!clients[page][sid] &&
+            !!clients[page][sid][tmpId]
+          ) {
+            clients[page][sid][tmpId].focus = (obj.code == "getfocus");
+          }
           if (page == "/") notifyRoom("/", obj.code, { page: "/" }, [sid]);
           else {
             // Notify game room + Hall:
-- 
2.44.0