From d9a7a1e40254bda6e545514596a7363048c084f9 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sun, 15 Mar 2020 11:26:23 +0100
Subject: [PATCH] Experimental news notification system + fix Eightpieces
 variant

---
 TODO                                  |  8 +++++++
 client/src/App.vue                    | 31 +++++++++++++++++++++++----
 client/src/components/ContactForm.vue | 12 -----------
 client/src/store.js                   |  3 +++
 client/src/utils/ajax.js              |  1 +
 client/src/variants/Eightpieces.js    | 28 ++++++++++++++++++++----
 client/src/views/News.vue             |  4 ++++
 server/db/create.sql                  |  3 ++-
 server/models/News.js                 | 14 ++++++++++++
 server/models/User.js                 | 11 ++++++++++
 server/routes/news.js                 | 20 ++++++++++-------
 server/routes/users.js                |  6 ++++++
 12 files changed, 112 insertions(+), 29 deletions(-)

diff --git a/TODO b/TODO
index 6916bdf3..f03eb182 100644
--- a/TODO
+++ b/TODO
@@ -1,3 +1,11 @@
+#Enhancements
+
+tabs "running" and  "completed" for MyGames page (default to running if any and my turn)
+"load more" option for completed games (act on corr games).
+
+"load more" option for problems as well: similar to news page.
+Also on corr challenges.
+
 # New variants
 Landing pieces from empty board:
 https://www.chessvariants.com/diffsetup.dir/unachess.html
diff --git a/client/src/App.vue b/client/src/App.vue
index d2f4845f..6a998080 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -34,10 +34,13 @@
     .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
       footer
         router-link.menuitem(to="/about") {{ st.tr["About"] }}
+        router-link.menuitem#newsMenu(to="/news") {{ st.tr["News"] }}
+        a.menuitem(href="https://discord.gg/a9ZFKBe")
+          span Discord
+          img(src="/images/icons/discord.svg")
         a.menuitem(href="https://github.com/yagu0/vchess")
           span {{ st.tr["Code"] }}
           img(src="/images/icons/github.svg")
-        router-link.menuitem(to="/news") {{ st.tr["News"] }}
         p.clickable(onClick="window.doClick('modalContact')")
           | {{ st.tr["Contact"] }}
 </template>
@@ -47,6 +50,7 @@ import ContactForm from "@/components/ContactForm.vue";
 import Settings from "@/components/Settings.vue";
 import UpsertUser from "@/components/UpsertUser.vue";
 import { store } from "@/store.js";
