From d9a7a1e40254bda6e545514596a7363048c084f9 Mon Sep 17 00:00:00 2001 From: Benjamin Auder 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"] }} @@ -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