X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fviews%2FHall.vue;h=e53dd56aa1231a8dcbb9a8ccd21e3cb044543b38;hb=8418f0d79395f40172b11d62eef8b83112f1d240;hp=8cd330ff490e79da9b7ece5801e2a20ad7b1202b;hpb=ac8f441c6441d827b43aabeb812cb4c79e9ee96b;p=vchess.git diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 8cd330ff..e53dd56a 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -18,7 +18,7 @@ main :selected="newchallenge.vid==v.id") | {{ v.name }} fieldset - label(for="timeControl") {{ st.tr["Time control"] }} * + label(for="timeControl") {{ st.tr["Cadence"] }} * div#predefinedTimeControls button 3+2 button 5+3 @@ -50,13 +50,13 @@ main #people h3.text-center {{ st.tr["Who's there?"] }} #players - p(v-for="p in Object.values(people)" v-if="!!p.name") - span {{ p.name }} + p(v-for="sid in Object.keys(people)" v-if="!!people[sid].name") + span {{ people[sid].name }} button.player-action( - v-if="p.name != st.user.name" - @click="challOrWatch(p,$event)" + v-if="people[sid].name != st.user.name" + @click="challOrWatch(sid, $event)" ) - | {{ whatPlayerDoes(p) }} + | {{ st.tr[!!people[sid].gamer ? 'Playing' : 'Available'] }} p.anonymous @nonymous ({{ anonymousCount }}) #chat Chat(:newChat="newChat" @mychat="processChat") @@ -78,6 +78,7 @@ import { store } from "@/store"; import { checkChallenge } from "@/data/challengeCheck"; import { ArrayFun } from "@/utils/array"; import { ajax } from "@/utils/ajax"; +import params from "@/parameters"; import { getRandString, shuffle } from "@/utils/alea"; import Chat from "@/components/Chat.vue"; import GameList from "@/components/GameList.vue"; @@ -109,6 +110,9 @@ export default { timeControl: localStorage.getItem("timeControl") || "", }, newChat: "", + conn: null, + page: "", + tempId: "", //to distinguish several tabs }; }, watch: { @@ -135,21 +139,8 @@ export default { created: function() { // Always add myself to players' list const my = this.st.user; - this.$set(this.people, my.sid, {id:my.id, name:my.name}); - // Retrieve live challenge (not older than 30 minute) if any: - const chall = JSON.parse(localStorage.getItem("challenge") || "false"); - if (!!chall) - { - // NOTE: a challenge survives 3 minutes, for potential connection issues - if ((Date.now() - chall.added)/1000 <= 3*60) - { - chall.added = Date.now(); //update added time, for next disconnect... - this.challenges.push(chall); - localStorage.setItem("challenge", JSON.stringify(chall)); - } - else - localStorage.removeItem("challenge"); - } + this.tempId = getRandString(); + this.$set(this.people, my.sid, {id:my.id, name:my.name, tmpId: [this.tempId]}); // Ask server for current corr games (all but mines) ajax( "/games", @@ -163,7 +154,7 @@ export default { })); } ); - // Also ask for corr challenges (open + sent to me) + // Also ask for corr challenges (open + sent by/to me) ajax( "/challenges", "GET", @@ -171,45 +162,67 @@ export default { response => { // Gather all senders names, and then retrieve full identity: // (TODO [perf]: some might be online...) - const uids = response.challenges.map(c => { return c.uid }); - ajax("/users", - "GET", - { ids: uids.join(",") }, - response2 => { - let names = {}; - response2.users.forEach(u => {names[u.id] = u.name}); - this.challenges = this.challenges.concat( - response.challenges.map(c => { - // (just players names in fact) - const from = {name: names[c.uid], id: c.uid}; - const type = this.classifyObject(c); - const vname = this.getVname(c.vid); - return Object.assign({}, c, {type: type, vname: vname, from: from}); - }) - ) - } - ); + let names = {}; + response.challenges.forEach(c => { + if (c.uid != this.st.user.id) + names[c.uid] = ""; //unknwon for now + else if (!!c.target && c.target != this.st.user.id) + names[c.target] = ""; + }); + const addChallenges = (newChalls) => { + names[this.st.user.id] = this.st.user.name; //in case of + this.challenges = this.challenges.concat( + response.challenges.map(c => { + const from = {name: names[c.uid], id: c.uid}; //or just name + const type = this.classifyObject(c); + const vname = this.getVname(c.vid); + return Object.assign({}, + { + type: type, + vname: vname, + from: from, + to: (!!c.target ? names[c.target] : ""), + }, + c); + }) + ); + }; + if (names !== {}) + { + ajax("/users", + "GET", + { ids: Object.keys(names).join(",") }, + response2 => { + response2.users.forEach(u => {names[u.id] = u.name}); + addChallenges(); + } + ); + } + else + addChallenges(); } ); // 0.1] Ask server for room composition: const funcPollClients = () => { - // Same strategy as in Game.vue: send connection - // after we're sure WebSocket is initialized - this.st.conn.send(JSON.stringify({code:"connect"})); - this.st.conn.send(JSON.stringify({code:"pollclients"})); - this.st.conn.send(JSON.stringify({code:"pollgamers"})); + this.conn.send(JSON.stringify({code:"connect"})); + this.conn.send(JSON.stringify({code:"pollclients"})); + this.conn.send(JSON.stringify({code:"pollgamers"})); }; - if (!!this.st.conn && this.st.conn.readyState == 1) //1 == OPEN state - funcPollClients(); - else //socket not ready yet (initial loading) - this.st.conn.onopen = funcPollClients; - this.st.conn.onmessage = this.socketMessageListener; + // Initialize connection + this.page = this.$route.path; + const connexionString = params.socketUrl + + "/?sid=" + this.st.user.sid + + "&tmpId=" + this.tempId + + "&page=" + encodeURIComponent(this.page); + this.conn = new WebSocket(connexionString); + this.conn.onopen = funcPollClients; + this.conn.onmessage = this.socketMessageListener; const socketCloseListener = () => { - store.socketCloseListener(); //reinitialize connexion (in store.js) - this.st.conn.addEventListener('message', this.socketMessageListener); - this.st.conn.addEventListener('close', socketCloseListener); + this.conn = new WebSocket(connexionString); + this.conn.addEventListener('message', this.socketMessageListener); + this.conn.addEventListener('close', socketCloseListener); }; - this.st.conn.onclose = socketCloseListener; + this.conn.onclose = socketCloseListener; }, mounted: function() { [document.getElementById("infoDiv"),document.getElementById("newgameDiv")] @@ -220,6 +233,9 @@ export default { )} ); }, + beforeDestroy: function() { + this.conn.send(JSON.stringify({code:"disconnect",page:this.page})); + }, methods: { // Helpers: filterChallenges: function(type) { @@ -229,7 +245,6 @@ export default { return this.games.filter(g => g.type == type); }, classifyObject: function(o) { //challenge or game - // Heuristic: should work for most cases... (TODO) return (o.timeControl.indexOf('d') === -1 ? "live" : "corr"); }, showGame: function(g) { @@ -253,49 +268,19 @@ export default { // this.st.variants might be uninitialized (variant == null) return (!!variant ? variant.name : ""); }, - -// TODO: now that we can distinguish people from Hall or Game, -// improve this --> the info is already known - - whatPlayerDoes: function(p) { - if (this.games.some(g => g.type == "live" - && g.players.some(pl => pl.sid == p.sid))) - { - return "Playing"; - } - return "Challenge"; //player is available - }, - -// Also debug: when from game going to Hall, player appears still connected. -// when reloading a finished (observed) game, some ghost moveToPlay errors - processChat: function(chat) { // When received on server, this will trigger a "notifyRoom" - this.st.conn.send(JSON.stringify({code:"newchat", chat: chat})); + this.conn.send(JSON.stringify({code:"newchat", chat: chat})); }, sendSomethingTo: function(to, code, obj, warnDisconnected) { const doSend = (code, obj, sid) => { - this.st.conn.send(JSON.stringify(Object.assign( + this.conn.send(JSON.stringify(Object.assign( {code: code}, obj, {target: sid} ))); }; - if (!!to) - { - // Challenge with targeted players - const targetSid = - Object.keys(this.people).find(sid => this.people[sid].name == to); - if (!targetSid) - { - if (!!warnDisconnected) - alert(this.st.tr["Warning: target is not connected"]); - return false; - } - else - doSend(code, obj, targetSid); - } - else + if (!to || (!to.sid && !to.name)) { // Open challenge: send to all connected players (me excepted) Object.keys(this.people).forEach(sid => { @@ -303,6 +288,27 @@ export default { doSend(code, obj, sid); }); } + else + { + let targetSid = ""; + if (!!to.sid) + targetSid = to.sid; + else + { + if (to.name == this.st.user.name) + return alert(this.st.tr["Cannot challenge self"]); + // Challenge with targeted players + targetSid = + Object.keys(this.people).find(sid => this.people[sid].name == to.name); + if (!targetSid) + { + if (!!warnDisconnected) + alert(this.st.tr["Target is not connected"]); + return false; + } + } + doSend(code, obj, targetSid); + } return true; }, // Messaging center: @@ -311,33 +317,34 @@ export default { switch (data.code) { case "duplicate": - alert(this.st.tr["Warning: multi-tabs not supported"]); + this.conn.send(JSON.stringify({code:"duplicate", page:"/"})); + this.conn.send = () => {}; + alert(this.st.tr["This tab is now offline"]); break; // 0.2] Receive clients list (just socket IDs) case "pollclients": data.sockIds.forEach(sid => { this.$set(this.people, sid, {id:0, name:""}); // Ask identity and challenges - this.st.conn.send(JSON.stringify({code:"askidentity", target:sid})); - this.st.conn.send(JSON.stringify({code:"askchallenge", target:sid})); + this.conn.send(JSON.stringify({code:"askidentity", target:sid})); + this.conn.send(JSON.stringify({code:"askchallenge", target:sid})); }); break; case "pollgamers": // NOTE: we could make a difference between people in hall // and gamers, but is it necessary? data.sockIds.forEach(sid => { - this.$set(this.people, sid, {id:0, name:""}); - this.st.conn.send(JSON.stringify({code:"askidentity", target:sid})); + this.$set(this.people, sid, {id:0, name:"", gamer:true}); + this.conn.send(JSON.stringify({code:"askidentity", target:sid})); }); // Also ask current games to all playing peers (TODO: some design issue) - this.st.conn.send(JSON.stringify({code:"askgames"})); + this.conn.send(JSON.stringify({code:"askgames"})); break; case "askidentity": - { // Request for identification: reply if I'm not anonymous if (this.st.user.id > 0) { - this.st.conn.send(JSON.stringify({code:"identity", + this.conn.send(JSON.stringify({code:"identity", user: { // NOTE: decompose to avoid revealing email name: this.st.user.name, @@ -347,13 +354,14 @@ export default { target:data.from})); } break; - } case "identity": - { this.$set(this.people, data.user.sid, - {id: data.user.id, name: data.user.name}); + { + id: data.user.id, + name: data.user.name, + gamer: this.people[data.user.sid].gamer, + }); break; - } case "askchallenge": { // Send my current live challenge (if any) @@ -362,14 +370,18 @@ export default { if (cIdx >= 0) { const c = this.challenges[cIdx]; - if (!!c.to) - { - // Only share targeted challenges to the targets: - const toSid = Object.keys(this.people).find(k => - this.people[k].name == c.to); - if (toSid != data.from) - return; - } + // TODO: code below requires "c.to" to have given his identity, + // but it can happen that the identity arrives later, which + // prevent him from receiving the challenge. + // ==> Filter later (when receiving challenge) +// if (!!c.to) +// { +// // Only share targeted challenges to the targets: +// const toSid = Object.keys(this.people).find(k => +// this.people[k].name == c.to); +// if (toSid != data.from) +// return; +// } const myChallenge = { // Minimal challenge informations: (from not required) @@ -378,47 +390,50 @@ export default { fen: c.fen, vid: c.vid, timeControl: c.timeControl, + added: c.added, }; - this.st.conn.send(JSON.stringify({code:"challenge", + this.conn.send(JSON.stringify({code:"challenge", chall:myChallenge, target:data.from})); } break; } case "challenge": - { // Receive challenge from some player (+sid) - let newChall = data.chall; - newChall.type = this.classifyObject(data.chall); - newChall.from = - Object.assign({sid:data.from}, this.people[data.from]); - newChall.added = Date.now(); //TODO: this is reception timestamp, not creation - newChall.vname = this.getVname(newChall.vid); - this.challenges.push(newChall); + // NOTE about next condition: see "askchallenge" case. + if (!data.chall.to || data.chall.to == this.st.user.name) + { + let newChall = data.chall; + newChall.type = this.classifyObject(data.chall); + newChall.from = + Object.assign({sid:data.from}, this.people[data.from]); + newChall.vname = this.getVname(newChall.vid); + this.challenges.push(newChall); + } break; - } case "game": { // Receive game from some player (+sid) // NOTE: it may be correspondance (if newgame while we are connected) // If duplicate found: select rid (remote ID) at random let game = this.games.find(g => g.id == data.game.id); - if (!!game && Math.random() < 0.5) - game.rid = data.from; + if (!!game) + { + if (Math.random() < 0.5) + game.rid = data.from; + } else { let newGame = data.game; newGame.type = this.classifyObject(data.game); newGame.vname = this.getVname(data.game.vid); newGame.rid = data.from; - newGame.score = "*"; + if (!data.game.score) + newGame.score = "*"; this.games.push(newGame); } break; } case "newgame": - { - // TODO: next line required ?! - //ArrayFun.remove(this.challenges, c => c.id == data.cid); // New game just started: data contain all information if (this.classifyObject(data.gameInfo) == "live") this.startNewGame(data.gameInfo); @@ -432,34 +447,28 @@ export default { setTimeout(() => { modalBox.checked = false; }, 3000); } break; - } case "newchat": this.newChat = data.chat; break; case "refusechallenge": - { ArrayFun.remove(this.challenges, c => c.id == data.cid); - localStorage.removeItem("challenge"); alert(this.st.tr["Challenge declined"]); break; - } case "deletechallenge": - { // NOTE: the challenge may be already removed ArrayFun.remove(this.challenges, c => c.id == data.cid); - localStorage.removeItem("challenge"); //in case of break; - } case "connect": case "gconnect": - this.$set(this.people, data.from, {name:"", id:0}); - this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from})); + this.$set(this.people, data.from, {name:"", id:0, gamer:data.code[0]=='g'}); + this.conn.send(JSON.stringify({code:"askidentity", target:data.from})); if (data.code == "connect") - this.st.conn.send(JSON.stringify({code:"askchallenge", target:data.from})); + this.conn.send(JSON.stringify({code:"askchallenge", target:data.from})); else - this.st.conn.send(JSON.stringify({code:"askgame", target:data.from})); + this.conn.send(JSON.stringify({code:"askgame", target:data.from})); break; case "disconnect": + case "gdisconnect": this.$delete(this.people, data.from); if (data.code == "disconnect") { @@ -477,28 +486,31 @@ export default { } }, // Challenge lifecycle: - tryChallenge: function(player) { - if (player.id == 0) + tryChallenge: function(sid) { + if (this.people[sid].id == 0) return; //anonymous players cannot be challenged - this.newchallenge.to = player.name; + // TODO: SID is available, so we could use it instead of searching from name + this.newchallenge.to = this.people[sid].name; doClick("modalNewgame"); }, - challOrWatch: function(p, e) { - switch (e.target.innerHTML) + challOrWatch: function(sid) { + if (!this.people[sid].gamer) { - case "Challenge": - this.tryChallenge(p); - break; - case "Playing": - // NOTE: this search for game was already done for rendering - this.showGame(this.games.find( - g => g.type=="live" && g.players.some(pl => pl.sid == p.sid))); - break; - }; + // Available, in Hall + this.tryChallenge(sid); + } + else + { + // Playing, in Game + this.showGame(this.games.find( + g => g.players.some(pl => pl.sid == sid || pl.uid == this.people[sid].id))); + } }, newChallenge: async function() { if (this.newchallenge.vid == "") return alert(this.st.tr["Please select a variant"]); + if (!!this.newchallenge.to && this.newchallenge.to == this.st.user.name) + return alert(this.st.tr["Self-challenge is forbidden"]); const vname = this.getVname(this.newchallenge.vid); const vModule = await import("@/variants/" + vname + ".js"); window.V = vModule.VariantRules; @@ -515,17 +527,17 @@ export default { const finishAddChallenge = (cid,warnDisconnected) => { chall.id = cid || "c" + getRandString(); // Send challenge to peers (if connected) - const isSent = this.sendSomethingTo(chall.to, "challenge", + const isSent = this.sendSomethingTo({name:chall.to}, "challenge", {chall:chall}, !!warnDisconnected); if (!isSent) return; - // Remove old challenge if any (only one at a time): + // Remove old challenge if any (only one at a time of a given type): const cIdx = this.challenges.findIndex(c => - c.from.sid == this.st.user.sid && c.type == ctype); + (c.from.sid == this.st.user.sid || c.from.id == this.st.user.id) && c.type == ctype); if (cIdx >= 0) { // Delete current challenge (will be replaced now) - this.sendSomethingTo(this.challenges[cIdx].to, + this.sendSomethingTo({name:this.challenges[cIdx].to}, "deletechallenge", {cid:this.challenges[cIdx].id}); if (ctype == "corr") { @@ -548,9 +560,7 @@ export default { name: this.st.user.name, }; this.challenges.push(chall); - if (ctype == "live") - localStorage.setItem("challenge", JSON.stringify(chall)); - // Also remember timeControl + vid for quicker further challenges: + // Remember timeControl + vid for quicker further challenges: localStorage.setItem("timeControl", chall.timeControl); localStorage.setItem("vid", chall.vid); document.getElementById("modalNewgame").checked = false; @@ -595,18 +605,11 @@ export default { } else { - this.st.conn.send(JSON.stringify({ + this.conn.send(JSON.stringify({ code: "refusechallenge", cid: c.id, target: c.from.sid})); } - // TODO: refactor the "sendSomethingTo()" function - if (!c.to) - this.sendSomethingTo(null, "deletechallenge", {cid:c.id}); - else - { - this.st.conn.send(JSON.stringify({ - code:"deletechallenge", target: c.from.sid, cid: c.id})); - } + this.sendSomethingTo(!!c.to ? {sid:c.from.sid} : null, "deletechallenge", {cid:c.id}); } else //my challenge { @@ -618,14 +621,12 @@ export default { {id: c.id} ); } - else //live - localStorage.removeItem("challenge"); - this.sendSomethingTo(c.to, "deletechallenge", {cid:c.id}); + this.sendSomethingTo({name:c.to}, "deletechallenge", {cid:c.id}); } // In all cases, the challenge is consumed: ArrayFun.remove(this.challenges, ch => ch.id == c.id); }, - // NOTE: when launching game, the challenge is already deleted + // NOTE: when launching game, the challenge is already being deleted launchGame: async function(c) { const vModule = await import("@/variants/" + c.vname + ".js"); window.V = vModule.VariantRules; @@ -648,7 +649,7 @@ export default { const tryNotifyOpponent = () => { if (!!oppsid) //opponent is online { - this.st.conn.send(JSON.stringify({code:"newgame", + this.conn.send(JSON.stringify({code:"newgame", gameInfo:gameInfo, target:oppsid, cid:c.id})); } }; @@ -675,7 +676,7 @@ export default { Object.keys(this.people).forEach(sid => { if (![this.st.user.sid,oppsid].includes(sid)) { - this.st.conn.send(JSON.stringify({code:"game", + this.conn.send(JSON.stringify({code:"game", game: { //minimal game info: id: gameInfo.id, players: gameInfo.players,