Bug 1248492 - Land version 1.1.2 of the Loop system add-on in mozilla-central - code updates. rs=Standard8 for already reviewed code

This commit is contained in:
Mark Banner 2016-02-15 23:43:42 +00:00
parent c20236ea7e
commit c41a32be75
57 changed files with 2105 additions and 666 deletions

View File

@ -24,11 +24,26 @@ XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
XPCOMUtils.defineLazyModuleGetter(this, "Task", XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm"); "resource://gre/modules/Task.jsm");
// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
const PREF_LOG_LEVEL = "loop.debug.loglevel";
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
let consoleOptions = {
maxLogLevelPref: PREF_LOG_LEVEL,
prefix: "Loop"
};
return new ConsoleAPI(consoleOptions);
});
/** /**
* This window listener gets loaded into each browser.xul window and is used * This window listener gets loaded into each browser.xul window and is used
* to provide the required loop functions for the window. * to provide the required loop functions for the window.
*/ */
var WindowListener = { var WindowListener = {
// Records the add-on version once we know it.
addonVersion: "unknown",
/** /**
* Sets up the chrome integration within browser windows for Loop. * Sets up the chrome integration within browser windows for Loop.
* *
@ -40,6 +55,7 @@ var WindowListener = {
let xhrClass = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]; let xhrClass = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"];
let FileReader = window.FileReader; let FileReader = window.FileReader;
let menuItem = null; let menuItem = null;
let isSlideshowOpen = false;
// the "exported" symbols // the "exported" symbols
var LoopUI = { var LoopUI = {
@ -74,6 +90,30 @@ var WindowListener = {
return browser; return browser;
}, },
get isSlideshowOpen() {
return isSlideshowOpen;
},
set isSlideshowOpen(aOpen) {
isSlideshowOpen = aOpen;
this.updateToolbarState();
},
/**
* @return {Object} Getter for the Loop constants
*/
get constants() {
if (!this._constants) {
// GetAllConstants is synchronous even though it's using a callback.
this.LoopAPI.sendMessageToHandler({
name: "GetAllConstants"
}, result => {
this._constants = result;
});
}
return this._constants;
},
/** /**
* @return {Promise} * @return {Promise}
*/ */
@ -114,6 +154,10 @@ var WindowListener = {
}); });
} }
if (this.isSlideshowOpen) {
return Promise.resolve();
}
return this.openPanel(event).then(mm => { return this.openPanel(event).then(mm => {
if (mm) { if (mm) {
mm.sendAsyncMessage("Social:EnsureFocusElement"); mm.sendAsyncMessage("Social:EnsureFocusElement");
@ -141,6 +185,12 @@ var WindowListener = {
mm.sendAsyncMessage("Social:WaitForDocumentVisible"); mm.sendAsyncMessage("Social:WaitForDocumentVisible");
mm.addMessageListener("Social:DocumentVisible", () => resolve(mm)); mm.addMessageListener("Social:DocumentVisible", () => resolve(mm));
let buckets = this.constants.LOOP_MAU_TYPE;
this.LoopAPI.sendMessageToHandler({
name: "TelemetryAddValue",
data: ["LOOP_MAU", buckets.OPEN_PANEL]
});
}; };
// Used to clear the temporary "login" state from the button. // Used to clear the temporary "login" state from the button.
@ -171,6 +221,17 @@ var WindowListener = {
}); });
}, },
/**
* Wrapper for openPanel - to support Firefox 46 and 45.
*
* @param {event} event The event opening the panel, used to anchor
* the panel to the button which triggers it.
* @return {Promise}
*/
openCallPanel: function(event) {
return this.openPanel(event);
},
/** /**
* Method to know whether actions to open the panel should instead resume the tour. * Method to know whether actions to open the panel should instead resume the tour.
* *
@ -229,7 +290,7 @@ var WindowListener = {
init: function() { init: function() {
// This is a promise for test purposes, but we don't want to be logging // This is a promise for test purposes, but we don't want to be logging
// expected errors to the console, so we catch them here. // expected errors to the console, so we catch them here.
this.MozLoopService.initialize().catch(ex => { this.MozLoopService.initialize(WindowListener.addonVersion).catch(ex => {
if (!ex.message || if (!ex.message ||
(!ex.message.contains("not enabled") && (!ex.message.contains("not enabled") &&
!ex.message.contains("not needed"))) { !ex.message.contains("not needed"))) {
@ -313,6 +374,8 @@ var WindowListener = {
if (this.MozLoopService.errors.size) { if (this.MozLoopService.errors.size) {
state = "error"; state = "error";
mozL10nId += "-error"; mozL10nId += "-error";
} else if (this.isSlideshowOpen) {
state = "slideshow";
} else if (this.MozLoopService.screenShareActive) { } else if (this.MozLoopService.screenShareActive) {
state = "action"; state = "action";
mozL10nId += "-screensharing"; mozL10nId += "-screensharing";
@ -481,8 +544,88 @@ var WindowListener = {
gBrowser.tabContainer.removeEventListener("TabSelect", this); gBrowser.tabContainer.removeEventListener("TabSelect", this);
gBrowser.removeEventListener("DOMTitleChanged", this); gBrowser.removeEventListener("DOMTitleChanged", this);
gBrowser.removeEventListener("mousemove", this); gBrowser.removeEventListener("mousemove", this);
this.removeRemoteCursor();
this._listeningToTabSelect = false; this._listeningToTabSelect = false;
this._browserSharePaused = false; this._browserSharePaused = false;
this._sendTelemetryEventsIfNeeded();
},
/**
* Sends telemetry events for pause/ resume buttons if needed.
*/
_sendTelemetryEventsIfNeeded: function() {
// The user can't click Resume button without clicking Pause button first.
if (!this._pauseButtonClicked) {
return;
}
let buckets = this.constants.SHARING_SCREEN;
this.LoopAPI.sendMessageToHandler({
name: "TelemetryAddValue",
data: [
"LOOP_INFOBAR_ACTION_BUTTONS",
buckets.PAUSED
]
});
if (this._resumeButtonClicked) {
this.LoopAPI.sendMessageToHandler({
name: "TelemetryAddValue",
data: [
"LOOP_INFOBAR_ACTION_BUTTONS",
buckets.RESUMED
]
});
}
this._pauseButtonClicked = false;
this._resumeButtonClicked = false;
},
/**
* If sharing is active, paints and positions the remote cursor
* over the screen
*
* @param cursorData Object with the correct position for the cursor
* {
* ratioX: position on the X axis (percentage value)
* ratioY: position on the Y axis (percentage value)
* }
*/
addRemoteCursor: function(cursorData) {
if (!this._listeningToTabSelect) {
return;
}
let browser = gBrowser.selectedBrowser;
let cursor = document.getElementById("loop-remote-cursor");
if (!cursor) {
cursor = document.createElement("image");
cursor.setAttribute("id", "loop-remote-cursor");
}
// Update the cursor's position.
cursor.setAttribute("left",
cursorData.ratioX * browser.boxObject.width);
cursor.setAttribute("top",
cursorData.ratioY * browser.boxObject.height);
// browser's parent is a xul:stack, so positioning with left/top works.
browser.parentNode.appendChild(cursor);
},
/**
* Removes the remote cursor from the screen
*
* @param browser OPT browser where the cursor should be removed from.
*/
removeRemoteCursor: function() {
let cursor = document.getElementById("loop-remote-cursor");
if (cursor) {
cursor.parentNode.removeChild(cursor);
}
}, },
/** /**
@ -539,6 +682,11 @@ var WindowListener = {
buttonNode.label = stringObj.label; buttonNode.label = stringObj.label;
buttonNode.accessKey = stringObj.accesskey; buttonNode.accessKey = stringObj.accesskey;
LoopUI.MozLoopService.toggleBrowserSharing(this._browserSharePaused); LoopUI.MozLoopService.toggleBrowserSharing(this._browserSharePaused);
if (this._browserSharePaused) {
this._pauseButtonClicked = true;
} else {
this._resumeButtonClicked = true;
}
return true; return true;
}, },
type: "pause" type: "pause"
@ -605,7 +753,10 @@ var WindowListener = {
let wasVisible = false; let wasVisible = false;
// Hide the infobar from the previous tab. // Hide the infobar from the previous tab.
if (event.detail.previousTab) { if (event.detail.previousTab) {
wasVisible = this._hideBrowserSharingInfoBar(event.detail.previousTab.linkedBrowser); wasVisible = this._hideBrowserSharingInfoBar(
event.detail.previousTab.linkedBrowser);
// And remove the cursor.
this.removeRemoteCursor();
} }
// We've changed the tab, so get the new window id. // We've changed the tab, so get the new window id.
@ -713,10 +864,18 @@ var WindowListener = {
window.LoopUI = LoopUI; window.LoopUI = LoopUI;
}, },
tearDownBrowserUI: function() { /**
// Take any steps to remove UI or anything from the browser window * Take any steps to remove UI or anything from the browser window
// document.getElementById() etc. will work here * document.getElementById() etc. will work here.
// XXX Add in tear-down of the panel. *
* @param {Object} window The window to remove the integration from.
*/
tearDownBrowserUI: function(window) {
if (window.LoopUI) {
window.LoopUI.removeMenuItem();
// XXX Bug 1229352 - Add in tear-down of the panel.
}
}, },
// nsIWindowMediatorListener functions. // nsIWindowMediatorListener functions.
@ -815,7 +974,10 @@ function loadDefaultPrefs() {
/** /**
* Called when the add-on is started, e.g. when installed or when Firefox starts. * Called when the add-on is started, e.g. when installed or when Firefox starts.
*/ */
function startup() { function startup(data) {
// Record the add-on version for when the UI is initialised.
WindowListener.addonVersion = data.version;
loadDefaultPrefs(); loadDefaultPrefs();
createLoopButton(); createLoopButton();

View File

@ -22,9 +22,6 @@ XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {}); const { EventEmitter } = Cu.import("resource://devtools/shared/event-emitter.js", {});
return new EventEmitter(); return new EventEmitter();
}); });
XPCOMUtils.defineLazyGetter(this, "gLoopBundle", function() {
return Services.strings.createBundle("chrome://loop/locale/loop.properties");
});
XPCOMUtils.defineLazyModuleGetter(this, "LoopRoomsCache", XPCOMUtils.defineLazyModuleGetter(this, "LoopRoomsCache",
"chrome://loop/content/modules/LoopRoomsCache.jsm"); "chrome://loop/content/modules/LoopRoomsCache.jsm");
@ -772,19 +769,13 @@ var LoopRoomsInternal = {
* Joins a room. The sessionToken that is returned by the server will be stored * Joins a room. The sessionToken that is returned by the server will be stored
* locally, for future use. * locally, for future use.
* *
* @param {String} roomToken The room token. * @param {String} roomToken The room token.
* @param {Function} callback Function that will be invoked once the operation * @param {String} displayName The user's display name.
* finished. The first argument passed will be an * @param {Function} callback Function that will be invoked once the operation
* `Error` object or `null`. * finished. The first argument passed will be an
* `Error` object or `null`.
*/ */
join: function(roomToken, callback) { join: function(roomToken, displayName, callback) {
let displayName;
if (MozLoopService.userProfile && MozLoopService.userProfile.email) {
displayName = MozLoopService.userProfile.email;
} else {
displayName = gLoopBundle.GetStringFromName("display_name_guest");
}
this._postToRoom(roomToken, { this._postToRoom(roomToken, {
action: "join", action: "join",
displayName: displayName, displayName: displayName,
@ -1067,8 +1058,8 @@ this.LoopRooms = {
return LoopRoomsInternal.delete(roomToken, callback); return LoopRoomsInternal.delete(roomToken, callback);
}, },
join: function(roomToken, callback) { join: function(roomToken, displayName, callback) {
return LoopRoomsInternal.join(roomToken, callback); return LoopRoomsInternal.join(roomToken, displayName, callback);
}, },
refreshMembership: function(roomToken, sessionToken, callback) { refreshMembership: function(roomToken, sessionToken, callback) {

View File

@ -138,6 +138,12 @@ const kMessageName = "Loop:Message";
const kPushMessageName = "Loop:Message:Push"; const kPushMessageName = "Loop:Message:Push";
const kPushSubscription = "pushSubscription"; const kPushSubscription = "pushSubscription";
const kRoomsPushPrefix = "Rooms:"; const kRoomsPushPrefix = "Rooms:";
const kMauPrefMap = new Map(
Object.getOwnPropertyNames(LOOP_MAU_TYPE).map(name => {
let parts = name.toLowerCase().split("_");
return [LOOP_MAU_TYPE[name], parts[0] + parts[1].charAt(0).toUpperCase() + parts[1].substr(1)];
})
);
const kMessageHandlers = { const kMessageHandlers = {
/** /**
* Start browser sharing, which basically means to start listening for tab * Start browser sharing, which basically means to start listening for tab
@ -185,6 +191,30 @@ const kMessageHandlers = {
reply(); reply();
}, },
/**
* Creates a layout for the remote cursor on the browser chrome,
* and positions it on the received coordinates.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its 'data' property:
* {
* ratioX: cursor's X position (between 0-1)
* ratioY: cursor's Y position (between 0-1)
* }
*
* @param {Function} reply Callback function, invoked with the result of the
* message handler. The result will be sent back to
* the senders' channel.
*/
AddRemoteCursorOverlay: function(message, reply) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
if (win) {
win.LoopUI.addRemoteCursor(message.data[0]);
}
reply();
},
/** /**
* Associates a session-id and a call-id with a window for debugging. * Associates a session-id and a call-id with a window for debugging.
* *
@ -366,9 +396,11 @@ const kMessageHandlers = {
GetAllConstants: function(message, reply) { GetAllConstants: function(message, reply) {
reply({ reply({
LOOP_SESSION_TYPE: LOOP_SESSION_TYPE, LOOP_SESSION_TYPE: LOOP_SESSION_TYPE,
LOOP_MAU_TYPE: LOOP_MAU_TYPE,
ROOM_CREATE: ROOM_CREATE, ROOM_CREATE: ROOM_CREATE,
ROOM_DELETE: ROOM_DELETE, ROOM_DELETE: ROOM_DELETE,
SHARING_ROOM_URL: SHARING_ROOM_URL, SHARING_ROOM_URL: SHARING_ROOM_URL,
SHARING_SCREEN: SHARING_SCREEN,
TWO_WAY_MEDIA_CONN_LENGTH: TWO_WAY_MEDIA_CONN_LENGTH TWO_WAY_MEDIA_CONN_LENGTH: TWO_WAY_MEDIA_CONN_LENGTH
}); });
}, },
@ -779,16 +811,13 @@ const kMessageHandlers = {
* *
* @param {Object} message Message meant for the handler function, containing * @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property: * the following parameters in its `data` property:
* [ * []
* {String} src Origin that starts or resumes the tour
* ]
* @param {Function} reply Callback function, invoked with the result of this * @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to * message handler. The result will be sent back to
* the senders' channel. * the senders' channel.
*/ */
OpenGettingStartedTour: function(message, reply) { OpenGettingStartedTour: function(message, reply) {
var src = message.data[0]; MozLoopService.openGettingStartedTour();
MozLoopService.openGettingStartedTour(src);
reply(); reply();
}, },
@ -1001,7 +1030,30 @@ const kMessageHandlers = {
*/ */
TelemetryAddValue: function(message, reply) { TelemetryAddValue: function(message, reply) {
let [histogramId, value] = message.data; let [histogramId, value] = message.data;
Services.telemetry.getHistogramById(histogramId).add(value);
if (histogramId === "LOOP_MAU") {
let pref = "mau." + kMauPrefMap.get(value);
let prefDate = MozLoopService.getLoopPref(pref) * 1000;
let delta = Date.now() - prefDate;
// Send telemetry event if period (30 days) passed.
// 0 is default value for pref.
// 2592000 seconds in 30 days
if (pref === 0 || delta >= 2592000 * 1000) {
try {
Services.telemetry.getHistogramById(histogramId).add(value);
} catch (ex) {
MozLoopService.log.error("TelemetryAddValue failed for histogram '" + histogramId + "'", ex);
}
MozLoopService.setLoopPref(pref, Math.floor(Date.now() / 1000));
}
} else {
try {
Services.telemetry.getHistogramById(histogramId).add(value);
} catch (ex) {
MozLoopService.log.error("TelemetryAddValue failed for histogram '" + histogramId + "'", ex);
}
}
reply(); reply();
} }
}; };
@ -1020,7 +1072,11 @@ const LoopAPIInternal = {
Cu.import("resource://gre/modules/RemotePageManager.jsm"); Cu.import("resource://gre/modules/RemotePageManager.jsm");
gPageListeners = [new RemotePages("about:looppanel"), new RemotePages("about:loopconversation")]; gPageListeners = [new RemotePages("about:looppanel"),
new RemotePages("about:loopconversation"),
// Slideshow added here to expose the loop api to make L10n work.
// XXX Can remove once slideshow is made remote.
new RemotePages("chrome://loop/content/panels/slideshow.html")];
for (let page of gPageListeners) { for (let page of gPageListeners) {
page.addMessageListener(kMessageName, this.handleMessage.bind(this)); page.addMessageListener(kMessageName, this.handleMessage.bind(this));
} }

View File

@ -59,6 +59,30 @@ const ROOM_DELETE = {
DELETE_FAIL: 1 DELETE_FAIL: 1
}; };
/**
* Values that we segment sharing screen pause/ resume action telemetry probes into.
*
* @type {{PAUSED: Number, RESUMED: Number}}
*/
const SHARING_SCREEN = {
PAUSED: 0,
RESUMED: 1
};
/**
* Values that we segment MAUs telemetry probes into.
*
* @type {{OPEN_PANEL: Number, OPEN_CONVERSATION: Number,
* ROOM_OPEN: Number, ROOM_SHARE: Number, ROOM_DELETE: Number}}
*/
const LOOP_MAU_TYPE = {
OPEN_PANEL: 0,
OPEN_CONVERSATION: 1,
ROOM_OPEN: 2,
ROOM_SHARE: 3,
ROOM_DELETE: 4
};
// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error". // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
const PREF_LOG_LEVEL = "loop.debug.loglevel"; const PREF_LOG_LEVEL = "loop.debug.loglevel";
@ -81,14 +105,17 @@ Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
Cu.importGlobalProperties(["URL"]); Cu.importGlobalProperties(["URL"]);
this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE", this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE", "LOOP_MAU_TYPE",
"TWO_WAY_MEDIA_CONN_LENGTH", "SHARING_ROOM_URL", "ROOM_CREATE", "ROOM_DELETE"]; "TWO_WAY_MEDIA_CONN_LENGTH", "SHARING_ROOM_URL", "SHARING_SCREEN",
"ROOM_CREATE", "ROOM_DELETE"];
XPCOMUtils.defineConstant(this, "LOOP_SESSION_TYPE", LOOP_SESSION_TYPE); XPCOMUtils.defineConstant(this, "LOOP_SESSION_TYPE", LOOP_SESSION_TYPE);
XPCOMUtils.defineConstant(this, "TWO_WAY_MEDIA_CONN_LENGTH", TWO_WAY_MEDIA_CONN_LENGTH); XPCOMUtils.defineConstant(this, "TWO_WAY_MEDIA_CONN_LENGTH", TWO_WAY_MEDIA_CONN_LENGTH);
XPCOMUtils.defineConstant(this, "SHARING_ROOM_URL", SHARING_ROOM_URL); XPCOMUtils.defineConstant(this, "SHARING_ROOM_URL", SHARING_ROOM_URL);
XPCOMUtils.defineConstant(this, "SHARING_SCREEN", SHARING_SCREEN);
XPCOMUtils.defineConstant(this, "ROOM_CREATE", ROOM_CREATE); XPCOMUtils.defineConstant(this, "ROOM_CREATE", ROOM_CREATE);
XPCOMUtils.defineConstant(this, "ROOM_DELETE", ROOM_DELETE); XPCOMUtils.defineConstant(this, "ROOM_DELETE", ROOM_DELETE);
XPCOMUtils.defineConstant(this, "LOOP_MAU_TYPE", LOOP_MAU_TYPE);
XPCOMUtils.defineLazyModuleGetter(this, "LoopAPI", XPCOMUtils.defineLazyModuleGetter(this, "LoopAPI",
"chrome://loop/content/modules/MozLoopAPI.jsm"); "chrome://loop/content/modules/MozLoopAPI.jsm");
@ -117,9 +144,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials", XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
"resource://services-common/hawkrequest.js"); "resource://services-common/hawkrequest.js");
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
"resource://gre/modules/MozSocialAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms", XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms",
"chrome://loop/content/modules/LoopRooms.jsm"); "chrome://loop/content/modules/LoopRooms.jsm");
@ -171,6 +195,7 @@ var gFxAOAuthClientPromise = null;
var gFxAOAuthClient = null; var gFxAOAuthClient = null;
var gErrors = new Map(); var gErrors = new Map();
var gConversationWindowData = new Map(); var gConversationWindowData = new Map();
var gAddonVersion = "unknown";
/** /**
* Internal helper methods and state * Internal helper methods and state
@ -614,7 +639,11 @@ var MozLoopServiceInternal = {
throw error; throw error;
}; };
return gHawkClient.request(path, method, credentials, payloadObj).then( var extraHeaders = {
"x-loop-addon-ver": gAddonVersion
};
return gHawkClient.request(path, method, credentials, payloadObj, extraHeaders).then(
(result) => { (result) => {
this.clearError("network"); this.clearError("network");
return result; return result;
@ -904,157 +933,174 @@ var MozLoopServiceInternal = {
* window when it opens. * window when it opens.
* @param {Function} windowCloseCallback Callback function that's invoked * @param {Function} windowCloseCallback Callback function that's invoked
* when the window closes. * when the window closes.
* @returns {Number} The id of the window, null if a window could not * @returns {Promise} That is resolved with the id of the window, null if a
* be opened. * window could not be opened.
*/ */
openChatWindow: function(conversationWindowData, windowCloseCallback) { openChatWindow: function(conversationWindowData, windowCloseCallback) {
// So I guess the origin is the loop server!? return new Promise(resolve => {
let origin = this.loopServerUri; // So I guess the origin is the loop server!?
let windowId = this.getChatWindowID(conversationWindowData); let origin = this.loopServerUri;
let windowId = this.getChatWindowID(conversationWindowData);
gConversationWindowData.set(windowId, conversationWindowData); gConversationWindowData.set(windowId, conversationWindowData);
let url = this.getChatURL(windowId); let url = this.getChatURL(windowId);
Chat.registerButton(kChatboxHangupButton); Chat.registerButton(kChatboxHangupButton);
let callback = chatbox => { let callback = chatbox => {
let mm = chatbox.content.messageManager; let mm = chatbox.content.messageManager;
let loaded = () => { let loaded = () => {
mm.removeMessageListener("DOMContentLoaded", loaded); mm.removeMessageListener("DOMContentLoaded", loaded);
mm.sendAsyncMessage("Social:ListenForEvents", { mm.sendAsyncMessage("Social:ListenForEvents", {
eventNames: ["LoopChatEnabled", "LoopChatMessageAppended", eventNames: ["LoopChatEnabled", "LoopChatMessageAppended",
"LoopChatDisabledMessageAppended", "socialFrameAttached", "LoopChatDisabledMessageAppended", "socialFrameAttached",
"socialFrameDetached", "socialFrameHide", "socialFrameShow"] "socialFrameDetached", "socialFrameHide", "socialFrameShow"]
}); });
let chatbar = chatbox.parentNode; const kEventNamesMap = {
socialFrameAttached: "Loop:ChatWindowAttached",
socialFrameDetached: "Loop:ChatWindowDetached",
socialFrameHide: "Loop:ChatWindowHidden",
socialFrameShow: "Loop:ChatWindowShown",
unload: "Loop:ChatWindowClosed"
};
const kEventNamesMap = { const kSizeMap = {
socialFrameAttached: "Loop:ChatWindowAttached", LoopChatEnabled: "loopChatEnabled",
socialFrameDetached: "Loop:ChatWindowDetached", LoopChatDisabledMessageAppended: "loopChatDisabledMessageAppended",
socialFrameHide: "Loop:ChatWindowHidden", LoopChatMessageAppended: "loopChatMessageAppended"
socialFrameShow: "Loop:ChatWindowShown", };
unload: "Loop:ChatWindowClosed"
let listeners = {};
let messageName = "Social:CustomEvent";
mm.addMessageListener(messageName, listeners[messageName] = message => {
let eventName = message.data.name;
if (kEventNamesMap[eventName]) {
eventName = kEventNamesMap[eventName];
UITour.clearAvailableTargetsCache();
UITour.notify(eventName);
} else {
// When the chat box or messages are shown, resize the panel or window
// to be slightly higher to accomodate them.
let customSize = kSizeMap[eventName];
let currSize = chatbox.getAttribute("customSize");
// If the size is already at the requested one or at the maximum size
// already, don't do anything. Especially don't make it shrink.
if (customSize && currSize != customSize && currSize != "loopChatMessageAppended") {
chatbox.setAttribute("customSize", customSize);
chatbox.parentNode.setAttribute("customSize", customSize);
}
}
});
// Handle window.close correctly on the chatbox.
mm.sendAsyncMessage("Social:HookWindowCloseForPanelClose");
messageName = "DOMWindowClose";
mm.addMessageListener(messageName, listeners[messageName] = () => {
// Remove message listeners.
for (let name of Object.getOwnPropertyNames(listeners)) {
mm.removeMessageListener(name, listeners[name]);
}
listeners = {};
windowCloseCallback();
if (conversationWindowData.type == "room") {
// NOTE: if you add something here, please also consider if something
// needs to be done on the content side as well (e.g.
// activeRoomStore#windowUnload).
LoopAPI.sendMessageToHandler({
name: "HangupNow",
data: [conversationWindowData.roomToken, windowId]
});
}
chatbox.close();
});
mm.sendAsyncMessage("Loop:MonitorPeerConnectionLifecycle");
messageName = "Loop:PeerConnectionLifecycleChange";
mm.addMessageListener(messageName, listeners[messageName] = message => {
// Chat Window Id, this is different that the internal winId
let chatWindowId = message.data.locationHash.slice(1);
var context = this.conversationContexts.get(chatWindowId);
var peerConnectionID = message.data.peerConnectionID;
var exists = peerConnectionID.match(/session=(\S+)/);
if (context && !exists) {
// Not ideal but insert our data amidst existing data like this:
// - 000 (id=00 url=http)
// + 000 (session=000 call=000 id=00 url=http)
var pair = peerConnectionID.split("(");
if (pair.length == 2) {
peerConnectionID = pair[0] + "(session=" + context.sessionId +
(context.callId ? " call=" + context.callId : "") + " " + pair[1];
}
}
if (message.data.type == "iceconnectionstatechange") {
switch (message.data.iceConnectionState) {
case "failed":
case "disconnected":
if (Services.telemetry.canRecordExtended) {
this.stageForTelemetryUpload(chatbox.content, message.data);
}
break;
}
}
});
// When a chat window is attached or detached, the docShells hosting
// about:loopconverstation is swapped to the newly created chat window.
// (Be it inside a popup or back inside a chatbox element attached to the
// chatbar.)
// Since a swapDocShells call does not swap the messageManager instances
// attached to a browser, we'll need to add the message listeners to
// the new messageManager. This is not a bug in swapDocShells, merely
// a design decision.
chatbox.content.addEventListener("SwapDocShells", function swapped(ev) {
chatbox.content.removeEventListener("SwapDocShells", swapped);
let otherBrowser = ev.detail;
chatbox = otherBrowser.ownerDocument.getBindingParent(otherBrowser);
mm = otherBrowser.messageManager;
otherBrowser.addEventListener("SwapDocShells", swapped);
for (let name of Object.getOwnPropertyNames(listeners)) {
mm.addMessageListener(name, listeners[name]);
}
});
UITour.notify("Loop:ChatWindowOpened");
resolve(windowId);
}; };
const kSizeMap = { mm.sendAsyncMessage("WaitForDOMContentLoaded");
LoopChatEnabled: "loopChatEnabled", mm.addMessageListener("DOMContentLoaded", loaded);
LoopChatDisabledMessageAppended: "loopChatDisabledMessageAppended",
LoopChatMessageAppended: "loopChatMessageAppended"
};
let listeners = {};
let messageName = "Social:CustomEvent";
mm.addMessageListener(messageName, listeners[messageName] = message => {
let eventName = message.data.name;
if (kEventNamesMap[eventName]) {
eventName = kEventNamesMap[eventName];
UITour.clearAvailableTargetsCache();
UITour.notify(eventName);
if (eventName == "Loop:ChatWindowDetached" || eventName == "Loop:ChatWindowAttached") {
// After detach, re-attach of the chatbox, refresh its reference so
// we can keep using it here.
let ref = chatbar.chatboxForURL.get(chatbox.src);
chatbox = ref && ref.get() || chatbox;
}
} else {
// When the chat box or messages are shown, resize the panel or window
// to be slightly higher to accomodate them.
let customSize = kSizeMap[eventName];
let currSize = chatbox.getAttribute("customSize");
// If the size is already at the requested one or at the maximum size
// already, don't do anything. Especially don't make it shrink.
if (customSize && currSize != customSize && currSize != "loopChatMessageAppended") {
chatbox.setAttribute("customSize", customSize);
chatbox.parentNode.setAttribute("customSize", customSize);
}
}
});
// Handle window.close correctly on the chatbox.
hookWindowCloseForPanelClose(chatbox.content);
messageName = "DOMWindowClose";
mm.addMessageListener(messageName, listeners[messageName] = () => {
// Remove message listeners.
for (let name of Object.getOwnPropertyNames(listeners)) {
mm.removeMessageListener(name, listeners[name]);
}
listeners = {};
windowCloseCallback();
if (conversationWindowData.type == "room") {
// NOTE: if you add something here, please also consider if something
// needs to be done on the content side as well (e.g.
// activeRoomStore#windowUnload).
LoopAPI.sendMessageToHandler({
name: "HangupNow",
data: [conversationWindowData.roomToken, windowId]
});
}
});
mm.sendAsyncMessage("Loop:MonitorPeerConnectionLifecycle");
messageName = "Loop:PeerConnectionLifecycleChange";
mm.addMessageListener(messageName, listeners[messageName] = message => {
// Chat Window Id, this is different that the internal winId
let chatWindowId = message.data.locationHash.slice(1);
var context = this.conversationContexts.get(chatWindowId);
var peerConnectionID = message.data.peerConnectionID;
var exists = peerConnectionID.match(/session=(\S+)/);
if (context && !exists) {
// Not ideal but insert our data amidst existing data like this:
// - 000 (id=00 url=http)
// + 000 (session=000 call=000 id=00 url=http)
var pair = peerConnectionID.split("(");
if (pair.length == 2) {
peerConnectionID = pair[0] + "(session=" + context.sessionId +
(context.callId ? " call=" + context.callId : "") + " " + pair[1];
}
}
if (message.data.type == "iceconnectionstatechange") {
switch (message.data.iceConnectionState) {
case "failed":
case "disconnected":
if (Services.telemetry.canRecordExtended) {
this.stageForTelemetryUpload(chatbox.content, message.data);
}
break;
}
}
});
UITour.notify("Loop:ChatWindowOpened");
}; };
mm.sendAsyncMessage("WaitForDOMContentLoaded"); LoopAPI.initialize();
mm.addMessageListener("DOMContentLoaded", loaded); let chatboxInstance = Chat.open(null, {
}; origin: origin,
title: "",
LoopAPI.initialize(); url: url,
let chatboxInstance = Chat.open(null, { remote: MozLoopService.getLoopPref("remote.autostart")
origin: origin, }, callback);
title: "", if (!chatboxInstance) {
url: url, resolve(null);
remote: MozLoopService.getLoopPref("remote.autostart") // It's common for unit tests to overload Chat.open.
}, callback); } else if (chatboxInstance.setAttribute) {
if (!chatboxInstance) { // Set properties that influence visual appearance of the chatbox right
return null; // away to circumvent glitches.
// It's common for unit tests to overload Chat.open. chatboxInstance.setAttribute("customSize", "loopDefault");
} else if (chatboxInstance.setAttribute) { chatboxInstance.parentNode.setAttribute("customSize", "loopDefault");
// Set properties that influence visual appearance of the chatbox right Chat.loadButtonSet(chatboxInstance, "minimize,swap," + kChatboxHangupButton.id);
// away to circumvent glitches. resolve(windowId);
chatboxInstance.setAttribute("customSize", "loopDefault"); }
chatboxInstance.parentNode.setAttribute("customSize", "loopDefault"); });
Chat.loadButtonSet(chatboxInstance, "minimize,swap," + kChatboxHangupButton.id);
}
return windowId;
}, },
/** /**
@ -1247,14 +1293,18 @@ this.MozLoopService = {
* *
* Note: this returns a promise for unit test purposes. * Note: this returns a promise for unit test purposes.
* *
* @param {String} addonVersion The name of the add-on
*
* @return {Promise} * @return {Promise}
*/ */
initialize: Task.async(function*() { initialize: Task.async(function*(addonVersion) {
// Ensure we don't setup things like listeners more than once. // Ensure we don't setup things like listeners more than once.
if (gServiceInitialized) { if (gServiceInitialized) {
return Promise.resolve(); return Promise.resolve();
} }
gAddonVersion = addonVersion;
gServiceInitialized = true; gServiceInitialized = true;
// Do this here, rather than immediately after definition, so that we can // Do this here, rather than immediately after definition, so that we can
@ -1605,6 +1655,19 @@ this.MozLoopService = {
} }
}, },
/*
* Returns current FTU version
*
* @return {Number}
*
* XXX must match number in panel.jsx; expose this via MozLoopAPI
* and kill that constant.
*/
get FTU_VERSION()
{
return 2;
},
/** /**
* Set any preference under "loop.". * Set any preference under "loop.".
* *
@ -1868,20 +1931,65 @@ this.MozLoopService = {
/** /**
* Opens the Getting Started tour in the browser. * Opens the Getting Started tour in the browser.
*
* @param {String} [aSrc] A string representing the entry point to begin the tour, optional.
*/ */
openGettingStartedTour: Task.async(function(aSrc = null) { openGettingStartedTour: Task.async(function() {
try { const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
let url = this.getTourURL(aSrc);
let win = Services.wm.getMostRecentWindow("navigator:browser"); // User will have _just_ clicked the tour menu item or the FTU
win.switchToTabHavingURI(url, true, { // button in the panel, (or else it wouldn't be visible), so...
ignoreFragment: true, let xulWin = Services.wm.getMostRecentWindow("navigator:browser");
replaceQueryString: true let xulDoc = xulWin.document;
});
} catch (ex) { let box = xulDoc.createElementNS(kNSXUL, "box");
log.error("Error opening Getting Started tour", ex); box.setAttribute("id", "loop-slideshow-container");
let appContent = xulDoc.getElementById("appcontent");
let tabBrowser = xulDoc.getElementById("content");
appContent.insertBefore(box, tabBrowser);
var xulBrowser = xulDoc.createElementNS(kNSXUL, "browser");
xulBrowser.setAttribute("id", "loop-slideshow-browser");
xulBrowser.setAttribute("flex", "1");
xulBrowser.setAttribute("type", "content");
box.appendChild(xulBrowser);
// Notify the UI, which has the side effect of disabling panel opening
// and updating the toolbar icon to visually indicate difference.
xulWin.LoopUI.isSlideshowOpen = true;
var removeSlideshow = function() {
try {
appContent.removeChild(box);
} catch (ex) {
log.error(ex);
}
this.setLoopPref("gettingStarted.latestFTUVersion", this.FTU_VERSION);
// Notify the UI, which has the side effect of re-enabling panel opening
// and updating the toolbar.
xulWin.LoopUI.isSlideshowOpen = false;
xulWin.removeEventListener("CloseSlideshow", removeSlideshow);
log.info("slideshow removed");
}.bind(this);
function xulLoadListener() {
xulBrowser.contentWindow.addEventListener("CloseSlideshow",
removeSlideshow);
log.info("CloseSlideshow handler added");
xulBrowser.removeEventListener("load", xulLoadListener, true);
} }
xulBrowser.addEventListener("load", xulLoadListener, true);
// XXX we are loading the slideshow page with chrome privs.
// To make this remote, we'll need to think through a better
// security model.
xulBrowser.setAttribute("src",
"chrome://loop/content/panels/slideshow.html");
}), }),
/** /**

View File

@ -28,22 +28,26 @@
<script type="text/javascript" src="shared/js/loopapi-client.js"></script> <script type="text/javascript" src="shared/js/loopapi-client.js"></script>
<script type="text/javascript" src="shared/js/utils.js"></script> <script type="text/javascript" src="shared/js/utils.js"></script>
<script type="text/javascript" src="shared/js/urlRegExps.js"></script>
<script type="text/javascript" src="shared/js/mixins.js"></script> <script type="text/javascript" src="shared/js/mixins.js"></script>
<script type="text/javascript" src="shared/js/actions.js"></script> <script type="text/javascript" src="shared/js/actions.js"></script>
<script type="text/javascript" src="shared/js/validate.js"></script> <script type="text/javascript" src="shared/js/validate.js"></script>
<script type="text/javascript" src="shared/js/dispatcher.js"></script> <script type="text/javascript" src="shared/js/dispatcher.js"></script>
<script type="text/javascript" src="shared/js/otSdkDriver.js"></script> <script type="text/javascript" src="shared/js/otSdkDriver.js"></script>
<!-- Stores need to be loaded before the views that uses them -->
<script type="text/javascript" src="shared/js/store.js"></script> <script type="text/javascript" src="shared/js/store.js"></script>
<script type="text/javascript" src="panels/js/roomStore.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script> <script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script> <script type="text/javascript" src="panels/js/conversationAppStore.js"></script>
<script type="text/javascript" src="shared/js/textChatStore.js"></script> <script type="text/javascript" src="shared/js/textChatStore.js"></script>
<script type="text/javascript" src="shared/js/remoteCursorStore.js"></script>
<!-- Views -->
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/textChatView.js"></script> <script type="text/javascript" src="shared/js/textChatView.js"></script>
<script type="text/javascript" src="shared/js/linkifiedTextView.js"></script> <script type="text/javascript" src="shared/js/linkifiedTextView.js"></script>
<script type="text/javascript" src="shared/js/urlRegExps.js"></script>
<script type="text/javascript" src="panels/js/conversationAppStore.js"></script>
<script type="text/javascript" src="panels/js/feedbackViews.js"></script> <script type="text/javascript" src="panels/js/feedbackViews.js"></script>
<script type="text/javascript" src="panels/js/roomStore.js"></script>
<script type="text/javascript" src="shared/js/remoteCursorStore.js"></script>
<script type="text/javascript" src="panels/js/roomViews.js"></script> <script type="text/javascript" src="panels/js/roomViews.js"></script>
<script type="text/javascript" src="panels/js/conversation.js"></script> <script type="text/javascript" src="panels/js/conversation.js"></script>
</body> </body>

View File

@ -0,0 +1,97 @@
/* 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/. */
.slideshow {
-moz-user-select: none;
cursor: default;
}
/* slide content */
.slide > div {
height: 100%;
width: 100%;
}
.slide-layout {
padding-top: 50px;
padding-left: 50px;
padding-bottom: 20px;
width: 600px;
}
.slide-layout > h2 {
font-family: sans-serif;
font-size: 3.0rem;
font-weight: 500;
white-space: normal;
color: #fff;
line-height: 3rem;
margin-bottom: 10px;
}
.slide-layout > .slide-text {
font-family: sans-serif;
font-size: 2.1rem;
font-weight: 300;
white-space: normal;
color: #fff;
line-height: 3.0rem;
}
.slide-layout > img {
display: block;
-moz-box-sizing: border-box;
box-sizing: border-box;
border: none;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
padding: 0;
float: right;
margin-top: -20px;
}
.slide1 {
background: #06a6e0;
}
.slide1-image {
background-image: url(../../shared/img/firefox-hello_tour-slide-01.svg);
height: 260px;
width: 295px;
background-size: 310px 310px;
}
.slide2 {
background: #0183b2;
}
.slide2-image {
background-image: url(../../shared/img/firefox-hello_tour-slide-02.svg);
height: 245px;
width: 245px;
background-size: 320px 320px;
}
.slide3 {
background: #005da5;
}
.slide3-image {
background-image: url(../../shared/img/firefox-hello_tour-slide-03.svg);
height: 260px;
width: 260px;
background-size: 310px 310px;
}
.slide4 {
background: #7ec24c;
}
.slide4-image {
background-image: url(../../shared/img/firefox-hello_tour-slide-04.svg);
height: 240px;
width: 240px;
background-size: 320px 320px;
}

View File

@ -24,10 +24,19 @@ loop.conversation = function (mozL10n) {
mixins: [Backbone.Events, loop.store.StoreMixin("conversationAppStore"), sharedMixins.DocumentTitleMixin, sharedMixins.WindowCloseMixin], mixins: [Backbone.Events, loop.store.StoreMixin("conversationAppStore"), sharedMixins.DocumentTitleMixin, sharedMixins.WindowCloseMixin],
propTypes: { propTypes: {
cursorStore: React.PropTypes.instanceOf(loop.store.RemoteCursorStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore) roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
}, },
componentWillMount: function () {
this.listenTo(this.props.cursorStore, "change:remoteCursorPosition", this._onRemoteCursorChange);
},
_onRemoteCursorChange: function () {
return loop.request("AddRemoteCursorOverlay", this.props.cursorStore.getStoreState("remoteCursorPosition"));
},
getInitialState: function () { getInitialState: function () {
return this.getStoreState(); return this.getStoreState();
}, },
@ -66,6 +75,7 @@ loop.conversation = function (mozL10n) {
{ {
return React.createElement(DesktopRoomConversationView, { return React.createElement(DesktopRoomConversationView, {
chatWindowDetached: this.state.chatWindowDetached, chatWindowDetached: this.state.chatWindowDetached,
cursorStore: this.props.cursorStore,
dispatcher: this.props.dispatcher, dispatcher: this.props.dispatcher,
facebookEnabled: this.state.facebookEnabled, facebookEnabled: this.state.facebookEnabled,
onCallTerminated: this.handleCallTerminated, onCallTerminated: this.handleCallTerminated,
@ -188,6 +198,7 @@ loop.conversation = function (mozL10n) {
}); });
React.render(React.createElement(AppControllerView, { React.render(React.createElement(AppControllerView, {
cursorStore: remoteCursorStore,
dispatcher: dispatcher, dispatcher: dispatcher,
roomStore: roomStore }), document.querySelector("#main")); roomStore: roomStore }), document.querySelector("#main"));
@ -198,6 +209,8 @@ loop.conversation = function (mozL10n) {
dispatcher.dispatch(new sharedActions.GetWindowData({ dispatcher.dispatch(new sharedActions.GetWindowData({
windowId: windowId windowId: windowId
})); }));
loop.request("TelemetryAddValue", "LOOP_MAU", constants.LOOP_MAU_TYPE.OPEN_CONVERSATION);
}); });
} }

View File

@ -12,7 +12,9 @@ loop.panel = function (_, mozL10n) {
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var Button = sharedViews.Button; var Button = sharedViews.Button;
var FTU_VERSION = 1; // XXX This must be kept in sync with the number in MozLoopService.jsm.
// We should expose that one through MozLoopAPI and kill this constant.
var FTU_VERSION = 2;
var GettingStartedView = React.createClass({ var GettingStartedView = React.createClass({
displayName: "GettingStartedView", displayName: "GettingStartedView",
@ -20,10 +22,7 @@ loop.panel = function (_, mozL10n) {
mixins: [sharedMixins.WindowCloseMixin], mixins: [sharedMixins.WindowCloseMixin],
handleButtonClick: function () { handleButtonClick: function () {
loop.requestMulti(["OpenGettingStartedTour", "getting-started"], ["SetLoopPref", "gettingStarted.latestFTUVersion", FTU_VERSION]).then(function () { loop.request("OpenGettingStartedTour");
var event = new CustomEvent("GettingStartedSeen");
window.dispatchEvent(event);
}.bind(this));
this.closeWindow(); this.closeWindow();
}, },
@ -292,7 +291,7 @@ loop.panel = function (_, mozL10n) {
}, },
openGettingStartedTour: function () { openGettingStartedTour: function () {
loop.request("OpenGettingStartedTour", "settings-menu"); loop.request("OpenGettingStartedTour");
this.closeWindow(); this.closeWindow();
}, },
@ -907,7 +906,6 @@ loop.panel = function (_, mozL10n) {
propTypes: { propTypes: {
onClick: React.PropTypes.func.isRequired onClick: React.PropTypes.func.isRequired
}, },
componentWillMount: function () { componentWillMount: function () {
loop.request("SetPanelHeight", 262); loop.request("SetPanelHeight", 262);
}, },
@ -1003,26 +1001,32 @@ loop.panel = function (_, mozL10n) {
}, },
_onStatusChanged: function () { _onStatusChanged: function () {
loop.requestMulti(["GetUserProfile"], ["GetHasEncryptionKey"]).then(function (results) { loop.requestMulti(["GetUserProfile"], ["GetHasEncryptionKey"], ["GetLoopPref", "gettingStarted.latestFTUVersion"]).then(function (results) {
var profile = results[0]; var profile = results[0];
var hasEncryptionKey = results[1]; var hasEncryptionKey = results[1];
var prefFTUVersion = results[2];
var stateToUpdate = {};
// It's possible that this state change was slideshow related
// so update that if the pref has changed.
var prefGettingStartedSeen = prefFTUVersion >= FTU_VERSION;
if (prefGettingStartedSeen !== this.state.gettingStartedSeen) {
stateToUpdate.gettingStartedSeen = prefGettingStartedSeen;
}
var currUid = this.state.userProfile ? this.state.userProfile.uid : null; var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
var newUid = profile ? profile.uid : null; var newUid = profile ? profile.uid : null;
if (currUid === newUid) { if (currUid === newUid) {
// Update the state of hasEncryptionKey as this might have changed now. // Update the state of hasEncryptionKey as this might have changed now.
this.setState({ hasEncryptionKey: hasEncryptionKey }); stateToUpdate.hasEncryptionKey = hasEncryptionKey;
} else { } else {
this.setState({ userProfile: profile }); stateToUpdate.userProfile = profile;
} }
this.updateServiceErrors();
}.bind(this));
},
_gettingStartedSeen: function () { this.setState(stateToUpdate);
loop.request("GetLoopPref", "gettingStarted.latestFTUVersion").then(function (result) {
this.setState({ this.updateServiceErrors();
gettingStartedSeen: result >= FTU_VERSION
});
}.bind(this)); }.bind(this));
}, },
@ -1032,12 +1036,10 @@ loop.panel = function (_, mozL10n) {
componentDidMount: function () { componentDidMount: function () {
loop.subscribe("LoopStatusChanged", this._onStatusChanged); loop.subscribe("LoopStatusChanged", this._onStatusChanged);
window.addEventListener("GettingStartedSeen", this._gettingStartedSeen);
}, },
componentWillUnmount: function () { componentWillUnmount: function () {
loop.unsubscribe("LoopStatusChanged", this._onStatusChanged); loop.unsubscribe("LoopStatusChanged", this._onStatusChanged);
window.removeEventListener("GettingStartedSeen", this._gettingStartedSeen);
}, },
handleContextMenu: function (e) { handleContextMenu: function (e) {

View File

@ -322,7 +322,10 @@ loop.store = loop.store || {};
console.error("No URL sharing type bucket found for '" + from + "'"); console.error("No URL sharing type bucket found for '" + from + "'");
return; return;
} }
loop.request("TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket); loop.requestMulti(
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_MAU", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
);
}, },
/** /**
@ -335,14 +338,18 @@ loop.store = loop.store || {};
loop.shared.utils.composeCallUrlEmail(actionData.roomUrl, null, loop.shared.utils.composeCallUrlEmail(actionData.roomUrl, null,
actionData.roomDescription); actionData.roomDescription);
var bucket = this._constants.SHARING_ROOM_URL["EMAIL_FROM_" + (from || "").toUpperCase()]; var bucket = this._constants.SHARING_ROOM_URL[
"EMAIL_FROM_" + (from || "").toUpperCase()
];
if (typeof bucket === "undefined") { if (typeof bucket === "undefined") {
console.error("No URL sharing type bucket found for '" + from + "'"); console.error("No URL sharing type bucket found for '" + from + "'");
return; return;
} }
loop.requestMulti( loop.requestMulti(
["NotifyUITour", "Loop:RoomURLEmailed"], ["NotifyUITour", "Loop:RoomURLEmailed"],
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket]); ["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_MAU", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
);
}, },
/** /**
@ -352,12 +359,25 @@ loop.store = loop.store || {};
*/ */
facebookShareRoomUrl: function(actionData) { facebookShareRoomUrl: function(actionData) {
var encodedRoom = encodeURIComponent(actionData.roomUrl); var encodedRoom = encodeURIComponent(actionData.roomUrl);
loop.request("GetLoopPref", "facebook.shareUrl")
.then(shareUrl => { loop.requestMulti(
loop.request("OpenURL", shareUrl.replace("%ROOM_URL%", encodedRoom)); ["GetLoopPref", "facebook.appId"],
}).then(() => { ["GetLoopPref", "facebook.fallbackUrl"],
loop.request("NotifyUITour", "Loop:RoomURLShared"); ["GetLoopPref", "facebook.shareUrl"]
}); ).then(results => {
var app_id = results[0];
var fallback_url = results[1];
var redirect_url = encodeURIComponent(actionData.originUrl ||
fallback_url);
var finalURL = results[2].replace("%ROOM_URL%", encodedRoom)
.replace("%APP_ID%", app_id)
.replace("%REDIRECT_URI%", redirect_url);
return loop.request("OpenURL", finalURL);
}).then(() => {
loop.request("NotifyUITour", "Loop:RoomURLShared");
});
var from = actionData.from; var from = actionData.from;
var bucket = this._constants.SHARING_ROOM_URL["FACEBOOK_FROM_" + from.toUpperCase()]; var bucket = this._constants.SHARING_ROOM_URL["FACEBOOK_FROM_" + from.toUpperCase()];
@ -365,7 +385,10 @@ loop.store = loop.store || {};
console.error("No URL sharing type bucket found for '" + from + "'"); console.error("No URL sharing type bucket found for '" + from + "'");
return; return;
} }
loop.request("TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket); loop.requestMulti(
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_MAU", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
);
}, },
/** /**
@ -419,8 +442,11 @@ loop.store = loop.store || {};
this.dispatchAction(new sharedActions.DeleteRoomError({ error: result })); this.dispatchAction(new sharedActions.DeleteRoomError({ error: result }));
} }
var buckets = this._constants.ROOM_DELETE; var buckets = this._constants.ROOM_DELETE;
loop.request("TelemetryAddValue", "LOOP_ROOM_DELETE", buckets[isError ? loop.requestMulti(
"DELETE_FAIL" : "DELETE_SUCCESS"]); ["TelemetryAddValue", "LOOP_ROOM_DELETE", buckets[isError ?
"DELETE_FAIL" : "DELETE_SUCCESS"]],
["TelemetryAddValue", "LOOP_MAU", this._constants.LOOP_MAU_TYPE.ROOM_DELETE]
);
}.bind(this)); }.bind(this));
}, },
@ -483,7 +509,10 @@ loop.store = loop.store || {};
* @param {sharedActions.OpenRoom} actionData The action data. * @param {sharedActions.OpenRoom} actionData The action data.
*/ */
openRoom: function(actionData) { openRoom: function(actionData) {
loop.request("Rooms:Open", actionData.roomToken); loop.requestMulti(
["Rooms:Open", actionData.roomToken],
["TelemetryAddValue", "LOOP_MAU", this._constants.LOOP_MAU_TYPE.ROOM_OPEN]
);
}, },
/** /**

View File

@ -246,7 +246,7 @@ loop.roomViews = function (mozL10n) {
displayName: "DesktopRoomInvitationView", displayName: "DesktopRoomInvitationView",
statics: { statics: {
TRIGGERED_RESET_DELAY: 2000 TRIGGERED_RESET_DELAY: 3000
}, },
mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")], mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
@ -324,6 +324,7 @@ loop.roomViews = function (mozL10n) {
} }
var cx = classNames; var cx = classNames;
return React.createElement( return React.createElement(
"div", "div",
{ className: "room-invitation-overlay" }, { className: "room-invitation-overlay" },
@ -331,18 +332,43 @@ loop.roomViews = function (mozL10n) {
"div", "div",
{ className: "room-invitation-content" }, { className: "room-invitation-content" },
React.createElement( React.createElement(
"p", "div",
{ className: "room-context-header" },
mozL10n.get("invite_header_text_bold2")
),
React.createElement(
"div",
null, null,
mozL10n.get("invite_header_text4")
)
),
React.createElement(
"div",
{ className: "input-button-group" },
React.createElement(
"div",
{ className: "input-button-group-label" },
mozL10n.get("invite_your_link")
),
React.createElement(
"div",
{ className: "input-button-content" },
React.createElement( React.createElement(
"span", "div",
{ className: "room-context-header" }, { className: "input-group group-item-top" },
mozL10n.get("invite_header_text_bold") React.createElement("input", { readOnly: true, type: "text", value: this.props.roomData.roomUrl })
), ),
" ",
React.createElement( React.createElement(
"span", "div",
null, { className: cx({
mozL10n.get("invite_header_text3") "group-item-bottom": true,
"btn": true,
"invite-button": true,
"btn-copy": true,
"triggered": this.state.copiedUrl
}),
onClick: this.handleCopyButtonClick },
mozL10n.get(this.state.copiedUrl ? "invite_copied_link_button" : "invite_copy_link_button")
) )
) )
), ),
@ -350,23 +376,8 @@ loop.roomViews = function (mozL10n) {
"div", "div",
{ className: cx({ { className: cx({
"btn-group": true, "btn-group": true,
"call-action-group": true "share-action-group": true
}) }, }) },
React.createElement(
"div",
{ className: cx({
"btn-copy": true,
"invite-button": true,
"triggered": this.state.copiedUrl
}),
onClick: this.handleCopyButtonClick },
React.createElement("img", { src: "shared/img/glyph-link-16x16.svg" }),
React.createElement(
"p",
null,
mozL10n.get(this.state.copiedUrl ? "invite_copied_link_button" : "invite_copy_link_button")
)
),
React.createElement( React.createElement(
"div", "div",
{ className: "btn-email invite-button", { className: "btn-email invite-button",
@ -374,7 +385,7 @@ loop.roomViews = function (mozL10n) {
onMouseOver: this.resetTriggeredButtons }, onMouseOver: this.resetTriggeredButtons },
React.createElement("img", { src: "shared/img/glyph-email-16x16.svg" }), React.createElement("img", { src: "shared/img/glyph-email-16x16.svg" }),
React.createElement( React.createElement(
"p", "div",
null, null,
mozL10n.get("invite_email_link_button") mozL10n.get("invite_email_link_button")
) )
@ -388,7 +399,7 @@ loop.roomViews = function (mozL10n) {
onMouseOver: this.resetTriggeredButtons }, onMouseOver: this.resetTriggeredButtons },
React.createElement("img", { src: "shared/img/glyph-facebook-16x16.svg" }), React.createElement("img", { src: "shared/img/glyph-facebook-16x16.svg" }),
React.createElement( React.createElement(
"p", "div",
null, null,
mozL10n.get("invite_facebook_button3") mozL10n.get("invite_facebook_button3")
) )
@ -416,6 +427,7 @@ loop.roomViews = function (mozL10n) {
propTypes: { propTypes: {
chatWindowDetached: React.PropTypes.bool.isRequired, chatWindowDetached: React.PropTypes.bool.isRequired,
cursorStore: React.PropTypes.instanceOf(loop.store.RemoteCursorStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
facebookEnabled: React.PropTypes.bool.isRequired, facebookEnabled: React.PropTypes.bool.isRequired,
// The poster URLs are for UI-showcase testing and development. // The poster URLs are for UI-showcase testing and development.
@ -454,19 +466,6 @@ loop.roomViews = function (mozL10n) {
} }
}, },
/**
* Used to control publishing a stream - i.e. to mute a stream
*
* @param {String} type The type of stream, e.g. "audio" or "video".
* @param {Boolean} enabled True to enable the stream, false otherwise.
*/
publishStream: function (type, enabled) {
this.props.dispatcher.dispatch(new sharedActions.SetMute({
type: type,
enabled: enabled
}));
},
/** /**
* Determine if the invitation controls should be shown. * Determine if the invitation controls should be shown.
* *
@ -596,6 +595,7 @@ loop.roomViews = function (mozL10n) {
React.createElement( React.createElement(
sharedViews.MediaLayoutView, sharedViews.MediaLayoutView,
{ {
cursorStore: this.props.cursorStore,
dispatcher: this.props.dispatcher, dispatcher: this.props.dispatcher,
displayScreenShare: false, displayScreenShare: false,
isLocalLoading: this._isLocalLoading(), isLocalLoading: this._isLocalLoading(),
@ -616,7 +616,6 @@ loop.roomViews = function (mozL10n) {
audio: { enabled: !this.state.audioMuted, visible: true }, audio: { enabled: !this.state.audioMuted, visible: true },
dispatcher: this.props.dispatcher, dispatcher: this.props.dispatcher,
hangup: this.leaveRoom, hangup: this.leaveRoom,
publishStream: this.publishStream,
showHangup: this.props.chatWindowDetached, showHangup: this.props.chatWindowDetached,
video: { enabled: !this.state.videoMuted, visible: true } }), video: { enabled: !this.state.videoMuted, visible: true } }),
React.createElement(DesktopRoomInvitationView, { React.createElement(DesktopRoomInvitationView, {

View File

@ -0,0 +1,81 @@
/* 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/. */
var loop = loop || {};
loop.slideshow = function (mozL10n) {
"use strict";
/**
* Slideshow initialisation.
*/
function init() {
var requests = [["GetAllStrings"], ["GetLocale"], ["GetPluralRule"]];
return loop.requestMulti.apply(null, requests).then(function (results) {
// `requestIdx` is keyed off the order of the `requests`
// array. Be careful to update both when making changes.
var requestIdx = 0;
// Do the initial L10n setup, we do this before anything
// else to ensure the L10n environment is setup correctly.
var stringBundle = results[requestIdx];
var locale = results[++requestIdx];
var pluralRule = results[++requestIdx];
mozL10n.initialize({
locale: locale,
pluralRule: pluralRule,
getStrings: function (key) {
if (!(key in stringBundle)) {
return "{ textContent: '' }";
}
return JSON.stringify({
textContent: stringBundle[key]
});
}
});
document.documentElement.setAttribute("lang", mozL10n.language.code);
document.documentElement.setAttribute("dir", mozL10n.language.direction);
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
var clientSuperShortname = mozL10n.get("clientSuperShortname");
var data = [{
id: "slide1",
imageClass: "slide1-image",
title: mozL10n.get("fte_slide_1_title"),
text: mozL10n.get("fte_slide_1_copy", {
clientShortname2: mozL10n.get("clientShortname2")
})
}, {
id: "slide2",
imageClass: "slide2-image",
title: mozL10n.get("fte_slide_2_title"),
text: mozL10n.get("fte_slide_2_copy")
}, {
id: "slide3",
imageClass: "slide3-image",
title: mozL10n.get("fte_slide_3_title"),
text: mozL10n.get("fte_slide_3_copy", {
clientSuperShortname: clientSuperShortname
})
}, {
id: "slide4",
imageClass: "slide4-image",
title: mozL10n.get("fte_slide_4_title", {
clientSuperShortname: clientSuperShortname
}),
text: mozL10n.get("fte_slide_4_copy", {
brandShortname: mozL10n.get("brandShortname")
})
}];
loop.SimpleSlideshow.init("#main", data);
});
}
return {
init: init
};
}(document.mozL10n);
document.addEventListener("DOMContentLoaded", loop.slideshow.init);

View File

@ -24,14 +24,18 @@
<script type="text/javascript" src="shared/js/utils.js"></script> <script type="text/javascript" src="shared/js/utils.js"></script>
<script type="text/javascript" src="shared/js/models.js"></script> <script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/mixins.js"></script> <script type="text/javascript" src="shared/js/mixins.js"></script>
<script type="text/javascript" src="shared/js/store.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="shared/js/remoteCursorStore.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/validate.js"></script>
<script type="text/javascript" src="shared/js/actions.js"></script> <script type="text/javascript" src="shared/js/actions.js"></script>
<script type="text/javascript" src="shared/js/dispatcher.js"></script> <script type="text/javascript" src="shared/js/dispatcher.js"></script>
<!-- Stores need to be loaded before the views that uses them -->
<script type="text/javascript" src="shared/js/store.js"></script>
<script type="text/javascript" src="panels/js/roomStore.js"></script> <script type="text/javascript" src="panels/js/roomStore.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="shared/js/remoteCursorStore.js"></script>
<!-- Views -->
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/validate.js"></script>
<script type="text/javascript" src="panels/js/panel.js"></script> <script type="text/javascript" src="panels/js/panel.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<!-- 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/. -->
<html>
<head>
<meta charset="utf-8">
<base href="chrome://loop/content">
<link rel="stylesheet" type="text/css" href="shared/css/reset.css">
<link rel="stylesheet" type="text/css" href="shared/css/common.css">
<link rel="stylesheet" type="text/css" href="panels/vendor/simpleSlideshow.css">
<link rel="stylesheet" type="text/css" href="panels/css/slideshow.css">
</head>
<body class="panel">
<div id="main"></div>
<script type="text/javascript" src="panels/vendor/l10n.js"></script>
<script type="text/javascript" src="shared/vendor/react.js"></script>
<script type="text/javascript" src="shared/vendor/lodash.js"></script>
<script type="text/javascript" src="shared/js/utils.js"></script>
<script type="text/javascript" src="shared/js/loopapi-client.js"></script>
<script type="text/javascript" src="shared/vendor/classnames.js"></script>
<script type="text/javascript" src="panels/vendor/simpleSlideshow.js"></script>
<script type="text/javascript" src="panels/js/slideshow.js"></script>
<script type="text/javascript">
function clickHandler() {
try {
var e = new CustomEvent("CloseSlideshow"); window.dispatchEvent(e);
} catch (ex) {
console.warn('CloseSlideshow dispatch exploded' + ex);
}
return;
}
</script>
<button onclick="clickHandler();" class="button-close" />
</body>
</html>

View File

@ -9,13 +9,14 @@ describe("loop.conversation", function() {
var expect = chai.expect; var expect = chai.expect;
var TestUtils = React.addons.TestUtils; var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var fakeWindow, sandbox, setLoopPrefStub, mozL10nGet, remoteCursorStore, dispatcher; var fakeWindow, sandbox, setLoopPrefStub, mozL10nGet,
remoteCursorStore, dispatcher, requestStubs;
beforeEach(function() { beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox(); sandbox = LoopMochaUtils.createSandbox();
setLoopPrefStub = sandbox.stub(); setLoopPrefStub = sandbox.stub();
LoopMochaUtils.stubLoopRequest({ LoopMochaUtils.stubLoopRequest(requestStubs = {
GetDoNotDisturb: function() { return true; }, GetDoNotDisturb: function() { return true; },
GetAllStrings: function() { GetAllStrings: function() {
return JSON.stringify({ textContent: "fakeText" }); return JSON.stringify({ textContent: "fakeText" });
@ -38,6 +39,13 @@ describe("loop.conversation", function() {
LOOP_SESSION_TYPE: { LOOP_SESSION_TYPE: {
GUEST: 1, GUEST: 1,
FXA: 2 FXA: 2
},
LOOP_MAU_TYPE: {
OPEN_PANEL: 0,
OPEN_CONVERSATION: 1,
ROOM_OPEN: 2,
ROOM_SHARE: 3,
ROOM_DELETE: 4
} }
}; };
}, },
@ -57,7 +65,8 @@ describe("loop.conversation", function() {
}, },
GetConversationWindowData: function() { GetConversationWindowData: function() {
return {}; return {};
} },
TelemetryAddValue: sinon.stub()
}); });
fakeWindow = { fakeWindow = {
@ -144,18 +153,30 @@ describe("loop.conversation", function() {
windowId: "42" windowId: "42"
})); }));
}); });
it("should log a telemetry event when opening the conversation window", function() {
var constants = requestStubs.GetAllConstants();
loop.conversation.init();
sinon.assert.calledOnce(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"],
"LOOP_MAU", constants.LOOP_MAU_TYPE.OPEN_CONVERSATION);
});
}); });
describe("AppControllerView", function() { describe("AppControllerView", function() {
var activeRoomStore, ccView; var activeRoomStore, ccView, addRemoteCursorStub;
var conversationAppStore, roomStore, feedbackPeriodMs = 15770000000; var conversationAppStore,
roomStore,
feedbackPeriodMs = 15770000000;
var ROOM_STATES = loop.store.ROOM_STATES; var ROOM_STATES = loop.store.ROOM_STATES;
function mountTestComponent() { function mountTestComponent() {
return TestUtils.renderIntoDocument( return TestUtils.renderIntoDocument(
React.createElement(loop.conversation.AppControllerView, { React.createElement(loop.conversation.AppControllerView, {
roomStore: roomStore, cursorStore: remoteCursorStore,
dispatcher: dispatcher dispatcher: dispatcher,
roomStore: roomStore
})); }));
} }
@ -168,6 +189,9 @@ describe("loop.conversation", function() {
activeRoomStore: activeRoomStore, activeRoomStore: activeRoomStore,
constants: {} constants: {}
}); });
remoteCursorStore = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: {}
});
conversationAppStore = new loop.store.ConversationAppStore({ conversationAppStore = new loop.store.ConversationAppStore({
activeRoomStore: activeRoomStore, activeRoomStore: activeRoomStore,
dispatcher: dispatcher, dispatcher: dispatcher,
@ -179,12 +203,43 @@ describe("loop.conversation", function() {
loop.store.StoreMixin.register({ loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore conversationAppStore: conversationAppStore
}); });
addRemoteCursorStub = sandbox.stub();
LoopMochaUtils.stubLoopRequest({
AddRemoteCursorOverlay: addRemoteCursorStub
});
}); });
afterEach(function() { afterEach(function() {
ccView = undefined; ccView = undefined;
}); });
it("should request AddRemoteCursorOverlay when cursor position changes", function() {
mountTestComponent();
remoteCursorStore.setStoreState({
"remoteCursorPosition": {
"ratioX": 10,
"ratioY": 10
}
});
sinon.assert.calledOnce(addRemoteCursorStub);
});
it("should NOT request AddRemoteCursorOverlay when cursor position DOES NOT changes", function() {
mountTestComponent();
remoteCursorStore.setStoreState({
"realVideoSize": {
"height": 400,
"width": 600
}
});
sinon.assert.notCalled(addRemoteCursorStub);
});
it("should display the RoomView for rooms", function() { it("should display the RoomView for rooms", function() {
conversationAppStore.setStoreState({ windowType: "room" }); conversationAppStore.setStoreState({ windowType: "room" });
activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY }); activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });

