From: Benjamin Auder <benjamin.auder@somewhere> Date: Thu, 26 Mar 2020 17:56:02 +0000 (+0100) Subject: Add basic Desktop notifications for game start + new move + game over X-Git-Url: https://git.auder.net/variants/current/css/img/pieces/%7B%7B%20pkg.url%20%7D%7D?a=commitdiff_plain;h=f5768809ae96cf4565c0bc3d2747ffc206837e20;p=vchess.git Add basic Desktop notifications for game start + new move + game over --- diff --git a/TODO b/TODO index df4231ac..67912987 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,3 @@ -# Enhancements: -Desktop notifications -Do not remove other challenges when one is accepted? - # New variants Finish first https://www.chessvariants.com/mvopponent.dir/dynamo.html https://echekk.fr/spip.php?page=article&id_article=599 diff --git a/client/src/utils/notifications.js b/client/src/utils/notifications.js new file mode 100644 index 00000000..f849be7e --- /dev/null +++ b/client/src/utils/notifications.js @@ -0,0 +1,13 @@ +// https://developer.mozilla.org/en-US/docs/Web/API/notification +export function notify(title, options) { + if (Notification.permission === "granted") + new Notification(title, options); + else if (Notification.permission !== 'denied') { + Notification.requestPermission(function (permission) { + if(!('permission' in Notification)) + Notification.permission = permission; + if (permission === "granted") + var notification = new Notification(title, options); + }); + } +} diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 6ef161f3..6d37d1ed 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -131,6 +131,7 @@ import Chat from "@/components/Chat.vue"; import { store } from "@/store"; import { GameStorage } from "@/utils/gameStorage"; import { ppt } from "@/utils/datetime"; +import { notify } from "@/utils/notifications"; import { ajax } from "@/utils/ajax"; import { extractTime } from "@/utils/timeControl"; import { getRandString } from "@/utils/alea"; @@ -154,6 +155,7 @@ export default { gameRef: "", nextIds: [], game: {}, //passed to BaseGame + focus: false, // virtualClocks will be initialized from true game.clocks virtualClocks: [], vr: null, //"variant rules" object initialized from FEN @@ -229,6 +231,8 @@ export default { cleanBeforeDestroy: function() { clearInterval(this.socketCloseListener); document.removeEventListener('visibilitychange', this.visibilityChange); + window.removeEventListener('focus', this.onFocus); + window.removeEventListener('blur', this.onBlur); if (!!this.askLastate) clearInterval(this.askLastate); if (!!this.retrySendmove) clearInterval(this.retrySendmove); if (!!this.clockUpdate) clearInterval(this.clockUpdate); @@ -238,11 +242,25 @@ export default { }, visibilityChange: function() { // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27 - this.send( - document.visibilityState == "visible" - ? "getfocus" - : "losefocus" - ); + this.focus = (document.visibilityState == "visible"); + if (!this.focus && !!this.rematchOffer) { + this.rematchOffer = ""; + this.send("rematchoffer", { data: false }); + // Do not remove rematch offer from (local) storage + } + this.send(this.focus ? "getfocus" : "losefocus"); + }, + onFocus: function() { + this.focus = true; + this.send("getfocus"); + }, + onBlur: function() { + this.focus = false; + if (!!this.rematchOffer) { + this.rematchOffer = ""; + this.send("rematchoffer", { data: false }); + } + this.send("losefocus"); }, participateInChat: function(p) { return Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus) && !!p.name; @@ -256,6 +274,8 @@ export default { }, atCreation: function() { document.addEventListener('visibilitychange', this.visibilityChange); + window.addEventListener('focus', this.onFocus); + window.addEventListener('blur', this.onBlur); // 0] (Re)Set variables this.gameRef = this.$route.params["id"]; // next = next corr games IDs to navigate faster (if applicable) @@ -680,9 +700,22 @@ export default { } else { this.gotMoveIdx = movePlus.index; const receiveMyMove = (movePlus.color == this.game.mycolor); - if (!receiveMyMove && !!this.game.mycolor) + const moveColIdx = ["w", "b"].indexOf(movePlus.color); + if (!receiveMyMove && !!this.game.mycolor) { // Notify opponent that I got the move: this.send("gotmove", {data: movePlus.index, target: data.from}); + // And myself if I'm elsewhere: + if (!this.focus) { + notify( + "New move", + { + body: + (this.game.players[moveColIdx].name || "@nonymous") + + " just played." + } + ); + } + } if (movePlus.cancelDrawOffer) { // Opponent refuses draw this.drawOffer = ""; @@ -696,7 +729,6 @@ export default { } } this.$refs["basegame"].play(movePlus.move, "received", null, true); - const moveColIdx = ["w", "b"].indexOf(movePlus.color); this.game.clocks[moveColIdx] = movePlus.clock; this.processMove( movePlus.move, @@ -1295,7 +1327,7 @@ export default { }); }; // The active tab can update storage immediately - if (!document.hidden) updateStorage(); + if (this.focus) updateStorage(); // Small random delay otherwise else setTimeout(updateStorage, 500 + 1000 * Math.random()); } @@ -1426,6 +1458,13 @@ export default { }; if (this.game.type == "live") { GameStorage.update(this.gameRef, scoreObj); + // Notify myself locally if I'm elsewhere: + if (!this.focus) { + notify( + "Game over", + { body: score + " : " + scoreMsg } + ); + } if (!!callback) callback(); } else this.updateCorrGame(scoreObj, callback); diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 8cbbf200..48fd68e6 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -205,6 +205,7 @@ main <script> import { store } from "@/store"; import { checkChallenge } from "@/data/challengeCheck"; +import { notify } from "@/utils/notifications"; import { ArrayFun } from "@/utils/array"; import { ajax } from "@/utils/ajax"; import params from "@/parameters"; @@ -248,6 +249,7 @@ export default { diag: "", //visualizing FEN memorize: false //put settings in localStorage }, + focus: true, tchallDiag: "", curChallToAccept: {from: {}}, presetChalls: JSON.parse(localStorage.getItem("presetChalls") || "[]"), @@ -275,6 +277,8 @@ export default { }, created: function() { document.addEventListener('visibilitychange', this.visibilityChange); + window.addEventListener('focus', this.onFocus); + window.addEventListener('blur', this.onBlur); window.addEventListener("beforeunload", this.cleanBeforeDestroy); if (this.st.variants.length > 0 && this.newchallenge.vid > 0) this.loadNewchallVariant(); @@ -408,6 +412,8 @@ export default { cleanBeforeDestroy: function() { clearInterval(this.socketCloseListener); document.removeEventListener('visibilitychange', this.visibilityChange); + window.removeEventListener('focus', this.onFocus); + window.removeEventListener('blur', this.onBlur); window.removeEventListener("beforeunload", this.cleanBeforeDestroy); this.conn.removeEventListener("message", this.socketMessageListener); this.send("disconnect"); @@ -432,11 +438,16 @@ export default { }, visibilityChange: function() { // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27 - this.send( - document.visibilityState == "visible" - ? "getfocus" - : "losefocus" - ); + this.focus = (document.visibilityState == "visible"); + this.send(this.focus ? "getfocus" : "losefocus"); + }, + onFocus: function() { + this.focus = true; + this.send("getfocus"); + }, + onBlur: function() { + this.focus = false; + this.send("losefocus"); }, partialResetNewchallenge: function() { // Reset potential target and custom FEN: @@ -1236,6 +1247,7 @@ export default { ); setTimeout( () => { + const myIdx = (game.players[0].sid == this.st.user.sid ? 0 : 1); GameStorage.add(game, (err) => { // If an error occurred, game is not added: a tab already // added the game. Maybe a focused one, maybe not. @@ -1243,10 +1255,16 @@ export default { // ==> Do not play it again. if (!err && this.st.settings.sound) new Audio("/sounds/newgame.flac").play().catch(() => {}); + if (!this.focus) { + notify( + "New live game", + { body: "vs " + game.players[1-myIdx].name || "@nonymous" } + ); + } this.$router.push("/game/" + gameInfo.id); }); }, - document.hidden ? 500 + 1000 * Math.random() : 0 + this.focus ? 500 + 1000 * Math.random() : 0 ); } }