+import { ajax } from "@/utils/ajax.js";
 export default {
   components: {
     ContactForm,
@@ -54,9 +58,19 @@ export default {
     UpsertUser
   },
   data: function() {
-    return {
-      st: store.state
-    };
+    return { st: store.state };
+  },
+  mounted: function() {
+    ajax(
+      "/newsts",
+      "GET",
+      {
+        success: (res) => {
+          if (this.st.user.newsRead < res.timestamp)
+            document.getElementById("newsMenu").classList.add("somenews");
+        }
+      }
+    );
   },
   methods: {
     hideDrawer: function(e) {
@@ -258,6 +272,15 @@ footer
   footer
     border: none
 
+@media screen and (max-width: 420px)
+  footer
+    display: block
+
+.menuitem.somenews
+  color: red
+  &:link, &:visited, &:hover
+    color: red
+
 // Styles for diagrams and board (partial).
 // TODO: where to put that ?
 
diff --git a/client/src/components/ContactForm.vue b/client/src/components/ContactForm.vue
index 89c4702d..e5d27326 100644
--- a/client/src/components/ContactForm.vue
+++ b/client/src/components/ContactForm.vue
@@ -10,9 +10,6 @@ div
   )
     .card
       label.modal-close(for="modalContact")
-      a#discordLink(href="https://discord.gg/a9ZFKBe")
-        span {{ st.tr["Discord invitation"] }}
-        img(src="/images/icons/discord.svg")
       fieldset
         label(for="userEmail") {{ st.tr["Email"] }}
         input#userEmail(type="email" :value="st.user.email")
@@ -101,15 +98,6 @@ textarea#mailContent
   width: 100%
   min-height: 100px
 
-#discordLink
-  display: block
-  margin-top: 7px
-  text-align: center
-  & > img
-    height: 1.2em
-    display: inline-block
-    margin-left: 5px
-
 #dialog
   padding: 5px
   color: blue
diff --git a/client/src/store.js b/client/src/store.js
index 76210ee3..792d2a5b 100644
--- a/client/src/store.js
+++ b/client/src/store.js
@@ -45,6 +45,7 @@ export const store = {
       name: localStorage.getItem("myname") || "", //"" for "anonymous"
       email: "", //unknown yet
       notify: false, //email notifications
+      newsRead: localStorage.getItem("newsRead") || 0,
       sid: mysid
     };
     // Slow verification through the server:
@@ -77,6 +78,8 @@ export const store = {
         localStorage.removeItem("myname");
       this.state.user.email = json.email;
       this.state.user.notify = json.notify;
+      if (!!json.newsRead && json.newsRead > this.state.user.newsRead)
+        this.state.user.newsRead = json.newsRead;
     });
     // Settings initialized with values from localStorage
     const getItemDefaultTrue = (item) => {
diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js
index e43e9094..7d520ec8 100644
--- a/client/src/utils/ajax.js
+++ b/client/src/utils/ajax.js
@@ -13,6 +13,7 @@ function toQueryString(data) {
 // TODO: use this syntax https://stackoverflow.com/a/29823632 ?
 // data, success, error: optional
 export function ajax(url, method, options) {
+  options = options || {};
   const data = options.data || {};
   // By default, do nothing on success and print errors:
   if (!options.success)
diff --git a/client/src/variants/Eightpieces.js b/client/src/variants/Eightpieces.js
index 1b2e9a7d..beaddbe9 100644
--- a/client/src/variants/Eightpieces.js
+++ b/client/src/variants/Eightpieces.js
@@ -290,12 +290,14 @@ export const VariantRules = class EightpiecesRules extends ChessRules {
 
   getPotentialMovesFrom([x, y]) {
     // At subTurn == 2, jailers aren't effective (Jeff K)
+    const piece = this.getPiece(x, y);
+    const L = this.sentryPush.length;
     if (this.subTurn == 1) {
       const jsq = this.isImmobilized([x, y]);
       if (!!jsq) {
         let moves = [];
         // Special pass move if king:
-        if (this.getPiece(x, y) == V.KING) {
+        if (piece == V.KING) {
           moves.push(
             new Move({
               appear: [],
@@ -305,11 +307,26 @@ export const VariantRules = class EightpiecesRules extends ChessRules {
             })
           );
         }
+        else if (piece == V.LANCER && !!this.sentryPush[L-1]) {
+          // A pushed lancer next to the jailer: reorient
+          const color = this.getColor(x, y);
+          const curDir = this.board[x][y].charAt(1);
+          Object.keys(V.LANCER_DIRS).forEach(k => {
+            moves.push(
+              new Move({
+                appear: [{ x: x, y: y, c: color, p: k }],
+                vanish: [{ x: x, y: y, c: color, p: curDir }],
+                start: { x: x, y: y },
+                end: { x: jsq[0], y: jsq[1] }
+              })
+            );
+          });
+        }
         return moves;
       }
     }
     let moves = [];
-    switch (this.getPiece(x, y)) {
+    switch (piece) {
       case V.JAILER:
         moves = this.getPotentialJailerMoves([x, y]);
         break;
@@ -323,12 +340,15 @@ export const VariantRules = class EightpiecesRules extends ChessRules {
         moves = super.getPotentialMovesFrom([x, y]);
         break;
     }
-    const L = this.sentryPush.length;
     if (!!this.sentryPush[L-1]) {
-      // Delete moves walking back on sentry push path
+      // Delete moves walking back on sentry push path,
+      // only if not a pawn, and the piece is the pushed one.
+      const pl = this.sentryPush[L-1].length;
+      const finalPushedSq = this.sentryPush[L-1][pl-1];
       moves = moves.filter(m => {
         if (
           m.vanish[0].p != V.PAWN &&
+          m.start.x == finalPushedSq.x && m.start.y == finalPushedSq.y &&
           this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
         ) {
           return false;
diff --git a/client/src/views/News.vue b/client/src/views/News.vue
index 1aebbd34..ec1517df 100644
--- a/client/src/views/News.vue
+++ b/client/src/views/News.vue
@@ -70,6 +70,10 @@ export default {
     );
   },
   mounted: function() {
+    // Mark that I've read the news:
+    localStorage.setItem("newsRead", Date.now());
+    if (this.st.user.id > 0) ajax("/newsread", "PUT");
+    document.getElementById("newsMenu").classList.remove("somenews");
     document
       .getElementById("newnewsDiv")
       .addEventListener("click", processModalClick);
diff --git a/server/db/create.sql b/server/db/create.sql
index 8f3daa84..5aba8ff0 100644
--- a/server/db/create.sql
+++ b/server/db/create.sql
@@ -14,7 +14,8 @@ create table Users (
   loginTime datetime,
   sessionToken varchar,
   created datetime,
-  notify boolean
+  notify boolean,
+  newsRead datetime
 );
 
 create table Problems (
diff --git a/server/models/News.js b/server/models/News.js
index 12f4bf59..3fa3caae 100644
--- a/server/models/News.js
+++ b/server/models/News.js
@@ -38,6 +38,20 @@ const NewsModel =
     });
   },
 
+  getTimestamp: function(cb)
+  {
+    db.serialize(function() {
+      const query =
+        "SELECT added " +
+        "FROM News " +
+        "ORDER BY added DESC " +
+        "LIMIT 1";
+      db.get(query, (err,ts) => {
+        cb(err, ts);
+      });
+    });
+  },
+
   update: function(news)
   {
     db.serialize(function() {
diff --git a/server/models/User.js b/server/models/User.js
index 9b3049be..021cadcb 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -75,6 +75,17 @@ const UserModel =
     });
   },
 
+  setNewsRead: function(uid)
+  {
+    db.serialize(function() {
+      const query =
+        "UPDATE Users " +
+        "SET newsRead = " + Date.now() + " " +
+        "WHERE id = " + uid;
+      db.run(query);
+    });
+  },
+
   // Set session token only if empty (first login)
   // NOTE: weaker security (but avoid to re-login everywhere after each logout)
   // TODO: option would be to reset all tokens periodically, e.g. every 3 months
diff --git a/server/routes/news.js b/server/routes/news.js
index 80b91299..e1efbdd9 100644
--- a/server/routes/news.js
+++ b/server/routes/news.js
@@ -9,25 +9,30 @@ router.post("/news", access.logged, access.ajax, (req,res) => {
   {
     const content = sanitizeHtml(req.body.news.content);
     NewsModel.create(content, req.userId, (err,ret) => {
-      res.json(err || {id:ret.nid});
+      res.json(err || { id: ret.nid });
     });
   }
 });
 
 router.get("/news", access.ajax, (req,res) => {
   const cursor = req.query["cursor"];
-  if (cursor.match(/^[0-9]+$/))
-  {
+  if (cursor.match(/^[0-9]+$/)) {
     NewsModel.getNext(cursor, (err,newsList) => {
-      res.json(err || {newsList:newsList});
+      res.json(err || { newsList: newsList });
     });
   }
 });
 
+router.get("/newsts", access.ajax, (req,res) => {
+  // Special query for footer: just return timestamp of last news
+  NewsModel.getTimestamp((err,ts) => {
+    res.json(err || { timestamp: ts.added });
+  });
+});
+
 router.put("/news", access.logged, access.ajax, (req,res) => {
   let news = req.body.news;
-  if (devs.includes(req.userId) && news.id.toString().match(/^[0-9]+$/))
-  {
+  if (devs.includes(req.userId) && news.id.toString().match(/^[0-9]+$/)) {
     news.content = sanitizeHtml(news.content);
     NewsModel.update(news);
     res.json({});
@@ -36,8 +41,7 @@ router.put("/news", access.logged, access.ajax, (req,res) => {
 
 router.delete("/news", access.logged, access.ajax, (req,res) => {
   const nid = req.query.id;
-  if (devs.includes(req.userId) && nid.toString().match(/^[0-9]+$/))
-  {
+  if (devs.includes(req.userId) && nid.toString().match(/^[0-9]+$/)) {
     NewsModel.remove(nid);
     res.json({});
   }
diff --git a/server/routes/users.js b/server/routes/users.js
index 4e51ee13..389625c1 100644
--- a/server/routes/users.js
+++ b/server/routes/users.js
@@ -81,6 +81,12 @@ router.put('/update', access.logged, access.ajax, (req,res) => {
   }
 });
 
+// Special route to update newsRead timestamp:
+router.put('/newsread', access.logged, access.ajax, (req,res) => {
+  UserModel.setNewsRead(req.userId);
+  res.json({});
+});
+
 // Authentication-related methods:
 
 // to: object user (to who we send an email)
-- 
2.44.0