View File

@ -49,18 +49,22 @@
<script src="/add-on/shared/js/validate.js"></script> <script src="/add-on/shared/js/validate.js"></script>
<script src="/add-on/shared/js/dispatcher.js"></script> <script src="/add-on/shared/js/dispatcher.js"></script>
<script src="/add-on/shared/js/otSdkDriver.js"></script> <script src="/add-on/shared/js/otSdkDriver.js"></script>
<!-- Stores need to be loaded before the views that uses them -->
<script src="/add-on/shared/js/store.js"></script> <script src="/add-on/shared/js/store.js"></script>
<script src="/add-on/shared/js/activeRoomStore.js"></script>
<script src="/add-on/shared/js/views.js"></script>
<script src="/add-on/shared/js/textChatStore.js"></script>
<script src="/add-on/shared/js/textChatView.js"></script>
<script src="/add-on/panels/js/conversationAppStore.js"></script>
<script src="/add-on/panels/js/roomStore.js"></script> <script src="/add-on/panels/js/roomStore.js"></script>
<script src="/add-on/shared/js/activeRoomStore.js"></script>
<script src="/add-on/panels/js/conversationAppStore.js"></script>
<script src="/add-on/shared/js/textChatStore.js"></script>
<script src="/add-on/shared/js/remoteCursorStore.js"></script>
<!-- Views -->
<script src="/add-on/shared/js/views.js"></script>
<script src="/add-on/shared/js/textChatView.js"></script>
<script src="/add-on/panels/js/roomViews.js"></script> <script src="/add-on/panels/js/roomViews.js"></script>
<script src="/add-on/panels/js/feedbackViews.js"></script> <script src="/add-on/panels/js/feedbackViews.js"></script>
<script src="/add-on/panels/js/conversation.js"></script> <script src="/add-on/panels/js/conversation.js"></script>
<script src="/add-on/panels/js/panel.js"></script> <script src="/add-on/panels/js/panel.js"></script>
<script src="/add-on/shared/js/remoteCursorStore.js"></script>
<!-- Test scripts --> <!-- Test scripts -->
<script src="conversationAppStore_test.js"></script> <script src="conversationAppStore_test.js"></script>

