123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
- define(["util", "session", "storage", "require", "templates"], function (util, session, storage, require, templates) {
- var peers = util.Module("peers");
- var assert = util.assert;
- var CHECK_ACTIVITY_INTERVAL = 10*1000; // Every 10 seconds see if someone has gone idle
- var IDLE_TIME = 3*60*1000; // Idle time is 3 minutes
- var TAB_IDLE_TIME = 2*60*1000; // When you tab away, after two minutes you'll say you are idle
- var BYE_TIME = 10*60*1000; // After 10 minutes of inactivity the person is considered to be "gone"
- var ui;
- require(["ui"], function (uiModule) {
- ui = uiModule;
- });
- var DEFAULT_NICKNAMES = templates("names").split(/,\s*/g);
- var Peer = util.Class({
- isSelf: false,
- constructor: function (id, attrs) {
- attrs = attrs || {};
- assert(id);
- assert(! Peer.peers[id]);
- this.id = id;
- this.identityId = attrs.identityId || null;
- this.status = attrs.status || "live";
- this.idle = attrs.status || "active";
- this.name = attrs.name || null;
- this.avatar = attrs.avatar || null;
- this.color = attrs.color || "#00FF00";
- this.view = ui.PeerView(this);
- this.lastMessageDate = 0;
- this.following = attrs.following || false;
- Peer.peers[id] = this;
- var joined = attrs.joined || false;
- if (attrs.fromHelloMessage) {
- this.updateFromHello(attrs.fromHelloMessage);
- if (attrs.fromHelloMessage.type == "hello") {
- joined = true;
- }
- }
- peers.emit("new-peer", this);
- if (joined) {
- this.view.notifyJoined();
- }
- this.view.update();
- },
- repr: function () {
- return "Peer(" + JSON.stringify(this.id) + ")";
- },
- serialize: function () {
- return {
- id: this.id,
- status: this.status,
- idle: this.idle,
- url: this.url,
- hash: this.hash,
- title: this.title,
- identityId: this.identityId,
- rtcSupported: this.rtcSupported,
- name: this.name,
- avatar: this.avatar,
- color: this.color,
- following: this.following
- };
- },
- destroy: function () {
- this.view.destroy();
- delete Peer.peers[this.id];
- },
- updateMessageDate: function (msg) {
- if (this.idle == "inactive") {
- this.update({idle: "active"});
- }
- if (this.status == "bye") {
- this.unbye();
- }
- this.lastMessageDate = Date.now();
- },
- updateFromHello: function (msg) {
- var urlUpdated = false;
- var activeRTC = false;
- var identityUpdated = false;
- if (msg.url && msg.url != this.url) {
- this.url = msg.url;
- this.hash = null;
- this.title = null;
- urlUpdated = true;
- }
- if (msg.hash != this.hash) {
- this.hash = msg.urlHash;
- urlUpdated = true;
- }
- if (msg.title != this.title) {
- this.title = msg.title;
- urlUpdated = true;
- }
- if (msg.rtcSupported !== undefined) {
- this.rtcSupported = msg.rtcSupported;
- }
- if (msg.identityId !== undefined) {
- this.identityId = msg.identityId;
- }
- if (msg.name && msg.name != this.name) {
- this.name = msg.name;
- identityUpdated = true;
- }
- if (msg.avatar && msg.avatar != this.avatar) {
- util.assertValidUrl(msg.avatar);
- this.avatar = msg.avatar;
- identityUpdated = true;
- }
- if (msg.color && msg.color != this.color) {
- this.color = msg.color;
- identityUpdated = true;
- }
- if (msg.isClient !== undefined) {
- this.isCreator = ! msg.isClient;
- }
- if (this.status != "live") {
- this.status = "live";
- peers.emit("status-updated", this);
- }
- if (this.idle != "active") {
- this.idle = "active";
- peers.emit("idle-updated", this);
- }
- if (msg.rtcSupported) {
- peers.emit("rtc-supported", this);
- }
- if (urlUpdated) {
- peers.emit("url-updated", this);
- }
- if (identityUpdated) {
- peers.emit("identity-updated", this);
- }
- // FIXME: I can't decide if this is the only time we need to emit
- // this message (and not .update() or other methods)
- if (this.following) {
- session.emit("follow-peer", this);
- }
- },
- update: function (attrs) {
- // FIXME: should probably test that only a couple attributes are settable
- // particularly status and idle
- if (attrs.idle) {
- this.idle = attrs.idle;
- }
- if (attrs.status) {
- this.status = attrs.status;
- }
- this.view.update();
- },
- className: function (prefix) {
- prefix = prefix || "";
- return prefix + util.safeClassName(this.id);
- },
- bye: function () {
- if (this.status != "bye") {
- this.status = "bye";
- peers.emit("status-updated", this);
- }
- this.view.update();
- },
- unbye: function () {
- if (this.status == "bye") {
- this.status = "live";
- peers.emit("status-updated", this);
- }
- this.view.update();
- },
- nudge: function () {
- session.send({
- type: "url-change-nudge",
- url: location.href,
- to: this.id
- });
- },
- follow: function () {
- if (this.following) {
- return;
- }
- peers.getAllPeers().forEach(function (p) {
- if (p.following) {
- p.unfollow();
- }
- });
- this.following = true;
- // We have to make sure we remember this, even if we change URLs:
- storeSerialization();
- this.view.update();
- session.emit("follow-peer", this);
- },
- unfollow: function () {
- this.following = false;
- storeSerialization();
- this.view.update();
- }
- });
- // FIXME: I can't decide where this should actually go, seems weird
- // that it is emitted and handled in the same module
- session.on("follow-peer", function (peer) {
- if (peer.url != session.currentUrl()) {
- var url = peer.url;
- if (peer.urlHash) {
- url += peer.urlHash;
- }
- location.href = url;
- }
- });
- Peer.peers = {};
- Peer.deserialize = function (obj) {
- obj.fromStorage = true;
- var peer = Peer(obj.id, obj);
- };
- peers.Self = undefined;
- session.on("start", function () {
- if (peers.Self) {
- return;
- }
- /* Same interface as Peer, represents oneself (local user): */
- peers.Self = util.mixinEvents({
- isSelf: true,
- id: session.clientId,
- identityId: session.identityId,
- status: "live",
- idle: "active",
- name: null,
- avatar: null,
- color: null,
- defaultName: null,
- loaded: false,
- isCreator: ! session.isClient,
- update: function (attrs) {
- var updatePeers = false;
- var updateIdle = false;
- var updateMsg = {type: "peer-update"};
- if (typeof attrs.name == "string" && attrs.name != this.name) {
- this.name = attrs.name;
- updateMsg.name = this.name;
- if (! attrs.fromLoad) {
- storage.settings.set("name", this.name);
- updatePeers = true;
- }
- }
- if (attrs.avatar && attrs.avatar != this.avatar) {
- util.assertValidUrl(attrs.avatar);
- this.avatar = attrs.avatar;
- updateMsg.avatar = this.avatar;
- if (! attrs.fromLoad) {
- storage.settings.set("avatar", this.avatar);
- updatePeers = true;
- }
- }
- if (attrs.color && attrs.color != this.color) {
- this.color = attrs.color;
- updateMsg.color = this.color;
- if (! attrs.fromLoad) {
- storage.settings.set("color", this.color);
- updatePeers = true;
- }
- }
- if (attrs.defaultName && attrs.defaultName != this.defaultName) {
- this.defaultName = attrs.defaultName;
- if (! attrs.fromLoad) {
- storage.settings.set("defaultName", this.defaultName);
- updatePeers = true;
- }
- }
- if (attrs.status && attrs.status != this.status) {
- this.status = attrs.status;
- peers.emit("status-updated", this);
- }
- if (attrs.idle && attrs.idle != this.idle) {
- this.idle = attrs.idle;
- updateIdle = true;
- peers.emit("idle-updated", this);
- }
- this.view.update();
- if (updatePeers && ! attrs.fromLoad) {
- session.emit("self-updated");
- session.send(updateMsg);
- }
- if (updateIdle && ! attrs.fromLoad) {
- session.send({
- type: "idle-status",
- idle: this.idle
- });
- }
- },
- className: function (prefix) {
- prefix = prefix || "";
- return prefix + "self";
- },
- _loadFromSettings: function () {
- return util.resolveMany(
- storage.settings.get("name"),
- storage.settings.get("avatar"),
- storage.settings.get("defaultName"),
- storage.settings.get("color")).then((function (name, avatar, defaultName, color) {
- if (! defaultName) {
- defaultName = util.pickRandom(DEFAULT_NICKNAMES);
- storage.settings.set("defaultName", defaultName);
- }
- if (! color) {
- color = Math.floor(Math.random() * 0xffffff).toString(16);
- while (color.length < 6) {
- color = "0" + color;
- }
- color = "#" + color;
- storage.settings.set("color", color);
- }
- if (! avatar) {
- avatar = TogetherJS.baseUrl + "/togetherjs/images/default-avatar.png";
- }
- this.update({
- name: name,
- avatar: avatar,
- defaultName: defaultName,
- color: color,
- fromLoad: true
- });
- peers._SelfLoaded.resolve();
- }).bind(this)); // FIXME: ignoring error
- },
- _loadFromApp: function () {
- // FIXME: I wonder if these should be optionally functions?
- // We could test typeof==function to distinguish between a getter and a concrete value
- var getUserName = TogetherJS.config.get("getUserName");
- var getUserColor = TogetherJS.config.get("getUserColor");
- var getUserAvatar = TogetherJS.config.get("getUserAvatar");
- var name, color, avatar;
- if (getUserName) {
- if (typeof getUserName == "string") {
- name = getUserName;
- } else {
- name = getUserName();
- }
- if (name && typeof name != "string") {
- // FIXME: test for HTML safe? Not that we require it, but
- // <>'s are probably a sign something is wrong.
- console.warn("Error in getUserName(): should return a string (got", name, ")");
- name = null;
- }
- }
- if (getUserColor) {
- if (typeof getUserColor == "string") {
- color = getUserColor;
- } else {
- color = getUserColor();
- }
- if (color && typeof color != "string") {
- // FIXME: would be nice to test for color-ness here.
- console.warn("Error in getUserColor(): should return a string (got", color, ")");
- color = null;
- }
- }
- if (getUserAvatar) {
- if (typeof getUserAvatar == "string") {
- avatar = getUserAvatar;
- } else {
- avatar = getUserAvatar();
- }
- if (avatar && typeof avatar != "string") {
- console.warn("Error in getUserAvatar(): should return a string (got", avatar, ")");
- avatar = null;
- }
- }
- if (name || color || avatar) {
- this.update({
- name: name,
- color: color,
- avatar: avatar
- });
- }
- }
- });
- peers.Self.view = ui.PeerView(peers.Self);
- storage.tab.get("peerCache").then(deserialize);
- peers.Self._loadFromSettings().then(function() {
- peers.Self._loadFromApp();
- peers.Self.view.update();
- session.emit("self-updated");
- });
- });
- session.on("refresh-user-data", function () {
- if (peers.Self) {
- peers.Self._loadFromApp();
- }
- });
- TogetherJS.config.track(
- "getUserName",
- TogetherJS.config.track(
- "getUserColor",
- TogetherJS.config.track(
- "getUserAvatar",
- function () {
- if (peers.Self) {
- peers.Self._loadFromApp();
- }
- }
- )
- )
- );
- peers._SelfLoaded = util.Deferred();
- function serialize() {
- var peers = [];
- util.forEachAttr(Peer.peers, function (peer) {
- peers.push(peer.serialize());
- });
- return {
- peers: peers
- };
- }
- function deserialize(obj) {
- if (! obj) {
- return;
- }
- obj.peers.forEach(function (peer) {
- Peer.deserialize(peer);
- });
- }
- peers.getPeer = function getPeer(id, message, ignoreMissing) {
- assert(id);
- var peer = Peer.peers[id];
- if (id === session.clientId) {
- return peers.Self;
- }
- if (message && ! peer) {
- peer = Peer(id, {fromHelloMessage: message});
- return peer;
- }
- if (ignoreMissing && !peer) {
- return null;
- }
- assert(peer, "No peer with id:", id);
- if (message &&
- (message.type == "hello" || message.type == "hello-back" ||
- message.type == "peer-update")) {
- peer.updateFromHello(message);
- peer.view.update();
- }
- return Peer.peers[id];
- };
- peers.getAllPeers = function (liveOnly) {
- var result = [];
- util.forEachAttr(Peer.peers, function (peer) {
- if (liveOnly && peer.status != "live") {
- return;
- }
- result.push(peer);
- });
- return result;
- };
- function checkActivity() {
- var ps = peers.getAllPeers();
- var now = Date.now();
- ps.forEach(function (p) {
- if (p.idle == "active" && now - p.lastMessageDate > IDLE_TIME) {
- p.update({idle: "inactive"});
- }
- if (p.status != "bye" && now - p.lastMessageDate > BYE_TIME) {
- p.bye();
- }
- });
- }
- session.hub.on("bye", function (msg) {
- var peer = peers.getPeer(msg.clientId);
- peer.bye();
- });
- var checkActivityTask = null;
- session.on("start", function () {
- if (checkActivityTask) {
- console.warn("Old peers checkActivityTask left over?");
- clearTimeout(checkActivityTask);
- }
- checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL);
- });
- session.on("close", function () {
- util.forEachAttr(Peer.peers, function (peer) {
- peer.destroy();
- });
- storage.tab.set("peerCache", undefined);
- clearTimeout(checkActivityTask);
- checkActivityTask = null;
- });
- var tabIdleTimeout = null;
- session.on("visibility-change", function (hidden) {
- if (hidden) {
- if (tabIdleTimeout) {
- clearTimeout(tabIdleTimeout);
- }
- tabIdleTimeout = setTimeout(function () {
- peers.Self.update({idle: "inactive"});
- }, TAB_IDLE_TIME);
- } else {
- if (tabIdleTimeout) {
- clearTimeout(tabIdleTimeout);
- }
- if (peers.Self.idle == "inactive") {
- peers.Self.update({idle: "active"});
- }
- }
- });
- session.hub.on("idle-status", function (msg) {
- msg.peer.update({idle: msg.idle});
- });
- // Pings are a straight alive check, and contain no more information:
- session.hub.on("ping", function () {
- session.send({type: "ping-back"});
- });
- window.addEventListener("pagehide", function () {
- // FIXME: not certain if this should be tab local or not:
- storeSerialization();
- }, false);
- function storeSerialization() {
- storage.tab.set("peerCache", serialize());
- }
- util.mixinEvents(peers);
- util.testExpose({
- setIdleTime: function (time) {
- IDLE_TIME = time;
- CHECK_ACTIVITY_INTERVAL = time / 2;
- if (TogetherJS.running) {
- clearTimeout(checkActivityTask);
- checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL);
- }
- }
- });
- util.testExpose({
- setByeTime: function (time) {
- BYE_TIME = time;
- CHECK_ACTIVITY_INTERVAL = Math.min(CHECK_ACTIVITY_INTERVAL, time / 2);
- if (TogetherJS.running) {
- clearTimeout(checkActivityTask);
- checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL);
- }
- }
- });
- return peers;
- });
|