+ return count;
+ },
+ visibilityChange: function() {
+ // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27
+ 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");
+ },
+ invColor: function(c) {
+ if (c == 'w') return this.st.tr["Black"];
+ return this.st.tr["White"];
+ },
+ partialResetNewchallenge: function() {
+ // Reset potential target and custom FEN:
+ this.newchallenge.to = "";
+ this.newchallenge.color = '';
+ this.newchallenge.fen = "";
+ this.newchallenge.diag = "";
+ this.newchallenge.memorize = false;
+ },
+ showNewchallengeForm: function() {
+ this.partialResetNewchallenge();
+ window.doClick("modalNewgame");
+ },
+ sameOptions: function(opt1, opt2) {
+ const keys1 = Object.keys(opt1),
+ keys2 = Object.keys(opt2);
+ if (keys1.length != keys2.length) return false;
+ for (const key1 of keys1) {
+ if (!keys2.includes(key1)) return false;
+ if (opt1[key1] != opt2[key1]) return false;
+ }
+ return true;
+ },
+ addPresetChall: function(chall) {
+ // Add only if not already existing:
+ if (this.presetChalls.some(c =>
+ c.vid == chall.vid &&
+ c.cadence == chall.cadence &&
+ this.sameOptions(c.options, chall.options)
+ )) {
+ return;
+ }
+ const L = this.presetChalls.length;
+ this.presetChalls.push({
+ index: L,
+ vid: chall.vid,
+ vname: chall.vname, //redundant, but easier
+ cadence: chall.cadence,
+ options: chall.options
+ });
+ localStorage.setItem("presetChalls", JSON.stringify(this.presetChalls));
+ },
+ removePresetChall: function(e, pchall) {
+ e.stopPropagation();
+ const pchallIdx =
+ this.presetChalls.findIndex(pc => pc.index == pchall.index);
+ this.presetChalls.splice(pchallIdx, 1);
+ localStorage.setItem("presetChalls", JSON.stringify(this.presetChalls));
+ },
+ tchallButtonsMargin: function() {
+ if (!!this.curChallToAccept.fen) return { "margin-top": "10px" };
+ return {};
+ },
+ changeChallTarget: function() {
+ if (!this.newchallenge.to) {
+ // Reset potential FEN + diagram
+ this.newchallenge.fen = "";
+ this.newchallenge.color = '';
+ this.newchallenge.diag = "";
+ }
+ },
+ cadenceFocusIfOpened: function() {
+ if (event.target.checked)
+ document.getElementById("cadence").focus();
+ },
+ send: function(code, obj) {
+ if (!!this.conn && this.conn.readyState == 1) {
+ this.conn.send(JSON.stringify(Object.assign({ code: code }, obj)));
+ }
+ },
+ setVname: function(obj) {
+ const variant = this.st.variants.find(v => v.id == obj.vid);
+ // this.st.variants might be uninitialized (variant == null)
+ if (!!variant) {
+ obj.vname = variant.name;
+ obj.vdisp = variant.display;
+ }
+ // NOTE: Next line is used in loadNewchallVariant
+ return (!variant ? "" : variant.name);
+ },
+ filterChallenges: function(type) {
+ return this.challenges.filter(c => c.type == type);
+ },
+ filterGames: function(type) {
+ return this.games.filter(g => g.type == type);
+ },
+ // o: challenge or game
+ classifyObject: function(o) {
+ // No imported games here
+ return (o.cadence.indexOf("d") >= 0 ? "corr" : "live");
+ },
+ setDisplay: function(letter, type, e) {
+ this[letter + "display"] = type;
+ localStorage.setItem(
+ "type-" + (letter == "c" ? "challenges" : "games"),
+ type
+ );
+ let elt = e
+ ? e.target
+ : document.getElementById("btn" + letter.toUpperCase() + type);
+ elt.classList.add("active");
+ elt.classList.remove("somethingnew"); //in case of
+ if (!!elt.previousElementSibling)
+ elt.previousElementSibling.classList.remove("active");
+ else elt.nextElementSibling.classList.remove("active");
+ },
+ isGamer: function(sid) {
+ return Object.values(this.people[sid].tmpIds)
+ .some(v => v.focus && v.page.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 &&
+ Object.values(this.people[sid].tmpIds)
+ .some(v => v.focus && v.page == "/")
+ );
+ },
+ challenge: function(sid) {
+ this.partialResetNewchallenge();
+ // Available, in Hall
+ this.newchallenge.to = this.people[sid].name;
+ // TODO: also store target sid to not re-search for it
+ document.getElementById("modalPeople").checked = false;
+ window.doClick("modalNewgame");
+ },
+ watchGame: function(sid) {
+ // In some game, maybe playing maybe not: show a random one
+ let gids = [];
+ Object.values(this.people[sid].tmpIds).forEach(v => {
+ if (v.focus) {
+ const matchGid = v.page.match(/[a-zA-Z0-9]+$/);
+ if (!!matchGid) gids.push(matchGid[0]);
+ }
+ });
+ const gid = gids[Math.floor(Math.random() * gids.length)];
+ window.open("/#/game/" + gid, "_blank");
+ },
+ showGame: function(g) {
+ // NOTE: we are an observer, since only games I don't play are shown here
+ // ==> Moves sent by connected remote player(s) if live game
+ window.open("/#/game/" + g.id, "_blank");
+ },
+ toggleSocialColor: function(action) {
+ if (!action && document.getElementById("modalPeople").checked)
+ document.getElementById("inputChat").focus();
+ else
+ document.getElementById("peopleBtn").classList.remove("somethingnew");
+ },
+ processChat: function(chat) {
+ this.send("newchat", { data: chat });
+ },
+ getOppsid: function(c) {
+ let oppsid = c.from.sid; //may not be defined if corr + offline opp
+ if (!oppsid) {
+ oppsid = Object.keys(this.people).find(
+ sid => this.people[sid].id == c.from.id
+ );
+ }
+ return oppsid;
+ },
+ // Messaging center:
+ socketMessageListener: function(msg) {
+ if (!this.conn) return;
+ const data = JSON.parse(msg.data);
+ switch (data.code) {
+ case "pollclientsandgamers": {
+ // TODO: shuffling and random filtering on server,
+ // if the room is really crowded.
+ Object.keys(data.sockIds).forEach(sid => {
+ if (sid != this.st.user.sid) {
+ // Pick a target tmpId (+page) at random:
+ const pt = Object.values(data.sockIds[sid]);
+ const randPage = pt[randInt(pt.length)].page;
+ this.send(
+ "askidentity",
+ {
+ target: sid,
+ page: randPage
+ }
+ );
+ }
+ if (!this.people[sid])
+ // Do not set name or id: identity unknown yet
+ this.people[sid] = { tmpIds: data.sockIds[sid] };
+ else
+ Object.assign(this.people[sid].tmpIds, data.sockIds[sid]);
+ if (Object.values(data.sockIds[sid]).some(v => v.page == "/"))
+ // Peer is in Hall
+ this.send("askchallenges", { target: sid });
+ Object.values(data.sockIds[sid]).forEach(v => {
+ if (
+ v.page.indexOf("/game/") >= 0 &&
+ !v.page.match(/\/[0-9]+$/)
+ ) {
+ // Peer is in Game: ask only if live game
+ this.send("askgame", { target: sid, page: v.page });
+ }
+ });
+ });
+ break;
+ }
+ case "connect":
+ case "gconnect": {
+ const page = data.page || "/";
+ if (data.code == "connect") {
+ // Ask challenges only on first connexion:
+ if (!this.people[data.from[0]])
+ this.send("askchallenges", { target: data.from[0] });
+ }
+ // Ask game only if live:
+ else if (!page.match(/\/[0-9]+$/))
+ this.send("askgame", { target: data.from[0], page: page });
+ if (!this.people[data.from[0]]) {
+ this.$set(
+ this.people,
+ data.from[0],