From a041d5d84031f29fc001597a9ac958d6a3e6de76 Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Mon, 9 Mar 2020 00:25:20 +0100 Subject: [PATCH] Experimental focus/blur detection --- client/src/views/Game.vue | 63 ++++++++++++++++++++++++---- client/src/views/Hall.vue | 87 +++++++++++++++++++++++++++++---------- server/sockets.js | 51 +++++++++++++++++------ 3 files changed, 158 insertions(+), 43 deletions(-) diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue index ce42f21d..fe613ec9 100644 --- a/client/src/views/Game.vue +++ b/client/src/views/Game.vue @@ -14,10 +14,12 @@ main span {{ Object.keys(people).length + " " + st.tr["participant(s):"] }} span( v-for="p in Object.values(people)" - v-if="p.name" + v-if="p.focus && !!p.name" ) | {{ p.name }} - span.anonymous(v-if="Object.values(people).some(p => !p.name && p.id === 0)") + span.anonymous( + v-if="Object.values(people).some(p => p.focus && !p.name)" + ) | + @nonymous Chat( ref="chatcomp" @@ -202,6 +204,7 @@ export default { this.atCreation(); }, mounted: function() { + document.addEventListener('visibilitychange', this.visibilityChange); document .getElementById("chatWrap") .addEventListener("click", processModalClick); @@ -213,9 +216,18 @@ export default { } }, beforeDestroy: function() { + document.removeEventListener('visibilitychange', this.visibilityChange); this.cleanBeforeDestroy(); }, methods: { + visibilityChange: function() { + // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27 + this.send( + document.visibilityState == "visible" + ? "getfocus" + : "losefocus" + ); + }, atCreation: function() { // 0] (Re)Set variables this.gameRef.id = this.$route.params["id"]; @@ -226,7 +238,15 @@ export default { this.nextIds = JSON.parse(this.$route.query["next"] || "[]"); // Always add myself to players' list const my = this.st.user; - this.$set(this.people, my.sid, { id: my.id, name: my.name }); + this.$set( + this.people, + my.sid, + { + id: my.id, + name: my.name, + focus: true + } + ); this.game = { players: [{ name: "" }, { name: "" }], chats: [], @@ -310,7 +330,7 @@ export default { }, isConnected: function(index) { const player = this.game.players[index]; - // Is it me ? + // Is it me ? In this case no need to bother with focus if (this.st.user.sid == player.sid || this.st.user.id == player.uid) // Still have to check for name (because of potential multi-accounts // on same browser, although this should be rare...) @@ -318,13 +338,15 @@ export default { // Try to find a match in people: return ( ( - player.sid && - Object.keys(this.people).some(sid => sid == player.sid) + !!player.sid && + Object.keys(this.people).some(sid => + sid == player.sid && this.people[sid].focus) ) || ( player.uid && - Object.values(this.people).some(p => p.id == player.uid) + Object.values(this.people).some(p => + p.id == player.uid && p.focus) ) ); }, @@ -400,12 +422,15 @@ export default { switch (data.code) { case "pollclients": data.sockIds.forEach(sid => { - if (sid != this.st.user.sid) + if (sid != this.st.user.sid) { + this.people[sid] = { focus: true }; this.send("askidentity", { target: sid }); + } }); break; case "connect": if (!this.people[data.from]) { + this.people[data.from] = { focus: true }; this.newConnect[data.from] = true; //for self multi-connects tests this.send("askidentity", { target: data.from }); } @@ -429,6 +454,22 @@ export default { case "mdisconnect": ArrayFun.remove(this.onMygames, sid => sid == data.from); break; + case "getfocus": { + let player = this.people[data.from]; + if (!!player) { + player.focus = true; + this.$forceUpdate(); //TODO: shouldn't be required + } + break; + } + case "losefocus": { + let player = this.people[data.from]; + if (!!player) { + player.focus = false; + this.$forceUpdate(); //TODO: shouldn't be required + } + break; + } case "killed": // I logged in elsewhere: this.conn = null; @@ -447,7 +488,11 @@ export default { } case "identity": { const user = data.data; - this.$set(this.people, user.sid, { name: user.name, id: user.id }); + let player = this.people[user.sid]; + // player.focus is already set + player.name = user.name; + player.id = user.id; + 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]) { if ( diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue index 5e7a10d4..271c0bcf 100644 --- a/client/src/views/Hall.vue +++ b/client/src/views/Hall.vue @@ -100,7 +100,7 @@ main ) | {{ st.tr["Observe"] }} button.player-action( - v-else-if="st.user.id > 0 && sid != st.user.sid" + v-else-if="isFocusedOnHall(sid)" @click="challenge(sid)" ) | {{ st.tr["Challenge"] }} @@ -233,7 +233,15 @@ export default { if (this.st.variants.length > 0 && this.newchallenge.vid > 0) this.loadNewchallVariant(); const my = this.st.user; - this.$set(this.people, my.sid, { id: my.id, name: my.name, pages: ["/"] }); + this.$set( + this.people, + my.sid, + { + id: my.id, + name: my.name, + pages: [{ path: "/", focus: true }] + } + ); // Ask server for current corr games (all but mines) ajax( "/games", @@ -324,6 +332,7 @@ export default { this.conn.onclose = this.socketCloseListener; }, mounted: function() { + document.addEventListener('visibilitychange', this.visibilityChange); ["peopleWrap", "infoDiv", "newgameDiv"].forEach(eltName => { let elt = document.getElementById(eltName); elt.addEventListener("click", processModalClick); @@ -342,9 +351,18 @@ export default { this.setDisplay("g", showGtype); }, beforeDestroy: function() { + document.removeEventListener('visibilitychange', this.visibilityChange); this.send("disconnect"); }, methods: { + visibilityChange: function() { + // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27 + this.send( + document.visibilityState == "visible" + ? "getfocus" + : "losefocus" + ); + }, // Helpers: cadenceFocusIfOpened: function() { if (event.target.checked) @@ -386,7 +404,16 @@ export default { else elt.nextElementSibling.classList.remove("active"); }, isGamer: function(sid) { - return this.people[sid].pages.some(p => p.indexOf("/game/") >= 0); + return this.people[sid].pages + .some(p => p.focus && p.path.indexOf("/game/") >= 0); + }, + isFocusedOnHall: function(sid) { + return ( + // This is meant to challenge people, thus the next 2 conditions: + this.st.user.id > 0 && + sid != this.st.user.sid && + this.people[sid].pages.some(p => p.path == "/" && p.focus) + ); }, challenge: function(sid) { // Available, in Hall (only) @@ -398,8 +425,10 @@ export default { // In some game, maybe playing maybe not: show a random one let gids = []; this.people[sid].pages.forEach(p => { - const matchGid = p.match(/[a-zA-Z0-9]+$/); - if (!!matchGid) gids.push(matchGid[0]); + if (p.focus) { + const matchGid = p.path.match(/[a-zA-Z0-9]+$/); + if (!!matchGid) gids.push(matchGid[0]); + } }); const gid = gids[Math.floor(Math.random() * gids.length)]; const game = this.games.find(g => g.id == gid); @@ -445,11 +474,12 @@ export default { this.send("askidentity", { target: s.sid, page: page }); identityAsked[s.sid] = true; } - if (!this.people[s.sid]) + if (!this.people[s.sid]) { // Do not set name or id: identity unknown yet - this.$set(this.people, s.sid, { pages: [page] }); - else if (this.people[s.sid].pages.indexOf(page) < 0) - this.people[s.sid].pages.push(page); + this.people[s.sid] = { pages: [{path: page, focus: true}] }; + } + else if (!(this.people[s.sid].pages.find(p => p.path == page))) + this.people[s.sid].pages.push({ path: page, focus: true }); if (!s.page) // Peer is in Hall this.send("askchallenge", { target: s.sid }); @@ -461,18 +491,16 @@ export default { case "connect": case "gconnect": { const page = data.page || "/"; - // NOTE: player could have been polled earlier, but might have logged in then - // So it's a good idea to ask identity if he was anonymous. - // But only ask game / challenge if currently disconnected. + // Only ask game / challenge if first connexion: if (!this.people[data.from]) { - this.$set(this.people, data.from, { pages: [page] }); + this.people[data.from] = { pages: [{ path: page, focus: true }] }; if (data.code == "connect") this.send("askchallenge", { target: data.from }); else this.send("askgame", { target: data.from, page: page }); } else { // Append page if not already in list - if (this.people[data.from].pages.indexOf(page) < 0) - this.people[data.from].pages.push(page); + if (!(this.people[data.from].pages.find(p => p.path == page))) + this.people[data.from].pages.push({ path: page, focus: true }); } if (!this.people[data.from].name && this.people[data.from].id !== 0) { // Identity not known yet @@ -510,11 +538,26 @@ export default { } } const page = data.page || "/"; - ArrayFun.remove(this.people[data.from].pages, p => p == page); + ArrayFun.remove(this.people[data.from].pages, p => p.path == page); if (this.people[data.from].pages.length == 0) this.$delete(this.people, data.from); break; } + case "getfocus": + // If user reload a page, focus may arrive earlier than connect + if (!!this.people[data.from]) { + this.people[data.from].pages + .find(p => p.path == data.page).focus = true; + this.$forceUpdate(); //TODO: shouldn't be required + } + break; + case "losefocus": + if (!!this.people[data.from]) { + this.people[data.from].pages + .find(p => p.path == data.page).focus = false; + this.$forceUpdate(); //TODO: shouldn't be required + } + break; case "killed": // I logged in elsewhere: this.conn = null; @@ -533,11 +576,13 @@ export default { } case "identity": { const user = data.data; - this.$set(this.people, user.sid, { - id: user.id, - name: user.name, - pages: this.people[user.sid].pages - }); + let player = this.people[user.sid]; + // player.pages is already set + player.id = user.id; + player.name = user.name; + // TODO: this.$set(people, ...) fails. So forceUpdate. + // But this shouldn't be like that! + this.$forceUpdate(); // If I multi-connect, kill current connexion if no mark (I'm older) if (this.newConnect[user.sid]) { if ( diff --git a/server/sockets.js b/server/sockets.js index 467036e8..39cf260b 100644 --- a/server/sockets.js +++ b/server/sockets.js @@ -21,6 +21,7 @@ function send(socket, message) { module.exports = function(wss) { // Associative array page --> sid --> tmpId --> socket // "page" is either "/" for hall or "/game/some_gid" for Game, + // or "/mygames" for Mygames page (simpler: no 'people' array). // tmpId is required if a same user (browser) has different tabs let clients = {}; wss.on("connection", (socket, req) => { @@ -34,7 +35,20 @@ module.exports = function(wss) { Object.keys(clients[page][k]).forEach(x => { if (k == sid && x == tmpId) return; send( - clients[page][k][x], + clients[page][k][x].socket, + Object.assign({ code: code, from: sid }, obj) + ); + }); + }); + }; + // For focus events: no need to target self + const notifyAllButMe = (page,code,obj={}) => { + if (!clients[page]) return; + Object.keys(clients[page]).forEach(k => { + if (k == sid) return; + Object.keys(clients[page][k]).forEach(x => { + send( + clients[page][k][x].socket, Object.assign({ code: code, from: sid }, obj) ); }); @@ -80,7 +94,7 @@ module.exports = function(wss) { // Self multi-connect: manual removal + disconnect const doKill = (pg) => { Object.keys(clients[pg][obj.sid]).forEach(x => { - send(clients[pg][obj.sid][x], {code: "killed"}); + send(clients[pg][obj.sid][x].socket, { code: "killed" }); }); delete clients[pg][obj.sid]; }; @@ -89,7 +103,7 @@ module.exports = function(wss) { if (k != obj.sid) { Object.keys(clients[pg][k]).forEach(x => { send( - clients[pg][k][x], + clients[pg][k][x].socket, Object.assign({ code: code, from: obj.sid }, o) ); }); @@ -154,7 +168,7 @@ module.exports = function(wss) { } const tmpId_idx = Math.floor(Math.random() * tmpIds.length); send( - clients[pg][obj.target][tmpIds[tmpId_idx]], + clients[pg][obj.target][tmpIds[tmpId_idx]].socket, { code: obj.code, from: [sid,tmpId,page] } ); } @@ -167,7 +181,7 @@ module.exports = function(wss) { Object.keys(clients[page][obj.target]).forEach(x => { if (obj.target != sid || x != tmpId) send( - clients[page][obj.target][x], + clients[page][obj.target][x].socket, { code: obj.code, data: obj.data } ); }); @@ -191,7 +205,7 @@ module.exports = function(wss) { if (!!obj.target && !!clients[page][obj.target]) { Object.keys(clients[page][obj.target]).forEach(x => { send( - clients[page][obj.target][x], + clients[page][obj.target][x].socket, Object.assign({ code: "newmove" }, dataWithFrom) ); }); @@ -207,7 +221,7 @@ module.exports = function(wss) { !!clients[page][obj.target[0]][obj.target[1]] ) { send( - clients[page][obj.target[0]][obj.target[1]], + clients[page][obj.target[0]][obj.target[1]].socket, { code: "gotmove" } ); } @@ -227,7 +241,7 @@ module.exports = function(wss) { Object.keys(clients[pg]).forEach(s => { Object.keys(clients[pg][s]).forEach(x => { send( - clients[pg][s][x], + clients[pg][s][x].socket, { code: "mconnect", from: sid } ); }); @@ -243,7 +257,7 @@ module.exports = function(wss) { if (!!clients[gamePg] && !!clients[gamePg][obj.target]) { Object.keys(clients[gamePg][obj.target]).forEach(x => { send( - clients[gamePg][obj.target][x], + clients[gamePg][obj.target][x].socket, { code: "abort" } ); }); @@ -251,6 +265,16 @@ module.exports = function(wss) { break; } + case "getfocus": + case "losefocus": + if (page == "/") notifyAllButMe("/", obj.code, { page: "/" }); + else { + // Notify game room + Hall: + notifyAllButMe(page, obj.code); + notifyAllButMe("/", obj.code, { page: page }); + } + break; + // Passing, relaying something: from isn't needed, // but target is fully identified (sid + tmpId) case "challenge": @@ -264,7 +288,7 @@ module.exports = function(wss) { // but leaving Hall, clients[pg] or clients[pg][target] could be undefined if (!!clients[pg] && !!clients[pg][obj.target[0]]) { send( - clients[pg][obj.target[0]][obj.target[1]], + clients[pg][obj.target[0]][obj.target[1]].socket, { code:obj.code, data:obj.data } ); } @@ -277,12 +301,13 @@ module.exports = function(wss) { doDisconnect(); }; // Update clients object: add new connexion + const newElt = { socket: socket, focus: true }; if (!clients[page]) - clients[page] = { [sid]: {[tmpId]: socket } }; + clients[page] = { [sid]: {[tmpId]: newElt } }; else if (!clients[page][sid]) - clients[page][sid] = { [tmpId]: socket }; + clients[page][sid] = { [tmpId]: newElt }; else - clients[page][sid][tmpId] = socket; + clients[page][sid][tmpId] = newElt; socket.on("message", messageListener); socket.on("close", closeListener); }); -- 2.44.0