View File

@ -85,7 +85,7 @@ describe("loop.panel", function() {
GetHasEncryptionKey: true, GetHasEncryptionKey: true,
GetUserProfile: null, GetUserProfile: null,
GetDoNotDisturb: false, GetDoNotDisturb: false,
"GetLoopPref|gettingStarted.latestFTUVersion": 1, "GetLoopPref|gettingStarted.latestFTUVersion": 2,
"GetLoopPref|legal.ToS_url": "", "GetLoopPref|legal.ToS_url": "",
"GetLoopPref|legal.privacy_url": "", "GetLoopPref|legal.privacy_url": "",
"GetLoopPref|remote.autostart": false, "GetLoopPref|remote.autostart": false,

View File

@ -43,10 +43,18 @@ describe("loop.store.RoomStore", function() {
ROOM_DELETE: { ROOM_DELETE: {
DELETE_SUCCESS: 0, DELETE_SUCCESS: 0,
DELETE_FAIL: 1 DELETE_FAIL: 1
},
LOOP_MAU_TYPE: {
OPEN_PANEL: 0,
OPEN_CONVERSATION: 1,
ROOM_OPEN: 2,
ROOM_SHARE: 3,
ROOM_DELETE: 4
} }
}; };
}, },
CopyString: sinon.stub(), CopyString: sinon.stub(),
ComposeEmail: sinon.stub(),
GetLoopPref: function(prefName) { GetLoopPref: function(prefName) {
if (prefName === "debug.dispatcher") { if (prefName === "debug.dispatcher") {
return false; return false;
@ -60,6 +68,7 @@ describe("loop.store.RoomStore", function() {
"Rooms:Open": sinon.stub(), "Rooms:Open": sinon.stub(),
"Rooms:Rename": sinon.stub(), "Rooms:Rename": sinon.stub(),
"Rooms:PushSubscription": sinon.stub(), "Rooms:PushSubscription": sinon.stub(),
"SetLoopPref": sinon.stub(),
TelemetryAddValue: sinon.stub() TelemetryAddValue: sinon.stub()
}); });
@ -433,8 +442,8 @@ describe("loop.store.RoomStore", function() {
roomToken: fakeRoomToken roomToken: fakeRoomToken
})); }));
sinon.assert.calledOnce(requestStubs.TelemetryAddValue); sinon.assert.calledTwice(requestStubs.TelemetryAddValue);
sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue, sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue.getCall(0),
"LOOP_ROOM_DELETE", 0); "LOOP_ROOM_DELETE", 0);
}); });
@ -447,8 +456,8 @@ describe("loop.store.RoomStore", function() {
roomToken: fakeRoomToken roomToken: fakeRoomToken
})); }));
sinon.assert.calledOnce(requestStubs.TelemetryAddValue); sinon.assert.calledTwice(requestStubs.TelemetryAddValue);
sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue, sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue.getCall(0),
"LOOP_ROOM_DELETE", 1); "LOOP_ROOM_DELETE", 1);
}); });
}); });
@ -470,8 +479,8 @@ describe("loop.store.RoomStore", function() {
from: "panel" from: "panel"
})); }));
sinon.assert.calledOnce(requestStubs.TelemetryAddValue); sinon.assert.calledTwice(requestStubs.TelemetryAddValue);
sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue, sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue.getCall(0),
"LOOP_SHARING_ROOM_URL", 0); "LOOP_SHARING_ROOM_URL", 0);
}); });
@ -481,8 +490,8 @@ describe("loop.store.RoomStore", function() {
from: "conversation" from: "conversation"
})); }));
sinon.assert.calledOnce(requestStubs.TelemetryAddValue); sinon.assert.calledTwice(requestStubs.TelemetryAddValue);
sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue, sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue.getCall(0),
"LOOP_SHARING_ROOM_URL", 1); "LOOP_SHARING_ROOM_URL", 1);
}); });
}); });
@ -520,26 +529,53 @@ describe("loop.store.RoomStore", function() {
describe("#facebookShareRoomUrl", function() { describe("#facebookShareRoomUrl", function() {
var getLoopPrefStub; var getLoopPrefStub;
var sharingSite = "www.sharing-site.com",
shareURL = sharingSite +
"?app_id=%APP_ID%" +
"&link=%ROOM_URL%" +
"&redirect_uri=%REDIRECT_URI%",
appId = "1234567890",
fallback = "www.fallback.com";
beforeEach(function() { beforeEach(function() {
getLoopPrefStub = function() { getLoopPrefStub = sinon.stub();
return "https://shared.site/?u=%ROOM_URL%"; getLoopPrefStub.withArgs("facebook.appId").returns(appId);
}; getLoopPrefStub.withArgs("facebook.shareUrl").returns(shareURL);
getLoopPrefStub.withArgs("facebook.fallbackUrl").returns(fallback);
LoopMochaUtils.stubLoopRequest({ LoopMochaUtils.stubLoopRequest({
GetLoopPref: getLoopPrefStub GetLoopPref: getLoopPrefStub
}); });
}); });
it("should open the facebook url with room URL", function() { it("should open the facebook share url with correct room and redirection", function() {
var room = "invalid.room",
origin = "origin.url";
store.facebookShareRoomUrl(new sharedActions.FacebookShareRoomUrl({ store.facebookShareRoomUrl(new sharedActions.FacebookShareRoomUrl({
from: "conversation", from: "conversation",
roomUrl: "http://invalid" originUrl: origin,
roomUrl: room
})); }));
sinon.assert.calledOnce(requestStubs.OpenURL); sinon.assert.calledOnce(requestStubs.OpenURL);
sinon.assert.calledWithExactly(requestStubs.OpenURL, "https://shared.site/?u=http%3A%2F%2Finvalid"); sinon.assert.calledWithMatch(requestStubs.OpenURL, sharingSite);
sinon.assert.calledWithMatch(requestStubs.OpenURL, room);
sinon.assert.calledWithMatch(requestStubs.OpenURL, origin);
});
it("if no origin URL, send fallback URL", function() {
var room = "invalid.room";
store.facebookShareRoomUrl(new sharedActions.FacebookShareRoomUrl({
from: "conversation",
roomUrl: room
}));
sinon.assert.calledOnce(requestStubs.OpenURL);
sinon.assert.calledWithMatch(requestStubs.OpenURL, sharingSite);
sinon.assert.calledWithMatch(requestStubs.OpenURL, room);
sinon.assert.calledWithMatch(requestStubs.OpenURL, fallback);
}); });
it("should send a telemetry event for facebook share from conversation", function() { it("should send a telemetry event for facebook share from conversation", function() {
@ -548,8 +584,8 @@ describe("loop.store.RoomStore", function() {
roomUrl: "http://invalid" roomUrl: "http://invalid"
})); }));
sinon.assert.calledOnce(requestStubs.TelemetryAddValue); sinon.assert.calledTwice(requestStubs.TelemetryAddValue);
sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue, sinon.assert.calledWithExactly(requestStubs.TelemetryAddValue.getCall(0),
"LOOP_SHARING_ROOM_URL", 4); "LOOP_SHARING_ROOM_URL", 4);
}); });
@ -731,7 +767,9 @@ describe("loop.store.RoomStore", function() {
var store; var store;
beforeEach(function() { beforeEach(function() {
store = new loop.store.RoomStore(dispatcher, { constants: {} }); store = new loop.store.RoomStore(dispatcher, {
constants: requestStubs.GetAllConstants()
});
}); });
it("should open the room via mozLoop", function() { it("should open the room via mozLoop", function() {
@ -881,4 +919,76 @@ describe("loop.store.RoomStore", function() {
expect(store.getStoreState().savingContext).to.eql(false); expect(store.getStoreState().savingContext).to.eql(false);
}); });
}); });
describe("MAU telemetry events", function() {
var getLoopPrefStub, store;
beforeEach(function() {
getLoopPrefStub = function(pref) {
if (pref === "facebook.shareUrl") {
return "https://shared.site/?u=%ROOM_URL%";
}
return 0;
};
LoopMochaUtils.stubLoopRequest({
GetLoopPref: getLoopPrefStub
});
store = new loop.store.RoomStore(dispatcher, {
constants: requestStubs.GetAllConstants()
});
});
it("should log telemetry event when opening a room", function() {
store.openRoom(new sharedActions.OpenRoom({ roomToken: "42abc" }));
sinon.assert.calledOnce(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"],
"LOOP_MAU", store._constants.LOOP_MAU_TYPE.ROOM_OPEN);
});
it("should log telemetry event when sharing a room (copy link)", function() {
store.copyRoomUrl(new sharedActions.CopyRoomUrl({
roomUrl: "http://invalid",
from: "conversation"
}));
sinon.assert.calledTwice(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"].getCall(1),
"LOOP_MAU", store._constants.LOOP_MAU_TYPE.ROOM_SHARE);
});
it("should log telemetry event when sharing a room (email)", function() {
store.emailRoomUrl(new sharedActions.EmailRoomUrl({
roomUrl: "http://invalid",
from: "conversation"
}));
sinon.assert.calledTwice(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"].getCall(1),
"LOOP_MAU", store._constants.LOOP_MAU_TYPE.ROOM_SHARE);
});
it("should log telemetry event when sharing a room (facebook)", function() {
store.facebookShareRoomUrl(new sharedActions.FacebookShareRoomUrl({
roomUrl: "http://invalid",
from: "conversation"
}));
sinon.assert.calledTwice(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"].getCall(1),
"LOOP_MAU", store._constants.LOOP_MAU_TYPE.ROOM_SHARE);
});
it("should log telemetry event when deleting a room", function() {
store.deleteRoom(new sharedActions.DeleteRoom({
roomToken: "42abc"
}));
sinon.assert.calledTwice(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"].getCall(1),
"LOOP_MAU", store._constants.LOOP_MAU_TYPE.ROOM_DELETE);
});
});
}); });

