X-Git-Url: https://git.auder.net/?p=vchess.git;a=blobdiff_plain;f=client%2Fsrc%2Fviews%2FGame.vue;h=5145e5883461fc61e1c1a4ae949b635ef94dc5a9;hp=317185542c194b1a8be4b0072636e885f5bc9a1d;hb=1ef65040168ab7d55ce921abc9d63644a937d689;hpb=882ec6fe88ac4a536dd25eca5fd50ff3d27f7994 diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index 31718554..5145e588 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -57,7 +57,7 @@ main span {{ st.tr["Cancel"] }} .row #aboveBoard.col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2 - span.variant-cadence {{ game.cadence }} + span.variant-cadence(v-if="game.type!='import'") {{ game.cadence }} span.variant-name {{ game.vname }} span#nextGame( v-if="nextIds.length > 0" @@ -96,7 +96,7 @@ main ) img(src="/images/icons/rematch.svg") #playersInfo - p + p(v-if="isLargeScreen()") 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" @@ -130,7 +153,9 @@ import BaseGame from "@/components/BaseGame.vue"; import Chat from "@/components/Chat.vue"; import { store } from "@/store"; import { GameStorage } from "@/utils/gameStorage"; +import { ImportgameStorage } from "@/utils/importgameStorage"; 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 +179,7 @@ export default { gameRef: "", nextIds: [], game: {}, //passed to BaseGame + focus: !document.hidden, //will not always work... TODO // virtualClocks will be initialized from true game.clocks virtualClocks: [], vr: null, //"variant rules" object initialized from FEN @@ -184,8 +210,7 @@ export default { retrySendmove: null, clockUpdate: null, // Related to (killing of) self multi-connects: - newConnect: {}, - killed: {} + newConnect: {} }; }, watch: { @@ -211,10 +236,14 @@ export default { this.atCreation(); }, mounted: function() { - ["chatWrap", "infoDiv"].forEach(eltName => { - document.getElementById(eltName) - .addEventListener("click", processModalClick); - }); + document.getElementById("chatWrap") + .addEventListener("click", (e) => { + processModalClick(e, () => { + this.toggleChat("close") + }); + }); + document.getElementById("infoDiv") + .addEventListener("click", processModalClick); if ("ontouchstart" in window) { // Disable tooltips on smartphones: document.querySelectorAll("#aboveBoard .tooltip").forEach(elt => { @@ -229,6 +258,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 +269,28 @@ 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"); + }, + isLargeScreen: function() { + return window.innerWidth >= 500; }, participateInChat: function(p) { return Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus) && !!p.name; @@ -256,6 +304,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) @@ -297,7 +347,6 @@ export default { this.retrySendmove = null; this.clockUpdate = null; this.newConnect = {}; - this.killed = {}; // 1] Initialize connection this.connexionString = params.socketUrl + @@ -312,7 +361,8 @@ export default { this.socketCloseListener = setInterval( () => { if (this.conn.readyState == 3) { - this.conn.removeEventListener("message", this.socketMessageListener); + this.conn.removeEventListener( + "message", this.socketMessageListener); this.conn = new WebSocket(this.connexionString); this.conn.addEventListener("message", this.socketMessageListener); } @@ -334,7 +384,7 @@ export default { this.loadVariantThenGame(game, () => socketInit(this.roomInit)); else // Live game stored remotely: need socket to retrieve it - // NOTE: the callback "roomInit" will be lost, so we don't provide it. + // NOTE: the callback "roomInit" will be lost, so it's not provided. // --> It will be given when receiving "fullgame" socket event. socketInit(() => { this.send("askfullgame"); }); }); @@ -351,7 +401,7 @@ export default { } }, send: function(code, obj) { - if (!!this.conn) + if (!!this.conn && this.conn.readyState == 1) this.conn.send(JSON.stringify(Object.assign({ code: code }, obj))); }, isConnected: function(index) { @@ -395,34 +445,52 @@ export default { if (!!oppsid && !!this.people[oppsid]) return oppsid; return null; }, - toggleChat: function() { - if (document.getElementById("modalChat").checked) + // NOTE: action if provided is always a closing action + toggleChat: function(action) { + if (!action && document.getElementById("modalChat").checked) // Entering chat document.getElementById("inputChat").focus(); - // TODO: next line is only required when exiting chat, - // but the event for now isn't well detected. - document.getElementById("chatBtn").classList.remove("somethingnew"); + else { + document.getElementById("chatBtn").classList.remove("somethingnew"); + if (!!this.game.mycolor) { + // Update "chatRead" variable either on server or locally + if (this.game.type == "corr") + this.updateCorrGame({ chatRead: this.game.mycolor }); + else if (this.game.type == "live") + GameStorage.update(this.gameRef, { chatRead: true }); + } + } }, processChat: function(chat) { this.send("newchat", { data: chat }); // NOTE: anonymous chats in corr games are not stored on server (TODO?) - if (this.game.type == "corr" && this.st.user.id > 0) - this.updateCorrGame({ chat: chat }); + if (!!this.game.mycolor) { + if (this.game.type == "corr") + this.updateCorrGame({ chat: chat }); + else { + // Live game + chat.added = Date.now(); + GameStorage.update(this.gameRef, { chat: chat }); + } + } }, clearChat: function() { - // Nothing more to do if game is live (chats not recorded) - if (this.game.type == "corr") { - if (!!this.game.mycolor) { + if (!!this.game.mycolor) { + if (this.game.type == "corr") { ajax( "/chats", "DELETE", { data: { gid: this.game.id } } ); + } else { + // Live game + GameStorage.update(this.gameRef, { delchat: true }); } this.$set(this.game, "chats", []); } }, getGameType: function(game) { + if (!!game.id.match(/^i/)) return "import"; return game.cadence.indexOf("d") >= 0 ? "corr" : "live"; }, // Notify something after a new move (to opponent and me on MyGames page) @@ -474,10 +542,12 @@ export default { // TODO: shuffling and random filtering on server, // if the room is really crowded. Object.keys(data.sockIds).forEach(sid => { - // TODO: test sid != user.sid was already done on server if (sid != this.st.user.sid) { - this.people[sid] = { tmpIds: data.sockIds[sid] }; this.send("askidentity", { target: sid }); + this.people[sid] = { tmpIds: data.sockIds[sid] }; + } else { + // Complete my tmpIds: + Object.assign(this.people[sid].tmpIds, data.sockIds[sid]); } }); break; @@ -493,7 +563,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 }; @@ -523,13 +594,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 = { @@ -550,57 +614,57 @@ 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; } case "askgame": - // Send current (live) game if not asked by any of the players + // Send current (live or import) game, + // if not asked by any of the players if ( - this.game.type == "live" && + this.game.type != "corr" && this.game.players.every(p => p.sid != data.from[0]) ) { const myGame = { id: this.game.id, + // FEN is current position, unused for now fen: this.game.fen, players: this.game.players, vid: this.game.vid, @@ -627,13 +691,18 @@ export default { this.send("fullgame", { data: gameToSend, target: data.from }); break; case "fullgame": - // Callback "roomInit" to poll clients only after game is loaded - this.loadVariantThenGame(data.data, this.roomInit); + if (!!data.data.empty) { + alert(this.st.tr["The game should be in another tab"]); + this.$router.go(-1); + } + else + // Callback "roomInit" to poll clients only after game is loaded + this.loadVariantThenGame(data.data, this.roomInit); break; case "asklastate": // Sending informative last state if I played a move or score != "*" // If the game or moves aren't loaded yet, delay the sending: - // TODO: since socket init after game load, the game is supposedly ready + // TODO: socket init after game load, so the game is supposedly ready if (!this.game || !this.game.moves) this.lastateAsked = true; else this.sendLastate(data.from); break; @@ -667,9 +736,25 @@ 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}); + 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 = ""; @@ -682,8 +767,8 @@ export default { GameStorage.update(this.gameRef, { drawOffer: "" }); } } - this.$refs["basegame"].play(movePlus.move, "received", null, true); - const moveColIdx = ["w", "b"].indexOf(movePlus.color); + this.$refs["basegame"].play( + movePlus.move, "received", null, true); this.game.clocks[moveColIdx] = movePlus.clock; this.processMove( movePlus.move, @@ -717,7 +802,7 @@ export default { case "drawoffer": // NOTE: observers don't know who offered draw this.drawOffer = "received"; - if (this.game.type == "live") { + if (!!this.game.mycolor && this.game.type == "live") { GameStorage.update( this.gameRef, { drawOffer: V.GetOppCol(this.game.mycolor) } @@ -727,7 +812,7 @@ export default { case "rematchoffer": // NOTE: observers don't know who offered rematch this.rematchOffer = data.data ? "received" : ""; - if (this.game.type == "live") { + if (!!this.game.mycolor && this.game.type == "live") { GameStorage.update( this.gameRef, { rematchOffer: V.GetOppCol(this.game.mycolor) } @@ -754,11 +839,18 @@ export default { } break; } - case "newchat": - this.$refs["chatcomp"].newChat(data.data); + case "newchat": { + let chat = data.data; + this.$refs["chatcomp"].newChat(chat); + if (this.game.type == "live") { + chat.added = Date.now(); + if (!!this.game.mycolor) + GameStorage.update(this.gameRef, { chat: chat }); + } if (!document.getElementById("modalChat").checked) document.getElementById("chatBtn").classList.add("somethingnew"); break; + } } }, updateCorrGame: function(obj, callback) { @@ -820,7 +912,7 @@ export default { } }, clickDraw: function() { - if (!this.game.mycolor) return; //I'm just spectator + if (!this.game.mycolor || this.game.type == "import") return; if (["received", "threerep"].includes(this.drawOffer)) { if (!confirm(this.st.tr["Accept draw?"])) return; const message = @@ -872,7 +964,7 @@ export default { }); }, clickRematch: function() { - if (!this.game.mycolor) return; //I'm just spectator + if (!this.game.mycolor || this.game.type == "import") return; if (this.rematchOffer == "received") { // Start a new game! let gameInfo = { @@ -931,7 +1023,8 @@ export default { } }, abortGame: function() { - if (!this.game.mycolor || !confirm(this.st.tr["Terminate game?"])) return; + if (!this.game.mycolor || !confirm(this.st.tr["Terminate game?"])) + return; this.gameOver("?", "Stop"); this.send("abort"); }, @@ -944,15 +1037,16 @@ export default { this.gameOver(score, side + " surrender"); }, loadGame: function(game, callback) { - this.vr = new V(game.fen); - const gtype = this.getGameType(game); + const gtype = game.type || this.getGameType(game); const tc = extractTime(game.cadence); const myIdx = game.players.findIndex(p => { return p.sid == this.st.user.sid || p.id == this.st.user.id; }); - const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers - if (!game.chats) game.chats = []; //live games don't have chat history + // "mycolor" is undefined for observers + const mycolor = [undefined, "w", "b"][myIdx + 1]; if (gtype == "corr") { + if (mycolor == 'w') game.chatRead = game.chatReadWhite; + else if (mycolor == 'b') game.chatRead = game.chatReadBlack; // NOTE: clocks in seconds game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of game.clocks = [tc.mainTime, tc.mainTime]; @@ -964,31 +1058,10 @@ export default { (Date.now() - game.moves[L-1].played) / 1000; } } - // Sort chat messages from newest to oldest - game.chats.sort((c1, c2) => { - return c2.added - c1.added; - }); - if (myIdx >= 0 && game.score == "*" && game.chats.length > 0) { - // Did a chat message arrive after my last move? - let dtLastMove = 0; - if (L == 1 && myIdx == 0) - dtLastMove = game.moves[0].played; - else if (L >= 2) { - if (L % 2 == 0) { - // It's now white turn - dtLastMove = game.moves[L-1-(1-myIdx)].played; - } else { - // Black turn: - dtLastMove = game.moves[L-1-myIdx].played; - } - } - if (dtLastMove < game.chats[0].added) - document.getElementById("chatBtn").classList.add("somethingnew"); - } // Now that we used idx and played, re-format moves as for live games game.moves = game.moves.map(m => m.squares); } - if (gtype == "live") { + else if (gtype == "live") { if (game.clocks[0] < 0) { // Game is unstarted. clock is ignored until move 2 game.clocks = [tc.mainTime, tc.mainTime]; @@ -1004,6 +1077,21 @@ export default { game.clocks[myIdx] -= (Date.now() - game.initime) / 1000; } } + else + // gtype == "import" + game.clocks = [tc.mainTime, tc.mainTime]; + // Live games before 26/03/2020 don't have chat history: + if (!game.chats) game.chats = []; //TODO: remove line + // Sort chat messages from newest to oldest + game.chats.sort((c1, c2) => c2.added - c1.added); + if ( + myIdx >= 0 && + game.chats.length > 0 && + (!game.chatRead || game.chatRead < game.chats[0].added) + ) { + // A chat message arrived since my last reading: + document.getElementById("chatBtn").classList.add("somethingnew"); + } // TODO: merge next 2 "if" conditions if (!!game.drawOffer) { if (game.drawOffer == "t") @@ -1037,15 +1125,17 @@ export default { } this.repeat = {}; //reset: scan past moves' FEN: let repIdx = 0; - let vr_tmp = new V(game.fenStart); + this.vr = new V(game.fenStart); let curTurn = "n"; game.moves.forEach(m => { - playMove(m, vr_tmp); - const fenIdx = vr_tmp.getFen().replace(/ /g, "_"); + playMove(m, this.vr); + const fenIdx = this.vr.getFenForRepeat(); this.repeat[fenIdx] = this.repeat[fenIdx] ? this.repeat[fenIdx] + 1 : 1; }); + // Imported games don't have current FEN + if (!game.fen) game.fen = this.vr.getFen(); if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep"; this.game = Object.assign( // NOTE: assign mycolor here, since BaseGame could also be VS computer @@ -1099,6 +1189,11 @@ export default { // - from server (one correspondance game I play[ed] or not) // - from remote peer (one live game I don't play, finished or not) fetchGame: function(callback) { + +console.log("fecth"); + console.log(this.gameRef); + console.log(this.gameRef.match(/^i/)); + if (Number.isInteger(this.gameRef) || !isNaN(parseInt(this.gameRef))) { // corr games identifiers are integers ajax( @@ -1114,8 +1209,12 @@ export default { } } ); - } else - // Local game (or live remote) + } + else if (!!this.gameRef.match(/^i/)) + // Game import (maybe remote) + ImportgameStorage.get(this.gameRef, callback); + else + // Local live game (or remote) GameStorage.get(this.gameRef, callback); }, re_setClocks: function() { @@ -1154,6 +1253,9 @@ export default { }, // Update variables and storage after a move: processMove: function(move, data) { + if (this.game.type == "import") + // Shouldn't receive any messages in this mode: + return; if (!data) data = {}; const moveCol = this.vr.turn; const colorIdx = ["w", "b"].indexOf(moveCol); @@ -1271,7 +1373,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()); } @@ -1281,7 +1383,8 @@ export default { let sendMove = { move: filtered_move, index: origMovescount, - // color is required to check if this is my move (if several tabs opened) + // color is required to check if this is my move + // (if several tabs opened) color: moveCol, cancelDrawOffer: this.drawOffer == "" }; @@ -1402,10 +1505,18 @@ 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); - // Notify the score to main Hall. TODO: only one player (currently double send) + // Notify the score to main Hall. + // TODO: only one player (currently double send) this.send("result", { gid: this.game.id, score: score }); // Also to MyGames page (TODO: doubled as well...) this.notifyMyGames(