View File

@ -11,7 +11,12 @@ describe("loop.roomViews", function() {
var ROOM_STATES = loop.store.ROOM_STATES; var ROOM_STATES = loop.store.ROOM_STATES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS; var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var sandbox, dispatcher, roomStore, activeRoomStore, view; var sandbox,
dispatcher,
roomStore,
activeRoomStore,
remoteCursorStore,
view;
var clock, fakeWindow, requestStubs; var clock, fakeWindow, requestStubs;
var favicon = "data:image/x-icon;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; var favicon = "data:image/x-icon;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
@ -69,6 +74,9 @@ describe("loop.roomViews", function() {
constants: {}, constants: {},
activeRoomStore: activeRoomStore activeRoomStore: activeRoomStore
}); });
remoteCursorStore = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: {}
});
var textChatStore = new loop.store.TextChatStore(dispatcher, { var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: {} sdkDriver: {}
}); });
@ -352,6 +360,7 @@ describe("loop.roomViews", function() {
function mountTestComponent(props) { function mountTestComponent(props) {
props = _.extend({ props = _.extend({
chatWindowDetached: false, chatWindowDetached: false,
cursorStore: remoteCursorStore,
dispatcher: dispatcher, dispatcher: dispatcher,
facebookEnabled: false, facebookEnabled: false,
roomStore: roomStore, roomStore: roomStore,

View File

@ -0,0 +1,109 @@
/* This is derived from PIOTR F's code,
currently available at https://github.com/piotrf/simple-react-slideshow
Simple React Slideshow Example
Original Author: PIOTR F.
License: MIT
Copyright (c) 2015 Piotr
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
html {
font-size: 10px;
font-family: menu;
color: #fff;
}
body {
background: none;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
line-height: 2.4rem; /* or 1.3rem original*/
}
.slides {
display: block;
width: 100%;
height: 390px;
overflow: hidden;
white-space: nowrap;
}
.slide {
display: none;
height: 100%;
width: 100%;
}
.slide.slide--active {
display: block;
}
.control-panel {
height: 60px;
background: #fff;
width: 100%;
}
.toggle {
color: white;
display: block;
padding: 0px;
position: absolute;
bottom: 20px;
background-color: transparent;
background-image: url(../../shared/img/arrow-01.svg);
background-repeat: no-repeat;
background-size: 20px 20px;
border: none;
/*padding: 0;*/
height: 20px;
width: 20px;
}
.toggle-prev {
left: 20px;
transform: scaleX(-1);
}
.toggle-next {
right: 20px;
}
.button-close {
display: block;
position: absolute;
background-color: transparent;
background-image: url(../../shared/img/close-02.svg);
background-repeat: no-repeat;
background-size: 20px 20px;
border: none;
padding: 0;
height: 20px;
width: 20px;
top: 20px;
right: 20px;
}

View File

@ -0,0 +1,217 @@
// This is derived from PIOTR F's code,
// currently available at https://github.com/piotrf/simple-react-slideshow
// Simple React Slideshow Example
//
// Original Author: PIOTR F.
// License: MIT
//
// Copyright (c) 2015 Piotr
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
var loop = loop || {};
loop.SimpleSlideshow = function () {
"use strict";
// App state
var state = {
currentSlide: 0,
data: []
};
// State transitions
var actions = {
toggleNext: function () {
var current = state.currentSlide;
var next = current + 1;
if (next < state.data.length) {
state.currentSlide = next;
}
render();
},
togglePrev: function () {
var current = state.currentSlide;
var prev = current - 1;
if (prev >= 0) {
state.currentSlide = prev;
}
render();
},
toggleSlide: function (id) {
var index = state.data.map(function (el) {
return el.id;
});
var currentIndex = index.indexOf(id);
state.currentSlide = currentIndex;
render();
}
};
var Slideshow = React.createClass({
displayName: "Slideshow",
propTypes: {
data: React.PropTypes.array.isRequired
},
render: function () {
return React.createElement(
"div",
{ className: "slideshow" },
React.createElement(Slides, { data: this.props.data }),
React.createElement(
"div",
{ className: "control-panel" },
React.createElement(Controls, null)
)
);
}
});
var Slides = React.createClass({
displayName: "Slides",
propTypes: {
data: React.PropTypes.array.isRequired
},
render: function () {
var slidesNodes = this.props.data.map(function (slideNode, index) {
var isActive = state.currentSlide === index;
return React.createElement(Slide, { active: isActive,
imageAlt: slideNode.imageAlt,
imageClass: slideNode.imageClass,
indexClass: slideNode.id,
key: slideNode.id,
text: slideNode.text,
title: slideNode.title });
});
return React.createElement(
"div",
{ className: "slides" },
slidesNodes
);
}
});
var Slide = React.createClass({
displayName: "Slide",
propTypes: {
active: React.PropTypes.bool.isRequired,
imageClass: React.PropTypes.string.isRequired,
indexClass: React.PropTypes.string.isRequired,
text: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired
},
render: function () {
var classes = classNames({
"slide": true,
"slide--active": this.props.active
});
return React.createElement(
"div",
{ className: classes },
React.createElement(
"div",
{ className: this.props.indexClass },
React.createElement(
"div",
{ className: "slide-layout" },
React.createElement("img", { className: this.props.imageClass }),
React.createElement(
"h2",
null,
this.props.title
),
React.createElement(
"div",
{ className: "slide-text" },
this.props.text
)
)
)
);
}
});
var Controls = React.createClass({
displayName: "Controls",
togglePrev: function () {
actions.togglePrev();
},
toggleNext: function () {
actions.toggleNext();
},
render: function () {
var showPrev, showNext;
var current = state.currentSlide;
var last = state.data.length;
if (current > 0) {
showPrev = React.createElement("div", { className: "toggle toggle-prev", onClick: this.togglePrev });
}
if (current < last - 1) {
showNext = React.createElement("div", { className: "toggle toggle-next", onClick: this.toggleNext });
}
return React.createElement(
"div",
{ className: "controls" },
showPrev,
showNext
);
}
});
var EmptyMessage = React.createClass({
displayName: "EmptyMessage",
render: function () {
return React.createElement(
"div",
{ className: "empty-message" },
"No Data"
);
}
});
function render(renderTo) {
var hasData = state.data.length > 0;
var component;
if (hasData) {
component = React.createElement(Slideshow, { data: state.data });
} else {
component = React.createElement(EmptyMessage, null);
}
React.render(component, document.querySelector(renderTo ? renderTo : "#main"));
}
function init(renderTo, data) {
state.data = data;
render(renderTo);
}
return {
init: init
};
}();

View File

@ -3,7 +3,6 @@ pref("loop.remote.autostart", false);
pref("loop.server", "https://loop.services.mozilla.com/v0"); pref("loop.server", "https://loop.services.mozilla.com/v0");
pref("loop.linkClicker.url", "https://hello.firefox.com/"); pref("loop.linkClicker.url", "https://hello.firefox.com/");
pref("loop.gettingStarted.latestFTUVersion", 1); pref("loop.gettingStarted.latestFTUVersion", 1);
pref("loop.facebook.shareUrl", "https://www.facebook.com/sharer/sharer.php?u=%ROOM_URL%");
pref("loop.gettingStarted.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/hello/start/"); pref("loop.gettingStarted.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/hello/start/");
pref("loop.gettingStarted.resumeOnFirstJoin", false); pref("loop.gettingStarted.resumeOnFirstJoin", false);
pref("loop.legal.ToS_url", "https://www.mozilla.org/about/legal/terms/firefox-hello/"); pref("loop.legal.ToS_url", "https://www.mozilla.org/about/legal/terms/firefox-hello/");
@ -21,6 +20,11 @@ pref("loop.feedback.dateLastSeenSec", 0);
pref("loop.feedback.periodSec", 15770000); // 6 months. pref("loop.feedback.periodSec", 15770000); // 6 months.
pref("loop.feedback.formURL", "https://www.mozilla.org/firefox/hello/npssurvey/"); pref("loop.feedback.formURL", "https://www.mozilla.org/firefox/hello/npssurvey/");
pref("loop.feedback.manualFormURL", "https://www.mozilla.org/firefox/hello/feedbacksurvey/"); pref("loop.feedback.manualFormURL", "https://www.mozilla.org/firefox/hello/feedbacksurvey/");
pref("loop.mau.openPanel", 0);
pref("loop.mau.openConversation", 0);
pref("loop.mau.roomOpen", 0);
pref("loop.mau.roomShare", 0);
pref("loop.mau.roomDelete", 0);
#ifdef DEBUG #ifdef DEBUG
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:"); pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
#else #else
@ -29,8 +33,7 @@ pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src * data:; font
pref("loop.fxa_oauth.tokendata", ""); pref("loop.fxa_oauth.tokendata", "");
pref("loop.fxa_oauth.profile", ""); pref("loop.fxa_oauth.profile", "");
pref("loop.support_url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/cobrowsing"); pref("loop.support_url", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/cobrowsing");
#ifdef LOOP_BETA
pref("loop.facebook.enabled", true); pref("loop.facebook.enabled", true);
#else pref("loop.facebook.appId", "1519239075036718");
pref("loop.facebook.enabled", false); pref("loop.facebook.shareUrl", "https://www.facebook.com/dialog/send?app_id=%APP_ID%&link=%ROOM_URL%&redirect_uri=%REDIRECT_URI%");
#endif pref("loop.facebook.fallbackUrl", "https://hello.firefox.com/");

View File

@ -181,43 +181,6 @@ html[dir="rtl"] .conversation-toolbar-media-btn-group-box > button:first-child:h
width: 100%; width: 100%;
} }
.call-action-group > .invite-button {
cursor: pointer;
margin: 0 4px;
position: relative;
}
.call-action-group > .invite-button > img {
background-color: #00a9dc;
border-radius: 100%;
height: 28px;
width: 28px;
}
.call-action-group > .invite-button:hover > img {
background-color: #5cccee;
}
.call-action-group > .invite-button.triggered > img {
background-color: #56b397;
}
.call-action-group > .invite-button > p {
display: none;
/* Position the text under the button while centering it without impacting the
* rest of the layout */
left: -10rem;
margin: .5rem 0 0;
position: absolute;
right: -10rem;
font-size: 0.8rem;
}
.call-action-group > .invite-button.triggered > p,
.call-action-group > .invite-button:hover > p {
display: block;
}
.room-failure { .room-failure {
/* This flex allows us to not calculate the height of the logo area /* This flex allows us to not calculate the height of the logo area
versus the buttons */ versus the buttons */
@ -443,22 +406,133 @@ html, .fx-embedded, #main,
height: 100%; height: 100%;
right: 0; right: 0;
left: 0; left: 0;
text-align: center;
color: #000; color: #000;
z-index: 1010; z-index: 1010;
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
justify-content: center;
align-items: stretch; align-items: stretch;
} }
.room-invitation-content { .room-invitation-content {
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
margin: 12px 0;
font-size: 1.4rem;
}
.room-invitation-content > * {
width: 100%;
margin: 0 15px;
}
.room-context-header {
font-weight: bold;
font-size: 1.6rem;
margin-bottom: 10px;
text-align: center;
}
/* Input Button Combo group */
.input-button-content {
margin: 0 15px 10px 15px;
min-width: 64px;
border-radius: 4px;
border: 1px solid #d2cece;;
}
.input-button-group-label {
color: #898a8a;
margin: 0 15px;
margin-bottom: 2px;
font-size: 1.2rem;
}
.input-button-content > * {
width: 100%;
padding: 0 4px;
}
.input-button-content > .input-group input {
font-size: 1.4rem;
padding: 0.7rem;
width: 100%;
border: 0;
}
.input-button-content > .group-item-top {
border-radius: 4px 4px 0 0;
}
.input-button-content > .group-item-bottom {
border-radius: 0 0 4px 4px;
}
.input-button-content > .input-group {
background: #FFF;
}
.input-button-content > .invite-button {
background: #00a9dc;
height: 34px;
text-align: center;
display: flex;
flex-wrap: nowrap;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.input-button-content > .invite-button.triggered {
background-color: #00a9dc;
}
.input-button-content > .invite-button:hover {
background-color: #008ACB;
}
.share-action-group {
display: flex;
padding: 0 15px;
width: 100%;
flex-wrap: nowrap;
flex-direction: row;
justify-content: center;
}
.share-action-group > .invite-button {
cursor: pointer;
height: 34px;
border-radius: 4px;
background-color: #ebebeb;
display: flex;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
flex-grow: 1;
margin-right: 20px;
}
.share-action-group > .invite-button:last-child {
margin-right: 0;
}
.share-action-group > .invite-button:hover {
background-color: #d4d4d4;
}
.share-action-group > .invite-button.triggered {
background-color: #d4d4d4;
}
.share-action-group > .invite-button > img {
height: 28px;
width: 28px;
}
.share-action-group > .invite-button > div {
display: inline;
color: #4a4a4a;
}
.share-service-dropdown { .share-service-dropdown {
color: #000; color: #000;
text-align: start; text-align: start;
@ -500,17 +574,6 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
margin: 0 1rem 1.5rem 1rem; margin: 0 1rem 1.5rem 1rem;
} }
.room-invitation-content {
color: #333;
font-size: 1rem;
margin: 1rem auto;
}
.room-invitation-content > p {
margin-left: 10px;
margin-right: 10px;
}
.media-layout { .media-layout {
height: 100%; height: 100%;
} }
@ -796,7 +859,7 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
.text-chat-view > .text-chat-entries { .text-chat-view > .text-chat-entries {
width: 100%; width: 100%;
overflow: auto; overflow: auto;
padding-top: .6rem; padding-top: 15px;
/* 40px is the height of .text-chat-box. */ /* 40px is the height of .text-chat-box. */
height: calc(100% - 40px); height: calc(100% - 40px);
} }

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path fill="#55565A" d="M47,25c0-1-0.4-2-1.1-2.7L26.4,2.9c-0.7-0.7-1.7-1.1-2.7-1.1c-1,0-2,0.4-2.7,1.1l-2.2,2.2 c-0.7,0.7-1.1,1.7-1.1,2.7s0.4,2,1.1,2.7l8.8,8.8h-21c-2.2,0-3.5,1.8-3.5,3.8v3.8c0,2,1.3,3.8,3.5,3.8h21l-8.8,8.8 c-0.7,0.7-1.1,1.7-1.1,2.7c0,1,0.4,2,1.1,2.7l2.2,2.2c0.7,0.7,1.7,1.1,2.7,1.1c1,0,2-0.4,2.7-1.1l19.5-19.5C46.6,27,47,26,47,25z"/></svg>

After

Width:  |  Height:  |  Size: 415 B

View File

@ -0,0 +1 @@
<svg width="31" height="24" viewBox="0 0 31 24" xmlns="http://www.w3.org/2000/svg"><g fill="none"><path d="M9.07 17.683l.644.644c.056.042.112.07.182.07.071 0 .126-.027.182-.07l1.975-1.989c.574.35 1.191.56 1.849.63v1.037h-2.003c-.14 0-.252.042-.35.154-.098.098-.154.21-.154.336 0 .14.056.252.154.35.098.098.21.154.35.154h5.001c.14 0 .252-.056.35-.154.098-.098.154-.21.154-.35 0-.126-.056-.238-.154-.336-.098-.112-.21-.154-.35-.154h-2.003v-1.037c1.135-.126 2.088-.602 2.858-1.457.771-.855 1.149-1.864 1.149-3.012v-.995c0-.14-.056-.252-.154-.35-.098-.098-.21-.154-.336-.154-.14 0-.252.056-.35.154-.098.098-.154.21-.154.35v.995c0 .967-.35 1.793-1.037 2.48-.687.687-1.513 1.022-2.465 1.022-.574 0-1.107-.126-1.611-.392l.757-.756c.28.098.56.154.854.154.687 0 1.274-.252 1.765-.743.49-.491.728-1.078.728-1.765v-.995l2.83-2.83c.042-.042.07-.112.07-.182 0-.056-.028-.126-.07-.168l-.644-.644c-.056-.056-.112-.084-.182-.084-.07 0-.126.028-.182.084l-9.652 9.637c-.042.056-.07.112-.07.182 0 .07.028.126.07.182zm1.149-3.502l.784-.798c-.07-.308-.112-.602-.112-.882v-.995c0-.14-.056-.252-.154-.35-.098-.098-.21-.154-.336-.154-.14 0-.252.056-.35.154-.098.098-.154.21-.154.35v.995c0 .574.112 1.135.323 1.681zm6.542-6.527c-.182-.491-.491-.882-.911-1.191-.434-.308-.911-.462-1.443-.462-.701 0-1.289.238-1.779.728-.49.491-.728 1.079-.728 1.765v4.007l4.861-4.847z" fill="#333"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +1 @@
<svg width="31" height="24" viewBox="0 0 31 24" xmlns="http://www.w3.org/2000/svg"><g fill="none"><path d="M19.002 11.5c0-.273-.226-.5-.5-.5-.273 0-.5.226-.5.5v1c0 1.93-1.57 3.5-3.5 3.5s-3.5-1.57-3.5-3.5v-1c0-.273-.226-.5-.5-.5-.273 0-.5.226-.5.5v1c0 2.312 1.75 4.219 4 4.468v1.031h-2c-.273 0-.5.226-.5.5 0 .273.226.5.5.5h5c.273 0 .5-.226.5-.5 0-.273-.226-.5-.5-.5h-2v-1.031c2.25-.25 4-2.156 4-4.468v-1zm-2-2.999c0-1.375-1.125-2.5-2.5-2.5s-2.5 1.125-2.5 2.5v4c0 1.375 1.125 2.5 2.5 2.5s2.5-1.125 2.5-2.5v-4z" fill="#00A9DC"/></g></svg> <svg width="31" height="24" viewBox="0 0 31 24" xmlns="http://www.w3.org/2000/svg"><g fill="none"><path d="M19.002 11.5c0-.273-.226-.5-.5-.5-.273 0-.5.226-.5.5v1c0 1.93-1.57 3.5-3.5 3.5s-3.5-1.57-3.5-3.5v-1c0-.273-.226-.5-.5-.5-.273 0-.5.226-.5.5v1c0 2.312 1.75 4.219 4 4.468v1.031h-2c-.273 0-.5.226-.5.5 0 .273.226.5.5.5h5c.273 0 .5-.226.5-.5 0-.273-.226-.5-.5-.5h-2v-1.031c2.25-.25 4-2.156 4-4.468v-1zm-2-2.999c0-1.375-1.125-2.5-2.5-2.5s-2.5 1.125-2.5 2.5v4c0 1.375 1.125 2.5 2.5 2.5s2.5-1.125 2.5-2.5v-4z" fill="#00A9DC"/></g></svg>

Before

Width:  |  Height:  |  Size: 535 B

After

Width:  |  Height:  |  Size: 536 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path fill="#ffffff" d="M47.3,38.6c0-0.9-0.4-1.9-1.1-2.6l-11-11l11-11c0.7-0.7,1.1-1.6,1.1-2.6c0-0.9-0.4-1.9-1.1-2.6l-5.1-5.1 c-0.7-0.7-1.6-1.1-2.6-1.1c-0.9,0-1.9,0.4-2.6,1.1l-11,11l-11-11c-0.7-0.7-1.6-1.1-2.6-1.1c-0.9,0-1.9,0.4-2.6,1.1L3.8,8.9 c-0.7,0.7-1.1,1.6-1.1,2.6c0,0.9,0.4,1.9,1.1,2.6l11,11l-11,11c-0.7,0.7-1.1,1.6-1.1,2.6c0,0.9,0.4,1.9,1.1,2.6l5.1,5.1 c0.7,0.7,1.6,1.1,2.6,1.1c0.9,0,1.9-0.4,2.6-1.1l11-11l11,11c0.7,0.7,1.6,1.1,2.6,1.1c0.9,0,1.9-0.4,2.6-1.1l5.1-5.1 C46.9,40.5,47.3,39.5,47.3,38.6z"/></svg>

After

Width:  |  Height:  |  Size: 573 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 225 50"><path fill="#1E92CE" d="M146.3,20.2c-0.1-0.1-0.2-0.2-0.4-0.3c-0.2-0.1-0.3-0.1-0.5-0.1c-0.2,0-0.3,0-0.5,0.1 c-0.2,0.1-0.3,0.2-0.4,0.3c-0.1,0.1-0.2,0.3-0.3,0.4c-0.1,0.2-0.1,0.3-0.1,0.5c0,0.2,0,0.4,0.1,0.5c0.1,0.2,0.2,0.3,0.3,0.4 c0.1,0.1,0.2,0.2,0.4,0.3c0.2,0.1,0.3,0.1,0.5,0.1c0.2,0,0.3,0,0.5-0.1c0.2-0.1,0.3-0.2,0.4-0.3c0.1-0.1,0.2-0.3,0.3-0.4 c0.1-0.2,0.1-0.3,0.1-0.5c0-0.2,0-0.3-0.1-0.5C146.5,20.4,146.4,20.3,146.3,20.2z M146.3,21.5c-0.1,0.1-0.1,0.2-0.2,0.3 c-0.1,0.1-0.2,0.2-0.3,0.2c-0.1,0.1-0.3,0.1-0.4,0.1c-0.1,0-0.3,0-0.4-0.1c-0.1-0.1-0.2-0.1-0.3-0.2c-0.1-0.1-0.2-0.2-0.2-0.3 c-0.1-0.1-0.1-0.3-0.1-0.4c0-0.2,0-0.3,0.1-0.4c0.1-0.1,0.1-0.2,0.2-0.3c0.1-0.1,0.2-0.2,0.3-0.2c0.1-0.1,0.3-0.1,0.4-0.1 c0.1,0,0.3,0,0.4,0.1c0.1,0.1,0.2,0.1,0.3,0.2c0.1,0.1,0.2,0.2,0.2,0.3c0.1,0.1,0.1,0.3,0.1,0.4C146.4,21.2,146.4,21.4,146.3,21.5 z M145.7,21.4C145.7,21.3,145.6,21.3,145.7,21.4c-0.1-0.1-0.1-0.1-0.1-0.2c0,0,0,0,0,0c0.1,0,0.2,0,0.3-0.1 c0.1-0.1,0.1-0.2,0.1-0.3c0-0.1,0-0.1,0-0.2c0,0,0-0.1-0.1-0.1c0,0-0.1-0.1-0.1-0.1c-0.1,0-0.1,0-0.2,0H145v1.4h0.2v-0.6 c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0.1,0.1,0.1,0.1c0,0.1,0.1,0.1,0.1,0.2l0.1,0.2h0.3L145.7,21.4C145.7,21.4,145.7,21.4,145.7,21.4z M145.3,21h-0.1v-0.5h0.1c0.1,0,0.2,0,0.2,0.1c0,0,0.1,0.1,0.1,0.2c0,0.1,0,0.1-0.1,0.2c0,0-0.1,0-0.1,0.1 C145.4,21,145.4,21,145.3,21z M54.4,37.8h4.1V26.5h6.8v-3.3h-6.8v-6.7h8.4l0.5-3.3h-13V37.8z M71.6,11.9c-1.5,0-2.6,1.2-2.6,2.6 c0,1.4,1.1,2.6,2.5,2.6c1.5,0,2.6-1.2,2.6-2.6C74.1,13.1,73,11.9,71.6,11.9z M69.5,37.8h3.9V19.4l-3.9,0.7V37.8z M85.4,19.3 c-1.7,0-3.2,0.9-4.6,2.9c0-1-0.2-2-0.7-2.8l-3.6,0.9c0.4,1.1,0.6,2.6,0.6,4.8v12.7h3.9V25.6c0.4-1.5,1.7-2.7,3.4-2.7 c0.4,0,0.7,0.1,1.1,0.2l1.2-3.6C86.3,19.4,86,19.3,85.4,19.3z M94,19.4c-2.3,0-4,0.7-5.5,2.4c-1.6,1.8-2.2,3.9-2.2,7 c0,5.7,3.2,9.4,8.3,9.4c2.4,0,4.6-0.8,6.4-2.4l-1.5-2.4c-1.3,1.2-2.8,1.8-4.5,1.8c-3.5,0-4.4-2.6-4.4-5.1v-0.3h10.7V29 c0-4.2-0.8-6.4-2.3-7.8C97.4,19.9,95.7,19.4,94,19.4z M90.6,27c0-2.9,1.2-4.6,3.4-4.6c1.9,0,3.2,1.7,3.2,4.6H90.6z M108,19.8v-2.7 c0-1.6,0.9-2.5,2.2-2.5c0.7,0,1.3,0.2,2.2,0.6l1.3-2.5c-1.2-0.7-2.5-1-4-1c-3.2,0-5.5,1.7-5.5,5.5c0,1.7,0.1,2.7,0.1,2.7h-1.7v2.7 h1.7v15.3h3.9V22.5h3.7l1-2.7H108z M119.9,19.4c-4.7,0-7.8,3.7-7.8,9.4c0,5.7,3,9.4,7.9,9.4c4.9,0,7.9-3.6,7.9-9.4 C127.9,23.2,125,19.4,119.9,19.4z M120.1,35.3c-2.3,0-3.6-1.5-3.6-6.7c0-4.4,1.1-6.2,3.5-6.2c2.3,0,3.7,1.5,3.7,6.6 C123.6,33.4,122.4,35.3,120.1,35.3z M143.2,19.8h-4.5c-0.5,0.7-2.3,4.3-2.8,5.5c-0.9-1.7-2.4-4.5-3.3-5.8l-4.2,0.9l5.2,7.7 l-6.7,9.7h4.9c0.7-1,3.2-5.4,3.9-6.7c0.4,0.6,3.3,5.7,3.9,6.7h4.9L138,28L143.2,19.8z M165.8,23.6h-10.3V13.3h-2.9v24.6h2.9V26 h10.3v11.9h2.9V13.3h-2.9V23.6z M179,19.5c-2.1,0-3.9,0.8-5.3,2.5c-1.5,1.8-2.1,3.7-2.1,6.7c0,5.9,3,9.5,7.9,9.5 c2.3,0,4.4-0.8,6-2.2l-1.1-1.8c-1.3,1.1-2.6,1.7-4.4,1.7c-1.8,0-3.4-0.6-4.4-2.2c-0.6-0.9-0.8-2.2-0.8-3.9v-0.4h11.1V29 c-0.1-4.3-0.5-5.9-2-7.5C182.6,20.2,181,19.5,179,19.5z M174.8,27.3c0.1-3.7,1.6-5.6,4.1-5.6c1.4,0,2.6,0.6,3.2,1.6 c0.5,0.9,0.8,2,0.8,4H174.8z M193.1,36.1c-0.8,0-1-0.4-1-1.8V16c0-2.7-0.5-4.2-0.5-4.2l-2.8,0.5c0,0,0.4,1.3,0.4,3.7v18.9 c0,1.3,0.3,2,0.9,2.5c0.5,0.5,1.3,0.8,2.1,0.8c0.8,0,1.1-0.1,1.8-0.4l-0.6-1.8C193.4,36,193.2,36.1,193.1,36.1z M200.3,36.1 c-0.8,0-1-0.4-1-1.8V16c0-2.7-0.5-4.2-0.5-4.2l-2.8,0.5c0,0,0.4,1.3,0.4,3.7v18.9c0,1.3,0.3,2,0.9,2.5c0.5,0.5,1.2,0.8,2.1,0.8 c0.7,0,1.1-0.1,1.8-0.4l-0.6-1.8C200.6,36,200.4,36.1,200.3,36.1z M216.3,22.6c-1.2-1.8-3.1-3.1-6.1-3.1c-4.7,0-7.6,3.5-7.6,9.4 c0,5.9,2.8,9.5,7.7,9.5c4.5,0,7.7-3.2,7.7-9.1C218,26.3,217.5,24.2,216.3,22.6z M214.3,33.3c-0.6,1.7-2,2.7-3.9,2.7 c-1.5,0-3-0.7-3.6-1.8c-0.7-1.1-1.1-3.3-1.1-5.8c0-2.1,0.3-3.5,0.9-4.7c0.6-1.2,2.1-1.9,3.6-1.9c1.5,0,3,0.7,3.8,2.3 c0.6,1.2,0.8,2.9,0.8,5.4C214.8,31.2,214.7,32.2,214.3,33.3z M26.1,7.7C16.1,7.7,8,14.9,8,23.6c0,4.4,2,8.3,5.2,11.2 c-0.6,2-1.7,4.7-3.9,7.3c0.4,0.7,6.6-1.7,11-3.4c1.8,0.5,3.7,0.8,5.7,0.8c10,0,18.1-7.1,18.1-15.9C44.1,14.9,36,7.7,26.1,7.7z M31.5,17.3c1.3,0,2.4,1.1,2.4,2.4c0,1.3-1.1,2.4-2.4,2.4c-1.3,0-2.4-1.1-2.4-2.4C29.1,18.3,30.2,17.3,31.5,17.3z M20.6,17.3 c1.3,0,2.4,1.1,2.4,2.4c0,1.3-1.1,2.4-2.4,2.4c-1.3,0-2.4-1.1-2.4-2.4C18.2,18.3,19.3,17.3,20.6,17.3z M26.1,34.7 C26.1,34.7,26,34.7,26.1,34.7c-0.1,0-0.1,0-0.1,0c-4.8,0-10.2-3.1-11.5-8.4c3.3,1.5,7.8,2.2,11.5,2.2c3.7,0,8.3-0.7,11.5-2.2 C36.3,31.6,30.9,34.7,26.1,34.7z"/></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 310 310"><defs><path id="a" d="M155,63.5c55.2,0,100,44.8,100,100s-44.8,100-100,100c-55.2,0-100-44.8-100-100S99.8,63.5,155,63.5z"/></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"/></clipPath><path fill="#FFF" fill-rule="evenodd" d="M155,63.5c55.2,0,100,44.8,100,100 s-44.8,100-100,100c-55.2,0-100-44.8-100-100S99.8,63.5,155,63.5z" clip-path="url(#b)" clip-rule="evenodd"/><g fill="#E7C1A8" clip-path="url(#b)"><path d="M80.3 199.7H123.3V254.7H80.3z" transform="rotate(71.16 101.822 227.175)"/><path d="M84.6,163c7.1-1,13.8,1.9,15,11.4l3,22.5c0.1,0.9,1.5,1.5,2.4,1.4c0.9-0.1,1.6-1,1.4-1.9l-10-74.2l0,0 l-0.5-3.5c-0.5-3.4,2-6.6,5.4-7.1c3.5-0.5,6.7,1.9,7.1,5.4l0.5,3.5l5.8,42.7l3-0.4l-7-51.6l0,0l-0.5-3.5c-0.5-3.4,2-6.6,5.4-7.1 c3.5-0.5,6.7,1.9,7.1,5.4l0.5,3.5l0,0l7,51.6l4.1-0.6l-5.8-42.7l-0.5-3.5c-0.5-3.4,2-6.6,5.4-7.1c3.5-0.5,6.7,1.9,7.1,5.4 l0.5,3.5l0,0l5.8,42.7l3.9-0.5L147,130l0,0l-0.5-3.5c-0.5-3.4,2-6.6,5.4-7.1c3.5-0.5,6.7,1.9,7.1,5.4l0.5,3.5c0,0,0,0,0,0 c1.2,9.3,8.9,65.7,9.1,67.8c2.9,21.5-12.3,41.3-33.9,44.2c-21.6,2.9-41.5-12.2-44.4-33.7C90.1,205.1,84.6,163,84.6,163z"/></g><g clip-path="url(#b)"><defs><path id="c" d="M84.6,163c7.1-1,13.8,1.9,15,11.4l3,22.5c0.1,0.9,1.5,1.5,2.4,1.4c0.9-0.1,1.6-1,1.4-1.9l-10-74.2l0,0 l-0.5-3.5c-0.5-3.4,2-6.6,5.4-7.1c3.5-0.5,6.7,1.9,7.1,5.4l0.5,3.5l5.8,42.7l3-0.4l-7-51.6l0,0l-0.5-3.5c-0.5-3.4,2-6.6,5.4-7.1 c3.5-0.5,6.7,1.9,7.1,5.4l0.5,3.5l0,0l7,51.6l4.1-0.6l-5.8-42.7l-0.5-3.5c-0.5-3.4,2-6.6,5.4-7.1c3.5-0.5,6.7,1.9,7.1,5.4 l0.5,3.5l0,0l5.8,42.7l3.9-0.5L147,130l0,0l-0.5-3.5c-0.5-3.4,2-6.6,5.4-7.1c3.5-0.5,6.7,1.9,7.1,5.4l0.5,3.5c0,0,0,0,0,0 c1.2,9.3,8.9,65.7,9.1,67.8c2.9,21.5-12.3,41.3-33.9,44.2c-21.6,2.9-41.5-12.2-44.4-33.7C90.1,205.1,84.6,163,84.6,163z"/></defs><clipPath id="d"><use xlink:href="#c" overflow="visible"/></clipPath><path fill="#F4D2BB" d="M104.7,196.9c-0.2,0-0.3,0.1-0.5,0.1l-6.4-47.6L78,152.1l7.1,52.8 c-12,8.9-19,23.9-16.8,39.8c3.1,23.3,24.5,39.6,47.8,36.4s39.6-24.5,36.4-47.8S127.9,193.8,104.7,196.9z" clip-path="url(#d)"/></g><path fill="#CE7C32" d="M215.7,212.9c0.8-2.7,1.4-5.5,1.6-8.5c0.2-2,4.4-58.9,5.1-68.2c0,0,0,0,0,0 l0.3-3.6c0.3-3.5-2.4-6.5-5.8-6.7c-3.5-0.3-6.5,2.3-6.8,5.8l-0.3,3.6l0,0l-2.1,28.5l-3.9-0.3l3.2-43l0,0l0.3-3.6 c0.3-3.5-2.4-6.5-5.8-6.7c-3.5-0.3-6.5,2.3-6.8,5.8l-0.3,3.6l-3.2,43l-4.1-0.3l3.9-52l0,0l0.3-3.6c0.3-3.5-2.4-6.5-5.8-6.7 c-3.5-0.3-6.5,2.3-6.8,5.8l-0.3,3.6l0,0l-3.9,52l-3-0.2l3.2-43l0.3-3.6c0.3-3.5-2.4-6.5-5.8-6.7c-3.5-0.3-6.5,2.3-6.8,5.8 l-0.3,3.6l0,0l-5.6,74.7c-0.1,0.9-0.9,1.6-1.8,1.6c-0.9-0.1-2.1-0.9-2.1-1.8l1.7-22.7c0.7-9.6-5.2-13.7-12.4-14.3 c0,0-3.3,42.3-3.4,43.9c-1.5,19.8,12.1,37.2,31,41.3l0,0.1l50.6,21.6l16.9-39.6L215.7,212.9z" clip-path="url(#b)"/><g fill="#71C4EA"><path d="M153.8,79.4c0,1.5,1.2,2.6,2.6,2.6c1.5,0,2.6-1.2,2.6-2.6V49.1c0-1.5-1.2-2.6-2.6-2.6 c-1.5,0-2.6,1.2-2.6,2.6V79.4z"/><path d="M174.7,86.4c-0.8,1.2-0.6,2.8,0.6,3.7c1.2,0.8,2.8,0.6,3.7-0.6l17.5-24.7c0.8-1.2,0.6-2.8-0.6-3.7 c-1.2-0.8-2.8-0.6-3.7,0.6L174.7,86.4z"/><path d="M138.1,86.4c0.8,1.2,0.6,2.8-0.6,3.7c-1.2,0.8-2.8,0.6-3.7-0.6l-17.5-24.7c-0.8-1.2-0.6-2.8,0.6-3.7 c1.2-0.8,2.8-0.6,3.7,0.6L138.1,86.4z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 310 310"><path fill="#71C4EA" d="M265.5,53.9c0-2.6-2.1-3.7-4.7-3.7h-79.6c-2.6,0-4.7,2.1-4.7,4.7V64h89V53.9z"/><circle cx="184.1" cy="56.9" r="1.6" fill="#FFF"/><circle cx="190.2" cy="56.9" r="1.6" fill="#FFF"/><circle cx="196.3" cy="56.9" r="1.6" fill="#FFF"/><path fill="#FFF" d="M181.2,118h79.6c2.6,0,4.7-2.1,4.7-4.7V64h-89v49.3C176.5,115.9,178.6,118,181.2,118z"/><path fill="#D6DCE5" d="M176.5 64H265.5V73H176.5z"/><path fill="#06A6E0" fill-rule="evenodd" d="M153.5,59.9c55.2,0,100,44.8,100,100s-44.8,100-100,100 c-55.2,0-100-44.8-100-100S98.3,59.9,153.5,59.9z" clip-rule="evenodd"/><path fill="none" d="M153.5,59.9c55.2,0,100,44.8,100,100s-44.8,100-100,100 c-55.2,0-100-44.8-100-100S98.3,59.9,153.5,59.9z" clip-rule="evenodd"/><defs><path id="a" d="M153.5,59.9c55.2,0,100,44.8,100,100s-44.8,100-100,100c-55.2,0-100-44.8-100-100S98.3,59.9,153.5,59.9z"/></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"/></clipPath><g clip-path="url(#b)"><path fill="#0283B1" d="M259.4,134.6L259.4,134.6C259.4,134.6,259.4,134.6,259.4,134.6c0-0.1,0-0.2-0.1-0.2h0 c-4.3-13.9-11.2-26.9-20.1-38.1l-2.2,0.8l-1.8,3.9L223,98l-6.8,5.9l1.8,3.4l-11.2-6.9l5.5-5.8l9.7-3.4l-1.5-3.4L210.3,92 l-7.3,7.5l6.3,7.2l-9.4,0l-14.2,9.3l-4.8-5l7.3,0.7l3.3-5.1l-18.5-4.3l-12.4,3l-11.2,15.1l-0.4,5.8l6.8-0.8l1.5,3.9l6.4-1.8 l0.4-5.1l-2.3-1.9l6.4-8.3l2.9,3l-5,3.9l2,4.3l6.8-2.4l1.9,2.4l-10.8,4.1l-0.4,2.8l-9.9,0.5l-1.9-2.9l-2.9,1.4l1.5,3.9l-1.6,1.6 l-4,3.6l-2.9,0.4l-5,3.9l2.9,3l-1.4,3.4l-7.9-0.5l4,11l11.2-10.4l2,1.1l3.3-3l5.9,4.4l1.4,5.8l3.4-4.8l-5.3-7.3l1.2-1.3l6.6,7.6 l1,5.8l5.5,4l-1-7.9l5.8,7.9l4.4-5.9v5.9l-2,2l-10.7-0.5l-3-2.5l-3.9,3.4l-7.9-2.5l0.5-2.5l-2.5-2.8l-9.8,0.9l-17.7,15.3 l0.4,15.1l8,7l13.7,0.4l6.8,4.4l-2.9,1.9l7,8.3v5.8l-2.6,3.4l8.3,22.6l9.4,0.5l11.7-14.2l0.5-5.5l4.5-3.4l-0.6-7.3l-1.8-1.9 l15.2-22.1l-8.4,0.6l-5.5-4.1l-7.2-14.6l0.4-4h2.9l10.8,20.7l10.3-3.9l4.4-5.5l-4.4-4.3l-4,1.4l-3.9-6.4l23.6,4.3l-0.4,6.6 l2.8,0.9l4,11.2l6.5-1.8v-5.9l8.2-6.4l1.1-4.5h2.4v5l8.4,18.2l-5.9-0.4l9.9,10.8c0.6-1.9,1.2-3.8,1.7-5.7l-1.8-0.2l-1-3l3.4,0.9 c0.3-1,0.5-2.1,0.7-3.1l-4.1-2.7l-1.5-6.4l1.5-0.4l3,5.7l2.1-1.2c0.5-2.9,0.9-5.9,1.2-8.9c0-0.3,0.1-0.6,0.1-0.9 c0.3-3.5,0.5-7,0.5-10.5C264.5,156.8,262.7,145.4,259.4,134.6z M181.9,149.5h-3l-3.9,3l-0.9-3.4l4.4-5.5l2.5,3.2l2.9-0.3 l-0.2-2.6l3.3,0.6l3.6,5.7L181.9,149.5z M201.7,157.9l-0.6-10.4l2.8-2.2l1.4,1.5l-2.7,3.7l3.2,6.4L201.7,157.9z M211.9,150.8 l-0.3-4l2.5-0.3l0.5,4L211.9,150.8z M192.3,219.7l-1.8,6.4l6.4,4.4l4.4-14.1l-2.6-1.6L192.3,219.7z M130.3,134.4l-0.2,0.4l0,0 l-0.3,0.5l3.3,3l3-3.3l-0.5-0.6h0l-3-3.8L130.3,134.4L130.3,134.4z M86.9,200.7l-0.9-7h-7.4l-0.6-6l-9.2-0.3l-4.8-1.8l-6.8,2.9 h-7l-1-5.9l-3.9-0.4l1-5.5l-2.6-1.4l-3.9,2.4H38l-4.1-3.9l4.1-7.3h10.8l2.9,4.4h3.4l-2-6.5l7-5.9l-0.6-3.9l2.9-2.9l3,0.4v-5.3 l17.2-9.8l-2.1-2.5h0l-10-12.1l-15.2-2.5l0,7.3l1.4,1.5l-1.9,5.8h-0.1l-0.5,1.5l-1.3-1.5h-0.1l-5-5.8l-6.4-1.9 c-1,2.5-1.9,5.1-2.7,7.8h0c-3.3,10.8-5.1,22.3-5.1,34.2c0,4.9,0.3,9.7,0.9,14.4l6.5,3.7l3.9-0.4l3,5.9h6.8v5l-2.9,4.4l6.8,17 l6.5,3l-3.8,22c3.1,3.6,6.4,7,9.9,10.2l29.1-31.1l1.1-8.8l3.9-4.5L92,200.7H86.9z M155.8,156.2l3,1.1l-1.2-4L155.8,156.2z M76.3,182.5l-13.9-5.9l-0.9,2.5l12.3,4.4L76.3,182.5z M50.5,176.6l7.5,1.6l1-3l-7.8-2.3L50.5,176.6z M58.1,167.7v3.4l3.9,1.9 L58.1,167.7z M126.9,110.6l-9.8,2.5l5.3,7.9l8.8-6.3L126.9,110.6z M138.8,134.8l-1.4,2.7h6.9l0.3-2.6l0.2-1.7l-4.3-3.5l-2-4.8 l-3.3,3.4l4.5,4.9L138.8,134.8z M123.5,83.7l5.8-4.9l-10.7-8.8l-36.4,4.4c-6.4,4.5-12.2,9.7-17.5,15.4l2.4,3.3l6.5-2l5.9,4.4 l2.9,15.2l7.3,11.3l6.4,0.4l3.5-8.4l14.1-4.3l8.9-13.7L123.5,83.7z M50.9,112.5l1.9-5.5l4.5,0l-1.6,8l12.8,2.3l6.8-7.3 L58.8,96.4c-5,6.3-9.4,13.1-13,20.3l6.5,0.6L50.9,112.5z"/></g><g fill="none" stroke="#F5C825" stroke-width="4" stroke-miterlimit="10" stroke-linecap="round"><path d="M128.5 57L128.5 57"/><path d="M117,53.3c-22.1-6-41.2-4-52.7,7.5c-25.5,25.5-4.4,87.8,47,139.3s113.8,72.5,139.3,47c11.6-11.6,13.6-30.9,7.3-53.4" stroke-dasharray="0,12.1246"/><path d="M256.2 187.9L256.2 187.9"/></g><path fill="#7DC14C" d="M166.5,172.1c0-3.5-2.9-5.1-6.4-5.1H50.9c-3.5,0-6.4,2.9-6.4,6.4V186h122V172.1z"/><circle cx="54.6" cy="176.2" r="2.2" fill="#FFF"/><circle cx="62.9" cy="176.2" r="2.2" fill="#FFF"/><circle cx="71.3" cy="176.2" r="2.2" fill="#FFF"/><path fill="#FFF" d="M50.9,260h109.2c3.5,0,6.4-2.9,6.4-6.4V186h-122v67.6C44.5,257.1,47.4,260,50.9,260z"/><path fill="#D6DCE5" d="M44.5 186H166.5V199H44.5z"/></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 310 310"><path fill="#A3D189" fill-rule="evenodd" d="M142.5,55c55.2,0,100,44.8,100,100s-44.8,100-100,100 c-55.2,0-100-44.8-100-100S87.3,55,142.5,55z" clip-rule="evenodd"/><defs><path id="a" d="M267.5,66h-79.4c-13.7-7-29.2-11-45.6-11c-55.2,0-100,44.8-100,100c0,55.2,44.8,100,100,100 c20.7,0,39.9-6.3,55.8-17h69.2V66z"/></defs><clipPath id="b"><use xlink:href="#a" overflow="visible"/></clipPath><g clip-path="url(#b)"><path fill="#D6DCE5" d="M14.5 111H234.5V142H14.5z"/><path fill="#383F44" d="M234.5,90c0-5.5-4.5-10-10-10h-200c-5.5,0-10,4.5-10,10v21h220V90z"/><path fill="#FFF" d="M14.5,219c0,5.5,4.5,10,10,10h200c5.5,0,10-4.5,10-10v-77h-220V219z"/><path fill="#D6DCE5" fill-rule="evenodd" d="M92.5,101c-0.8-4.2-2.1-12-12.6-12H54.1 c-10.4,0-11.8,7.8-12.6,12c-1.2,6.6-3.8,10.1-10.7,10.1c4.6,0,67.8,0,72.5,0C96.3,111.1,93.7,107.6,92.5,101z" clip-rule="evenodd"/><path fill="#FFF" d="M29.5 118H165.5V135H29.5z"/><circle cx="32" cy="126.5" r="10.5" fill="#41484E"/><path fill="#FFF" d="M210.5 119.5c0 .8.7 1.5 1.5 1.5h14c.8 0 1.5-.7 1.5-1.5 0-.8-.7-1.5-1.5-1.5h-14C211.2 118 210.5 118.7 210.5 119.5 210.5 119.5 210.5 118.7 210.5 119.5zM210.5 126.5c0 .8.7 1.5 1.5 1.5h14c.8 0 1.5-.7 1.5-1.5 0-.8-.7-1.5-1.5-1.5h-14C211.2 125 210.5 125.7 210.5 126.5 210.5 126.5 210.5 125.7 210.5 126.5zM210.5 133.5c0 .8.7 1.5 1.5 1.5h14c.8 0 1.5-.7 1.5-1.5s-.7-1.5-1.5-1.5h-14C211.2 132 210.5 132.7 210.5 133.5 210.5 133.5 210.5 132.7 210.5 133.5z"/><path fill="#FFF" fill-rule="evenodd" d="M35.7,125.1h-4.4l2-2c0.3-0.3,0.4-0.8,0.1-1l-0.9-0.9 c-0.2-0.2-0.7-0.2-1,0.1l-4.6,4.7c-0.1,0.1-0.1,0.1-0.1,0.2l0,0c0,0,0,0,0,0c0,0,0,0.1-0.1,0.1c0,0.1-0.1,0.1-0.1,0.2 c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1c0,0.1,0,0.1,0.1,0.2c0,0,0,0.1,0.1,0.1c0,0,0,0,0,0l0,0c0,0.1,0.1,0.1,0.1,0.2l4.6,4.7 c0.3,0.3,0.8,0.4,1,0.1l0.9-0.9c0.2-0.2,0.2-0.7-0.1-1l-2-2h4.5c0.4,0,0.7-0.3,0.7-0.7v-1.5C36.4,125.4,36.1,125.1,35.7,125.1z" clip-rule="evenodd"/><circle cx="187.5" cy="127" r="9" fill="#FFF"/><path fill="#0283B1" d="M187.5,114.4c-7.2,0-13,5.1-13,11.3c0,3.1,1.4,5.9,3.8,8c-0.4,1.4-1.2,3.3-2.8,5.2 c0.3,0.5,4.7-1.2,7.9-2.5c1.3,0.4,2.7,0.6,4.1,0.6c7.2,0,13-5.1,13-11.3C200.5,119.5,194.7,114.4,187.5,114.4z M191.4,121.2 c0.9,0,1.7,0.8,1.7,1.7c0,0.9-0.8,1.7-1.7,1.7c-0.9,0-1.7-0.8-1.7-1.7C189.7,121.9,190.4,121.2,191.4,121.2z M183.5,121.2 c0.9,0,1.7,0.8,1.7,1.7c0,0.9-0.8,1.7-1.7,1.7c-1,0-1.7-0.8-1.7-1.7C181.8,121.9,182.6,121.2,183.5,121.2z M187.5,133.6 C187.5,133.6,187.5,133.6,187.5,133.6c-0.1,0-0.1,0-0.1,0c-3.5,0-7.3-2.2-8.2-6c2.3,1.1,5.6,1.5,8.3,1.5c2.7,0,5.9-0.5,8.3-1.5 C194.9,131.4,191,133.6,187.5,133.6z"/></g><path fill="#383F44" d="M195.5,155.8v-19.5l17,12.5l-7.4,1.2l3,7.6l-3.3,1.4l-3.9-6.8L195.5,155.8z"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#FFF" d="M12 10.4c0 .2-.1.4-.2.5-.1.1-.3.2-.5.2H4.7c-.2 0-.4-.1-.5-.2-.1-.1-.2-.3-.2-.5V6.9c.1.1.3.3.5.4 1 .7 1.8 1.2 2.2 1.5.1.1.3.2.4.3.1.1.2.1.4.2s.3.1.5.1.3 0 .5-.1.3-.1.4-.2c.1-.1.3-.2.4-.3.5-.4 1.2-.9 2.2-1.5.2-.1.4-.3.5-.4v3.5zm-.2-4.2c-.1.2-.3.4-.5.5-1.1.8-1.8 1.3-2.1 1.5 0 0-.1.1-.2.1-.1.2-.2.2-.3.3-.1 0-.1.1-.2.1-.1.1-.2.1-.3.1h-.4c-.1 0-.2-.1-.3-.1-.1-.1-.2-.1-.2-.1-.1-.1-.2-.1-.3-.2-.1-.1-.1-.1-.1-.2-.3-.1-.7-.4-1.2-.8-.5-.3-.8-.5-.9-.6-.2-.1-.4-.3-.6-.5S4 5.9 4 5.7c0-.2.1-.4.2-.6.1-.2.3-.2.5-.2h6.6c.2 0 .4.1.5.2.1.1.2.3.2.5s-.1.4-.2.6z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#4a4a4a" d="M12 10.4c0 .2-.1.4-.2.5-.1.1-.3.2-.5.2H4.7c-.2 0-.4-.1-.5-.2-.1-.1-.2-.3-.2-.5V6.9c.1.1.3.3.5.4 1 .7 1.8 1.2 2.2 1.5.1.1.3.2.4.3.1.1.2.1.4.2s.3.1.5.1.3 0 .5-.1.3-.1.4-.2c.1-.1.3-.2.4-.3.5-.4 1.2-.9 2.2-1.5.2-.1.4-.3.5-.4v3.5zm-.2-4.2c-.1.2-.3.4-.5.5-1.1.8-1.8 1.3-2.1 1.5 0 0-.1.1-.2.1-.1.2-.2.2-.3.3-.1 0-.1.1-.2.1-.1.1-.2.1-.3.1h-.4c-.1 0-.2-.1-.3-.1-.1-.1-.2-.1-.2-.1-.1-.1-.2-.1-.3-.2-.1-.1-.1-.1-.1-.2-.3-.1-.7-.4-1.2-.8-.5-.3-.8-.5-.9-.6-.2-.1-.4-.3-.6-.5S4 5.9 4 5.7c0-.2.1-.4.2-.6.1-.2.3-.2.5-.2h6.6c.2 0 .4.1.5.2.1.1.2.3.2.5s-.1.4-.2.6z"/></svg>

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 638 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#FFF" d="M12 11.6c0 .1 0 .2-.1.3s-.2.1-.3.1h-2V8.9h1l.2-1.2H9.5v-.8c0-.2 0-.3.1-.4.1-.1.2-.1.5-.1h.6V5.3h-.9c-.4-.1-.8 0-1.1.3-.3.3-.4.7-.4 1.2v.9h-1v1.2h1V12H4.4c-.1 0-.2 0-.3-.1-.1-.1-.1-.2-.1-.3V4.4c0-.1 0-.2.1-.3.1-.1.2-.1.3-.1h7.1c.1 0 .2 0 .3.1.2.1.2.2.2.3v7.2z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#4a4a4a" d="M12 11.6c0 .1 0 .2-.1.3s-.2.1-.3.1h-2V8.9h1l.2-1.2H9.5v-.8c0-.2 0-.3.1-.4.1-.1.2-.1.5-.1h.6V5.3h-.9c-.4-.1-.8 0-1.1.3-.3.3-.4.7-.4 1.2v.9h-1v1.2h1V12H4.4c-.1 0-.2 0-.3-.1-.1-.1-.1-.2-.1-.3V4.4c0-.1 0-.2.1-.3.1-.1.2-.1.3-.1h7.1c.1 0 .2 0 .3.1.2.1.2.2.2.3v7.2z"/></svg>

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 351 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#5a5a5a" d="M10.716 5.643c0 1.943-2.158 1.812-2.158 3.154v.3H6.83v-.37C6.83 6.65 8.74 6.793 8.74 5.81c0-.43-.312-.683-.84-.683-.49 0-.972.24-1.403.73l-1.21-.934C5.966 4.12 6.854 3.64 8.09 3.64c1.75 0 2.626.936 2.626 2.003zm-1.92 5.625c0 .6-.478 1.092-1.078 1.092s-1.08-.492-1.08-1.092c0-.588.48-1.08 1.08-1.08s1.08.492 1.08 1.08z"/></svg>

After

Width:  |  Height:  |  Size: 411 B

View File

@ -0,0 +1 @@
<svg width="13" height="10" viewBox="0 0 13 10" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"><title>Shape Copy</title><desc>Created with Sketch.</desc><path d="M6.302 2.225h-3.067c-.602 0-1.095.49-1.095 1.095v3.287c0 .609.49 1.095 1.095 1.095h3.067v1.55c0 .617.397.811.887.449l5.458-4.036c.498-.368.49-.949 0-1.312l-5.458-4.036c-.498-.368-.887-.161-.887.449v1.46zm-6.302-2.191h5.348v1.095h-4.278v7.668h4.278v1.106h-5.348v-9.869z" sketch:type="MSShapeGroup" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 521 B

View File

@ -0,0 +1 @@
<svg width="31" height="24" viewBox="0 0 31 24" xmlns="http://www.w3.org/2000/svg"><g fill="none"><path d="M13.506 17.003h4.478c.554 0 1.033-.202 1.424-.592.404-.403.592-.87.592-1.437v-1.16l2.836 2.836c.089.089.189.126.316.126l.176-.038c.189-.076.277-.215.277-.416v-7.652c0-.189-.088-.327-.277-.416l-.176-.025c-.126 0-.227.038-.316.126l-2.836 2.824v-.671l1.83-1.83c.042-.042.07-.112.07-.182 0-.056-.028-.126-.07-.168l-.644-.644c-.056-.056-.112-.084-.182-.084-.07 0-.126.028-.182.084l-9.652 9.637c-.042.056-.07.112-.07.182 0 .07.028.126.07.182l.644.644c.056.042.112.07.182.07.071 0 .126-.027.182-.07l1.327-1.327zm4.948-8.95c-.15-.034-.307-.05-.471-.05h-4.954c-.567 0-1.034.189-1.437.592-.391.391-.592.869-.592 1.424v4.954c0 .165.017.322.051.471l7.403-7.392z" fill="#333"/></g></svg>

After

Width:  |  Height:  |  Size: 782 B

View File

@ -130,6 +130,15 @@ loop.shared.actions = (function() {
// sentTimestamp: String (optional) // sentTimestamp: String (optional)
}), }),
/**
* Used to send cursor data to the other peer
*/
SendCursorData: Action.define("sendCursorData", {
ratioX: Number,
ratioY: Number,
type: String
}),
/** /**
* Notifies that cursor data has been received from the other peer. * Notifies that cursor data has been received from the other peer.
*/ */
@ -396,11 +405,13 @@ loop.shared.actions = (function() {
* XXX: should move to some roomActions module - refs bug 1079284 * XXX: should move to some roomActions module - refs bug 1079284
* @from: where the invitation is shared from. * @from: where the invitation is shared from.
* Possible values ['panel', 'conversation'] * Possible values ['panel', 'conversation']
* @roomUrl: the URL that is shared * @roomUrl: the URL that is shared.
* @roomOrigin: the URL browsed when the sharing is started - Optional.
*/ */
FacebookShareRoomUrl: Action.define("facebookShareRoomUrl", { FacebookShareRoomUrl: Action.define("facebookShareRoomUrl", {
from: String, from: String,
roomUrl: String roomUrl: String
// roomOrigin: String
}), }),
/** /**

View File

@ -32,7 +32,7 @@ loop.store.ROOM_STATES = {
CLOSING: "room-closing" CLOSING: "room-closing"
}; };
loop.store.ActiveRoomStore = (function() { loop.store.ActiveRoomStore = (function(mozL10n) {
"use strict"; "use strict";
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
@ -693,7 +693,8 @@ loop.store.ActiveRoomStore = (function() {
gotMediaPermission: function() { gotMediaPermission: function() {
this.setStoreState({ roomState: ROOM_STATES.JOINING }); this.setStoreState({ roomState: ROOM_STATES.JOINING });
loop.request("Rooms:Join", this._storeState.roomToken).then(function(result) { loop.request("Rooms:Join", this._storeState.roomToken,
mozL10n.get("display_name_guest")).then(function(result) {
if (result.isError) { if (result.isError) {
this.dispatchAction(new sharedActions.RoomFailure({ this.dispatchAction(new sharedActions.RoomFailure({
error: result, error: result,
@ -1243,4 +1244,4 @@ loop.store.ActiveRoomStore = (function() {
}); });
return ActiveRoomStore; return ActiveRoomStore;
})(); })(navigator.mozL10n || document.mozL10n);

View File

@ -4,7 +4,7 @@
var loop = loop || {}; var loop = loop || {};
loop.shared = loop.shared || {}; loop.shared = loop.shared || {};
loop.shared.models = (function(l10n) { loop.shared.models = (function() {
"use strict"; "use strict";
/** /**
@ -24,71 +24,11 @@ loop.shared.models = (function(l10n) {
* Notification collection * Notification collection
*/ */
var NotificationCollection = Backbone.Collection.extend({ var NotificationCollection = Backbone.Collection.extend({
model: NotificationModel, model: NotificationModel
/**
* Adds a warning notification to the stack and renders it.
*
* @return {String} message
*/
warn: function(message) {
this.add({ level: "warning", message: message });
},
/**
* Adds a l10n warning notification to the stack and renders it.
*
* @param {String} messageId L10n message id
*/
warnL10n: function(messageId) {
this.warn(l10n.get(messageId));
},
/**
* Adds an error notification to the stack and renders it.
*
* @return {String} message
*/
error: function(message) {
this.add({ level: "error", message: message });
},
/**
* Adds a l10n error notification to the stack and renders it.
*
* @param {String} messageId L10n message id
* @param {Object} [l10nProps] An object with variables to be interpolated
* into the translation. All members' values must be
* strings or numbers.
*/
errorL10n: function(messageId, l10nProps) {
this.error(l10n.get(messageId, l10nProps));
},
/**
* Adds a success notification to the stack and renders it.
*
* @return {String} message
*/
success: function(message) {
this.add({ level: "success", message: message });
},
/**
* Adds a l10n success notification to the stack and renders it.
*
* @param {String} messageId L10n message id
* @param {Object} [l10nProps] An object with variables to be interpolated
* into the translation. All members' values must be
* strings or numbers.
*/
successL10n: function(messageId, l10nProps) {
this.success(l10n.get(messageId, l10nProps));
}
}); });
return { return {
NotificationCollection: NotificationCollection, NotificationCollection: NotificationCollection,
NotificationModel: NotificationModel NotificationModel: NotificationModel
}; };
})(navigator.mozL10n || document.mozL10n); })();

View File

@ -5,6 +5,9 @@
var loop = loop || {}; var loop = loop || {};
loop.store = loop.store || {}; loop.store = loop.store || {};
/**
* Manages the different cursors' events being exchanged between the parts.
*/
loop.store.RemoteCursorStore = (function() { loop.store.RemoteCursorStore = (function() {
"use strict"; "use strict";
@ -15,6 +18,7 @@ loop.store.RemoteCursorStore = (function() {
*/ */
var RemoteCursorStore = loop.store.createStore({ var RemoteCursorStore = loop.store.createStore({
actions: [ actions: [
"sendCursorData",
"receivedCursorData", "receivedCursorData",
"videoDimensionsChanged", "videoDimensionsChanged",
"videoScreenStreamChanged" "videoScreenStreamChanged"
@ -36,7 +40,9 @@ loop.store.RemoteCursorStore = (function() {
} }
this._sdkDriver = options.sdkDriver; this._sdkDriver = options.sdkDriver;
loop.subscribe("CursorPositionChange", this._cursorPositionChangeListener.bind(this));
loop.subscribe("CursorPositionChange",
this._cursorPositionChangeListener.bind(this));
}, },
/** /**
@ -52,13 +58,13 @@ loop.store.RemoteCursorStore = (function() {
/** /**
* Sends cursor position through the sdk. * Sends cursor position through the sdk.
* *
* @param {Object} event An object containing the cursor position and stream dimensions * @param {Object} event An object containing the cursor position and
* It should contains: * stream dimensions. It should contain:
* - ratioX: Left position. Number between 0 and 1. * - ratioX: Left position. Number between 0 and 1.
* - ratioY: Top position. Number between 0 and 1. * - ratioY: Top position. Number between 0 and 1.
*/ */
_cursorPositionChangeListener: function(event) { _cursorPositionChangeListener: function(event) {
this._sdkDriver.sendCursorMessage({ this.sendCursorData({
ratioX: event.ratioX, ratioX: event.ratioX,
ratioY: event.ratioY, ratioY: event.ratioY,
type: CURSOR_MESSAGE_TYPES.POSITION type: CURSOR_MESSAGE_TYPES.POSITION
@ -66,14 +72,32 @@ loop.store.RemoteCursorStore = (function() {
}, },
/** /**
* Receives cursor data. * Sends the cursor data to the SDK for broadcasting.
*
* @param {sharedActions.SendCursorData}
* actionData Contains the updated information for the cursor's position
* {
* ratioX {[0-1]} Cursor's position on the X axis
* ratioY {[0-1]} Cursor's position on the Y axis
* type {String} Type of the data being sent
* }
*/
sendCursorData: function(actionData) {
switch (actionData.type) {
case CURSOR_MESSAGE_TYPES.POSITION:
this._sdkDriver.sendCursorMessage(actionData);
break;
}
},
/**
* Receives cursor data and updates the store.
* *
* @param {sharedActions.receivedCursorData} actionData * @param {sharedActions.receivedCursorData} actionData
*/ */
receivedCursorData: function(actionData) { receivedCursorData: function(actionData) {
switch (actionData.type) { switch (actionData.type) {
case CURSOR_MESSAGE_TYPES.POSITION: case CURSOR_MESSAGE_TYPES.POSITION:
// TODO: handle cursor position if it's desktop instead of standalone
this.setStoreState({ this.setStoreState({
remoteCursorPosition: { remoteCursorPosition: {
ratioX: actionData.ratioX, ratioX: actionData.ratioX,
@ -103,8 +127,10 @@ loop.store.RemoteCursorStore = (function() {
}, },
/** /**
* Listen to screen stream changes. * Listen to screen stream changes. Because the cursor's position is likely
* * to be different respect to the new screen size, it's better to delete the
* previous position and keep waiting for the next one.
* @param {sharedActions.VideoScreenStreamChanged} actionData * @param {sharedActions.VideoScreenStreamChanged} actionData
*/ */
videoScreenStreamChanged: function(actionData) { videoScreenStreamChanged: function(actionData) {

View File

@ -6,7 +6,19 @@
var loop = loop || {}; var loop = loop || {};
loop.shared = loop.shared || {}; loop.shared = loop.shared || {};
var inChrome = typeof Components != "undefined" && "utils" in Components; var inChrome = typeof Components != "undefined" &&
"utils" in Components;
// The slideshow is special, and currently loads with chrome privs, but
// needs to use this module like the rest of the content does. Once we make
// it load remotely, this can go away.
if (inChrome) {
if (typeof window != "undefined" &&
window.location.href === "chrome://loop/content/panels/slideshow.html") {
inChrome = false;
}
}
(function() { (function() {
"use strict"; "use strict";
@ -191,6 +203,13 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
return "blackberry"; return "blackberry";
} }
// Checks if the platform is Android. Due to the difficulties of detecting an
// android device, we need to rely on window.navigator.userAgent instead of
// using window.navigator.platform.
if (rootNavigator.userAgent.toLowerCase().indexOf("android") > -1) {
return "android";
}
return null; return null;
} }

View File

@ -47,14 +47,14 @@ loop.shared.views = function (_, mozL10n) {
* - {String} scope Media scope, can be "local" or "remote". * - {String} scope Media scope, can be "local" or "remote".
* - {String} type Media type, can be "audio" or "video". * - {String} type Media type, can be "audio" or "video".
* - {Function} action Function to be executed on click. * - {Function} action Function to be executed on click.
* - {Enabled} enabled Stream activation status (default: true). * - {Bool} muted Stream activation status (default: false).
*/ */
var MediaControlButton = React.createClass({ var MediaControlButton = React.createClass({
displayName: "MediaControlButton", displayName: "MediaControlButton",
propTypes: { propTypes: {
action: React.PropTypes.func.isRequired, action: React.PropTypes.func.isRequired,
enabled: React.PropTypes.bool.isRequired, muted: React.PropTypes.bool.isRequired,
scope: React.PropTypes.string.isRequired, scope: React.PropTypes.string.isRequired,
title: React.PropTypes.string, title: React.PropTypes.string,
type: React.PropTypes.string.isRequired, type: React.PropTypes.string.isRequired,
@ -62,7 +62,7 @@ loop.shared.views = function (_, mozL10n) {
}, },
getDefaultProps: function () { getDefaultProps: function () {
return { enabled: true, visible: true }; return { muted: false, visible: true };
}, },
handleClick: function () { handleClick: function () {
@ -77,7 +77,7 @@ loop.shared.views = function (_, mozL10n) {
"media-control": true, "media-control": true,
"transparent-button": true, "transparent-button": true,
"local-media": this.props.scope === "local", "local-media": this.props.scope === "local",
"muted": !this.props.enabled, "muted": this.props.muted,
"hide": !this.props.visible "hide": !this.props.visible
}; };
classesObj["btn-mute-" + this.props.type] = true; classesObj["btn-mute-" + this.props.type] = true;
@ -89,7 +89,7 @@ loop.shared.views = function (_, mozL10n) {
return this.props.title; return this.props.title;
} }
var prefix = this.props.enabled ? "mute" : "unmute"; var prefix = this.props.muted ? "unmute" : "mute";
var suffix = this.props.type === "video" ? "button_title2" : "button_title"; var suffix = this.props.type === "video" ? "button_title2" : "button_title";
var msgId = [prefix, this.props.scope, this.props.type, suffix].join("_"); var msgId = [prefix, this.props.scope, this.props.type, suffix].join("_");
return mozL10n.get(msgId); return mozL10n.get(msgId);
@ -126,7 +126,6 @@ loop.shared.views = function (_, mozL10n) {
audio: React.PropTypes.object.isRequired, audio: React.PropTypes.object.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
hangup: React.PropTypes.func.isRequired, hangup: React.PropTypes.func.isRequired,
publishStream: React.PropTypes.func.isRequired,
showHangup: React.PropTypes.bool, showHangup: React.PropTypes.bool,
video: React.PropTypes.object.isRequired video: React.PropTypes.object.isRequired
}, },
@ -135,14 +134,6 @@ loop.shared.views = function (_, mozL10n) {
this.props.hangup(); this.props.hangup();
}, },
handleToggleVideo: function () {
this.props.publishStream("video", !this.props.video.enabled);
},
handleToggleAudio: function () {
this.props.publishStream("audio", !this.props.audio.enabled);
},
componentDidMount: function () { componentDidMount: function () {
this.userActivity = false; this.userActivity = false;
this.startIdleCountDown(); this.startIdleCountDown();
@ -230,20 +221,56 @@ loop.shared.views = function (_, mozL10n) {
React.createElement( React.createElement(
"div", "div",
{ className: mediaButtonGroupCssClasses }, { className: mediaButtonGroupCssClasses },
React.createElement(MediaControlButton, { action: this.handleToggleVideo, React.createElement(VideoMuteButton, { dispatcher: this.props.dispatcher,
enabled: this.props.video.enabled, muted: !this.props.video.enabled }),
scope: "local", type: "video", React.createElement(AudioMuteButton, { dispatcher: this.props.dispatcher,
visible: this.props.video.visible }), muted: !this.props.audio.enabled })
React.createElement(MediaControlButton, { action: this.handleToggleAudio,
enabled: this.props.audio.enabled,
scope: "local", type: "audio",
visible: this.props.audio.visible })
) )
) )
); );
} }
}); });
var AudioMuteButton = React.createClass({
displayName: "AudioMuteButton",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
muted: React.PropTypes.bool.isRequired
},
toggleAudio: function () {
this.props.dispatcher.dispatch(new sharedActions.SetMute({ type: "audio", enabled: this.props.muted }));
},
render: function () {
return React.createElement(MediaControlButton, { action: this.toggleAudio,
muted: this.props.muted,
scope: "local",
type: "audio" });
}
});
var VideoMuteButton = React.createClass({
displayName: "VideoMuteButton",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
muted: React.PropTypes.bool.isRequired
},
toggleVideo: function () {
this.props.dispatcher.dispatch(new sharedActions.SetMute({ type: "video", enabled: this.props.muted }));
},
render: function () {
return React.createElement(MediaControlButton, { action: this.toggleVideo,
muted: this.props.muted,
scope: "local",
type: "video" });
}
});
/** /**
* Notification view. * Notification view.
*/ */
@ -633,11 +660,13 @@ loop.shared.views = function (_, mozL10n) {
mixins: [React.addons.PureRenderMixin], mixins: [React.addons.PureRenderMixin],
propTypes: { propTypes: {
cursorStore: React.PropTypes.instanceOf(loop.store.RemoteCursorStore),
dispatcher: React.PropTypes.object,
displayAvatar: React.PropTypes.bool.isRequired, displayAvatar: React.PropTypes.bool.isRequired,
isLoading: React.PropTypes.bool.isRequired, isLoading: React.PropTypes.bool.isRequired,
mediaType: React.PropTypes.string.isRequired, mediaType: React.PropTypes.string.isRequired,
posterUrl: React.PropTypes.string, posterUrl: React.PropTypes.string,
shouldRenderRemoteCursor: React.PropTypes.bool, shareCursor: React.PropTypes.bool,
// Expecting "local" or "remote". // Expecting "local" or "remote".
srcMediaElement: React.PropTypes.object srcMediaElement: React.PropTypes.object
}, },
@ -653,7 +682,7 @@ loop.shared.views = function (_, mozL10n) {
this.attachVideo(this.props.srcMediaElement); this.attachVideo(this.props.srcMediaElement);
} }
if (this.props.shouldRenderRemoteCursor) { if (this.props.shareCursor) {
this.handleVideoDimensions(); this.handleVideoDimensions();
window.addEventListener("resize", this.handleVideoDimensions); window.addEventListener("resize", this.handleVideoDimensions);
} }
@ -661,12 +690,13 @@ loop.shared.views = function (_, mozL10n) {
componentWillUnmount: function () { componentWillUnmount: function () {
var videoElement = this.getDOMNode().querySelector("video"); var videoElement = this.getDOMNode().querySelector("video");
if (!this.props.shouldRenderRemoteCursor || !videoElement) { if (!this.props.shareCursor || !videoElement) {
return; return;
} }
window.removeEventListener("resize", this.handleVideoDimensions); window.removeEventListener("resize", this.handleVideoDimensions);
videoElement.removeEventListener("loadeddata", this.handleVideoDimensions); videoElement.removeEventListener("loadeddata", this.handleVideoDimensions);
videoElement.removeEventListener("mousemove", this.handleMousemove);
}, },
componentDidUpdate: function () { componentDidUpdate: function () {
@ -689,6 +719,42 @@ loop.shared.views = function (_, mozL10n) {
}); });
}, },
MIN_CURSOR_DELTA: 3,
MIN_CURSOR_INTERVAL: 100,
lastCursorTime: 0,
lastCursorX: -1,
lastCursorY: -1,
handleMouseMove: function (event) {
// Only update every so often.
var now = Date.now();
if (now - this.lastCursorTime < this.MIN_CURSOR_INTERVAL) {
return;
}
this.lastCursorTime = now;
var storeState = this.props.cursorStore.getStoreState();
var deltaX = event.clientX - storeState.videoLetterboxing.left;
var deltaY = event.clientY - storeState.videoLetterboxing.top;
// Skip the update if cursor is out of bounds
if (deltaX < 0 || deltaX > storeState.streamVideoWidth || deltaY < 0 || deltaY > storeState.streamVideoHeight ||
// or the cursor didn't move the minimum.
Math.abs(deltaX - this.lastCursorX) < this.MIN_CURSOR_DELTA && Math.abs(deltaY - this.lastCursorY) < this.MIN_CURSOR_DELTA) {
return;
}
this.lastCursorX = deltaX;
this.lastCursorY = deltaY;
this.props.dispatcher.dispatch(new sharedActions.SendCursorData({
ratioX: deltaX / storeState.streamVideoWidth,
ratioY: deltaY / storeState.streamVideoHeight,
type: loop.shared.utils.CURSOR_MESSAGE_TYPES.POSITION
}));
},
/** /**
* Attaches a video stream from a donor video element to this component's * Attaches a video stream from a donor video element to this component's
* video element if the component is displaying one. * video element if the component is displaying one.
@ -706,16 +772,16 @@ loop.shared.views = function (_, mozL10n) {
} }
var videoElement = this.getDOMNode().querySelector("video"); var videoElement = this.getDOMNode().querySelector("video");
if (this.props.shouldRenderRemoteCursor) {
videoElement.addEventListener("loadeddata", this.handleVideoDimensions);
}
if (!videoElement || videoElement.tagName.toLowerCase() !== "video") { if (!videoElement || videoElement.tagName.toLowerCase() !== "video") {
// Must be displaying the avatar view, so don't try and attach video. // Must be displaying the avatar view, so don't try and attach video.
return; return;
} }
if (this.props.shareCursor) {
videoElement.addEventListener("loadeddata", this.handleVideoDimensions);
videoElement.addEventListener("mousemove", this.handleMouseMove);
}
// Set the src of our video element // Set the src of our video element
var attrName = ""; var attrName = "";
if ("srcObject" in videoElement) { if ("srcObject" in videoElement) {
@ -736,6 +802,7 @@ loop.shared.views = function (_, mozL10n) {
if (videoElement[attrName] !== srcMediaElement[attrName]) { if (videoElement[attrName] !== srcMediaElement[attrName]) {
videoElement[attrName] = srcMediaElement[attrName]; videoElement[attrName] = srcMediaElement[attrName];
} }
videoElement.play(); videoElement.play();
}, },
@ -769,7 +836,7 @@ loop.shared.views = function (_, mozL10n) {
return React.createElement( return React.createElement(
"div", "div",
{ className: "remote-video-box" }, { className: "remote-video-box" },
this.state.videoElementSize && this.props.shouldRenderRemoteCursor ? React.createElement(RemoteCursorView, { this.state.videoElementSize && this.props.shareCursor ? React.createElement(RemoteCursorView, {
videoElementSize: this.state.videoElementSize }) : null, videoElementSize: this.state.videoElementSize }) : null,
React.createElement("video", _extends({}, optionalProps, { React.createElement("video", _extends({}, optionalProps, {
className: this.props.mediaType + "-video", className: this.props.mediaType + "-video",
@ -783,6 +850,7 @@ loop.shared.views = function (_, mozL10n) {
propTypes: { propTypes: {
children: React.PropTypes.node, children: React.PropTypes.node,
cursorStore: React.PropTypes.instanceOf(loop.store.RemoteCursorStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
displayScreenShare: React.PropTypes.bool.isRequired, displayScreenShare: React.PropTypes.bool.isRequired,
isLocalLoading: React.PropTypes.bool.isRequired, isLocalLoading: React.PropTypes.bool.isRequired,
@ -903,11 +971,13 @@ loop.shared.views = function (_, mozL10n) {
"div", "div",
{ className: screenShareStreamClasses }, { className: screenShareStreamClasses },
React.createElement(MediaView, { React.createElement(MediaView, {
cursorStore: this.props.cursorStore,
dispatcher: this.props.dispatcher,
displayAvatar: false, displayAvatar: false,
isLoading: this.props.isScreenShareLoading, isLoading: this.props.isScreenShareLoading,
mediaType: "screen-share", mediaType: "screen-share",
posterUrl: this.props.screenSharePosterUrl, posterUrl: this.props.screenSharePosterUrl,
shouldRenderRemoteCursor: true, shareCursor: true,
srcMediaElement: this.props.screenShareMediaElement }), srcMediaElement: this.props.screenShareMediaElement }),
this.props.displayScreenShare ? this.props.children : null this.props.displayScreenShare ? this.props.children : null
), ),
@ -931,10 +1001,7 @@ loop.shared.views = function (_, mozL10n) {
}, },
getInitialState: function () { getInitialState: function () {
return { return this.getStoreState();
realVideoSize: null,
videoLetterboxing: null
};
}, },
componentWillMount: function () { componentWillMount: function () {
@ -963,7 +1030,7 @@ loop.shared.views = function (_, mozL10n) {
if (!this.state.videoLetterboxing) { if (!this.state.videoLetterboxing) {
// If this is the first time we receive the event, we must calculate the // If this is the first time we receive the event, we must calculate the
// video letterboxing. // video letterboxing.
this._calculateVideoLetterboxing(); this._calculateVideoLetterboxing(nextState.realVideoSize);
return; return;
} }
@ -992,7 +1059,7 @@ loop.shared.views = function (_, mozL10n) {
var streamVideoHeight = isWider ? clientHeight : clientWidth / realVideoRatio; var streamVideoHeight = isWider ? clientHeight : clientWidth / realVideoRatio;
var streamVideoWidth = isWider ? clientHeight * realVideoRatio : clientWidth; var streamVideoWidth = isWider ? clientHeight * realVideoRatio : clientWidth;
this.setState({ this.getStore().setStoreState({
videoLetterboxing: { videoLetterboxing: {
left: (clientWidth - streamVideoWidth) / 2, left: (clientWidth - streamVideoWidth) / 2,
top: (clientHeight - streamVideoHeight) / 2 top: (clientHeight - streamVideoHeight) / 2
@ -1028,17 +1095,20 @@ loop.shared.views = function (_, mozL10n) {
}); });
return { return {
AudioMuteButton: AudioMuteButton,
AvatarView: AvatarView, AvatarView: AvatarView,
Button: Button, Button: Button,
ButtonGroup: ButtonGroup, ButtonGroup: ButtonGroup,
Checkbox: Checkbox, Checkbox: Checkbox,
ContextUrlView: ContextUrlView, ContextUrlView: ContextUrlView,
ConversationToolbar: ConversationToolbar, ConversationToolbar: ConversationToolbar,
HangUpControlButton: HangUpControlButton,
MediaControlButton: MediaControlButton, MediaControlButton: MediaControlButton,
MediaLayoutView: MediaLayoutView, MediaLayoutView: MediaLayoutView,
MediaView: MediaView, MediaView: MediaView,
LoadingView: LoadingView, LoadingView: LoadingView,
NotificationListView: NotificationListView, NotificationListView: NotificationListView,
RemoteCursorView: RemoteCursorView RemoteCursorView: RemoteCursorView,
VideoMuteButton: VideoMuteButton
}; };
}(_, navigator.mozL10n || document.mozL10n); }(_, navigator.mozL10n || document.mozL10n);

View File

@ -68,6 +68,10 @@ describe("loop.store.ActiveRoomStore", function() {
store = new loop.store.ActiveRoomStore(dispatcher, { store = new loop.store.ActiveRoomStore(dispatcher, {
sdkDriver: fakeSdkDriver sdkDriver: fakeSdkDriver
}); });
sandbox.stub(document.mozL10n ? document.mozL10n : navigator.mozL10n, "get", function(x) {
return x;
});
}); });
afterEach(function() { afterEach(function() {
@ -1003,7 +1007,7 @@ describe("loop.store.ActiveRoomStore", function() {
store.gotMediaPermission(); store.gotMediaPermission();
sinon.assert.calledOnce(requestStubs["Rooms:Join"]); sinon.assert.calledOnce(requestStubs["Rooms:Join"]);
sinon.assert.calledWith(requestStubs["Rooms:Join"], "tokenFake"); sinon.assert.calledWith(requestStubs["Rooms:Join"], "tokenFake", "display_name_guest");
}); });
it("should dispatch `JoinedRoom` on success", function() { it("should dispatch `JoinedRoom` on success", function() {

View File

@ -43,6 +43,7 @@
<!-- App scripts --> <!-- App scripts -->
<script src="/shared/js/loopapi-client.js"></script> <script src="/shared/js/loopapi-client.js"></script>
<script src="/shared/js/utils.js"></script> <script src="/shared/js/utils.js"></script>
<script src="/shared/js/urlRegExps.js"></script>
<script src="/shared/js/models.js"></script> <script src="/shared/js/models.js"></script>
<script src="/shared/js/mixins.js"></script> <script src="/shared/js/mixins.js"></script>
<script src="/shared/js/crypto.js"></script> <script src="/shared/js/crypto.js"></script>
@ -50,31 +51,33 @@
<script src="/shared/js/actions.js"></script> <script src="/shared/js/actions.js"></script>
<script src="/shared/js/dispatcher.js"></script> <script src="/shared/js/dispatcher.js"></script>
<script src="/shared/js/otSdkDriver.js"></script> <script src="/shared/js/otSdkDriver.js"></script>
<!-- Stores need to be loaded before the views that uses them -->
<script src="/shared/js/store.js"></script> <script src="/shared/js/store.js"></script>
<script src="/shared/js/activeRoomStore.js"></script> <script src="/shared/js/activeRoomStore.js"></script>
<script src="/shared/js/views.js"></script>
<script src="/shared/js/textChatStore.js"></script> <script src="/shared/js/textChatStore.js"></script>
<script src="/shared/js/textChatView.js"></script>
<script src="/shared/js/urlRegExps.js"></script>
<script src="/shared/js/linkifiedTextView.js"></script>
<script src="/shared/js/remoteCursorStore.js"></script> <script src="/shared/js/remoteCursorStore.js"></script>
<!-- Views -->
<script src="/shared/js/views.js"></script>
<script src="/shared/js/textChatView.js"></script>
<script src="/shared/js/linkifiedTextView.js"></script>
<!-- Test scripts --> <!-- Test scripts -->
<script src="models_test.js"></script>
<script src="mixins_test.js"></script> <script src="mixins_test.js"></script>
<script src="utils_test.js"></script> <script src="utils_test.js"></script>
<script src="crypto_test.js"></script> <script src="crypto_test.js"></script>
<script src="views_test.js"></script>
<script src="validate_test.js"></script> <script src="validate_test.js"></script>
<script src="dispatcher_test.js"></script> <script src="dispatcher_test.js"></script>
<script src="activeRoomStore_test.js"></script>
<script src="otSdkDriver_test.js"></script> <script src="otSdkDriver_test.js"></script>
<script src="store_test.js"></script> <script src="store_test.js"></script>
<script src="activeRoomStore_test.js"></script>
<script src="textChatStore_test.js"></script> <script src="textChatStore_test.js"></script>
<script src="remoteCursorStore_test.js"></script>
<script src="views_test.js"></script>
<script src="textChatView_test.js"></script> <script src="textChatView_test.js"></script>
<script src="linkifiedTextView_test.js"></script> <script src="linkifiedTextView_test.js"></script>
<script src="loopapi-client_test.js"></script> <script src="loopapi-client_test.js"></script>
<script src="remoteCursorStore_test.js"></script>
<script> <script>
LoopMochaUtils.addErrorCheckingTests(); LoopMochaUtils.addErrorCheckingTests();

View File

@ -1,80 +0,0 @@
/* 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/. */
describe("loop.shared.models", function() {
"use strict";
var expect = chai.expect;
var l10n = navigator.mozL10n || document.mozL10n;
var sharedModels = loop.shared.models;
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
afterEach(function() {
sandbox.restore();
});
describe("NotificationCollection", function() {
var collection;
beforeEach(function() {
collection = new sharedModels.NotificationCollection();
sandbox.stub(l10n, "get", function(x, y) {
return "translated:" + x + (y ? ":" + y : "");
});
});
describe("#warn", function() {
it("should add a warning notification to the stack", function() {
collection.warn("watch out");
expect(collection).to.have.length.of(1);
expect(collection.at(0).get("level")).eql("warning");
expect(collection.at(0).get("message")).eql("watch out");
});
});
describe("#warnL10n", function() {
it("should warn using a l10n string id", function() {
collection.warnL10n("fakeId");
expect(collection).to.have.length.of(1);
expect(collection.at(0).get("level")).eql("warning");
expect(collection.at(0).get("message")).eql("translated:fakeId");
});
});
describe("#error", function() {
it("should add an error notification to the stack", function() {
collection.error("wrong");
expect(collection).to.have.length.of(1);
expect(collection.at(0).get("level")).eql("error");
expect(collection.at(0).get("message")).eql("wrong");
});
});
describe("#errorL10n", function() {
it("should notify an error using a l10n string id", function() {
collection.errorL10n("fakeId");
expect(collection).to.have.length.of(1);
expect(collection.at(0).get("level")).eql("error");
expect(collection.at(0).get("message")).eql("translated:fakeId");
});
it("should notify an error using a l10n string id + l10n properties",
function() {
collection.errorL10n("fakeId", "fakeProp");
expect(collection).to.have.length.of(1);
expect(collection.at(0).get("level")).eql("error");
expect(collection.at(0).get("message")).eql("translated:fakeId:fakeProp");
});
});
});
});

View File

@ -66,7 +66,52 @@ describe("loop.store.RemoteCursorStore", function() {
}); });
}); });
describe("#sendCursorData", function() {
it("should do nothing if not a proper event", function() {
var fakeData = {
ratioX: 10,
ratioY: 10,
type: "not-a-position-event"
};
store.sendCursorData(new sharedActions.SendCursorData(fakeData));
sinon.assert.notCalled(fakeSdkDriver.sendCursorMessage);
});
it("should send cursor data through the sdk", function() {
var fakeData = {
ratioX: 10,
ratioY: 10,
type: CURSOR_MESSAGE_TYPES.POSITION
};
store.sendCursorData(new sharedActions.SendCursorData(fakeData));
sinon.assert.calledOnce(fakeSdkDriver.sendCursorMessage);
sinon.assert.calledWith(fakeSdkDriver.sendCursorMessage, {
name: "sendCursorData",
type: fakeData.type,
ratioX: fakeData.ratioX,
ratioY: fakeData.ratioY
});
});
});
describe("#receivedCursorData", function() { describe("#receivedCursorData", function() {
it("should do nothing if not a proper event", function() {
sandbox.stub(store, "setStoreState");
store.receivedCursorData(new sharedActions.ReceivedCursorData({
ratioX: 10,
ratioY: 10,
type: "not-a-position-event"
}));
sinon.assert.notCalled(store.setStoreState);
});
it("should save the state", function() { it("should save the state", function() {
store.receivedCursorData(new sharedActions.ReceivedCursorData({ store.receivedCursorData(new sharedActions.ReceivedCursorData({
type: CURSOR_MESSAGE_TYPES.POSITION, type: CURSOR_MESSAGE_TYPES.POSITION,

View File

@ -50,7 +50,7 @@ describe("loop.shared.views", function() {
scope: "local", scope: "local",
type: "audio", type: "audio",
action: function() {}, action: function() {},
enabled: true muted: false
})); }));
expect(comp.getDOMNode().classList.contains("muted")).eql(false); expect(comp.getDOMNode().classList.contains("muted")).eql(false);
@ -62,7 +62,7 @@ describe("loop.shared.views", function() {
scope: "local", scope: "local",
type: "audio", type: "audio",
action: function() {}, action: function() {},
enabled: false muted: true
})); }));
expect(comp.getDOMNode().classList.contains("muted")).eql(true); expect(comp.getDOMNode().classList.contains("muted")).eql(true);
@ -74,7 +74,7 @@ describe("loop.shared.views", function() {
scope: "local", scope: "local",
type: "video", type: "video",
action: function() {}, action: function() {},
enabled: true muted: false
})); }));
expect(comp.getDOMNode().classList.contains("muted")).eql(false); expect(comp.getDOMNode().classList.contains("muted")).eql(false);
@ -86,15 +86,123 @@ describe("loop.shared.views", function() {
scope: "local", scope: "local",
type: "video", type: "video",
action: function() {}, action: function() {},
enabled: false muted: true
})); }));
expect(comp.getDOMNode().classList.contains("muted")).eql(true); expect(comp.getDOMNode().classList.contains("muted")).eql(true);
}); });
}); });
describe("AudioMuteButton", function() {
it("should set the muted class when not enabled", function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.AudioMuteButton, {
muted: true
}));
var node = comp.getDOMNode();
expect(node.classList.contains("muted")).eql(true);
});
it("should not set the muted class when enabled", function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.AudioMuteButton, {
muted: false
}));
var node = comp.getDOMNode();
expect(node.classList.contains("muted")).eql(false);
});
it("should dispatch SetMute('audio', false) if clicked when audio is disabled",
function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.AudioMuteButton, {
dispatcher: dispatcher,
muted: false
}));
TestUtils.Simulate.click(comp.getDOMNode());
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetMute({ type: "audio", enabled: false })
);
});
it("should dispatch SetMute('audio', true) if clicked when audio is enabled",
function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.AudioMuteButton, {
dispatcher: dispatcher,
muted: true
}));
TestUtils.Simulate.click(comp.getDOMNode());
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetMute({ type: "audio", enabled: true })
);
});
});
describe("VideoMuteButton", function() {
it("should set the muted class when not enabled", function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.VideoMuteButton, {
muted: true
}));
var node = comp.getDOMNode();
expect(node.classList.contains("muted")).eql(true);
});
it("should not set the muted class when enabled", function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.VideoMuteButton, {
muted: false
}));
var node = comp.getDOMNode();
expect(node.classList.contains("muted")).eql(false);
});
it("should dispatch SetMute('audio', false) if clicked when audio is disabled",
function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.VideoMuteButton, {
dispatcher: dispatcher,
muted: false
}));
TestUtils.Simulate.click(comp.getDOMNode());
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetMute({ type: "video", enabled: false })
);
});
it("should dispatch SetMute('audio', true) if clicked when audio is enabled",
function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.VideoMuteButton, {
dispatcher: dispatcher,
muted: true
}));
TestUtils.Simulate.click(comp.getDOMNode());
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetMute({ type: "video", enabled: true })
);
});
});
describe("ConversationToolbar", function() { describe("ConversationToolbar", function() {
var hangup, publishStream; var hangup;
function mountTestComponent(props) { function mountTestComponent(props) {
props = _.extend({ props = _.extend({
@ -106,14 +214,12 @@ describe("loop.shared.views", function() {
beforeEach(function() { beforeEach(function() {
hangup = sandbox.stub(); hangup = sandbox.stub();
publishStream = sandbox.stub();
}); });
it("should start no idle", function() { it("should start no idle", function() {
var comp = mountTestComponent({ var comp = mountTestComponent({
hangupButtonLabel: "foo", hangupButtonLabel: "foo",
hangup: hangup, hangup: hangup
publishStream: publishStream
}); });
expect(comp.getDOMNode().classList.contains("idle")).eql(false); expect(comp.getDOMNode().classList.contains("idle")).eql(false);
}); });
@ -121,8 +227,7 @@ describe("loop.shared.views", function() {
it("should be on idle state after 6 seconds", function() { it("should be on idle state after 6 seconds", function() {
var comp = mountTestComponent({ var comp = mountTestComponent({
hangupButtonLabel: "foo", hangupButtonLabel: "foo",
hangup: hangup, hangup: hangup
publishStream: publishStream
}); });
expect(comp.getDOMNode().classList.contains("idle")).eql(false); expect(comp.getDOMNode().classList.contains("idle")).eql(false);
@ -133,8 +238,7 @@ describe("loop.shared.views", function() {
it("should remove idle state when the user moves the mouse", function() { it("should remove idle state when the user moves the mouse", function() {
var comp = mountTestComponent({ var comp = mountTestComponent({
hangupButtonLabel: "foo", hangupButtonLabel: "foo",
hangup: hangup, hangup: hangup
publishStream: publishStream
}); });
clock.tick(6001); clock.tick(6001);
@ -148,8 +252,7 @@ describe("loop.shared.views", function() {
it("should accept a showHangup optional prop", function() { it("should accept a showHangup optional prop", function() {
var comp = mountTestComponent({ var comp = mountTestComponent({
showHangup: false, showHangup: false,
hangup: hangup, hangup: hangup
publishStream: publishStream
}); });
expect(comp.getDOMNode().querySelector(".btn-hangup-entry")).to.eql(null); expect(comp.getDOMNode().querySelector(".btn-hangup-entry")).to.eql(null);
@ -158,7 +261,6 @@ describe("loop.shared.views", function() {
it("should hangup when hangup button is clicked", function() { it("should hangup when hangup button is clicked", function() {
var comp = mountTestComponent({ var comp = mountTestComponent({
hangup: hangup, hangup: hangup,
publishStream: publishStream,
audio: { enabled: true } audio: { enabled: true }
}); });
@ -168,62 +270,6 @@ describe("loop.shared.views", function() {
sinon.assert.calledOnce(hangup); sinon.assert.calledOnce(hangup);
sinon.assert.calledWithExactly(hangup); sinon.assert.calledWithExactly(hangup);
}); });
it("should unpublish audio when audio mute btn is clicked", function() {
var comp = mountTestComponent({
hangup: hangup,
publishStream: publishStream,
audio: { enabled: true }
});
TestUtils.Simulate.click(
comp.getDOMNode().querySelector(".btn-mute-audio"));
sinon.assert.calledOnce(publishStream);
sinon.assert.calledWithExactly(publishStream, "audio", false);
});
it("should publish audio when audio mute btn is clicked", function() {
var comp = mountTestComponent({
hangup: hangup,
publishStream: publishStream,
audio: { enabled: false }
});
TestUtils.Simulate.click(
comp.getDOMNode().querySelector(".btn-mute-audio"));
sinon.assert.calledOnce(publishStream);
sinon.assert.calledWithExactly(publishStream, "audio", true);
});
it("should unpublish video when video mute btn is clicked", function() {
var comp = mountTestComponent({
hangup: hangup,
publishStream: publishStream,
video: { enabled: true }
});
TestUtils.Simulate.click(
comp.getDOMNode().querySelector(".btn-mute-video"));
sinon.assert.calledOnce(publishStream);
sinon.assert.calledWithExactly(publishStream, "video", false);
});
it("should publish video when video mute btn is clicked", function() {
var comp = mountTestComponent({
hangup: hangup,
publishStream: publishStream,
video: { enabled: false }
});
TestUtils.Simulate.click(
comp.getDOMNode().querySelector(".btn-mute-video"));
sinon.assert.calledOnce(publishStream);
sinon.assert.calledWithExactly(publishStream, "video", true);
});
}); });
describe("NotificationListView", function() { describe("NotificationListView", function() {
@ -518,6 +564,7 @@ describe("loop.shared.views", function() {
describe("MediaView", function() { describe("MediaView", function() {
var view; var view;
var remoteCursorStore;
function mountTestComponent(props) { function mountTestComponent(props) {
props = _.extend({ props = _.extend({
@ -527,6 +574,13 @@ describe("loop.shared.views", function() {
React.createElement(sharedViews.MediaView, props)); React.createElement(sharedViews.MediaView, props));
} }
beforeEach(function() {
remoteCursorStore = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: {}
});
loop.store.StoreMixin.register({ remoteCursorStore: remoteCursorStore });
});
it("should display an avatar view", function() { it("should display an avatar view", function() {
view = mountTestComponent({ view = mountTestComponent({
displayAvatar: true, displayAvatar: true,
@ -580,13 +634,14 @@ describe("loop.shared.views", function() {
// We test this function by itself, as otherwise we'd be into creating fake // We test this function by itself, as otherwise we'd be into creating fake
// streams etc. // streams etc.
describe("#attachVideo", function() { describe("#attachVideo", function() {
var fakeViewElement, fakeVideoElement; var fakeViewElement,
fakeVideoElement;
beforeEach(function() { beforeEach(function() {
fakeVideoElement = { fakeVideoElement = {
play: sinon.stub(), play: sinon.stub(),
tagName: "VIDEO", tagName: "VIDEO",
addEventListener: function() {} addEventListener: sinon.stub()
}; };
fakeViewElement = { fakeViewElement = {
@ -597,8 +652,10 @@ describe("loop.shared.views", function() {
}; };
view = mountTestComponent({ view = mountTestComponent({
cursorStore: remoteCursorStore,
displayAvatar: false, displayAvatar: false,
mediaType: "local", mediaType: "local",
shareCursor: true,
srcMediaElement: { srcMediaElement: {
fake: 1 fake: 1
} }
@ -616,7 +673,8 @@ describe("loop.shared.views", function() {
tagName: "DIV", tagName: "DIV",
querySelector: function() { querySelector: function() {
return { return {
tagName: "DIV" tagName: "DIV",
addEventListener: sinon.stub()
}; };
} }
}); });
@ -638,6 +696,18 @@ describe("loop.shared.views", function() {
expect(fakeVideoElement.srcObject).eql({ fake: 1 }); expect(fakeVideoElement.srcObject).eql({ fake: 1 });
}); });
it("should attach events to the video", function() {
fakeVideoElement.srcObject = null;
sinon.stub(view, "getDOMNode").returns(fakeViewElement);
view.attachVideo({
src: { fake: 1 }
});
sinon.assert.calledTwice(fakeVideoElement.addEventListener);
sinon.assert.calledWith(fakeVideoElement.addEventListener, "loadeddata");
sinon.assert.calledWith(fakeVideoElement.addEventListener, "mousemove");
});
it("should attach a video object for Firefox", function() { it("should attach a video object for Firefox", function() {
fakeVideoElement.mozSrcObject = null; fakeVideoElement.mozSrcObject = null;
@ -705,10 +775,13 @@ describe("loop.shared.views", function() {
}); });
describe("MediaLayoutView", function() { describe("MediaLayoutView", function() {
var textChatStore, view; var textChatStore,
remoteCursorStore,
view;
function mountTestComponent(extraProps) { function mountTestComponent(extraProps) {
var defaultProps = { var defaultProps = {
cursorStore: remoteCursorStore,
dispatcher: dispatcher, dispatcher: dispatcher,
displayScreenShare: false, displayScreenShare: false,
isLocalLoading: false, isLocalLoading: false,
@ -730,6 +803,9 @@ describe("loop.shared.views", function() {
textChatStore = new loop.store.TextChatStore(dispatcher, { textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: {} sdkDriver: {}
}); });
remoteCursorStore = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: {}
});
loop.store.StoreMixin.register({ textChatStore: textChatStore }); loop.store.StoreMixin.register({ textChatStore: textChatStore });
}); });

View File

@ -50,6 +50,15 @@
-moz-image-region: rect(1px, 125px, 17px, 109px); -moz-image-region: rect(1px, 125px, 17px, 109px);
} }
/* The slideshow state disables the button for that window and makes it look
the same as the hover state to visually indicate that it's in-use.
*/
#loop-button[state="slideshow"] {
background: var(--toolbarbutton-hover-background);
border-color: var(--toolbarbutton-hover-bordercolor);
box-shadow: var(--toolbarbutton-hover-boxshadow);
}
@media (min-resolution: 1.1dppx) { @media (min-resolution: 1.1dppx) {
#loop-button { #loop-button {
list-style-image: url("chrome://loop/skin/toolbar@2x.png"); list-style-image: url("chrome://loop/skin/toolbar@2x.png");
@ -276,4 +285,38 @@
background-color: #ef6745; background-color: #ef6745;
border-color: #ef6745; border-color: #ef6745;
} }
#loop-remote-cursor {
background: url("chrome://loop/content/shared/img/cursor.svg#blue") no-repeat;
height: 20px;
width: 15px;
/*
* Svg cursor has a white outline so we need to get rid off it to ensure
* that the cursor points at a more precise position
*/
margin: -2px;
pointer-events: none;
position: absolute;
}
#loop-slideshow-container {
/* cover the entire viewport, mouse interaction with non-slideshow
content is now impossible. */
position: fixed;
z-index: 1;
width: 100%;
height: 100%;
/* darken the background content */
background: rgba(0, 0, 0, .8);
}
#loop-slideshow-browser {
width: 620px;
height:450px;
margin-top: 10%;
/* XXX derived from width, so should be 50% - (620px / 2)? */
-moz-margin-start: calc(50% - 310px);
}
} }

View File

@ -41,7 +41,7 @@ function* checkFxA401() {
} }
add_task(function* setup() { add_task(function* setup() {
Services.prefs.setIntPref("loop.gettingStarted.latestFTUVersion", 1); Services.prefs.setIntPref("loop.gettingStarted.latestFTUVersion", 2);
MozLoopServiceInternal.mocks.pushHandler = mockPushHandler; MozLoopServiceInternal.mocks.pushHandler = mockPushHandler;
// Normally the same pushUrl would be registered but we change it in the test // Normally the same pushUrl would be registered but we change it in the test
// to be able to check for success on the second registration. // to be able to check for success on the second registration.

View File

@ -18,7 +18,7 @@ add_task(function* test_mozLoop_nochat() {
}); });
add_task(function* test_mozLoop_openchat() { add_task(function* test_mozLoop_openchat() {
let windowId = LoopRooms.open("fake1234"); let windowId = yield LoopRooms.open("fake1234");
Assert.ok(isAnyLoopChatOpen(), "chat window should have been opened"); Assert.ok(isAnyLoopChatOpen(), "chat window should have been opened");
let chatboxesForRoom = [...Chat.chatboxes].filter(chatbox => { let chatboxesForRoom = [...Chat.chatboxes].filter(chatbox => {
@ -28,7 +28,7 @@ add_task(function* test_mozLoop_openchat() {
}); });
add_task(function* test_mozLoop_hangupAllChatWindows() { add_task(function* test_mozLoop_hangupAllChatWindows() {
LoopRooms.open("fake2345"); yield LoopRooms.open("fake2345");
yield promiseWaitForCondition(() => { yield promiseWaitForCondition(() => {
MozLoopService.hangupAllChatWindows(); MozLoopService.hangupAllChatWindows();
@ -49,7 +49,7 @@ add_task(function* test_mozLoop_hangupOnClose() {
} }
}); });
let windowId = LoopRooms.open(roomToken); let windowId = yield LoopRooms.open(roomToken);
yield promiseWaitForCondition(() => { yield promiseWaitForCondition(() => {
MozLoopService.hangupAllChatWindows(); MozLoopService.hangupAllChatWindows();

View File

@ -132,3 +132,106 @@ add_task(function* test_mozLoop_telemetryAdd_roomSessionWithChat() {
Assert.strictEqual(snapshot.counts[0], i); Assert.strictEqual(snapshot.counts[0], i);
} }
}); });
// Skip until bug 1208416 has landed.
/*
add_task(function* test_mozLoop_telemetryAdd_infobarActionButtons() {
let histogramId = "LOOP_INFOBAR_ACTION_BUTTONS";
let histogram = Services.telemetry.getHistogramById(histogramId);
const ACTION_TYPES = gConstants.SHARING_SCREEN;
histogram.clear();
for (let value of [ACTION_TYPES.PAUSED,
ACTION_TYPES.PAUSED,
ACTION_TYPES.RESUMED]) {
gHandlers.TelemetryAddValue({ data: [histogramId, value] }, () => {});
}
let snapshot = histogram.snapshot();
Assert.strictEqual(snapshot.counts[ACTION_TYPES.RESUMED], 1,
"SHARING_SCREEN.RESUMED");
Assert.strictEqual(snapshot.counts[ACTION_TYPES.PAUSED], 2,
"SHARING_SCREEN.PAUSED");
});
add_task(function* test_mozLoop_telemetryAdd_loopMauType_buckets() {
let histogramId = "LOOP_MAU";
let histogram = Services.telemetry.getHistogramById(histogramId);
const ACTION_TYPES = gConstants.LOOP_MAU_TYPE;
histogram.clear();
for (let value of [ACTION_TYPES.OPEN_PANEL,
ACTION_TYPES.OPEN_CONVERSATION,
ACTION_TYPES.ROOM_OPEN,
ACTION_TYPES.ROOM_SHARE,
ACTION_TYPES.ROOM_DELETE]) {
gHandlers.TelemetryAddValue({ data: [histogramId, value] }, () => {});
}
let snapshot = histogram.snapshot();
Assert.strictEqual(snapshot.counts[ACTION_TYPES.OPEN_PANEL], 1,
"LOOP_MAU_TYPE.OPEN_PANEL");
Assert.strictEqual(snapshot.counts[ACTION_TYPES.OPEN_CONVERSATION], 1,
"LOOP_MAU_TYPE.OPEN_CONVERSATION");
Assert.strictEqual(snapshot.counts[ACTION_TYPES.ROOM_OPEN], 1,
"LOOP_MAU_TYPE.ROOM_OPEN");
Assert.strictEqual(snapshot.counts[ACTION_TYPES.ROOM_SHARE], 1,
"LOOP_MAU_TYPE.ROOM_SHARE");
Assert.strictEqual(snapshot.counts[ACTION_TYPES.ROOM_DELETE], 1,
"LOOP_MAU_TYPE.ROOM_DELETE");
});
*/
/**
* Tests that only one event is sent every 30 days
*/
// Skip until bug 1208416 has landed.
/*
add_task(function* test_mozLoop_telemetryAdd_loopMau_more_than_30_days() {
let histogramId = "LOOP_MAU";
let histogram = Services.telemetry.getHistogramById(histogramId);
const ACTION_TYPES = gConstants.LOOP_MAU_TYPE;
histogram.clear();
gHandlers.TelemetryAddValue({ data: [histogramId, ACTION_TYPES.OPEN_PANEL] }, () => {});
let snapshot = histogram.snapshot();
Assert.strictEqual(snapshot.counts[ACTION_TYPES.OPEN_PANEL], 1,
"LOOP_MAU_TYPE.OPEN_PANEL");
// Let's be sure that the last event was sent a month ago
let timestamp = (Math.floor(Date.now() / 1000)) - 2593000;
Services.prefs.setIntPref("loop.mau.openPanel", timestamp);
gHandlers.TelemetryAddValue({ data: [histogramId, ACTION_TYPES.OPEN_PANEL] }, () => {});
snapshot = histogram.snapshot();
Assert.strictEqual(snapshot.counts[ACTION_TYPES.OPEN_PANEL], 2,
"LOOP_MAU_TYPE.OPEN_PANEL");
Services.prefs.clearUserPref("loop.mau.openPanel");
});
add_task.skip(function* test_mozLoop_telemetryAdd_loopMau_less_than_30_days() {
let histogramId = "LOOP_MAU";
let histogram = Services.telemetry.getHistogramById(histogramId);
const ACTION_TYPES = gConstants.LOOP_MAU_TYPE;
histogram.clear();
gHandlers.TelemetryAddValue({ data: [histogramId, ACTION_TYPES.OPEN_PANEL] }, () => {});
let snapshot = histogram.snapshot();
Assert.strictEqual(snapshot.counts[ACTION_TYPES.OPEN_PANEL], 1,
"LOOP_MAU_TYPE.OPEN_PANEL");
let timestamp = (Math.floor(Date.now() / 1000)) - 1000;
Services.prefs.setIntPref("loop.mau.openPanel", timestamp);
gHandlers.TelemetryAddValue({ data: [histogramId, ACTION_TYPES.OPEN_PANEL] }, () => {});
snapshot = histogram.snapshot();
Assert.strictEqual(snapshot.counts[ACTION_TYPES.OPEN_PANEL], 1,
"LOOP_MAU_TYPE.OPEN_PANEL");
Services.prefs.clearUserPref("loop.mau.openPanel");
});
*/

View File

@ -8,7 +8,7 @@
"use strict"; "use strict";
Components.utils.import("resource://gre/modules/Promise.jsm", this); Components.utils.import("resource://gre/modules/Promise.jsm", this);
Services.prefs.setIntPref("loop.gettingStarted.latestFTUVersion", 1); Services.prefs.setIntPref("loop.gettingStarted.latestFTUVersion", 2);
const fxASampleToken = { const fxASampleToken = {
token_type: "bearer", token_type: "bearer",

View File

@ -14,7 +14,7 @@ add_test(function test_intialize() {
LoopAPIInternal.initialize(); LoopAPIInternal.initialize();
let [, , pageListeners2] = LoopAPI.inspect(); let [, , pageListeners2] = LoopAPI.inspect();
Assert.equal(pageListeners2.length, 2, "Two page listeners should be added"); Assert.equal(pageListeners2.length, 3, "Three page listeners should be added");
let pageListenersStub = {}; let pageListenersStub = {};
LoopAPI.stub([pageListenersStub]); LoopAPI.stub([pageListenersStub]);

View File

@ -522,25 +522,10 @@ add_task(function* test_joinRoomGuest() {
MozLoopServiceInternal.fxAOAuthProfile = null; MozLoopServiceInternal.fxAOAuthProfile = null;
let roomToken = "_nxD4V4FflQ"; let roomToken = "_nxD4V4FflQ";
let joinedData = yield LoopRooms.promise("join", roomToken); let joinedData = yield LoopRooms.promise("join", roomToken, "guest");
Assert.equal(joinedData.action, "join"); Assert.equal(joinedData.action, "join");
}); });
// Test if joining a room as FxA user works as expected.
add_task(function* test_joinRoom() {
// We need these set up for getting the email address.
MozLoopServiceInternal.fxAOAuthTokenData = { token_type: "bearer" };
MozLoopServiceInternal.fxAOAuthProfile = { email: "fake@invalid.com" };
let roomToken = "_nxD4V4FflQ";
let joinedData = yield LoopRooms.promise("join", roomToken);
Assert.equal(joinedData.action, "join");
Assert.equal(joinedData.displayName, "fake@invalid.com");
MozLoopServiceInternal.fxAOAuthTokenData = null;
MozLoopServiceInternal.fxAOAuthProfile = null;
});
// Test if refreshing a room works as expected. // Test if refreshing a room works as expected.
add_task(function* test_refreshMembership() { add_task(function* test_refreshMembership() {
let roomToken = "_nxD4V4FflQ"; let roomToken = "_nxD4V4FflQ";

View File

@ -29,6 +29,32 @@ add_task(function* request_with_unicode() {
() => Assert.ok(false, "Should have accepted")); () => Assert.ok(false, "Should have accepted"));
}); });
add_task(function* request_with_unicode() {
loopServer.registerPathHandler("/fake", (request, response) => {
Assert.ok(request.hasHeader("x-loop-addon-ver"), "Should have an add-on version header");
Assert.equal(request.getHeader("x-loop-addon-ver"), "3.1", "Should have the correct add-on version");
response.setStatusLine(null, 200, "OK");
response.processAsync();
response.finish();
});
// Pretend we're not enabled so full initialisation doesn't take place.
Services.prefs.setBoolPref("loop.enabled", false);
try {
yield MozLoopService.initialize("3.1");
} catch (ex) {
// Do nothing - this will throw due to being disabled, that's ok.
}
Services.prefs.clearUserPref("loop.enabled");
yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/fake", "POST", {}).then(
() => Assert.ok(true, "Should have accepted"),
() => Assert.ok(false, "Should have accepted"));
});
function run_test() { function run_test() {
setupFakeLoopServer(); setupFakeLoopServer();

View File

@ -9,7 +9,7 @@
<Description about="urn:mozilla:install-manifest"> <Description about="urn:mozilla:install-manifest">
<em:id>loop@mozilla.org</em:id> <em:id>loop@mozilla.org</em:id>
<em:bootstrap>true</em:bootstrap> <em:bootstrap>true</em:bootstrap>
<em:version>0.3.0</em:version> <em:version>1.1.2</em:version>
<em:type>2</em:type> <em:type>2</em:type>
<!-- Target Application this extension can install into, <!-- Target Application this extension can install into,

View File

@ -14,7 +14,7 @@ FIREFOX_PREFERENCES = {
"devtools.debugger.prompt-connection": False, "devtools.debugger.prompt-connection": False,
"devtools.debugger.remote-enabled": True, "devtools.debugger.remote-enabled": True,
"media.volume_scale": "0", "media.volume_scale": "0",
"loop.gettingStarted.latestFTUVersion": 1, "loop.gettingStarted.latestFTUVersion": 2,
# this dialog is fragile, and likely to introduce intermittent failures # this dialog is fragile, and likely to introduce intermittent failures
"media.navigator.permission.disabled": True, "media.navigator.permission.disabled": True,