Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2016-03-23 16:28:48 +01:00
Родитель ad54655f27 b5709cdb58
Коммит be0fcafff8
176 изменённых файлов: 13014 добавлений и 2517 удалений

Просмотреть файл

@ -27,6 +27,10 @@ function isUTF8(charset) {
throw new Error("The charset argument can be only 'utf-8'");
}
function toOctetChar(c) {
return String.fromCharCode(c.charCodeAt(0) & 0xFF);
}
exports.decode = function (data, charset) {
if (isUTF8(charset))
return decodeURIComponent(escape(atob(data)))
@ -38,6 +42,6 @@ exports.encode = function (data, charset) {
if (isUTF8(charset))
return btoa(unescape(encodeURIComponent(data)))
data = String.fromCharCode(...Array.from(data, c => (c.charCodeAt(0) & 0xff)));
data = data.replace(/[^\x00-\xFF]/g, toOctetChar);
return btoa(data);
}

Просмотреть файл

@ -13,6 +13,10 @@ const utf8text = "\u2713 à la mode";
const badutf8text = "\u0013 à la mode";
const b64utf8text = "4pyTIMOgIGxhIG1vZGU=";
// 1 MB string
const longtext = 'fff'.repeat(333333);
const b64longtext = 'ZmZm'.repeat(333333);
exports["test base64.encode"] = function (assert) {
assert.equal(base64.encode(text), b64text, "encode correctly")
}
@ -33,6 +37,26 @@ exports["test base64.decode Unicode"] = function (assert) {
"decode correctly Unicode strings.")
}
exports["test base64.encode long string"] = function (assert) {
assert.equal(base64.encode(longtext), b64longtext, "encode long strings")
}
exports["test base64.decode long string"] = function (assert) {
assert.equal(base64.decode(b64longtext), longtext, "decode long strings")
}
exports["test base64.encode treats input as octet string"] = function (assert) {
assert.equal(base64.encode("\u0066"), "Zg==",
"treat octet string as octet string")
assert.equal(base64.encode("\u0166"), "Zg==",
"treat non-octet string as octet string")
assert.equal(base64.encode("\uff66"), "Zg==",
"encode non-octet string as octet string")
}
exports["test base64.encode with wrong charset"] = function (assert) {
assert.throws(function() {

Просмотреть файл

@ -1675,3 +1675,5 @@ pref("browser.laterrun.enabled", false);
// Enable browser frames for use on desktop. Only exposed to chrome callers.
pref("dom.mozBrowserFramesEnabled", true);
pref("extensions.pocket.enabled", true);

Просмотреть файл

@ -2,7 +2,8 @@
* http://creativecommons.org/publicdomain/zero/1.0/
*/
requestLongerTimeout(2);
// This test needs to be split up. See bug 1258717.
requestLongerTimeout(4);
ignoreAllUncaughtExceptions();
XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils",
@ -70,6 +71,7 @@ add_task(function* () {
info("Check that performing a search fires a search event and records to Telemetry.");
yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
let currEngine = Services.search.currentEngine;
let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
// Make this actually work in healthreport by giving it an ID:
Object.defineProperty(engine.wrappedJSObject, "identifier",
@ -118,6 +120,7 @@ add_task(function* () {
Assert.equal(hs[histogramKey].sum, numSearchesBefore + 1,
"histogram sum should be incremented");
Services.search.currentEngine = currEngine;
try {
Services.search.removeEngine(engine);
} catch (ex) {}
@ -162,7 +165,7 @@ add_task(function* () {
});
add_task(function* () {
info("Check if the 'Know Your Rights default snippet is shown when " +
info("Check if the 'Know Your Rights' default snippet is shown when " +
"'browser.rights.override' pref is set");
Services.prefs.setBoolPref("browser.rights.override", false);
@ -181,7 +184,7 @@ add_task(function* () {
});
add_task(function* () {
info("Check if the 'Know Your Rights default snippet is NOT shown when " +
info("Check if the 'Know Your Rights' default snippet is NOT shown when " +
"'browser.rights.override' pref is NOT set");
Services.prefs.setBoolPref("browser.rights.override", true);
@ -205,8 +208,8 @@ add_task(function* () {
yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
return new Promise(resolve => {
let currEngine = Services.search.defaultEngine;
let searchObserver = Task.async(function* search_observer(subject, topic, data) {
let currEngine = Services.search.defaultEngine;
let engine = subject.QueryInterface(Ci.nsISearchEngine);
info("Observer: " + data + " for " + engine.name);
@ -216,6 +219,8 @@ add_task(function* () {
if (engine.name != "POST Search")
return;
Services.obs.removeObserver(searchObserver, "browser-search-engine-modified");
// Ready to execute the tests!
let needle = "Search for something awesome.";
@ -223,11 +228,6 @@ add_task(function* () {
Services.search.defaultEngine = engine;
yield p;
registerCleanupFunction(function() {
Services.search.removeEngine(engine);
Services.search.defaultEngine = currEngine;
});
let promise = BrowserTestUtils.browserLoaded(browser);
yield ContentTask.spawn(browser, { needle }, function* (args) {
@ -246,12 +246,13 @@ add_task(function* () {
"Search text should arrive correctly");
});
Services.search.defaultEngine = currEngine;
try {
Services.search.removeEngine(engine);
} catch (ex) {}
resolve();
});
Services.obs.addObserver(searchObserver, "browser-search-engine-modified", false);
registerCleanupFunction(function () {
Services.obs.removeObserver(searchObserver, "browser-search-engine-modified");
});
Services.search.addEngine("http://test:80/browser/browser/base/content/test/general/POSTSearchEngine.xml",
null, null, false);
});
@ -300,6 +301,7 @@ add_task(function* () {
yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
// Add a test engine that provides suggestions and switch to it.
let currEngine = Services.search.currentEngine;
let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
let p = promiseContentSearchChange(browser, engine.name);
Services.search.currentEngine = engine;
@ -346,6 +348,7 @@ add_task(function* () {
"Search suggestion table hidden");
});
Services.search.currentEngine = currEngine;
try {
Services.search.removeEngine(engine);
} catch (ex) { }
@ -357,6 +360,7 @@ add_task(function* () {
yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, function* (browser) {
// Add a test engine that provides suggestions and switch to it.
let currEngine = Services.search.currentEngine;
let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
let p = promiseContentSearchChange(browser, engine.name);
Services.search.currentEngine = engine;
@ -431,6 +435,11 @@ add_task(function* () {
row.removeAttribute("id");
}
});
Services.search.currentEngine = currEngine;
try {
Services.search.removeEngine(engine);
} catch (ex) { }
});
});
@ -564,14 +573,12 @@ function* withSnippetsMap(setupFn, testFn, testArgs = null) {
}
yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (browser) {
// The snippets should already be ready by this point. Here we're
// just obtaining a reference to the snippets map.
yield ContentTask.spawn(browser, {
setupFnSource,
version: AboutHomeUtils.snippetsVersion,
}, function* (args) {
yield new Promise(resolve => {
let onAfterLocationChange = () => {
let promiseAfterLocationChange = () => {
return ContentTask.spawn(browser, {
setupFnSource,
version: AboutHomeUtils.snippetsVersion,
}, function* (args) {
return new Promise(resolve => {
let document = content.document;
// We're not using Promise-based listeners, because they resolve asynchronously.
// The snippets test setup code relies on synchronous behaviour here.
@ -605,35 +612,32 @@ function* withSnippetsMap(setupFn, testFn, testArgs = null) {
resolve();
});
});
};
// We'd like to listen to the 'AboutHomeLoadSnippets' event on a fresh
// document as soon as technically possible, so we use webProgress.
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
let wpl = {
onLocationChange: () => {
onAfterLocationChange();
webProgress.removeProgressListener(wpl);
},
onProgressChange: () => {},
onStatusChange: () => {},
onSecurityChange: () => {},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference,
Ci.nsISupports
])
};
webProgress.addProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST |
Ci.nsIWebProgress.NOTIFY_LOCATION);
// Set the URL to 'about:home' here to allow capturing the 'AboutHomeLoadSnippets'
// event.
content.document.location.href = "about:home";
});
});
};
// We'd like to listen to the 'AboutHomeLoadSnippets' event on a fresh
// document as soon as technically possible, so we use webProgress.
let promise = new Promise(resolve => {
let wpl = {
onLocationChange() {
gBrowser.removeProgressListener(wpl);
// Phase 2: retrieving the snippets map is the next promise on our agenda.
promiseAfterLocationChange().then(resolve);
},
onProgressChange() {},
onStatusChange() {},
onSecurityChange() {}
};
gBrowser.addProgressListener(wpl);
});
// Set the URL to 'about:home' here to allow capturing the 'AboutHomeLoadSnippets'
// event.
browser.loadURI("about:home");
// Wait for LocationChange.
yield promise;
yield ContentTask.spawn(browser, testArgs, testFn);
});
}

Просмотреть файл

@ -249,6 +249,13 @@ var CustomizableUIInternal = {
navbarPlacements.push("webide-button");
}
// Place this last, when createWidget is called for pocket, it will
// append to the toolbar.
if (Services.prefs.getPrefType("extensions.pocket.enabled") != Services.prefs.PREF_INVALID &&
Services.prefs.getBoolPref("extensions.pocket.enabled")) {
navbarPlacements.push("pocket-button");
}
this.registerArea(CustomizableUI.AREA_NAVBAR, {
legacy: true,
type: CustomizableUI.TYPE_TOOLBAR,

Просмотреть файл

@ -553,6 +553,10 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
query: function(queryInfo) {
let pattern = null;
if (queryInfo.url !== null) {
if (!extension.hasPermission("tabs")) {
return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
}
pattern = new MatchPattern(queryInfo.url);
}

Просмотреть файл

@ -666,6 +666,20 @@ global.WindowManager = {
return "normal";
},
updateGeometry(window, options) {
if (options.left !== null || options.top !== null) {
let left = options.left !== null ? options.left : window.screenX;
let top = options.top !== null ? options.top : window.screenY;
window.moveTo(left, top);
}
if (options.width !== null || options.height !== null) {
let width = options.width !== null ? options.width : window.outerWidth;
let height = options.height !== null ? options.height : window.outerHeight;
window.resizeTo(width, height);
}
},
getId(window) {
if (this._windows.has(window)) {
return this._windows.get(window);
@ -738,6 +752,11 @@ global.WindowManager = {
state = "fullscreen";
}
let xulWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIXULWindow);
let result = {
id: this.getId(window),
focused: window.document.hasFocus(),
@ -748,6 +767,7 @@ global.WindowManager = {
incognito: PrivateBrowsingUtils.isWindowPrivate(window),
type: this.windowType(window),
state,
alwaysOnTop: xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ,
};
if (getInfo && getInfo.populate) {

Просмотреть файл

@ -65,11 +65,14 @@ extensions.registerSchemaAPI("windows", null, (extension, context) => {
},
create: function(createData) {
if (createData.state !== null && createData.state != "normal") {
if (createData.left !== null || createData.top !== null ||
createData.width !== null || createData.height !== null) {
let needResize = (createData.left !== null || createData.top !== null ||
createData.width !== null || createData.height !== null);
if (needResize) {
if (createData.state !== null && createData.state != "normal") {
return Promise.reject({message: `"state": "${createData.state}" may not be combined with "left", "top", "width", or "height"`});
}
createData.state = "normal";
}
function mkstr(s) {
@ -133,16 +136,7 @@ extensions.registerSchemaAPI("windows", null, (extension, context) => {
let window = Services.ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
features.join(","), args);
if (createData.left !== null || createData.top !== null) {
let left = createData.left !== null ? createData.left : window.screenX;
let top = createData.top !== null ? createData.top : window.screenY;
window.moveTo(left, top);
}
if (createData.width !== null || createData.height !== null) {
let width = createData.width !== null ? createData.width : window.outerWidth;
let height = createData.height !== null ? createData.height : window.outerHeight;
window.resizeTo(width, height);
}
WindowManager.updateGeometry(window, createData);
// TODO: focused, type
@ -176,13 +170,12 @@ extensions.registerSchemaAPI("windows", null, (extension, context) => {
},
update: function(windowId, updateInfo) {
// TODO: When we support size/position updates:
// if (updateInfo.state !== null && updateInfo.state != "normal") {
// if (updateInfo.left !== null || updateInfo.top !== null ||
// updateInfo.width !== null || updateInfo.height !== null) {
// return Promise.reject({message: `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"`});
// }
// }
if (updateInfo.state !== null && updateInfo.state != "normal") {
if (updateInfo.left !== null || updateInfo.top !== null ||
updateInfo.width !== null || updateInfo.height !== null) {
return Promise.reject({message: `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"`});
}
}
let window = WindowManager.getWindow(windowId, context);
if (updateInfo.focused) {
@ -198,6 +191,8 @@ extensions.registerSchemaAPI("windows", null, (extension, context) => {
window.getAttention();
}
WindowManager.updateGeometry(window, updateInfo);
// TODO: All the other properties, focused=false...
return Promise.resolve(WindowManager.convert(extension, window));

Просмотреть файл

@ -88,7 +88,6 @@
"description": "The state of this browser window."
},
"alwaysOnTop": {
"unsupported": true,
"type": "boolean",
"description": "Whether the window is set to be always on top."
},
@ -378,26 +377,22 @@
"name": "updateInfo",
"properties": {
"left": {
"unsupported": true,
"type": "integer",
"optional": true,
"description": "The offset from the left edge of the screen to move the window to in pixels. This value is ignored for panels."
},
"top": {
"unsupported": true,
"type": "integer",
"optional": true,
"description": "The offset from the top edge of the screen to move the window to in pixels. This value is ignored for panels."
},
"width": {
"unsupported": true,
"type": "integer",
"minimum": 0,
"optional": true,
"description": "The width to resize the window to in pixels. This value is ignored for panels."
},
"height": {
"unsupported": true,
"type": "integer",
"minimum": 0,
"optional": true,

Просмотреть файл

@ -56,6 +56,9 @@ support-files =
[browser_ext_windows_create.js]
tags = fullscreen
[browser_ext_windows_create_tabId.js]
[browser_ext_windows.js]
[browser_ext_windows_size.js]
skip-if = os == 'mac' # Fails when windows are randomly opened in fullscreen mode
[browser_ext_windows_update.js]
tags = fullscreen
[browser_ext_contentscript_connect.js]

Просмотреть файл

@ -136,3 +136,50 @@ add_task(function* () {
yield BrowserTestUtils.removeTab(tab2);
yield BrowserTestUtils.removeTab(tab3);
});
add_task(function* testQueryPermissions() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": [],
},
background: function(x) {
browser.tabs.query({currentWindow: true, active: true}).then((tabs) => {
browser.test.assertEq(tabs.length, 1, "Expect query to return tabs");
browser.test.notifyPass("queryPermissions");
}).catch((e) => {
browser.test.notifyFail("queryPermissions");
});
},
});
yield extension.startup();
yield extension.awaitFinish("queryPermissions");
yield extension.unload();
});
add_task(function* testQueryWithURLPermissions() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": [],
},
background: function(x) {
browser.tabs.query({"url": "http://www.bbc.com/"}).then(() => {
browser.test.notifyFail("queryWithURLPermissions");
}).catch((e) => {
browser.test.assertEq('The "tabs" permission is required to use the query API with the "url" parameter',
e.message, "Expected permissions error message");
browser.test.notifyPass("queryWithURLPermissions");
});
},
});
yield extension.startup();
yield extension.awaitFinish("queryWithURLPermissions");
yield extension.unload();
});

Просмотреть файл

@ -0,0 +1,33 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* () {
let raisedWin = Services.ww.openWindow(
null, Services.prefs.getCharPref("browser.chromeURL"), "_blank",
"chrome,dialog=no,all,alwaysRaised", null);
yield TestUtils.topicObserved("browser-delayed-startup-finished",
subject => subject == raisedWin);
let extension = ExtensionTestUtils.loadExtension({
background: function() {
browser.windows.getAll((wins) => {
browser.test.assertEq(wins.length, 2, "Expect two windows");
browser.test.assertEq(false, wins[0].alwaysOnTop,
"Expect first window not to be always on top");
browser.test.assertEq(true, wins[1].alwaysOnTop,
"Expect first window to be always on top");
browser.test.notifyPass("alwaysOnTop");
});
},
});
yield extension.startup();
yield extension.awaitFinish("alwaysOnTop");
yield extension.unload();
yield BrowserTestUtils.closeWindow(raisedWin);
});

Просмотреть файл

@ -0,0 +1,114 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* testWindowCreate() {
let extension = ExtensionTestUtils.loadExtension({
background() {
let _checkWindowPromise;
browser.test.onMessage.addListener((msg, arg) => {
if (msg == "checked-window") {
_checkWindowPromise.resolve(arg);
_checkWindowPromise = null;
}
});
let getWindowSize = () => {
return new Promise(resolve => {
_checkWindowPromise = {resolve};
browser.test.sendMessage("check-window");
});
};
const KEYS = ["left", "top", "width", "height"];
function checkGeom(expected, actual) {
for (let key of KEYS) {
browser.test.assertEq(expected[key], actual[key], `Expected '${key}' value`);
}
}
let windowId;
function checkWindow(expected, retries = 5) {
return getWindowSize().then(geom => {
if (retries && KEYS.some(key => expected[key] != geom[key])) {
browser.test.log(`Got mismatched size (${JSON.stringify(expected)} != ${JSON.stringify(geom)}). ` +
`Retrying after a short delay.`);
return new Promise(resolve => {
setTimeout(resolve, 200);
}).then(() => {
return checkWindow(expected, retries - 1);
});
}
browser.test.log(`Check actual window size`);
checkGeom(expected, geom);
browser.test.log("Check API-reported window size");
return browser.windows.get(windowId).then(geom => {
checkGeom(expected, geom);
});
});
}
let geom = {left: 100, top: 100, width: 500, height: 300};
return browser.windows.create(geom).then(window => {
windowId = window.id;
return checkWindow(geom);
}).then(() => {
let update = {left: 150, width: 600};
Object.assign(geom, update);
return browser.windows.update(windowId, update);
}).then(() => {
return checkWindow(geom);
}).then(() => {
let update = {top: 150, height: 400};
Object.assign(geom, update);
return browser.windows.update(windowId, update);
}).then(() => {
return checkWindow(geom);
}).then(() => {
geom = {left: 200, top: 200, width: 800, height: 600};
return browser.windows.update(windowId, geom);
}).then(() => {
return checkWindow(geom);
}).then(() => {
return browser.windows.remove(windowId);
}).then(() => {
browser.test.notifyPass("window-size");
}).catch(e => {
browser.test.fail(`${e} :: ${e.stack}`);
browser.test.notifyFail("window-size");
});
},
});
let latestWindow;
let windowListener = (window, topic) => {
if (topic == "domwindowopened") {
latestWindow = window;
}
};
Services.ww.registerNotification(windowListener);
extension.onMessage("check-window", () => {
extension.sendMessage("checked-window", {
top: latestWindow.screenY,
left: latestWindow.screenX,
width: latestWindow.outerWidth,
height: latestWindow.outerHeight,
});
});
yield extension.startup();
yield extension.awaitFinish("window-size");
yield extension.unload();
Services.ww.unregisterNotification(windowListener);
latestWindow = null;
});

Просмотреть файл

@ -148,3 +148,41 @@ add_task(function* () {
yield BrowserTestUtils.closeWindow(window2);
});
// Tests that incompatible parameters can't be used together.
add_task(function* testWindowUpdateParams() {
let extension = ExtensionTestUtils.loadExtension({
background() {
function* getCalls() {
for (let state of ["minimized", "maximized", "fullscreen"]) {
for (let param of ["left", "top", "width", "height"]) {
let expected = `"state": "${state}" may not be combined with "left", "top", "width", or "height"`;
let windowId = browser.windows.WINDOW_ID_CURRENT;
yield browser.windows.update(windowId, {state, [param]: 100}).then(
val => {
browser.test.fail(`Expected error but got "${val}" instead`);
},
error => {
browser.test.assertTrue(
error.message.includes(expected),
`Got expected error (got: '${error.message}', expected: '${expected}'`);
});
}
}
}
Promise.all(getCalls()).then(() => {
browser.test.notifyPass("window-update-params");
}).catch(e => {
browser.test.fail(`${e} :: ${e.stack}`);
browser.test.notifyFail("window-update-params");
});
},
});
yield extension.startup();
yield extension.awaitFinish("window-update-params");
yield extension.unload();
});

85
browser/extensions/loop/bootstrap.js поставляемый
Просмотреть файл

@ -10,8 +10,9 @@ const { interfaces: Ci, utils: Cu, classes: Cc } = Components;
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const kBrowserSharingNotificationId = "loop-sharing-notification";
const MIN_CURSOR_DELTA = 3;
const MIN_CURSOR_INTERVAL = 100;
const CURSOR_MIN_DELTA = 3;
const CURSOR_MIN_INTERVAL = 100;
const CURSOR_CLICK_DELAY = 1000;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -189,7 +190,7 @@ var WindowListener = {
let buckets = this.constants.LOOP_MAU_TYPE;
this.LoopAPI.sendMessageToHandler({
name: "TelemetryAddValue",
data: ["LOOP_MAU", buckets.OPEN_PANEL]
data: ["LOOP_ACTIVITY_COUNTER", buckets.OPEN_PANEL]
});
};
@ -298,6 +299,12 @@ var WindowListener = {
}
});
// If we're in private browsing mode, then don't add the menu item,
// also don't add the listeners as we don't want to update the button.
if (PrivateBrowsingUtils.isWindowPrivate(window)) {
return;
}
this.addMenuItem();
// Don't do the rest if this is for the hidden window - we don't
@ -523,6 +530,7 @@ var WindowListener = {
// Add this event to the parent gBrowser to avoid adding and removing
// it for each individual tab's browsers.
gBrowser.addEventListener("mousemove", this);
gBrowser.addEventListener("click", this);
}
this._maybeShowBrowserSharingInfoBar();
@ -543,8 +551,12 @@ var WindowListener = {
this._hideBrowserSharingInfoBar();
gBrowser.tabContainer.removeEventListener("TabSelect", this);
gBrowser.removeEventListener("DOMTitleChanged", this);
// Remove shared pointers related events
gBrowser.removeEventListener("mousemove", this);
gBrowser.removeEventListener("click", this);
this.removeRemoteCursor();
this._listeningToTabSelect = false;
this._browserSharePaused = false;
this._sendTelemetryEventsIfNeeded();
@ -598,7 +610,6 @@ var WindowListener = {
}
let browser = gBrowser.selectedBrowser;
let cursor = document.getElementById("loop-remote-cursor");
if (!cursor) {
// Create a container to keep the pointer inside.
@ -608,7 +619,6 @@ var WindowListener = {
cursor = document.createElement("img");
cursor.setAttribute("id", "loop-remote-cursor");
cursorContainer.appendChild(cursor);
// Note that browser.parent is a xul:stack so container will use
// 100% of space if no other constrains added.
@ -623,9 +633,36 @@ var WindowListener = {
},
/**
* Removes the remote cursor from the screen
* Adds the ripple effect animation to the cursor to show a click on the
* remote end of the conversation.
* Will only add it when:
* - A click is received (cursorData = true)
* - Sharing is active (this._listeningToTabSelect = true)
* - Remote cursor is being painted (cursor != undefined)
*
* @param browser OPT browser where the cursor should be removed from.
* @param clickData bool click event
*/
clickRemoteCursor: function(clickData) {
if (!clickData || !this._listeningToTabSelect) {
return;
}
let class_name = "clicked";
let cursor = document.getElementById("loop-remote-cursor");
if (!cursor) {
return;
}
cursor.classList.add(class_name);
// after the proper time, we get rid of the animation
window.setTimeout(() => {
cursor.classList.remove(class_name);
}, CURSOR_CLICK_DELAY);
},
/**
* Removes the remote cursor from the screen
*/
removeRemoteCursor: function() {
let cursor = document.getElementById("loop-remote-cursor");
@ -782,6 +819,9 @@ var WindowListener = {
case "mousemove":
this.handleMousemove(event);
break;
case "click":
this.handleMouseClick(event);
break;
}
},
@ -798,7 +838,7 @@ var WindowListener = {
// Only update every so often.
let now = Date.now();
if (now - this.lastCursorTime < MIN_CURSOR_INTERVAL) {
if (now - this.lastCursorTime < CURSOR_MIN_INTERVAL) {
return;
}
this.lastCursorTime = now;
@ -809,8 +849,8 @@ var WindowListener = {
let deltaY = event.screenY - browserBox.screenY;
if (deltaX < 0 || deltaX > browserBox.width ||
deltaY < 0 || deltaY > browserBox.height ||
(Math.abs(deltaX - this.lastCursorX) < MIN_CURSOR_DELTA &&
Math.abs(deltaY - this.lastCursorY) < MIN_CURSOR_DELTA)) {
(Math.abs(deltaX - this.lastCursorX) < CURSOR_MIN_DELTA &&
Math.abs(deltaY - this.lastCursorY) < CURSOR_MIN_DELTA)) {
return;
}
this.lastCursorX = deltaX;
@ -822,6 +862,20 @@ var WindowListener = {
});
},
/**
* Handles mouse click events from gBrowser and send a broadcast message
* with all the data needed for sending link generator cursor click position
* through the sdk.
*/
handleMouseClick: function() {
// We want to stop sending events if sharing is paused.
if (this._browserSharePaused) {
return;
}
this.LoopAPI.broadcastPushMessage("CursorClick");
},
/**
* Fetch the favicon of the currently selected tab in the format of a data-uri.
*
@ -985,6 +1039,17 @@ function loadDefaultPrefs() {
}
}
});
if (Services.vc.compare(Services.appinfo.version, "47.0a1") < 0) {
branch.setBoolPref("loop.remote.autostart", false);
}
// Don't enable pop-outs in Firefox 47 - that's where e10s is enabled, and popping
// out currently fails (bug 1245813).
if (Services.vc.compare(Services.appinfo.version, "47.0a1") >= 0 &&
Services.vc.compare(Services.appinfo.version, "48.0a1") < 0) {
branch.setBoolPref("loop.conversationPopOut.enabled", false);
}
}
/**

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -23,6 +23,8 @@ XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
return new EventEmitter();
});
XPCOMUtils.defineLazyModuleGetter(this, "DomainWhitelist",
"chrome://loop/content/modules/DomainWhitelist.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopRoomsCache",
"chrome://loop/content/modules/LoopRoomsCache.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "loopUtils",
@ -864,6 +866,80 @@ var LoopRoomsInternal = {
}, callback);
},
_domainLog: {
domainMap: new Map(),
roomToken: null
},
/**
* Record a url associated to a room for batch submission if whitelisted.
*
* @param {String} roomToken The room token
* @param {String} url Url with the domain to record
*/
_recordUrl(roomToken, url) {
// Reset the log of domains if somehow we changed room tokens.
if (this._domainLog.roomToken !== roomToken) {
this._domainLog.roomToken = roomToken;
this._domainLog.domainMap.clear();
}
let domain;
try {
domain = Services.eTLD.getBaseDomain(Services.io.newURI(url, null, null));
}
catch (ex) {
// Failed to extract domain, so don't record it.
return;
}
// Only record domains that are whitelisted.
if (!DomainWhitelist.check(domain)) {
return;
}
// Increment the count for previously recorded domains.
if (this._domainLog.domainMap.has(domain)) {
this._domainLog.domainMap.get(domain).count++;
}
// Initialize the map for the domain with a value that can be submitted.
else {
this._domainLog.domainMap.set(domain, { count: 1, domain });
}
},
/**
* Log the domains associated to a room token.
*
* @param {String} roomToken The room token
* @param {Function} callback Function that will be invoked once the operation
* finished. The first argument passed will be an
* `Error` object or `null`.
*/
logDomains(roomToken, callback) {
if (!callback) {
callback = error => {
if (error) {
MozLoopService.log.error(error);
}
};
}
// Submit the domains that have been collected so far.
if (this._domainLog.roomToken === roomToken &&
this._domainLog.domainMap.size > 0) {
this._postToRoom(roomToken, {
action: "logDomain",
domains: [...this._domainLog.domainMap.values()]
}, callback);
this._domainLog.domainMap.clear();
}
// Indicate that nothing was logged.
else {
callback(null);
}
},
/**
* Updates a room.
*
@ -893,7 +969,13 @@ var LoopRoomsInternal = {
}
if (roomData.urls && roomData.urls.length) {
// For now we only support adding one URL to the room context.
room.decryptedContext.urls = [roomData.urls[0]];
let context = roomData.urls[0];
room.decryptedContext.urls = [context];
// Record the url for reporting if enabled.
if (Services.prefs.getBoolPref("loop.logDomains")) {
this._recordUrl(roomToken, context.location);
}
}
Task.spawn(function* () {
@ -1075,6 +1157,10 @@ this.LoopRooms = {
return LoopRoomsInternal.sendConnectionStatus(roomToken, sessionToken, status, callback);
},
logDomains(roomToken, callback) {
return LoopRoomsInternal.logDomains(roomToken, callback);
},
update: function(roomToken, roomData, callback) {
return LoopRoomsInternal.update(roomToken, roomData, callback);
},

Просмотреть файл

@ -203,7 +203,7 @@ const kMessageHandlers = {
* 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
* @param {Object} message Message meant for the handler function, contains
* the following parameters in its 'data' property:
* {
* ratioX: cursor's X position (between 0-1)
@ -223,6 +223,25 @@ const kMessageHandlers = {
reply();
},
/**
* Shows the click event on the remote cursor.
*
* @param {Object} message Message meant for the handler function, contains
* a boolean for the click event in its 'data' prop.
*
* @param {Function} reply Callback function, invoked with the result of the
* message handler. The result will be sent back to
* the senders' channel.
*/
ClickRemoteCursor: function(message, reply) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
if (win) {
win.LoopUI.clickRemoteCursor(message.data[0]);
}
reply();
},
/**
* Associates a session-id and a call-id with a window for debugging.
*
@ -539,20 +558,6 @@ const kMessageHandlers = {
return reply(errors);
},
/**
* Returns TRUE if Firefox Accounts are enabled and can be used.
*
* @param {Object} message Message meant for the handler function, containing
* the following parameters in its `data` property:
* [ ]
* @param {Function} reply Callback function, invoked with the result of this
* message handler. The result will be sent back to
* the senders' channel.
*/
GetFxAEnabled: function(message, reply) {
reply(MozLoopService.fxAEnabled);
},
/**
* Returns true if this profile has an encryption key.
*
@ -728,6 +733,7 @@ const kMessageHandlers = {
windowId = sessionToken;
}
LoopRooms.logDomains(roomToken);
LoopRooms.leave(roomToken);
MozLoopService.setScreenShareState(windowId, false);
LoopAPI.sendMessageToHandler({
@ -1055,7 +1061,7 @@ const kMessageHandlers = {
TelemetryAddValue: function(message, reply) {
let [histogramId, value] = message.data;
if (histogramId === "LOOP_MAU") {
if (histogramId === "LOOP_ACTIVITY_COUNTER") {
let pref = "mau." + kMauPrefMap.get(value);
let prefDate = MozLoopService.getLoopPref(pref) * 1000;
let delta = Date.now() - prefDate;

Просмотреть файл

@ -190,7 +190,6 @@ function getJSONPref(aName) {
var gHawkClient = null;
var gLocalizedStrings = new Map();
var gFxAEnabled = true;
var gFxAOAuthClientPromise = null;
var gFxAOAuthClient = null;
var gErrors = new Map();
@ -1009,6 +1008,17 @@ var MozLoopServiceInternal = {
}
});
// Disable drag feature if needed
if (!MozLoopService.getLoopPref("conversationPopOut.enabled")) {
let document = chatbox.ownerDocument;
let titlebarNode = document.getAnonymousElementByAttribute(chatbox, "class",
"chat-titlebar");
titlebarNode.addEventListener("dragend", event => {
event.stopPropagation();
return false;
});
}
// Handle window.close correctly on the chatbox.
mm.sendAsyncMessage("Social:HookWindowCloseForPanelClose");
messageName = "Social:DOMWindowClose";
@ -1118,7 +1128,11 @@ var MozLoopServiceInternal = {
// away to circumvent glitches.
chatboxInstance.setAttribute("customSize", "loopDefault");
chatboxInstance.parentNode.setAttribute("customSize", "loopDefault");
Chat.loadButtonSet(chatboxInstance, "minimize,swap," + kChatboxHangupButton.id);
let buttons = "minimize,";
if (MozLoopService.getLoopPref("conversationPopOut.enabled")) {
buttons += "swap,";
}
Chat.loadButtonSet(chatboxInstance, buttons + kChatboxHangupButton.id);
// Final fall-through in case a unit test overloaded Chat.open. Here we can
// immediately resolve the promise.
} else {
@ -1343,13 +1357,6 @@ this.MozLoopService = {
return Promise.reject(new Error("loop is not enabled"));
}
if (Services.prefs.getPrefType("loop.fxa.enabled") == Services.prefs.PREF_BOOL) {
gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled");
if (!gFxAEnabled) {
yield this.logOutFromFxA();
}
}
// The Loop toolbar button should change icon when the room participant count
// changes from 0 to something.
const onRoomsChange = (e) => {
@ -1590,10 +1597,6 @@ this.MozLoopService = {
MozLoopServiceInternal.doNotDisturb = aFlag;
},
get fxAEnabled() {
return gFxAEnabled;
},
/**
* Gets the user profile, but only if there is
* tokenData present. Without tokenData, the

Просмотреть файл

@ -129,10 +129,13 @@ function anonymizeIPv4(text) {
* - (id=35 url=about:loopconversation#incoming/1403134352854)
* + (id=35 url=about:loopconversation#incoming/xxxx)
*
* - (id=35 url=about:loopconversation#1403134352854)
* + (id=35 url=about:loopconversation#/xxxx)
*
* @param {DOMString} text The text.
*/
function sanitizeUrls(text) {
let trimUrl = url => url.replace(/(#call|#incoming).*/g,
let trimUrl = url => url.replace(/(#call|#incoming|#).*/g,
(match, type) => type + "/xxxx");
return text.replace(/\(id=(\d+) url=([^\)]+)\)/g,
(match, id, url) =>

Просмотреть файл

@ -10,6 +10,7 @@
<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/css/desktop.css">
<link rel="stylesheet" type="text/css" href="shared/css/conversation.css">
</head>
<body class="fx-embedded">
@ -47,6 +48,7 @@
<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/linkifiedTextView.js"></script>
<script type="text/javascript" src="panels/js/desktopViews.js"></script>
<script type="text/javascript" src="panels/js/feedbackViews.js"></script>
<script type="text/javascript" src="panels/js/roomViews.js"></script>
<script type="text/javascript" src="panels/js/conversation.js"></script>

Просмотреть файл

@ -0,0 +1,252 @@
/* 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/. */
.panel-content.showing-share-panel,
.panel-content.showing-share-panel > .panel-container {
min-height: 220px;
}
/* Room share panel */
.room-invitation-overlay {
position: absolute;
background: rgba(255, 255, 255, 0.85);
top: 0;
height: 100%;
right: 0;
left: 0;
color: #000;
z-index: 1010;
display: flex;
flex-flow: column nowrap;
align-items: stretch;
}
.room-invitation-content {
display: flex;
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;
}
.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: 1px solid #d2cece;;
border-radius: 4px 4px 0 0;
border-bottom: 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;
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;
}
html[dir="rtl"] .share-action-group > .invite-button {
margin-left: 20px;
margin-right: initial;
}
.share-action-group > .invite-button:last-child {
margin-right: 0;
}
html[dir="rtl"] .share-action-group > .invite-button:last-child {
margin-left: 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 {
color: #000;
text-align: start;
bottom: auto;
top: 0;
overflow: hidden;
overflow-y: auto;
}
/* When the dropdown is showing a vertical scrollbar, compensate for its width. */
body[platform="other"] .share-service-dropdown.overflow > .dropdown-menu-item,
body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
-moz-padding-end: 20px;
}
.share-service-dropdown > .dropdown-menu-item > .icon {
width: 14px;
height: 14px;
margin-right: 4px;
}
.dropdown-menu-item > .icon-add-share-service {
background-image: url("../img/icons-16x16.svg#add");
background-repeat: no-repeat;
background-size: 12px 12px;
width: 12px;
height: 12px;
}
.dropdown-menu-item:hover > .icon-add-share-service {
background-image: url("../img/icons-16x16.svg#add-hover");
}
.dropdown-menu-item:hover:active > .icon-add-share-service {
background-image: url("../img/icons-16x16.svg#add-active");
}
.share-panel-container {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 999;
}
.share-panel-container > .share-panel-overlay {
background-color: rgba(0, 0, 0, .3);
bottom: 0;
display: none;
left: 0;
position: absolute;
right: 0;
top: 0;
}
.share-panel-container > .room-invitation-overlay {
background-color: #fff;
flex: 1;
flex-flow: column nowrap;
justify-content: center;
transform: translateX(100%);
transition: transform ease 300ms;
width: 294px;
right: 0;
left: initial;
}
html[dir="rtl"] .share-panel-container > .room-invitation-overlay {
left: 0;
right: initial;
transform: translate(-100%);
}
.share-panel-container > .room-invitation-overlay > .room-invitation-content {
margin: 0 0 12px;
}
.share-panel-container > .room-invitation-overlay > .room-invitation-content > * {
width: initial;
}
.share-panel-open > .room-invitation-overlay,
html[dir="rtl"] .share-panel-open > .room-invitation-overlay {
transform: translateX(0);
}
.share-panel-open > .share-panel-overlay {
display: block;
}
.share-panel-container > .room-invitation-overlay > .room-invitation-content > .room-context-header {
text-align: left;
}
html[dir="rtl"] .share-panel-container > .room-invitation-overlay > .room-invitation-content > .room-context-header {
text-align: right;
}

Просмотреть файл

@ -41,6 +41,15 @@ body {
z-index: 1000;
}
.panel-container {
flex: 1;
display: flex;
flex-flow: column nowrap;
width: 100%;
height: 100%;
position: relative;
}
/* Notifications displayed over tabs */
.panel .messages {
@ -157,6 +166,11 @@ body {
.rooms {
display: block;
width: 100%;
flex-grow: 2;
}
.rooms > .fte-get-started-content {
padding-top: 2.4rem;
}
.rooms > h1 {
@ -260,6 +274,14 @@ body {
white-space: nowrap;
}
.room-list > .room-entry > input {
display: inline-block;
vertical-align: middle;
/* See .room-entry-context-item for the margin/size reductions. */
width: calc(100% - 1rem - 16px);
font-size: 1.3rem;
}
.room-list > .room-entry.room-active:not(.room-opened) > h2 {
font-weight: bold;
}
@ -567,8 +589,8 @@ html[dir="rtl"] .generate-url-spinner {
/* Settings (gear) menu */
.button-settings {
width: 10px;
height: 10px;
width: 14px;
height: 14px;
margin: 0;
padding: 0;
border: none;
@ -578,7 +600,6 @@ html[dir="rtl"] .generate-url-spinner {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
-moz-margin-start: .5em;
}
.settings-menu .dropdown-menu {
@ -639,11 +660,7 @@ html[dir="rtl"] .settings-menu .dropdown-menu {
.fte-get-started-content {
/* Manual vertical centering */
flex: 1;
padding: 2rem 0 0;
display: flex;
flex-direction: column;
height: 553px;
}
.fte-title {
@ -656,6 +673,8 @@ html[dir="rtl"] .settings-menu .dropdown-menu {
img.fte-logo {
height: 32px;
/* Setting svg img to display:block fixed svg/panel sizing issue */
display: block;
}
.fte-subheader, .fte-content {
@ -681,10 +700,11 @@ img.fte-logo {
}
img.fte-hello-web-share {
/* Set custom SVG size and position to align with powered-by-wrapper element. */
/* Set custom SVG size to align with powered-by-wrapper element. */
width: 290px;
height: 148px;
margin-bottom: -4px;
height: 168px;
/* Setting svg img to display:block fixed svg/panel sizing issue */
display: block;
}
.fte-get-started-content + .powered-by-wrapper {
@ -703,6 +723,11 @@ img.fte-hello-web-share {
background: #fbfbfb;
}
.fte-get-started-content-borders {
border-top: 1px solid #D8D8D8;
border-bottom: 1px solid #D8D8D8;
}
.fte-button-container {
border-top: 1px solid #ccc;
padding: 8px 8px 0;

Просмотреть файл

@ -14,29 +14,29 @@
}
.slide-layout {
padding-top: 50px;
padding-top: 90px;
padding-left: 50px;
padding-bottom: 20px;
width: 600px;
width: 590px;
}
.slide-layout > h2 {
font-family: sans-serif;
font-size: 3.0rem;
font-weight: 500;
font-size: 2.5rem;
font-weight: 600;
white-space: normal;
color: #fff;
line-height: 3rem;
margin-bottom: 10px;
line-height: 2.8rem;
margin-bottom: 1.4rem;
}
.slide-layout > .slide-text {
font-family: sans-serif;
font-size: 2.1rem;
font-weight: 300;
font-size: 1.8rem;
font-weight: 400;
white-space: normal;
color: #fff;
line-height: 3.0rem;
line-height: 2.8rem;
}
.slide-layout > img {
@ -49,7 +49,7 @@
background-position: center;
padding: 0;
float: right;
margin-top: -20px;
margin-top: -40px;
}
.slide1 {

Просмотреть файл

@ -30,11 +30,27 @@ loop.conversation = function (mozL10n) {
},
componentWillMount: function () {
this.listenTo(this.props.cursorStore, "change:remoteCursorPosition", this._onRemoteCursorChange);
this.listenTo(this.props.cursorStore, "change:remoteCursorPosition", this._onRemoteCursorPositionChange);
this.listenTo(this.props.cursorStore, "change:remoteCursorClick", this._onRemoteCursorClick);
},
_onRemoteCursorChange: function () {
return loop.request("AddRemoteCursorOverlay", this.props.cursorStore.getStoreState("remoteCursorPosition"));
_onRemoteCursorPositionChange: function () {
loop.request("AddRemoteCursorOverlay", this.props.cursorStore.getStoreState("remoteCursorPosition"));
},
_onRemoteCursorClick: function () {
let click = this.props.cursorStore.getStoreState("remoteCursorClick");
// if the click is 'false', assume it is a storeState reset,
// so don't do anything
if (!click) {
return;
}
this.props.cursorStore.setStoreState({
remoteCursorClick: false
});
loop.request("ClickRemoteCursor", click);
},
getInitialState: function () {
@ -53,16 +69,7 @@ loop.conversation = function (mozL10n) {
* the window.
*/
handleCallTerminated: function () {
var delta = new Date() - new Date(this.state.feedbackTimestamp);
// Show timestamp if feedback period (6 months) passed.
// 0 is default value for pref. Always show feedback form on first use.
if (this.state.feedbackTimestamp === 0 || delta >= this.state.feedbackPeriod) {
this.props.dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
return;
}
this.closeWindow();
this.props.dispatcher.dispatch(new sharedActions.LeaveConversation());
},
render: function () {
@ -210,7 +217,7 @@ loop.conversation = function (mozL10n) {
windowId: windowId
}));
loop.request("TelemetryAddValue", "LOOP_MAU", constants.LOOP_MAU_TYPE.OPEN_CONVERSATION);
loop.request("TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", constants.LOOP_MAU_TYPE.OPEN_CONVERSATION);
});
}

Просмотреть файл

@ -51,7 +51,8 @@ loop.store.ConversationAppStore = (function() {
this._dispatcher.register(this, [
"getWindowData",
"showFeedbackForm"
"showFeedbackForm",
"leaveConversation"
]);
};
@ -147,19 +148,35 @@ loop.store.ConversationAppStore = (function() {
* the window when no session is currently active.
*/
LoopHangupNowHandler: function() {
switch (this.getStoreState().windowType) {
case "room":
if (this._activeRoomStore.getStoreState().used &&
!this._storeState.showFeedbackForm) {
this._dispatcher.dispatch(new loop.shared.actions.LeaveRoom());
} else {
loop.shared.mixins.WindowCloseMixin.closeWindow();
}
break;
default:
loop.shared.mixins.WindowCloseMixin.closeWindow();
break;
this._dispatcher.dispatch(new loop.shared.actions.LeaveConversation());
},
/**
* Handles leaving the conversation, displaying the feedback form if it
* is time to.
*/
leaveConversation: function() {
if (this.getStoreState().windowType !== "room" ||
!this._activeRoomStore.getStoreState().used ||
this.getStoreState().showFeedbackForm) {
loop.shared.mixins.WindowCloseMixin.closeWindow();
return;
}
var delta = new Date() - new Date(this.getStoreState().feedbackTimestamp);
// Show timestamp if feedback period (6 months) passed.
// 0 is default value for pref. Always show feedback form on first use.
if (this.getStoreState().feedbackTimestamp === 0 ||
delta >= this.getStoreState().feedbackPeriod) {
this._dispatcher.dispatch(new loop.shared.actions.LeaveRoom({
windowStayingOpen: true
}));
this._dispatcher.dispatch(new loop.shared.actions.ShowFeedbackForm());
return;
}
loop.shared.mixins.WindowCloseMixin.closeWindow();
},
/**

Просмотреть файл

@ -0,0 +1,342 @@
/* 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.shared = loop.shared || {};
loop.shared.desktopViews = function (mozL10n) {
"use strict";
var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins;
var sharedUtils = loop.shared.utils;
var CopyLinkButton = React.createClass({
displayName: "CopyLinkButton",
statics: {
TRIGGERED_RESET_DELAY: 2000
},
mixins: [React.addons.PureRenderMixin],
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
roomData: React.PropTypes.object.isRequired
},
getInitialState: function () {
return {
copiedUrl: false
};
},
handleCopyButtonClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
roomUrl: this.props.roomData.roomUrl,
from: this.props.locationForMetrics
}));
this.setState({ copiedUrl: true });
setTimeout(this.resetTriggeredButtons, this.constructor.TRIGGERED_RESET_DELAY);
},
/**
* Reset state of triggered buttons if necessary
*/
resetTriggeredButtons: function () {
if (this.state.copiedUrl) {
this.setState({ copiedUrl: false });
this.props.callback && this.props.callback();
}
},
render: function () {
var cx = classNames;
return React.createElement(
"div",
{ className: cx({
"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")
);
}
});
var EmailLinkButton = React.createClass({
displayName: "EmailLinkButton",
mixins: [React.addons.PureRenderMixin],
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
roomData: React.PropTypes.object.isRequired
},
handleEmailButtonClick: function (event) {
event.preventDefault();
var roomData = this.props.roomData;
var contextURL = roomData.roomContextUrls && roomData.roomContextUrls[0];
if (contextURL) {
if (contextURL.location === null) {
contextURL = undefined;
} else {
contextURL = sharedUtils.formatURL(contextURL.location).hostname;
}
}
this.props.dispatcher.dispatch(new sharedActions.EmailRoomUrl({
roomUrl: roomData.roomUrl,
roomDescription: contextURL,
from: this.props.locationForMetrics
}));
this.props.callback && this.props.callback();
},
render: function () {
return React.createElement(
"div",
{ className: "btn-email invite-button",
onClick: this.handleEmailButtonClick },
React.createElement("img", { src: "shared/img/glyph-email-16x16.svg" }),
React.createElement(
"p",
null,
mozL10n.get("invite_email_link_button")
)
);
}
});
var FacebookShareButton = React.createClass({
displayName: "FacebookShareButton",
mixins: [React.addons.PureRenderMixin],
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
roomData: React.PropTypes.object.isRequired
},
handleFacebookButtonClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.FacebookShareRoomUrl({
from: this.props.locationForMetrics,
roomUrl: this.props.roomData.roomUrl
}));
this.props.callback && this.props.callback();
},
render: function () {
return React.createElement(
"div",
{ className: "btn-facebook invite-button",
onClick: this.handleFacebookButtonClick },
React.createElement("img", { src: "shared/img/glyph-facebook-16x16.svg" }),
React.createElement(
"p",
null,
mozL10n.get("invite_facebook_button3")
)
);
}
});
var SharePanelView = React.createClass({
displayName: "SharePanelView",
mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
facebookEnabled: React.PropTypes.bool.isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array
},
render: function () {
if (!this.props.show || !this.props.roomData.roomUrl) {
return null;
}
var cx = classNames;
return React.createElement(
"div",
{ className: "room-invitation-overlay" },
React.createElement(
"div",
{ className: "room-invitation-content" },
React.createElement(
"div",
{ className: "room-context-header" },
mozL10n.get("invite_header_text_bold2")
),
React.createElement(
"div",
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(
"div",
{ className: "input-group group-item-top" },
React.createElement("input", { readOnly: true, type: "text", value: this.props.roomData.roomUrl })
),
React.createElement(CopyLinkButton, {
callback: this.props.callback,
dispatcher: this.props.dispatcher,
locationForMetrics: this.props.locationForMetrics,
roomData: this.props.roomData })
)
),
React.createElement(
"div",
{ className: cx({
"btn-group": true,
"share-action-group": true
}) },
React.createElement(EmailLinkButton, {
callback: this.props.callback,
dispatcher: this.props.dispatcher,
locationForMetrics: this.props.locationForMetrics,
roomData: this.props.roomData }),
(() => {
if (this.props.facebookEnabled) {
return React.createElement(FacebookShareButton, {
callback: this.props.callback,
dispatcher: this.props.dispatcher,
locationForMetrics: this.props.locationForMetrics,
roomData: this.props.roomData });
}
})()
),
React.createElement(SocialShareDropdown, {
dispatcher: this.props.dispatcher,
ref: "menu",
roomUrl: this.props.roomData.roomUrl,
show: this.state.showMenu,
socialShareProviders: this.props.socialShareProviders })
);
}
});
var SocialShareDropdown = React.createClass({
displayName: "SocialShareDropdown",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomUrl: React.PropTypes.string,
show: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array
},
handleAddServiceClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
},
handleProviderClick: function (event) {
event.preventDefault();
var origin = event.currentTarget.dataset.provider;
var provider = this.props.socialShareProviders.filter(function (socialProvider) {
return socialProvider.origin === origin;
})[0];
this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
provider: provider,
roomUrl: this.props.roomUrl,
previews: []
}));
},
render: function () {
// Don't render a thing when no data has been fetched yet.
if (!this.props.socialShareProviders) {
return null;
}
var cx = classNames;
var shareDropdown = cx({
"share-service-dropdown": true,
"dropdown-menu": true,
"visually-hidden": true,
"hide": !this.props.show
});
return React.createElement(
"ul",
{ className: shareDropdown },
React.createElement(
"li",
{ className: "dropdown-menu-item", onClick: this.handleAddServiceClick },
React.createElement("i", { className: "icon icon-add-share-service" }),
React.createElement(
"span",
null,
mozL10n.get("share_add_service_button")
)
),
this.props.socialShareProviders.length ? React.createElement("li", { className: "dropdown-menu-separator" }) : null,
this.props.socialShareProviders.map(function (provider, idx) {
return React.createElement(
"li",
{ className: "dropdown-menu-item",
"data-provider": provider.origin,
key: "provider-" + idx,
onClick: this.handleProviderClick },
React.createElement("img", { className: "icon", src: provider.iconURL }),
React.createElement(
"span",
null,
provider.name
)
);
}.bind(this))
);
}
});
return {
CopyLinkButton: CopyLinkButton,
EmailLinkButton: EmailLinkButton,
FacebookShareButton: FacebookShareButton,
SharePanelView: SharePanelView,
SocialShareDropdown: SocialShareDropdown
};
}(navigator.mozL10n || document.mozL10n);

Просмотреть файл

@ -6,10 +6,11 @@ var loop = loop || {};
loop.panel = function (_, mozL10n) {
"use strict";
var sharedViews = loop.shared.views;
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
var sharedDesktopViews = loop.shared.desktopViews;
var sharedViews = loop.shared.views;
var Button = sharedViews.Button;
// XXX This must be kept in sync with the number in MozLoopService.jsm.
@ -21,15 +22,41 @@ loop.panel = function (_, mozL10n) {
mixins: [sharedMixins.WindowCloseMixin],
propTypes: {
displayRoomListContent: React.PropTypes.bool
},
getDefaultProps: function () {
return { displayRoomListContent: false };
},
handleButtonClick: function () {
loop.request("OpenGettingStartedTour");
this.closeWindow();
},
renderGettingStartedButton: function () {
if (!this.props.displayRoomListContent) {
return React.createElement(
"div",
{ className: "fte-button-container" },
React.createElement(Button, { additionalClass: "fte-get-started-button",
caption: mozL10n.get("first_time_experience_button_label2"),
htmlId: "fte-button",
onClick: this.handleButtonClick })
);
}
},
render: function () {
var fteClasses = classNames({
"fte-get-started-content": true,
"fte-get-started-content-borders": this.props.displayRoomListContent
});
return React.createElement(
"div",
{ className: "fte-get-started-content" },
{ className: fteClasses },
React.createElement(
"div",
{ className: "fte-title" },
@ -37,24 +64,17 @@ loop.panel = function (_, mozL10n) {
React.createElement(
"div",
{ className: "fte-subheader" },
mozL10n.get("first_time_experience_subheading2")
this.props.displayRoomListContent ? mozL10n.get("first_time_experience_subheading_button_above") : mozL10n.get("first_time_experience_subheading2")
),
React.createElement("hr", { className: "fte-separator" }),
this.props.displayRoomListContent ? null : React.createElement("hr", { className: "fte-separator" }),
React.createElement(
"div",
{ className: "fte-content" },
mozL10n.get("first_time_experience_content")
this.props.displayRoomListContent ? mozL10n.get("first_time_experience_content2") : mozL10n.get("first_time_experience_content")
),
React.createElement("img", { className: "fte-hello-web-share", src: "shared/img/hello-web-share.svg" })
),
React.createElement(
"div",
{ className: "fte-button-container" },
React.createElement(Button, { additionalClass: "fte-get-started-button",
caption: mozL10n.get("first_time_experience_button_label2"),
htmlId: "fte-button",
onClick: this.handleButtonClick })
)
this.renderGettingStartedButton()
);
}
});
@ -226,17 +246,15 @@ loop.panel = function (_, mozL10n) {
getInitialState: function () {
return {
signedIn: !!loop.getStoredRequest(["GetUserProfile"]),
fxAEnabled: loop.getStoredRequest(["GetFxAEnabled"]),
doNotDisturb: loop.getStoredRequest(["GetDoNotDisturb"])
};
},
componentWillUpdate: function (nextProps, nextState) {
if (nextState.showMenu !== this.state.showMenu) {
loop.requestMulti(["GetUserProfile"], ["GetFxAEnabled"], ["GetDoNotDisturb"]).then(function (results) {
loop.requestMulti(["GetUserProfile"], ["GetDoNotDisturb"]).then(function (results) {
this.setState({
signedIn: !!results[0],
fxAEnabled: results[1],
doNotDisturb: results[2]
});
}.bind(this));
@ -315,7 +333,7 @@ loop.panel = function (_, mozL10n) {
label: mozL10n.get(notificationsLabel),
onClick: this.handleToggleNotifications }),
React.createElement(SettingsDropdownEntry, {
displayed: this.state.signedIn && this.state.fxAEnabled,
displayed: this.state.signedIn,
extraCSSClass: "entry-settings-account",
label: mozL10n.get("settings_menu_item_account"),
onClick: this.handleClickAccountEntry }),
@ -327,8 +345,7 @@ loop.panel = function (_, mozL10n) {
React.createElement(SettingsDropdownEntry, { extraCSSClass: "entry-settings-feedback",
label: mozL10n.get("settings_menu_item_feedback"),
onClick: this.handleSubmitFeedback }),
React.createElement(SettingsDropdownEntry, { displayed: this.state.fxAEnabled,
extraCSSClass: accountEntryCSSClass,
React.createElement(SettingsDropdownEntry, { extraCSSClass: accountEntryCSSClass,
label: this.state.signedIn ? mozL10n.get("settings_menu_item_signout") : mozL10n.get("settings_menu_item_signin"),
onClick: this.handleClickAuthEntry }),
React.createElement(SettingsDropdownEntry, { extraCSSClass: "entry-settings-help",
@ -348,7 +365,6 @@ loop.panel = function (_, mozL10n) {
mixins: [sharedMixins.WindowCloseMixin],
propTypes: {
fxAEnabled: React.PropTypes.bool.isRequired,
userProfile: userProfileValidator
},
@ -358,10 +374,6 @@ loop.panel = function (_, mozL10n) {
},
render: function () {
if (!this.props.fxAEnabled) {
return null;
}
if (this.props.userProfile && this.props.userProfile.email) {
return React.createElement(
"div",
@ -451,17 +463,34 @@ loop.panel = function (_, mozL10n) {
getInitialState: function () {
return {
eventPosY: 0
editMode: false,
eventPosY: 0,
newRoomName: this._getRoomTitle()
};
},
_getRoomTitle: function () {
var urlData = (this.props.room.decryptedContext.urls || [])[0] || {};
return this.props.room.decryptedContext.roomName || urlData.description || urlData.location || mozL10n.get("room_name_untitled_page");
},
_isActive: function () {
return this.props.room.participants.length > 0;
},
componentDidUpdate: function () {
if (this.state.editMode) {
this.getDOMNode().querySelector(".edit-room-input").focus();
}
},
handleClickEntry: function (event) {
event.preventDefault();
if (this.state.editMode) {
return;
}
this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
roomToken: this.props.room.roomToken
}));
@ -490,6 +519,37 @@ loop.panel = function (_, mozL10n) {
this.toggleDropdownMenu();
},
handleEditButtonClick: function (e) {
e.preventDefault();
e.stopPropagation();
// Enter Edit mode
this.setState({ editMode: true });
this.toggleDropdownMenu();
},
/**
* Handles a key being pressed - looking for the return key for saving
* the new room name.
*/
handleKeyDown: function (event) {
if (event.which === 13) {
this.exitEditMode();
}
},
exitEditMode: function () {
this.props.dispatcher.dispatch(new sharedActions.UpdateRoomContext({
roomToken: this.props.room.roomToken,
newRoomName: this.state.newRoomName
}));
this.setState({ editMode: false });
},
handleEditInputChange: function (event) {
this.setState({ newRoomName: event.target.value });
},
/**
* Callback called when moving cursor away from the conversation entry.
* Will close the dropdown menu.
@ -506,8 +566,7 @@ loop.panel = function (_, mozL10n) {
"room-active": this._isActive(),
"room-opened": this.props.isOpenedRoom
});
var urlData = (this.props.room.decryptedContext.urls || [])[0] || {};
var roomTitle = this.props.room.decryptedContext.roomName || urlData.description || urlData.location || mozL10n.get("room_name_untitled_page");
var roomTitle = this._getRoomTitle();
return React.createElement(
"div",
@ -517,15 +576,22 @@ loop.panel = function (_, mozL10n) {
ref: "roomEntry" },
React.createElement(RoomEntryContextItem, {
roomUrls: this.props.room.decryptedContext.urls }),
React.createElement(
!this.state.editMode ? React.createElement(
"h2",
null,
roomTitle
),
this.props.isOpenedRoom ? null : React.createElement(RoomEntryContextButtons, {
) : React.createElement("input", {
className: "edit-room-input",
onBlur: this.exitEditMode,
onChange: this.handleEditInputChange,
onKeyDown: this.handleKeyDown,
type: "text",
value: this.state.newRoomName }),
this.props.isOpenedRoom || this.state.editMode ? null : React.createElement(RoomEntryContextButtons, {
dispatcher: this.props.dispatcher,
eventPosY: this.state.eventPosY,
handleClick: this.handleClick,
handleEditButtonClick: this.handleEditButtonClick,
ref: "contextActions",
room: this.props.room,
showMenu: this.state.showMenu,
@ -546,6 +612,7 @@ loop.panel = function (_, mozL10n) {
dispatcher: React.PropTypes.object.isRequired,
eventPosY: React.PropTypes.number.isRequired,
handleClick: React.PropTypes.func.isRequired,
handleEditButtonClick: React.PropTypes.func.isRequired,
room: React.PropTypes.object.isRequired,
showMenu: React.PropTypes.bool.isRequired,
toggleDropdownMenu: React.PropTypes.func.isRequired
@ -598,12 +665,40 @@ loop.panel = function (_, mozL10n) {
eventPosY: this.props.eventPosY,
handleCopyButtonClick: this.handleCopyButtonClick,
handleDeleteButtonClick: this.handleDeleteButtonClick,
handleEditButtonClick: this.props.handleEditButtonClick,
handleEmailButtonClick: this.handleEmailButtonClick,
ref: "menu" }) : null
);
}
});
/**
* Compute Adjusted Top Position for Menu Dropdown
* Extracted from react component so we could run unit test against it
* takes clickYPos, menuNodeHeight, listTop, listHeight, clickOffset
* parameters, determines whether menu should be above or below click
* position and calculates where the top of the dropdown menu
* should reside. If less than 0, which will result in the top of the dropdown
* being cutoff, will set top position to 0
*/
function computeAdjustedTopPosition(clickYPos, menuNodeHeight, listTop, listHeight, clickOffset) {
var topPos = 0;
if (clickYPos + menuNodeHeight >= listTop + listHeight) {
// Position above click area.
topPos = clickYPos - menuNodeHeight - listTop - clickOffset;
} else {
// Position below click area.
topPos = clickYPos - listTop + clickOffset;
}
// Ensure menu is not cut off at top
if (topPos < 0) {
topPos = 0;
}
return topPos;
}
/**
* Dropdown menu for each conversation entry.
* Because the container element has overflow we need to position the menu
@ -618,6 +713,7 @@ loop.panel = function (_, mozL10n) {
eventPosY: React.PropTypes.number.isRequired,
handleCopyButtonClick: React.PropTypes.func.isRequired,
handleDeleteButtonClick: React.PropTypes.func.isRequired,
handleEditButtonClick: React.PropTypes.func.isRequired,
handleEmailButtonClick: React.PropTypes.func.isRequired
},
@ -637,15 +733,9 @@ loop.panel = function (_, mozL10n) {
var listNodeRect = listNode.getBoundingClientRect();
// Click offset to not display the menu right next to the area clicked.
var offset = 10;
var topPos = computeAdjustedTopPosition(this.props.eventPosY, menuNodeRect.height, listNodeRect.top, listNodeRect.height, 10);
if (this.props.eventPosY + menuNodeRect.height >= listNodeRect.top + listNodeRect.height) {
// Position above click area.
menuNode.style.top = this.props.eventPosY - menuNodeRect.height - listNodeRect.top - offset + "px";
} else {
// Position below click area.
menuNode.style.top = this.props.eventPosY - listNodeRect.top + offset + "px";
}
menuNode.style.top = topPos + "px";
},
render: function () {
@ -673,6 +763,14 @@ loop.panel = function (_, mozL10n) {
ref: "emailButton" },
mozL10n.get("email_link_menuitem")
),
React.createElement(
"li",
{
className: "dropdown-menu-item",
onClick: this.props.handleEditButtonClick,
ref: "editButton" },
mozL10n.get("edit_name_menuitem")
),
React.createElement(
"li",
{
@ -725,14 +823,6 @@ loop.panel = function (_, mozL10n) {
this.stopListening(this.props.store);
},
componentWillUpdate: function (nextProps, nextState) {
// If we've just created a room, close the panel - the store will open
// the room.
if (this.state.pendingCreation && !nextState.pendingCreation && !nextState.error) {
this.closeWindow();
}
},
_onStoreStateChanged: function () {
this.setState(this.props.store.getStoreState());
},
@ -798,7 +888,7 @@ loop.panel = function (_, mozL10n) {
null,
mozL10n.get(this.state.openedRoom === null ? "rooms_list_recently_browsed2" : "rooms_list_currently_browsing2")
),
!this.state.rooms.length ? React.createElement("div", { className: "room-list-empty" }) : React.createElement(
!this.state.rooms.length ? React.createElement(GettingStartedView, { displayRoomListContent: true }) : React.createElement(
"div",
{ className: roomListClasses },
this.state.rooms.map(function (room) {
@ -936,6 +1026,118 @@ loop.panel = function (_, mozL10n) {
}
});
var SharePanelView = React.createClass({
displayName: "SharePanelView",
statics: {
SHOW_PANEL_DELAY: 300
},
mixins: [Backbone.Events, sharedMixins.DocumentVisibilityMixin, sharedMixins.WindowCloseMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// Force render for testing purpose
forceRender: React.PropTypes.bool,
onSharePanelDisplayChange: React.PropTypes.func.isRequired,
store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
},
getInitialState: function () {
return this.props.store.getStoreState();
},
componentWillMount: function () {
loop.request("GetLoopPref", "facebook.enabled").then(result => {
this.setState({
facebookEnabled: result
});
});
},
componentDidMount: function () {
this.listenTo(this.props.store, "change", this._onStoreStateChanged);
},
componentDidUpdate: function () {
if (this.state.showPanel) {
setTimeout(() => {
this.getDOMNode().classList.add("share-panel-open");
}, this.constructor.SHOW_PANEL_DELAY);
}
},
componentWillUnmount: function () {
this.stopListening(this.props.store);
},
_onStoreStateChanged: function () {
this.setState(this.props.store.getStoreState());
},
onDocumentHidden: function () {
this.state.showPanel && this.handleClosePanel();
},
componentWillUpdate: function (nextProps, nextState) {
// If we've just created a room, open the panel
if (this.state.pendingCreation && !nextState.pendingCreation && !nextState.error) {
this.props.onSharePanelDisplayChange();
this.setState({
showPanel: true
});
}
},
handleClosePanel: function () {
this.props.onSharePanelDisplayChange();
this.setState({
showPanel: false
});
this.openRoom();
this.closeWindow();
},
openRoom: function () {
var activeRoom = this.state.activeRoom;
this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
roomToken: activeRoom.roomToken
}));
},
render: function () {
if (!this.state.showPanel && !this.props.forceRender) {
return null;
}
var sharePanelClasses = classNames({
"share-panel-container": true,
"share-panel-open": this.props.forceRender
});
var activeRoom = this.state.activeRoom;
var roomData = {
roomUrl: activeRoom.roomUrl,
roomContextUrls: activeRoom.decryptedContext && activeRoom.decryptedContext.urls || []
};
return React.createElement(
"div",
{ className: sharePanelClasses },
React.createElement("div", { className: "share-panel-overlay", onClick: this.handleClosePanel }),
React.createElement(sharedDesktopViews.SharePanelView, {
callback: this.handleClosePanel,
dispatcher: this.props.dispatcher,
error: this.state.error,
facebookEnabled: this.state.facebookEnabled,
locationForMetrics: "panel",
roomData: roomData,
show: true,
socialShareProviders: this.state.socialShareProviders })
);
}
});
/**
* Panel view.
*/
@ -960,12 +1162,12 @@ loop.panel = function (_, mozL10n) {
getInitialState: function () {
return {
fxAEnabled: loop.getStoredRequest(["GetFxAEnabled"]),
hasEncryptionKey: loop.getStoredRequest(["GetHasEncryptionKey"]),
userProfile: loop.getStoredRequest(["GetUserProfile"]),
gettingStartedSeen: loop.getStoredRequest(["GetLoopPref", "gettingStarted.latestFTUVersion"]) >= FTU_VERSION,
multiProcessActive: loop.getStoredRequest(["IsMultiProcessActive"]),
remoteAutoStart: loop.getStoredRequest(["GetLoopPref", "remote.autostart"])
remoteAutoStart: loop.getStoredRequest(["GetLoopPref", "remote.autostart"]),
sharePanelOpened: false
};
},
@ -1055,6 +1257,12 @@ loop.panel = function (_, mozL10n) {
});
},
toggleSharePanelState: function () {
this.setState({
sharePanelOpened: !this.state.sharePanelOpened
});
},
render: function () {
var NotificationListView = sharedViews.NotificationListView;
@ -1078,26 +1286,38 @@ loop.panel = function (_, mozL10n) {
return React.createElement(SignInRequestView, null);
}
var cssClasses = classNames({
"panel-content": true,
"showing-share-panel": this.state.sharePanelOpened
});
return React.createElement(
"div",
{ className: "panel-content",
{ className: cssClasses,
onContextMenu: this.handleContextMenu },
React.createElement("div", { className: "beta-ribbon" }),
React.createElement(NotificationListView, {
clearOnDocumentHidden: true,
notifications: this.props.notifications }),
React.createElement(RoomList, { dispatcher: this.props.dispatcher,
store: this.props.roomStore }),
React.createElement(
"div",
{ className: "footer" },
React.createElement(AccountLink, { fxAEnabled: this.state.fxAEnabled,
userProfile: this.props.userProfile || this.state.userProfile }),
{ className: "panel-container" },
React.createElement(RoomList, { dispatcher: this.props.dispatcher,
store: this.props.roomStore }),
React.createElement(
"div",
{ className: "signin-details" },
React.createElement(SettingsDropdown, null)
)
{ className: "footer" },
React.createElement(AccountLink, { userProfile: this.props.userProfile || this.state.userProfile }),
React.createElement(
"div",
{ className: "signin-details" },
React.createElement(SettingsDropdown, null)
)
),
React.createElement(SharePanelView, {
dispatcher: this.props.dispatcher,
onSharePanelDisplayChange: this.toggleSharePanelState,
store: this.props.roomStore })
)
);
}
@ -1108,7 +1328,7 @@ loop.panel = function (_, mozL10n) {
*/
function init() {
var requests = [["GetAllConstants"], ["GetAllStrings"], ["GetLocale"], ["GetPluralRule"]];
var prefetch = [["GetLoopPref", "gettingStarted.latestFTUVersion"], ["GetLoopPref", "legal.ToS_url"], ["GetLoopPref", "legal.privacy_url"], ["GetLoopPref", "remote.autostart"], ["GetUserProfile"], ["GetFxAEnabled"], ["GetDoNotDisturb"], ["GetHasEncryptionKey"], ["IsMultiProcessActive"]];
var prefetch = [["GetLoopPref", "gettingStarted.latestFTUVersion"], ["GetLoopPref", "legal.ToS_url"], ["GetLoopPref", "legal.privacy_url"], ["GetLoopPref", "remote.autostart"], ["GetUserProfile"], ["GetDoNotDisturb"], ["GetHasEncryptionKey"], ["IsMultiProcessActive"]];
return loop.requestMulti.apply(null, requests.concat(prefetch)).then(function (results) {
// `requestIdx` is keyed off the order of the `requests` and `prefetch`
@ -1163,6 +1383,7 @@ loop.panel = function (_, mozL10n) {
return {
AccountLink: AccountLink,
computeAdjustedTopPosition: computeAdjustedTopPosition,
ConversationDropdown: ConversationDropdown,
E10sNotSupported: E10sNotSupported,
GettingStartedView: GettingStartedView,
@ -1173,6 +1394,7 @@ loop.panel = function (_, mozL10n) {
RoomEntryContextButtons: RoomEntryContextButtons,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
SharePanelView: SharePanelView,
SignInRequestView: SignInRequestView,
ToSView: ToSView
};

Просмотреть файл

@ -269,7 +269,9 @@ loop.store = loop.store || {};
}
this.dispatchAction(new sharedActions.CreatedRoom({
roomToken: result.roomToken
decryptedContext: result.decryptedContext,
roomToken: result.roomToken,
roomUrl: result.roomUrl
}));
loop.request("TelemetryAddValue", "LOOP_ROOM_CREATE", buckets.CREATE_SUCCESS);
}.bind(this));
@ -279,12 +281,14 @@ loop.store = loop.store || {};
* Executed when a room has been created
*/
createdRoom: function(actionData) {
this.setStoreState({ pendingCreation: false });
// Opens the newly created room
this.dispatchAction(new sharedActions.OpenRoom({
roomToken: actionData.roomToken
}));
this.setStoreState({
activeRoom: {
decryptedContext: actionData.decryptedContext,
roomToken: actionData.roomToken,
roomUrl: actionData.roomUrl
},
pendingCreation: false
});
},
/**
@ -324,7 +328,7 @@ loop.store = loop.store || {};
}
loop.requestMulti(
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_MAU", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
);
},
@ -348,7 +352,7 @@ loop.store = loop.store || {};
loop.requestMulti(
["NotifyUITour", "Loop:RoomURLEmailed"],
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_MAU", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
);
},
@ -387,7 +391,7 @@ loop.store = loop.store || {};
}
loop.requestMulti(
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_MAU", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
);
},
@ -445,7 +449,7 @@ loop.store = loop.store || {};
loop.requestMulti(
["TelemetryAddValue", "LOOP_ROOM_DELETE", buckets[isError ?
"DELETE_FAIL" : "DELETE_SUCCESS"]],
["TelemetryAddValue", "LOOP_MAU", this._constants.LOOP_MAU_TYPE.ROOM_DELETE]
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_DELETE]
);
}.bind(this));
},
@ -511,7 +515,7 @@ loop.store = loop.store || {};
openRoom: function(actionData) {
loop.requestMulti(
["Rooms:Open", actionData.roomToken],
["TelemetryAddValue", "LOOP_MAU", this._constants.LOOP_MAU_TYPE.ROOM_OPEN]
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_OPEN]
);
},

Просмотреть файл

@ -10,7 +10,7 @@ loop.roomViews = function (mozL10n) {
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins;
var sharedUtils = loop.shared.utils;
var sharedDesktopViews = loop.shared.desktopViews;
var sharedViews = loop.shared.views;
/**
@ -161,262 +161,6 @@ loop.roomViews = function (mozL10n) {
}
});
var SocialShareDropdown = React.createClass({
displayName: "SocialShareDropdown",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomUrl: React.PropTypes.string,
show: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array
},
handleAddServiceClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
},
handleProviderClick: function (event) {
event.preventDefault();
var origin = event.currentTarget.dataset.provider;
var provider = this.props.socialShareProviders.filter(function (socialProvider) {
return socialProvider.origin === origin;
})[0];
this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
provider: provider,
roomUrl: this.props.roomUrl,
previews: []
}));
},
render: function () {
// Don't render a thing when no data has been fetched yet.
if (!this.props.socialShareProviders) {
return null;
}
var cx = classNames;
var shareDropdown = cx({
"share-service-dropdown": true,
"dropdown-menu": true,
"visually-hidden": true,
"hide": !this.props.show
});
return React.createElement(
"ul",
{ className: shareDropdown },
React.createElement(
"li",
{ className: "dropdown-menu-item", onClick: this.handleAddServiceClick },
React.createElement("i", { className: "icon icon-add-share-service" }),
React.createElement(
"span",
null,
mozL10n.get("share_add_service_button")
)
),
this.props.socialShareProviders.length ? React.createElement("li", { className: "dropdown-menu-separator" }) : null,
this.props.socialShareProviders.map(function (provider, idx) {
return React.createElement(
"li",
{ className: "dropdown-menu-item",
"data-provider": provider.origin,
key: "provider-" + idx,
onClick: this.handleProviderClick },
React.createElement("img", { className: "icon", src: provider.iconURL }),
React.createElement(
"span",
null,
provider.name
)
);
}.bind(this))
);
}
});
/**
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({
displayName: "DesktopRoomInvitationView",
statics: {
TRIGGERED_RESET_DELAY: 3000
},
mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
facebookEnabled: React.PropTypes.bool.isRequired,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array
},
getInitialState: function () {
return {
copiedUrl: false,
newRoomName: ""
};
},
handleEmailButtonClick: function (event) {
event.preventDefault();
var roomData = this.props.roomData;
var contextURL = roomData.roomContextUrls && roomData.roomContextUrls[0];
if (contextURL) {
if (contextURL.location === null) {
contextURL = undefined;
} else {
contextURL = sharedUtils.formatURL(contextURL.location).hostname;
}
}
this.props.dispatcher.dispatch(new sharedActions.EmailRoomUrl({
roomUrl: roomData.roomUrl,
roomDescription: contextURL,
from: "conversation"
}));
},
handleFacebookButtonClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.FacebookShareRoomUrl({
from: "conversation",
roomUrl: this.props.roomData.roomUrl
}));
},
handleCopyButtonClick: function (event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
roomUrl: this.props.roomData.roomUrl,
from: "conversation"
}));
this.setState({ copiedUrl: true });
setTimeout(this.resetTriggeredButtons, this.constructor.TRIGGERED_RESET_DELAY);
},
/**
* Reset state of triggered buttons if necessary
*/
resetTriggeredButtons: function () {
if (this.state.copiedUrl) {
this.setState({ copiedUrl: false });
}
},
render: function () {
if (!this.props.show || !this.props.roomData.roomUrl) {
return null;
}
var cx = classNames;
return React.createElement(
"div",
{ className: "room-invitation-overlay" },
React.createElement(
"div",
{ className: "room-invitation-content" },
React.createElement(
"div",
{ className: "room-context-header" },
mozL10n.get("invite_header_text_bold2")
),
React.createElement(
"div",
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(
"div",
{ className: "input-group group-item-top" },
React.createElement("input", { readOnly: true, type: "text", value: this.props.roomData.roomUrl })
),
React.createElement(
"div",
{ className: cx({
"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")
)
)
),
React.createElement(
"div",
{ className: cx({
"btn-group": true,
"share-action-group": true
}) },
React.createElement(
"div",
{ className: "btn-email invite-button",
onClick: this.handleEmailButtonClick,
onMouseOver: this.resetTriggeredButtons },
React.createElement("img", { src: "shared/img/glyph-email-16x16.svg" }),
React.createElement(
"div",
null,
mozL10n.get("invite_email_link_button")
)
),
(() => {
if (this.props.facebookEnabled) {
return React.createElement(
"div",
{ className: "btn-facebook invite-button",
onClick: this.handleFacebookButtonClick,
onMouseOver: this.resetTriggeredButtons },
React.createElement("img", { src: "shared/img/glyph-facebook-16x16.svg" }),
React.createElement(
"div",
null,
mozL10n.get("invite_facebook_button3")
)
);
}
})()
),
React.createElement(SocialShareDropdown, {
dispatcher: this.props.dispatcher,
ref: "menu",
roomUrl: this.props.roomData.roomUrl,
show: this.state.showMenu,
socialShareProviders: this.props.socialShareProviders })
);
}
});
/**
* Desktop room conversation view.
*/
@ -449,8 +193,10 @@ loop.roomViews = function (mozL10n) {
}));
}
// Automatically start sharing a tab now we're ready to share.
if (this.state.roomState !== ROOM_STATES.SESSION_CONNECTED && nextState.roomState === ROOM_STATES.SESSION_CONNECTED) {
// Now that we're ready to share, automatically start sharing a tab only
// if we're not already connected to the room via the sdk, e.g. not in the
// case a remote participant just left.
if (nextState.roomState === ROOM_STATES.SESSION_CONNECTED && !(this.state.roomState === ROOM_STATES.SESSION_CONNECTED || this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS)) {
this.props.dispatcher.dispatch(new sharedActions.StartBrowserShare());
}
},
@ -460,8 +206,14 @@ loop.roomViews = function (mozL10n) {
*/
leaveRoom: function () {
if (this.state.used) {
// The room has been used, so we might want to display the feedback view.
// Therefore we leave the room to ensure that we have stopped sharing etc,
// then trigger the call terminated handler that'll do the right thing
// for the feedback view.
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
this.props.onCallTerminated();
} else {
// If the room hasn't been used, we simply close the window.
this.closeWindow();
}
},
@ -547,13 +299,6 @@ loop.roomViews = function (mozL10n) {
return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS && !this.state.remoteSrcMediaElement && !this.state.mediaConnected);
},
componentDidUpdate: function (prevProps, prevState) {
// Handle timestamp and window closing only when the call has terminated.
if (prevState.roomState === ROOM_STATES.ENDED && this.state.roomState === ROOM_STATES.ENDED) {
this.props.onCallTerminated();
}
},
handleContextMenu: function (e) {
e.preventDefault();
},
@ -611,17 +356,18 @@ loop.roomViews = function (mozL10n) {
screenShareMediaElement: this.state.screenShareMediaElement,
screenSharePosterUrl: null,
showInitialContext: false,
useDesktopPaths: true },
showTile: false },
React.createElement(sharedViews.ConversationToolbar, {
audio: { enabled: !this.state.audioMuted, visible: true },
dispatcher: this.props.dispatcher,
hangup: this.leaveRoom,
showHangup: this.props.chatWindowDetached,
video: { enabled: !this.state.videoMuted, visible: true } }),
React.createElement(DesktopRoomInvitationView, {
React.createElement(sharedDesktopViews.SharePanelView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
facebookEnabled: this.props.facebookEnabled,
locationForMetrics: "conversation",
roomData: roomData,
show: shouldRenderInvitationOverlay,
socialShareProviders: this.state.socialShareProviders })
@ -636,8 +382,6 @@ loop.roomViews = function (mozL10n) {
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
FailureInfoView: FailureInfoView,
RoomFailureView: RoomFailureView,
SocialShareDropdown: SocialShareDropdown,
DesktopRoomConversationView: DesktopRoomConversationView,
DesktopRoomInvitationView: DesktopRoomInvitationView
DesktopRoomConversationView: DesktopRoomConversationView
};
}(document.mozL10n || navigator.mozL10n);

Просмотреть файл

@ -49,8 +49,10 @@ loop.slideshow = function (mozL10n) {
}, {
id: "slide2",
imageClass: "slide2-image",
title: mozL10n.get("fte_slide_2_title"),
text: mozL10n.get("fte_slide_2_copy")
title: mozL10n.get("fte_slide_2_title2"),
text: mozL10n.get("fte_slide_2_copy2", {
clientShortname2: mozL10n.get("clientShortname2")
})
}, {
id: "slide3",
imageClass: "slide3-image",

Просмотреть файл

@ -9,6 +9,7 @@
<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/css/panel.css">
<link rel="stylesheet" type="text/css" href="panels/css/desktop.css">
</head>
<body class="panel">
@ -36,6 +37,7 @@
<!-- 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/desktopViews.js"></script>
<script type="text/javascript" src="panels/js/panel.js"></script>
</body>
</html>

Просмотреть файл

@ -6,10 +6,12 @@ describe("loop.store.ConversationAppStore", function() {
var expect = chai.expect;
var sharedActions = loop.shared.actions;
var sandbox, activeRoomStore, dispatcher, roomUsed;
var sandbox, activeRoomStore, dispatcher, feedbackPeriodMs, roomUsed;
beforeEach(function() {
roomUsed = false;
feedbackPeriodMs = 15770000000;
activeRoomStore = {
getStoreState: function() { return { used: roomUsed }; }
};
@ -219,29 +221,25 @@ describe("loop.store.ConversationAppStore", function() {
});
describe("#LoopHangupNowHandler", function() {
beforeEach(function() {
sandbox.stub(loop.shared.mixins.WindowCloseMixin, "closeWindow");
});
it("should dispatch the correct action when a room was used", function() {
store.setStoreState({ windowType: "room" });
roomUsed = true;
it("should dispatch a LeaveConversation action", function() {
store.LoopHangupNowHandler();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.LeaveRoom());
sinon.assert.notCalled(loop.shared.mixins.WindowCloseMixin.closeWindow);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveConversation());
});
});
describe("#leaveConversation", function() {
beforeEach(function() {
sandbox.stub(loop.shared.mixins.WindowCloseMixin, "closeWindow");
roomUsed = true;
});
it("should close the window when a room was used and it showed feedback", function() {
store.setStoreState({
showFeedbackForm: true,
windowType: "room"
});
roomUsed = true;
it("should close the window for window types other than `room`", function() {
store.setStoreState({ windowType: "foobar" });
store.LoopHangupNowHandler();
store.leaveConversation();
sinon.assert.notCalled(dispatcher.dispatch);
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
@ -249,21 +247,97 @@ describe("loop.store.ConversationAppStore", function() {
it("should close the window when a room was not used", function() {
store.setStoreState({ windowType: "room" });
roomUsed = false;
store.LoopHangupNowHandler();
store.leaveConversation();
sinon.assert.notCalled(dispatcher.dispatch);
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
});
it("should close the window for all other window types", function() {
store.setStoreState({ windowType: "foobar" });
it("should close the window when a room was used and it showed feedback", function() {
store.setStoreState({
showFeedbackForm: true,
windowType: "room"
});
store.LoopHangupNowHandler();
store.leaveConversation();
sinon.assert.notCalled(dispatcher.dispatch);
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
});
it("should dispatch a LeaveRoom action if timestamp is 0", function() {
store.setStoreState({
feedbackTimestamp: 0,
windowType: "room"
});
store.leaveConversation();
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveRoom({
windowStayingOpen: true
}));
});
it("should dispatch a ShowFeedbackForm action if timestamp is 0", function() {
store.setStoreState({
feedbackTimestamp: 0,
windowType: "room"
});
store.leaveConversation();
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShowFeedbackForm());
});
it("should dispatch a LeaveRoom action if delta > feedback period", function() {
var feedbackTimestamp = new Date() - feedbackPeriodMs;
store.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs,
windowType: "room"
});
store.leaveConversation();
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveRoom({
windowStayingOpen: true
}));
});
it("should dispatch a ShowFeedbackForm action if delta > feedback period", function() {
var feedbackTimestamp = new Date() - feedbackPeriodMs;
store.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs,
windowType: "room"
});
store.leaveConversation();
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShowFeedbackForm());
});
it("should close the window if delta < feedback period", function() {
var feedbackTimestamp = new Date().getTime();
store.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs
});
store.leaveConversation();
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
});
});
describe("#socialFrameAttachedHandler", function() {

Просмотреть файл

@ -160,15 +160,17 @@ describe("loop.conversation", function() {
sinon.assert.calledOnce(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"],
"LOOP_MAU", constants.LOOP_MAU_TYPE.OPEN_CONVERSATION);
"LOOP_ACTIVITY_COUNTER", constants.LOOP_MAU_TYPE.OPEN_CONVERSATION);
});
});
describe("AppControllerView", function() {
var activeRoomStore, ccView, addRemoteCursorStub;
var activeRoomStore,
ccView,
addRemoteCursorStub,
clickRemoteCursorStub;
var conversationAppStore,
roomStore,
feedbackPeriodMs = 15770000000;
roomStore;
var ROOM_STATES = loop.store.ROOM_STATES;
function mountTestComponent() {
@ -205,9 +207,18 @@ describe("loop.conversation", function() {
});
addRemoteCursorStub = sandbox.stub();
clickRemoteCursorStub = sandbox.stub();
LoopMochaUtils.stubLoopRequest({
AddRemoteCursorOverlay: addRemoteCursorStub
AddRemoteCursorOverlay: addRemoteCursorStub,
ClickRemoteCursor: clickRemoteCursorStub
});
loop.config = {
tilesIframeUrl: null,
tilesSupportUrl: null
};
sinon.stub(dispatcher, "dispatch");
});
afterEach(function() {
@ -240,6 +251,26 @@ describe("loop.conversation", function() {
sinon.assert.notCalled(addRemoteCursorStub);
});
it("should request ClickRemoteCursor when click event detected", function() {
mountTestComponent();
remoteCursorStore.setStoreState({
"remoteCursorClick": true
});
sinon.assert.calledOnce(clickRemoteCursorStub);
});
it("should NOT request ClickRemoteCursor when reset click on store", function() {
mountTestComponent();
remoteCursorStore.setStoreState({
"remoteCursorClick": false
});
sinon.assert.notCalled(clickRemoteCursorStub);
});
it("should display the RoomView for rooms", function() {
conversationAppStore.setStoreState({ windowType: "room" });
activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
@ -297,79 +328,14 @@ describe("loop.conversation", function() {
TestUtils.findRenderedComponentWithType(ccView, FeedbackView);
});
it("should dispatch a ShowFeedbackForm action if timestamp is 0",
function() {
conversationAppStore.setStoreState({ feedbackTimestamp: 0 });
sandbox.stub(dispatcher, "dispatch");
ccView = mountTestComponent();
ccView.handleCallTerminated();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShowFeedbackForm());
});
it("should set feedback timestamp if delta is > feedback period",
function() {
var feedbackTimestamp = new Date() - feedbackPeriodMs;
conversationAppStore.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs
});
ccView = mountTestComponent();
ccView.handleCallTerminated();
sinon.assert.calledOnce(setLoopPrefStub);
});
it("should dispatch a ShowFeedbackForm action if delta > feedback period",
function() {
var feedbackTimestamp = new Date() - feedbackPeriodMs;
conversationAppStore.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs
});
sandbox.stub(dispatcher, "dispatch");
ccView = mountTestComponent();
ccView.handleCallTerminated();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShowFeedbackForm());
});
it("should close the window if delta < feedback period", function() {
var feedbackTimestamp = new Date().getTime();
conversationAppStore.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs
});
it("should dispatch LeaveConversation when handleCallTerminated is called", function() {
ccView = mountTestComponent();
var closeWindowStub = sandbox.stub(ccView, "closeWindow");
ccView.handleCallTerminated();
sinon.assert.calledOnce(closeWindowStub);
});
it("should set the correct timestamp for dateLastSeenSec", function() {
var feedbackTimestamp = new Date().getTime();
conversationAppStore.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs
});
ccView = mountTestComponent();
var closeWindowStub = sandbox.stub(ccView, "closeWindow");
ccView.handleCallTerminated();
sinon.assert.calledOnce(closeWindowStub);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveConversation());
});
});
});

Просмотреть файл

@ -0,0 +1,367 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loop.shared.desktopViews", function() {
"use strict";
var expect = chai.expect;
var l10n = navigator.mozL10n || document.mozL10n;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedDesktopViews = loop.shared.desktopViews;
var sandbox, clock, dispatcher;
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
clock = sandbox.useFakeTimers(); // exposes sandbox.clock as a fake timer
sandbox.stub(l10n, "get", function(x) {
return "translated:" + x;
});
LoopMochaUtils.stubLoopRequest({
GetLoopPref: function() {
return true;
}
});
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
});
afterEach(function() {
sandbox.restore();
LoopMochaUtils.restore();
});
describe("CopyLinkButton", function() {
var view;
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
locationForMetrics: "conversation",
roomData: {
roomUrl: "http://invalid",
roomContextUrls: [{
location: "fakeLocation",
url: "fakeUrl"
}]
}
}, props || {});
return TestUtils.renderIntoDocument(
React.createElement(sharedDesktopViews.CopyLinkButton, props));
}
beforeEach(function() {
view = mountTestComponent();
});
it("should dispatch a CopyRoomUrl action when the copy button is pressed", function() {
var copyBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(copyBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
roomUrl: "http://invalid",
from: "conversation"
}));
});
it("should change the text when the url has been copied", function() {
var copyBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(copyBtn);
expect(copyBtn.textContent).eql("translated:invite_copied_link_button");
});
it("should keep the text for a while after the url has been copied", function() {
var copyBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(copyBtn);
clock.tick(sharedDesktopViews.CopyLinkButton.TRIGGERED_RESET_DELAY / 2);
expect(copyBtn.textContent).eql("translated:invite_copied_link_button");
});
it("should reset the text a bit after the url has been copied", function() {
var copyBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(copyBtn);
clock.tick(sharedDesktopViews.CopyLinkButton.TRIGGERED_RESET_DELAY);
expect(copyBtn.textContent).eql("translated:invite_copy_link_button");
});
it("should invoke callback if defined", function() {
var callback = sinon.stub();
view = mountTestComponent({
callback: callback
});
var copyBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(copyBtn);
clock.tick(sharedDesktopViews.CopyLinkButton.TRIGGERED_RESET_DELAY);
sinon.assert.calledOnce(callback);
});
});
describe("EmailLinkButton", function() {
var view;
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
locationForMetrics: "conversation",
roomData: {
roomUrl: "http://invalid",
roomContextUrls: []
}
}, props || {});
return TestUtils.renderIntoDocument(
React.createElement(sharedDesktopViews.EmailLinkButton, props));
}
beforeEach(function() {
view = mountTestComponent();
});
it("should dispatch an EmailRoomUrl with no description" +
" for rooms without context when the email button is pressed",
function() {
var emailBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(emailBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.EmailRoomUrl({
roomUrl: "http://invalid",
roomDescription: undefined,
from: "conversation"
}));
});
it("should dispatch an EmailRoomUrl with a domain name description for rooms with context",
function() {
view = mountTestComponent({
roomData: {
roomUrl: "http://invalid",
roomContextUrls: [{ location: "http://www.mozilla.com/" }]
}
});
var emailBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(emailBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.EmailRoomUrl({
roomUrl: "http://invalid",
roomDescription: "www.mozilla.com",
from: "conversation"
}));
});
it("should invoke callback if defined", function() {
var callback = sinon.stub();
view = mountTestComponent({
callback: callback
});
var emailBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(emailBtn);
sinon.assert.calledOnce(callback);
});
});
describe("FacebookShareButton", function() {
var view;
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
locationForMetrics: "conversation",
roomData: {
roomUrl: "http://invalid",
roomContextUrls: []
}
}, props || {});
return TestUtils.renderIntoDocument(
React.createElement(sharedDesktopViews.FacebookShareButton, props));
}
it("should dispatch a FacebookShareRoomUrl action when the facebook button is clicked",
function() {
view = mountTestComponent();
var facebookBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(facebookBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.FacebookShareRoomUrl({
from: "conversation",
roomUrl: "http://invalid"
}));
});
it("should invoke callback if defined", function() {
var callback = sinon.stub();
view = mountTestComponent({
callback: callback
});
var facebookBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(facebookBtn);
sinon.assert.calledOnce(callback);
});
});
describe("SharePanelView", function() {
var view;
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
facebookEnabled: false,
locationForMetrics: "conversation",
roomData: { roomUrl: "http://invalid" },
savingContext: false,
show: true,
showEditContext: false
}, props);
return TestUtils.renderIntoDocument(
React.createElement(sharedDesktopViews.SharePanelView, props));
}
it("should not display the Facebook Share button when it is disabled in prefs",
function() {
view = mountTestComponent({
facebookEnabled: false
});
expect(view.getDOMNode().querySelectorAll(".btn-facebook"))
.to.have.length.of(0);
});
it("should display the Facebook Share button only when it is enabled in prefs",
function() {
view = mountTestComponent({
facebookEnabled: true
});
expect(view.getDOMNode().querySelectorAll(".btn-facebook"))
.to.have.length.of(1);
});
it("should not display the panel when show prop is false", function() {
view = mountTestComponent({
show: false
});
expect(view.getDOMNode()).eql(null);
});
it("should not display the panel when roomUrl is not defined", function() {
view = mountTestComponent({
roomData: {}
});
expect(view.getDOMNode()).eql(null);
});
});
describe("SocialShareDropdown", function() {
var fakeProvider, view;
beforeEach(function() {
fakeProvider = {
name: "foo",
origin: "https://foo",
iconURL: "http://example.com/foo.png"
};
});
afterEach(function() {
fakeProvider = null;
});
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
show: true
}, props);
return TestUtils.renderIntoDocument(
React.createElement(sharedDesktopViews.SocialShareDropdown, props));
}
describe("#render", function() {
it("should show no contents when the Social Providers have not been fetched yet", function() {
view = mountTestComponent();
expect(view.getDOMNode()).to.eql(null);
});
it("should show an empty list when no Social Providers are available", function() {
view = mountTestComponent({
socialShareProviders: []
});
var node = view.getDOMNode();
expect(node.querySelector(".icon-add-share-service")).to.not.eql(null);
expect(node.querySelectorAll(".dropdown-menu-item").length).to.eql(1);
});
it("should show a list of available Social Providers", function() {
view = mountTestComponent({
socialShareProviders: [fakeProvider]
});
var node = view.getDOMNode();
expect(node.querySelector(".icon-add-share-service")).to.not.eql(null);
expect(node.querySelector(".dropdown-menu-separator")).to.not.eql(null);
var dropdownNodes = node.querySelectorAll(".dropdown-menu-item");
expect(dropdownNodes.length).to.eql(2);
expect(dropdownNodes[1].querySelector("img").src).to.eql(fakeProvider.iconURL);
expect(dropdownNodes[1].querySelector("span").textContent)
.to.eql(fakeProvider.name);
});
});
describe("#handleAddServiceClick", function() {
it("should dispatch an action when the 'add provider' item is clicked", function() {
view = mountTestComponent({
socialShareProviders: []
});
var addItem = view.getDOMNode().querySelector(".dropdown-menu-item:first-child");
React.addons.TestUtils.Simulate.click(addItem);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.AddSocialShareProvider());
});
});
describe("#handleProviderClick", function() {
it("should dispatch an action when a provider item is clicked", function() {
view = mountTestComponent({
roomUrl: "http://example.com",
socialShareProviders: [fakeProvider]
});
var providerItem = view.getDOMNode().querySelector(".dropdown-menu-item:last-child");
React.addons.TestUtils.Simulate.click(providerItem);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShareRoomUrl({
provider: fakeProvider,
roomUrl: "http://example.com",
previews: []
}));
});
});
});
});

Просмотреть файл

@ -27,6 +27,7 @@
<script src="/shared/vendor/react.js"></script>
<script src="/shared/vendor/classnames.js"></script>
<script src="/shared/vendor/backbone.js"></script>
<script src="/add-on/panels/vendor/simpleSlideshow.js"></script>
<!-- test dependencies -->
<script src="/test/vendor/mocha.js"></script>
@ -61,23 +62,28 @@
<!-- 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/desktopViews.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/conversation.js"></script>
<script src="/add-on/panels/js/panel.js"></script>
<script src="/add-on/panels/js/slideshow.js"></script>
<!-- Test scripts -->
<script src="conversationAppStore_test.js"></script>
<script src="conversation_test.js"></script>
<script src="feedbackViews_test.js"></script>
<script src="panel_test.js"></script>
<script src="desktopViews_test.js"></script>
<script src="roomViews_test.js"></script>
<script src="l10n_test.js"></script>
<script src="roomStore_test.js"></script>
<script src="slideshow_test.js"></script>
<script>
// Stop the default init functions running to avoid conflicts in tests
document.removeEventListener('DOMContentLoaded', loop.panel.init);
document.removeEventListener('DOMContentLoaded', loop.conversation.init);
document.removeEventListener("DOMContentLoaded", loop.slideshow.init);
LoopMochaUtils.addErrorCheckingTests();
LoopMochaUtils.runTests();

Просмотреть файл

@ -33,6 +33,7 @@ describe("loop.panel", function() {
fakeWindow = {
close: sandbox.stub(),
addEventListener: function() {},
removeEventListener: function() {},
document: { addEventListener: function() {} },
setTimeout: function(callback) { callback(); }
};
@ -44,7 +45,6 @@ describe("loop.panel", function() {
GetDoNotDisturb: function() { return true; },
SetDoNotDisturb: sinon.stub(),
GetErrors: function() { return null; },
GetFxAEnabled: function() { return true; },
GetAllStrings: function() {
return JSON.stringify({ textContent: "fakeText" });
},
@ -52,12 +52,17 @@ describe("loop.panel", function() {
GetLocale: function() {
return "en-US";
},
GetPluralRule: sinon.stub(),
GetPluralRule: function() {
return 1;
},
SetLoopPref: sinon.stub(),
GetLoopPref: function(prefName) {
if (prefName === "debug.dispatcher") {
return false;
} else if (prefName === "facebook.enabled") {
return true;
}
return 1;
},
SetPanelHeight: function() { return null; },
@ -77,12 +82,12 @@ describe("loop.panel", function() {
NotifyUITour: sinon.stub(),
OpenURL: sinon.stub(),
GettingStartedURL: sinon.stub().returns("http://fakeFTUUrl.com"),
OpenGettingStartedTour: sinon.stub(),
GetSelectedTabMetadata: sinon.stub().returns({}),
GetUserProfile: function() { return null; }
});
loop.storedRequests = {
GetFxAEnabled: true,
GetHasEncryptionKey: true,
GetUserProfile: null,
GetDoNotDisturb: false,
@ -283,8 +288,7 @@ describe("loop.panel", function() {
it("should hide the account entry when FxA is not enabled", function() {
LoopMochaUtils.stubLoopRequest({
GetUserProfile: function() { return { email: "test@example.com" }; },
GetFxAEnabled: function() { return false; }
GetUserProfile: function() { return { email: "test@example.com" }; }
});
var view = TestUtils.renderIntoDocument(
@ -326,27 +330,12 @@ describe("loop.panel", function() {
sinon.assert.calledOnce(prevent);
});
it("should be hidden if FxA is not enabled", function() {
LoopMochaUtils.stubLoopRequest({
GetFxAEnabled: function() { return false; }
});
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.AccountLink, {
fxAEnabled: false,
userProfile: null
}));
expect(view.getDOMNode()).to.be.null;
});
it("should warn when user profile is different from {} or null",
function() {
var warnstub = sandbox.stub(console, "warn");
TestUtils.renderIntoDocument(React.createElement(
loop.panel.AccountLink, {
fxAEnabled: false,
userProfile: []
}
));
@ -362,7 +351,6 @@ describe("loop.panel", function() {
TestUtils.renderIntoDocument(React.createElement(
loop.panel.AccountLink, {
fxAEnabled: false,
userProfile: {}
}
));
@ -699,6 +687,18 @@ describe("loop.panel", function() {
});
});
describe("GettingStartedView", function() {
it("should render the Slidehow when clicked on the button", function() {
loop.storedRequests["GetLoopPref|gettingStarted.latestFTUVersion"] = 0;
var view = createTestPanelView();
TestUtils.Simulate.click(view.getDOMNode().querySelector(".fte-get-started-button"));
sinon.assert.calledOnce(requestStubs.OpenGettingStartedTour);
});
});
});
describe("loop.panel.RoomEntry", function() {
@ -945,12 +945,17 @@ describe("loop.panel", function() {
});
describe("Room name updated", function() {
it("should update room name", function() {
var roomEntry = mountRoomEntry({
var roomEntry;
beforeEach(function() {
roomEntry = mountRoomEntry({
dispatcher: dispatcher,
isOpenedRoom: false,
room: new loop.store.Room(roomData)
});
});
it("should update room name", function() {
var updatedRoom = new loop.store.Room(_.extend({}, roomData, {
decryptedContext: {
roomName: "New room name"
@ -964,6 +969,63 @@ describe("loop.panel", function() {
roomEntry.getDOMNode().textContent)
.eql("New room name");
});
it("should enter in edit mode when edit button is clicked", function() {
roomEntry.handleEditButtonClick(fakeEvent);
expect(roomEntry.state.editMode).eql(true);
});
it("should render an input while edit mode is active", function() {
roomEntry.setState({
editMode: true
});
expect(roomEntry.getDOMNode().querySelector("input")).not.eql(null);
});
it("should exit edit mode and update the room name when input lose focus", function() {
roomEntry.setState({
editMode: true
});
sandbox.stub(dispatcher, "dispatch");
var input = roomEntry.getDOMNode().querySelector("input");
input.value = "fakeName";
TestUtils.Simulate.change(input);
TestUtils.Simulate.blur(input);
expect(roomEntry.state.editMode).eql(false);
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.UpdateRoomContext({
roomToken: roomData.roomToken,
newRoomName: "fakeName"
}));
});
it("should exit edit mode and update the room name when Enter key is pressed", function() {
roomEntry.setState({
editMode: true
});
sandbox.stub(dispatcher, "dispatch");
var input = roomEntry.getDOMNode().querySelector("input");
input.value = "fakeName";
TestUtils.Simulate.change(input);
TestUtils.Simulate.keyDown(input, {
key: "Enter",
which: 13
});
expect(roomEntry.state.editMode).eql(false);
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.UpdateRoomContext({
roomToken: roomData.roomToken,
newRoomName: "fakeName"
}));
});
});
describe("Room name priority", function() {
@ -1067,7 +1129,7 @@ describe("loop.panel", function() {
sinon.assert.calledWithExactly(dispatch, new sharedActions.GetAllRooms());
});
it("should close the panel once a room is created and there is no error", function() {
it("should not close the panel once a room is created and there is no error", function() {
createTestComponent();
roomStore.setStoreState({ pendingCreation: true });
@ -1076,13 +1138,13 @@ describe("loop.panel", function() {
roomStore.setStoreState({ pendingCreation: false });
sinon.assert.calledOnce(fakeWindow.close);
sinon.assert.notCalled(fakeWindow.close);
});
it("should have room-list-empty element and not room-list element when no rooms", function() {
it("should have FTE element and not room-list element when room-list is empty", function() {
var view = createTestComponent();
var node = view.getDOMNode();
expect(node.querySelectorAll(".room-list-empty").length).to.eql(1);
expect(node.querySelectorAll(".fte-get-started-content").length).to.eql(1);
expect(node.querySelectorAll(".room-list").length).to.eql(0);
});
@ -1246,6 +1308,16 @@ describe("loop.panel", function() {
var node = view.getDOMNode();
expect(node.querySelector(".room-entry h2").textContent).to.equal("Fake title");
});
describe("computeAdjustedTopPosition", function() {
it("should return 0 if clickYPos, menuNodeHeight, listTop, listHeight and clickOffset cause it to be less than 0",
function() {
var topPosTest = loop.panel.computeAdjustedTopPosition(119, 124, 0, 152, 10) < 0;
expect(topPosTest).to.equal(false);
});
});
});
describe("loop.panel.NewRoomView", function() {
@ -1393,6 +1465,7 @@ describe("loop.panel", function() {
React.createElement(loop.panel.ConversationDropdown, {
handleCopyButtonClick: sandbox.stub(),
handleDeleteButtonClick: sandbox.stub(),
handleEditButtonClick: sandbox.stub(),
handleEmailButtonClick: sandbox.stub(),
eventPosY: 0
}));
@ -1422,6 +1495,13 @@ describe("loop.panel", function() {
sinon.assert.calledOnce(view.props.handleDeleteButtonClick);
});
it("should trigger handleEditButtonClick when edit button is clicked",
function() {
TestUtils.Simulate.click(view.refs.editButton.getDOMNode());
sinon.assert.calledOnce(view.props.handleEditButtonClick);
});
});
describe("RoomEntryContextButtons", function() {
@ -1434,7 +1514,8 @@ describe("loop.panel", function() {
showMenu: false,
room: roomData,
toggleDropdownMenu: sandbox.stub(),
handleClick: sandbox.stub()
handleClick: sandbox.stub(),
handleEditButtonClick: sandbox.stub()
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(loop.panel.RoomEntryContextButtons, props));
@ -1494,4 +1575,152 @@ describe("loop.panel", function() {
new sharedActions.DeleteRoom({ roomToken: roomData.roomToken }));
});
});
describe("SharePanelView", function() {
var view, dispatcher, roomStore;
function createTestComponent(extraProps) {
var props = _.extend({
dispatcher: dispatcher,
onSharePanelDisplayChange: sinon.stub(),
store: roomStore
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(loop.panel.SharePanelView, props));
}
beforeEach(function() {
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
roomStore = new loop.store.RoomStore(dispatcher, {
constants: {}
});
roomStore.setStoreState({
activeRoom: {
roomToken: "fakeToken"
},
openedRoom: null,
pendingCreation: false,
pendingInitialRetrieval: false,
rooms: [],
error: undefined
});
view = createTestComponent();
sandbox.stub(view, "closeWindow");
});
it("should not open the panel if there is no room pending of creation", function() {
expect(view.getDOMNode()).eql(null);
});
it("should open the panel after room creation", function() {
var clock = sinon.useFakeTimers();
// Simulate that the user has click the browse button
roomStore.setStoreState({
pendingCreation: true
});
// Room has been created succesfully
roomStore.setStoreState({
pendingCreation: false
});
var panel = view.getDOMNode();
clock.tick(loop.panel.SharePanelView.SHOW_PANEL_DELAY);
expect(view.state.showPanel).eql(true);
expect(panel.classList.contains("share-panel-open")).eql(true);
});
it("should close the share panel when clicking the overlay", function() {
view.setState({
showPanel: true
});
var overlay = view.getDOMNode().querySelector(".share-panel-overlay");
var panel = view.getDOMNode();
TestUtils.Simulate.click(overlay);
expect(view.state.showPanel).eql(false);
expect(panel.classList.contains("share-panel-open")).eql(false);
});
it("should close the share panel when clicking outside the panel", function() {
view.setState({
showPanel: true
});
var panel = view.getDOMNode();
view._onDocumentVisibilityChanged({
target: {
hidden: true
}
});
expect(view.state.showPanel).eql(false);
expect(panel.classList.contains("share-panel-open")).eql(false);
});
it("should close the hello panel when clicking outside the panel", function() {
view.setState({
showPanel: true
});
view._onDocumentVisibilityChanged({
target: {
hidden: true
}
});
expect(view.state.showPanel).eql(false);
sinon.assert.calledOnce(view.closeWindow);
});
it("should call openRoom when hello panel is closed", function() {
var openRoomSpy = sinon.spy(view, "openRoom");
view.setState({
showPanel: true
});
view._onDocumentVisibilityChanged({
target: {
hidden: true
}
});
sinon.assert.calledOnce(openRoomSpy);
});
it("should invoke onSharePanelDisplayChange when hello panel is closed", function() {
view.setState({
showPanel: true
});
view._onDocumentVisibilityChanged({
target: {
hidden: true
}
});
sinon.assert.calledOnce(view.props.onSharePanelDisplayChange);
});
it("should invoke onSharePanelDisplayChange when hello panel is displayed", function() {
// Simulate that the user has click the browse button
roomStore.setStoreState({
pendingCreation: true
});
// Room has been created succesfully
roomStore.setStoreState({
pendingCreation: false
});
sinon.assert.calledOnce(view.props.onSharePanelDisplayChange);
});
});
});

Просмотреть файл

@ -62,7 +62,11 @@ describe("loop.store.RoomStore", function() {
},
NotifyUITour: function() {},
OpenURL: sinon.stub(),
"Rooms:Create": sinon.stub().returns({ roomToken: "fakeToken" }),
"Rooms:Create": sinon.stub().returns({
decryptedContext: [],
roomToken: "fakeToken",
roomUrl: "fakeUrl"
}),
"Rooms:Delete": sinon.stub(),
"Rooms:GetAll": sinon.stub(),
"Rooms:Open": sinon.stub(),
@ -290,7 +294,9 @@ describe("loop.store.RoomStore", function() {
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.CreatedRoom({
roomToken: "fakeToken"
decryptedContext: [],
roomToken: "fakeToken",
roomUrl: "fakeUrl"
}));
});
@ -362,24 +368,38 @@ describe("loop.store.RoomStore", function() {
store.setStoreState({ pendingCreation: true });
store.createdRoom(new sharedActions.CreatedRoom({
roomToken: "fakeToken"
decryptedContext: [],
roomToken: "fakeToken",
roomUrl: "fakeUrl"
}));
expect(store.getStoreState().pendingCreation).eql(false);
});
it("should dispatch an OpenRoom action once the operation is done",
it("should not dispatch an OpenRoom action once the operation is done",
function() {
store.createdRoom(new sharedActions.CreatedRoom({
roomToken: "fakeToken"
decryptedContext: [],
roomToken: "fakeToken",
roomUrl: "fakeUrl"
}));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.OpenRoom({
roomToken: "fakeToken"
}));
sinon.assert.notCalled(dispatcher.dispatch);
});
it("should save the state of the active room", function() {
store.createdRoom(new sharedActions.CreatedRoom({
decryptedContext: [],
roomToken: "fakeToken",
roomUrl: "fakeUrl"
}));
expect(store.getStoreState().activeRoom).eql({
decryptedContext: [],
roomToken: "fakeToken",
roomUrl: "fakeUrl"
});
});
});
describe("#createRoomError", function() {
@ -945,7 +965,7 @@ describe("loop.store.RoomStore", function() {
sinon.assert.calledOnce(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"],
"LOOP_MAU", store._constants.LOOP_MAU_TYPE.ROOM_OPEN);
"LOOP_ACTIVITY_COUNTER", store._constants.LOOP_MAU_TYPE.ROOM_OPEN);
});
it("should log telemetry event when sharing a room (copy link)", function() {
@ -956,7 +976,7 @@ describe("loop.store.RoomStore", function() {
sinon.assert.calledTwice(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"].getCall(1),
"LOOP_MAU", store._constants.LOOP_MAU_TYPE.ROOM_SHARE);
"LOOP_ACTIVITY_COUNTER", store._constants.LOOP_MAU_TYPE.ROOM_SHARE);
});
it("should log telemetry event when sharing a room (email)", function() {
@ -967,7 +987,7 @@ describe("loop.store.RoomStore", function() {
sinon.assert.calledTwice(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"].getCall(1),
"LOOP_MAU", store._constants.LOOP_MAU_TYPE.ROOM_SHARE);
"LOOP_ACTIVITY_COUNTER", store._constants.LOOP_MAU_TYPE.ROOM_SHARE);
});
it("should log telemetry event when sharing a room (facebook)", function() {
@ -978,7 +998,7 @@ describe("loop.store.RoomStore", function() {
sinon.assert.calledTwice(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"].getCall(1),
"LOOP_MAU", store._constants.LOOP_MAU_TYPE.ROOM_SHARE);
"LOOP_ACTIVITY_COUNTER", store._constants.LOOP_MAU_TYPE.ROOM_SHARE);
});
it("should log telemetry event when deleting a room", function() {
@ -988,7 +1008,7 @@ describe("loop.store.RoomStore", function() {
sinon.assert.calledTwice(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"].getCall(1),
"LOOP_MAU", store._constants.LOOP_MAU_TYPE.ROOM_DELETE);
"LOOP_ACTIVITY_COUNTER", store._constants.LOOP_MAU_TYPE.ROOM_DELETE);
});
});
});

Просмотреть файл

@ -193,153 +193,6 @@ describe("loop.roomViews", function() {
});
});
describe("DesktopRoomInvitationView", function() {
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
facebookEnabled: false,
roomData: { roomUrl: "http://invalid" },
savingContext: false,
show: true,
showEditContext: false
}, props);
return TestUtils.renderIntoDocument(
React.createElement(loop.roomViews.DesktopRoomInvitationView, props));
}
it("should dispatch an EmailRoomUrl with no description" +
" for rooms without context when the email button is pressed",
function() {
view = mountTestComponent();
var emailBtn = view.getDOMNode().querySelector(".btn-email");
React.addons.TestUtils.Simulate.click(emailBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.EmailRoomUrl({
roomUrl: "http://invalid",
roomDescription: undefined,
from: "conversation"
}));
});
it("should dispatch an EmailRoomUrl with a domain name description for rooms with context",
function() {
var url = "http://invalid";
view = mountTestComponent({
roomData: {
roomUrl: url,
roomContextUrls: [{ location: "http://www.mozilla.com/" }]
}
});
var emailBtn = view.getDOMNode().querySelector(".btn-email");
React.addons.TestUtils.Simulate.click(emailBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.EmailRoomUrl({
roomUrl: url,
roomDescription: "www.mozilla.com",
from: "conversation"
}));
});
it("should not display the Facebook Share button when it is disabled in prefs",
function() {
view = mountTestComponent({
facebookEnabled: false
});
expect(view.getDOMNode().querySelectorAll(".btn-facebook"))
.to.have.length.of(0);
});
it("should display the Facebook Share button only when it is enabled in prefs",
function() {
view = mountTestComponent({
facebookEnabled: true
});
expect(view.getDOMNode().querySelectorAll(".btn-facebook"))
.to.have.length.of(1);
});
it("should dispatch a FacebookShareRoomUrl action when the facebook button is clicked",
function() {
var url = "http://invalid";
view = mountTestComponent({
facebookEnabled: true,
roomData: {
roomUrl: url
}
});
var facebookBtn = view.getDOMNode().querySelector(".btn-facebook");
React.addons.TestUtils.Simulate.click(facebookBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.FacebookShareRoomUrl({
from: "conversation",
roomUrl: url
}));
});
describe("Copy Button", function() {
beforeEach(function() {
view = mountTestComponent();
});
it("should dispatch a CopyRoomUrl action when the copy button is pressed", function() {
var copyBtn = view.getDOMNode().querySelector(".btn-copy");
React.addons.TestUtils.Simulate.click(copyBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
roomUrl: "http://invalid",
from: "conversation"
}));
});
it("should change the text when the url has been copied", function() {
var copyBtn = view.getDOMNode().querySelector(".btn-copy");
React.addons.TestUtils.Simulate.click(copyBtn);
expect(copyBtn.textContent).eql("invite_copied_link_button");
});
it("should keep the text for a while after the url has been copied", function() {
var copyBtn = view.getDOMNode().querySelector(".btn-copy");
React.addons.TestUtils.Simulate.click(copyBtn);
clock.tick(loop.roomViews.DesktopRoomInvitationView.TRIGGERED_RESET_DELAY / 2);
expect(copyBtn.textContent).eql("invite_copied_link_button");
});
it("should reset the text a bit after the url has been copied", function() {
var copyBtn = view.getDOMNode().querySelector(".btn-copy");
React.addons.TestUtils.Simulate.click(copyBtn);
clock.tick(loop.roomViews.DesktopRoomInvitationView.TRIGGERED_RESET_DELAY);
expect(copyBtn.textContent).eql("invite_copy_link_button");
});
it("should reset the text after the url has been copied then mouse over another button", function() {
var copyBtn = view.getDOMNode().querySelector(".btn-copy");
React.addons.TestUtils.Simulate.click(copyBtn);
var emailBtn = view.getDOMNode().querySelector(".btn-email");
React.addons.TestUtils.Simulate.mouseOver(emailBtn);
expect(copyBtn.textContent).eql("invite_copy_link_button");
});
});
});
describe("DesktopRoomConversationView", function() {
var onCallTerminatedStub;
@ -354,6 +207,11 @@ describe("loop.roomViews", function() {
});
onCallTerminatedStub = sandbox.stub();
loop.config = {
tilesIframeUrl: null,
tilesSupportUrl: null
};
activeRoomStore.setStoreState({ roomUrl: "http://invalid " });
});
@ -447,29 +305,74 @@ describe("loop.roomViews", function() {
sinon.match.hasOwn("name", "setMute"));
});
describe("#componentWillUpdate", function() {
function expectActionDispatched() {
describe("#leaveRoom", function() {
it("should close the window when leaving a room that hasn't been used", function() {
view = mountTestComponent();
view.setState({ used: false });
view.leaveRoom();
sinon.assert.calledOnce(fakeWindow.close);
});
it("should dispatch `LeaveRoom` action when leaving a room that has been used", function() {
view = mountTestComponent();
view.setState({ used: true });
view.leaveRoom();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
sinon.match.instanceOf(sharedActions.SetupStreamElements));
}
new sharedActions.LeaveRoom());
});
it("should call onCallTerminated when leaving a room that has been used", function() {
view = mountTestComponent();
view.setState({ used: true });
view.leaveRoom();
sinon.assert.calledOnce(onCallTerminatedStub);
});
});
describe("#componentWillUpdate", function() {
it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state is entered", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
mountTestComponent();
view = mountTestComponent();
sandbox.stub(view, "getDefaultPublisherConfig").returns({
fake: "config"
});
activeRoomStore.setStoreState({ roomState: ROOM_STATES.MEDIA_WAIT });
expectActionDispatched();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetupStreamElements({
publisherConfig: {
fake: "config"
}
}));
});
it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is re-entered", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.ENDED });
mountTestComponent();
view = mountTestComponent();
sandbox.stub(view, "getDefaultPublisherConfig").returns({
fake: "config"
});
activeRoomStore.setStoreState({ roomState: ROOM_STATES.MEDIA_WAIT });
expectActionDispatched();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetupStreamElements({
publisherConfig: {
fake: "config"
}
}));
});
it("should dispatch a `StartBrowserShare` action when the SESSION_CONNECTED state is entered", function() {
@ -478,7 +381,31 @@ describe("loop.roomViews", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED });
expectActionDispatched();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.StartBrowserShare());
});
it("should not dispatch a `StartBrowserShare` action when the previous state was HAS_PARTICIPANTS", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.HAS_PARTICIPANTS });
mountTestComponent();
activeRoomStore.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED });
sinon.assert.notCalled(dispatcher.dispatch);
});
it("should not dispatch a `StartBrowserShare` action when the previous state was SESSION_CONNECTED", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED });
mountTestComponent();
activeRoomStore.setStoreState({
roomState: ROOM_STATES.SESSION_CONNECTED,
// Additional change to force an update.
screenSharingState: "fake"
});
sinon.assert.notCalled(dispatcher.dispatch);
});
});
@ -524,7 +451,7 @@ describe("loop.roomViews", function() {
view = mountTestComponent();
expect(TestUtils.findRenderedComponentWithType(view,
loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.not.eql(null);
loop.shared.desktopViews.SharePanelView).getDOMNode()).to.not.eql(null);
});
it("should render the DesktopRoomInvitationView if roomState is `JOINED` with just owner",
@ -537,7 +464,7 @@ describe("loop.roomViews", function() {
view = mountTestComponent();
expect(TestUtils.findRenderedComponentWithType(view,
loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.not.eql(null);
loop.shared.desktopViews.SharePanelView).getDOMNode()).to.not.eql(null);
});
it("should render the DesktopRoomConversationView if roomState is `JOINED` with remote participant",
@ -552,7 +479,7 @@ describe("loop.roomViews", function() {
TestUtils.findRenderedComponentWithType(view,
loop.roomViews.DesktopRoomConversationView);
expect(TestUtils.findRenderedComponentWithType(view,
loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.eql(null);
loop.shared.desktopViews.SharePanelView).getDOMNode()).to.eql(null);
});
it("should render the DesktopRoomConversationView if roomState is `JOINED` with participants",
@ -567,7 +494,7 @@ describe("loop.roomViews", function() {
TestUtils.findRenderedComponentWithType(view,
loop.roomViews.DesktopRoomConversationView);
expect(TestUtils.findRenderedComponentWithType(view,
loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.eql(null);
loop.shared.desktopViews.SharePanelView).getDOMNode()).to.eql(null);
});
it("should render the DesktopRoomConversationView if roomState is `HAS_PARTICIPANTS`",
@ -579,22 +506,9 @@ describe("loop.roomViews", function() {
TestUtils.findRenderedComponentWithType(view,
loop.roomViews.DesktopRoomConversationView);
expect(TestUtils.findRenderedComponentWithType(view,
loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.eql(null);
loop.shared.desktopViews.SharePanelView).getDOMNode()).to.eql(null);
});
it("should call onCallTerminated when the call ended", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.ENDED,
used: true
});
view = mountTestComponent();
// Force a state change so that it triggers componentDidUpdate
view.setState({ foo: "bar" });
sinon.assert.calledOnce(onCallTerminatedStub);
});
it("should display loading spinner when localSrcMediaElement is null",
function() {
activeRoomStore.setStoreState({
@ -736,98 +650,4 @@ describe("loop.roomViews", function() {
});
});
});
describe("SocialShareDropdown", function() {
var fakeProvider;
beforeEach(function() {
fakeProvider = {
name: "foo",
origin: "https://foo",
iconURL: "http://example.com/foo.png"
};
});
afterEach(function() {
fakeProvider = null;
});
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
show: true
}, props);
return TestUtils.renderIntoDocument(
React.createElement(loop.roomViews.SocialShareDropdown, props));
}
describe("#render", function() {
it("should show no contents when the Social Providers have not been fetched yet", function() {
view = mountTestComponent();
expect(view.getDOMNode()).to.eql(null);
});
it("should show an empty list when no Social Providers are available", function() {
view = mountTestComponent({
socialShareProviders: []
});
var node = view.getDOMNode();
expect(node.querySelector(".icon-add-share-service")).to.not.eql(null);
expect(node.querySelectorAll(".dropdown-menu-item").length).to.eql(1);
});
it("should show a list of available Social Providers", function() {
view = mountTestComponent({
socialShareProviders: [fakeProvider]
});
var node = view.getDOMNode();
expect(node.querySelector(".icon-add-share-service")).to.not.eql(null);
expect(node.querySelector(".dropdown-menu-separator")).to.not.eql(null);
var dropdownNodes = node.querySelectorAll(".dropdown-menu-item");
expect(dropdownNodes.length).to.eql(2);
expect(dropdownNodes[1].querySelector("img").src).to.eql(fakeProvider.iconURL);
expect(dropdownNodes[1].querySelector("span").textContent)
.to.eql(fakeProvider.name);
});
});
describe("#handleAddServiceClick", function() {
it("should dispatch an action when the 'add provider' item is clicked", function() {
view = mountTestComponent({
socialShareProviders: []
});
var addItem = view.getDOMNode().querySelector(".dropdown-menu-item:first-child");
React.addons.TestUtils.Simulate.click(addItem);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.AddSocialShareProvider());
});
});
describe("#handleProviderClick", function() {
it("should dispatch an action when a provider item is clicked", function() {
view = mountTestComponent({
roomUrl: "http://example.com",
socialShareProviders: [fakeProvider]
});
var providerItem = view.getDOMNode().querySelector(".dropdown-menu-item:last-child");
React.addons.TestUtils.Simulate.click(providerItem);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShareRoomUrl({
provider: fakeProvider,
roomUrl: "http://example.com",
previews: []
}));
});
});
});
});

Просмотреть файл

@ -0,0 +1,104 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loop.slideshow", function() {
"use strict";
var expect = chai.expect;
var sharedUtils = loop.shared.utils;
var sandbox;
var data;
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
LoopMochaUtils.stubLoopRequest({
GetAllStrings: function() {
return JSON.stringify({ textContent: "fakeText" });
},
GetLocale: function() {
return "en-US";
},
GetPluralRule: function() {
return 1;
},
GetPluralForm: function() {
return "fakeText";
}
});
data = [
{
id: "slide1",
imageClass: "slide1-image",
title: "fakeString",
text: "fakeString"
},
{
id: "slide2",
imageClass: "slide2-image",
title: "fakeString",
text: "fakeString"
},
{
id: "slide3",
imageClass: "slide3-image",
title: "fakeString",
text: "fakeString"
},
{
id: "slide4",
imageClass: "slide4-image",
title: "fakeString",
text: "fakeString"
}
];
document.mozL10n.initialize({
getStrings: function() {
return JSON.stringify({ textContent: "fakeText" });
},
locale: "en-US"
});
sandbox.stub(document.mozL10n, "get").returns("fakeString");
sandbox.stub(sharedUtils, "getPlatform").returns("other");
});
afterEach(function() {
sandbox.restore();
LoopMochaUtils.restore();
});
describe("#init", function() {
beforeEach(function() {
sandbox.stub(React, "render");
sandbox.stub(document.mozL10n, "initialize");
sandbox.stub(loop.SimpleSlideshow, "init");
});
it("should initalize L10n", function() {
loop.slideshow.init();
sinon.assert.calledOnce(document.mozL10n.initialize);
sinon.assert.calledWith(document.mozL10n.initialize, sinon.match({ locale: "en-US" }));
});
it("should call the slideshow init with the right arguments", function() {
loop.slideshow.init();
sinon.assert.calledOnce(loop.SimpleSlideshow.init);
sinon.assert.calledWith(loop.SimpleSlideshow.init, sinon.match("#main", data));
});
it("should set the document attributes correctly", function() {
loop.slideshow.init();
expect(document.documentElement.getAttribute("lang")).to.eql("en-US");
expect(document.documentElement.getAttribute("dir")).to.eql("ltr");
expect(document.body.getAttribute("platform")).to.eql("other");
});
});
});

Просмотреть файл

@ -18,8 +18,9 @@ pref("loop.debug.sdk", false);
pref("loop.debug.twoWayMediaTelemetry", false);
pref("loop.feedback.dateLastSeenSec", 0);
pref("loop.feedback.periodSec", 15770000); // 6 months.
pref("loop.feedback.formURL", "https://www.mozilla.org/firefox/hello/npssurvey/");
pref("loop.feedback.formURL", "https://www.surveygizmo.com/s3/2651383/Firefox-Hello-Product-Survey-II");
pref("loop.feedback.manualFormURL", "https://www.mozilla.org/firefox/hello/feedbacksurvey/");
pref("loop.logDomains", false);
pref("loop.mau.openPanel", 0);
pref("loop.mau.openConversation", 0);
pref("loop.mau.roomOpen", 0);
@ -37,3 +38,4 @@ pref("loop.facebook.enabled", true);
pref("loop.facebook.appId", "1519239075036718");
pref("loop.facebook.shareUrl", "https://www.facebook.com/dialog/send?app_id=%APP_ID%&link=%ROOM_URL%&redirect_uri=%REDIRECT_URI%");
pref("loop.facebook.fallbackUrl", "https://hello.firefox.com/");
pref("loop.conversationPopOut.enabled", true);

Просмотреть файл

@ -597,13 +597,62 @@ html[dir="rtl"] .context-wrapper > .context-preview {
width: 100%;
}
.remote-video-box > .remote-cursor {
background: url('../img/cursor.svg#orange') no-repeat;
height: 20px;
.remote-video-box > .remote-cursor-container {
/* Svg cursor has a white outline so we need to offset it to ensure that the
* cursor points at a more precise position with the negative margin. */
margin: -2px;
position: absolute;
width: 15px;
z-index: 65534;
}
.remote-video-box > .remote-cursor-container > .remote-cursor {
background: url('../img/cursor.svg#blue') no-repeat;
height: 20px;
width: 15px;
}
.remote-video-box > .remote-cursor-container:after,
.remote-video-box > .remote-cursor-container:before {
background-color: rgba(231, 149, 68, 0.5);
border-radius: 50%;
content: "";
height: 50px;
left: 50%;
margin: -35px 0 0 -35px;
opacity: 0;
pointer-events: none;
position: absolute;
top: 50%;
width: 50px;
z-index: -1;
}
@keyframes double-pulse-1 {
0% {
opacity: 1;
transform: scale3d(0.1, 0.1, 1);
}
100% {
opacity: 0;
transform: scale3d(1.1, 1.1, 1);
}
}
@keyframes double-pulse-2 {
0% {
opacity: 1;
transform: scale3d(0.5, 0.5, 1);
}
50%, 100% {
opacity: 0;
transform: scale3d(1.2, 1.2, 1);
}
}
.remote-video-box > .remote-cursor-clicked:after {
animation: double-pulse-1 800ms ease-out forwards;
}
.remote-video-box > .remote-cursor-clicked:before {
animation: double-pulse-2 800ms ease-out forwards;
}

Просмотреть файл

@ -437,177 +437,6 @@ html, .fx-embedded, #main,
height: 100%;
}
.room-invitation-overlay {
position: absolute;
background: rgba(255, 255, 255, 0.85);
top: 0;
height: 100%;
right: 0;
left: 0;
color: #000;
z-index: 1010;
display: flex;
flex-flow: column nowrap;
align-items: stretch;
}
.room-invitation-content {
display: flex;
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;
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 {
color: #000;
text-align: start;
bottom: auto;
top: 0;
overflow: hidden;
overflow-y: auto;
}
/* When the dropdown is showing a vertical scrollbar, compensate for its width. */
body[platform="other"] .share-service-dropdown.overflow > .dropdown-menu-item,
body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
-moz-padding-end: 20px;
}
.share-service-dropdown > .dropdown-menu-item > .icon {
width: 14px;
height: 14px;
margin-right: 4px;
}
.dropdown-menu-item > .icon-add-share-service {
background-image: url("../img/icons-16x16.svg#add");
background-repeat: no-repeat;
background-size: 12px 12px;
width: 12px;
height: 12px;
}
.dropdown-menu-item:hover > .icon-add-share-service {
background-image: url("../img/icons-16x16.svg#add-hover");
}
.dropdown-menu-item:hover:active > .icon-add-share-service {
background-image: url("../img/icons-16x16.svg#add-active");
}
.context-url-view-wrapper > .context-content {
margin: 0 1rem 1.5rem 1rem;
}
@ -624,13 +453,13 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
.media-wrapper > .focus-stream {
display: flex;
/* We want this to be the width, minus 200px which is for the right-side text
chat and video displays. */
width: calc(100% - 200px);
/* 100% height to fill up media-layout, thus forcing other elements into the
second column that's 200px wide */
height: 100%;
background-color: #d8d8d8;
/* 100% height to fill up media-layout, thus forcing other elements into the
second column that's 272px wide */
height: 100%;
/* We want this to be the width, minus 272px which is for the right-side text
chat and video displays. */
width: calc(100% - 272px);
}
.media-wrapper > .focus-stream.screen-sharing-paused > .remote-video-box {
@ -639,8 +468,8 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
.media-wrapper > .local {
flex: 0 1 auto;
width: 200px;
height: 150px;
height: 204px;
width: 272px;
}
.media-wrapper > .local .local-video,
@ -676,15 +505,15 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
fix its height. */
.media-wrapper > .text-chat-view {
flex: 0 0 auto;
/* Text chat is a fixed 200px width for normal displays. */
width: 200px;
height: 100%;
/* Text chat is a fixed 272px width for normal displays. */
width: 272px;
}
.media-wrapper.showing-local-streams > .text-chat-view {
/* When we're displaying the local streams, then we need to make the text
chat view a bit shorter to give room. */
height: calc(100% - 150px);
height: calc(100% - 204px);
}
.media-wrapper.showing-local-streams.receiving-screen-share {
@ -693,8 +522,8 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
.media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view {
/* When we're displaying the local streams, then we need to make the text
chat view a bit shorter to give room. */
height: calc(100% - 300px);
chat view a bit shorter to give room. 2 streams x 204px each*/
height: calc(100% - 408px);
}
.media-wrapper.receiving-screen-share > .screen {
@ -706,10 +535,11 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
}
.media-wrapper.receiving-screen-share > .remote {
order: 3;
flex: 0 1 auto;
width: 200px;
height: 150px;
order: 3;
/* to keep the 4:3 ratio set both height and width */
height: 204px;
width: 272px;
}
.media-wrapper.receiving-screen-share > .local {
@ -907,8 +737,8 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
}
.text-chat-disabled > .text-chat-entries {
/* When text chat is disabled, the entries box should be 100% height. */
height: 100%;
/* When text chat is disabled, we want to show a tile ad with 220px height */
height: calc(100% - 220px);
}
.text-chat-entry,
@ -960,7 +790,7 @@ html[dir="rtl"] .text-chat-entry.received {
/* leave some room for the chat bubble arrow */
max-width: 70%;
border-radius: 15px;
border: 1px solid #5cccee;
border: 1px solid #d8d8d8;
background: #fff;
word-wrap: break-word;
flex: 0 1 auto;
@ -978,7 +808,7 @@ html[dir="rtl"] .text-chat-entry.received {
.text-chat-entry.received > p {
border-top-left-radius: 0;
border-color: #d8d8d8;
border-color: #5cccee;
}
html[dir="rtl"] .text-chat-entry.sent > p {
@ -1072,6 +902,43 @@ html[dir="rtl"] .text-chat-entry.received > p:after {
align-self: auto;
}
.text-chat-notif {
display: flex;
margin-bottom: .5em;
}
.text-chat-notif > .content-wrapper {
margin-left: 10px;
display: flex;
border: 1px solid #B1B1B1;
}
.content-wrapper > .notification-icon {
margin: 8px 5px 8px 10px;
}
.content-wrapper > p {
margin-top: 8px;
margin-right: 10px;
margin-bottom: 8px;
color: #757575;
font-size: 12px;
}
html[dir="rtl"] .text-chat-notif > .content-wrapper {
margin-right: 10px;
margin-left: 0px;
}
html[dir="rtl"] .content-wrapper > .notification-icon {
margin: 8px 10px 8px 5px;
}
html[dir="rtl"] .content-wrapper > p {
margin-left: 10px;
margin-right: 0px;
}
html[dir="rtl"] .text-chat-arrow {
transform: scaleX(-1);
}

Просмотреть файл

@ -5,10 +5,10 @@
<g>
<title>Layer 1</title>
<g transform="rotate(180 6.2844319343566895,3.8364052772521973) " id="svg_1" fill="none">
<path id="svg_2" fill="#d8d8d8" d="m12.061934,7.656905l-9.299002,0l0,-1l6.088001,0c-2.110002,-0.967001 -4.742001,-2.818 -6.088001,-6.278l0.932,-0.363c2.201999,5.664001 8.377999,6.637 8.439999,6.646c0.259001,0.039 0.444,0.27 0.426001,0.531c-0.019001,0.262 -0.237,0.464 -0.498999,0.464l-12.072001,-0.352001"/>
<path id="svg_2" fill="#5cccee" d="m12.061934,7.656905l-9.299002,0l0,-1l6.088001,0c-2.110002,-0.967001 -4.742001,-2.818 -6.088001,-6.278l0.932,-0.363c2.201999,5.664001 8.377999,6.637 8.439999,6.646c0.259001,0.039 0.444,0.27 0.426001,0.531c-0.019001,0.262 -0.237,0.464 -0.498999,0.464l-12.072001,-0.352001"/>
</g>
<line id="svg_13" y2="0.529488" x2="13.851821" y1="0.529488" x1="17.916953" stroke="#d8d8d8" fill="none"/>
<line id="svg_26" y2="0.529488" x2="9.79687" y1="0.529488" x1="13.862002" stroke="#d8d8d8" fill="none"/>
<line id="svg_27" y2="0.529488" x2="15.908413" y1="0.529488" x1="19.973545" stroke="#d8d8d8" fill="none"/>
<line id="svg_13" y2="0.529488" x2="13.851821" y1="0.529488" x1="17.916953" stroke="#5cccee" fill="none"/>
<line id="svg_26" y2="0.529488" x2="9.79687" y1="0.529488" x1="13.862002" stroke="#5cccee" fill="none"/>
<line id="svg_27" y2="0.529488" x2="15.908413" y1="0.529488" x1="19.973545" stroke="#5cccee" fill="none"/>
</g>
</svg>
</svg>

До

Ширина:  |  Высота:  |  Размер: 969 B

После

Ширина:  |  Высота:  |  Размер: 970 B

Просмотреть файл

@ -5,10 +5,10 @@
<g>
<title>Layer 1</title>
<g id="svg_1" fill="none">
<path id="svg_2" fill="#5cccee" d="m19.505243,8.972466l-9.299002,0l0,-1l6.088001,0c-2.110002,-0.967 -4.742001,-2.818 -6.088001,-6.278l0.932,-0.363c2.202,5.664 8.377999,6.637 8.44,6.646c0.259001,0.039 0.444,0.27 0.426001,0.531c-0.019001,0.262 -0.237,0.464 -0.498999,0.464l-12.072001,-0.352"/>
<path id="svg_2" fill="#d8d8d8" d="m19.505243,8.972466l-9.299002,0l0,-1l6.088001,0c-2.110002,-0.967 -4.742001,-2.818 -6.088001,-6.278l0.932,-0.363c2.202,5.664 8.377999,6.637 8.44,6.646c0.259001,0.039 0.444,0.27 0.426001,0.531c-0.019001,0.262 -0.237,0.464 -0.498999,0.464l-12.072001,-0.352"/>
</g>
<line id="svg_13" y2="8.474788" x2="6.200791" y1="8.474788" x1="10.265923" stroke="#22a4ff" fill="none"/>
<line id="svg_26" y2="8.474788" x2="2.14584" y1="8.474788" x1="6.210972" stroke="#22a4ff" fill="none"/>
<line id="svg_27" y2="8.474788" x2="0.000501" y1="8.474788" x1="4.065633" stroke="#22a4ff" fill="none"/>
<line id="svg_13" y2="8.474788" x2="6.200791" y1="8.474788" x1="10.265923" stroke="#d8d8d8" fill="none"/>
<line id="svg_26" y2="8.474788" x2="2.14584" y1="8.474788" x1="6.210972" stroke="#d8d8d8" fill="none"/>
<line id="svg_27" y2="8.474788" x2="0.000501" y1="8.474788" x1="4.065633" stroke="#d8d8d8" fill="none"/>
</g>
</svg>
</svg>

До

Ширина:  |  Высота:  |  Размер: 886 B

После

Ширина:  |  Высота:  |  Размер: 887 B

Просмотреть файл

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:x="http://www.w3.org/1999/xlink" viewBox="0 0 15 20"><style>use,:target~#orange{display:none}#orange,:target{display:inherit}#orange{fill:#f7a25e}#blue{fill:#00a9dc}</style><defs><g id="cursor"><path d="M3.387 12.783l2.833 5.009.469.829.851-.428 1.979-.995h-.001l.938-.472-.517-.914-2.553-4.513h5.244l-1.966-1.747-9-8-1.664-1.479v17.227-.001l1.8-2.4 1.587-2.116z" fill="#fff"/><path fill-rule="evenodd" d="M3.505 10.96l3.586 6.34 1.979-.995-3.397-6.005h4.327l-9-8v12l2.505-3.34z"/></g></defs><use id="blue" x:href="#cursor"/><use id="orange" x:href="#cursor"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:x="http://www.w3.org/1999/xlink" viewBox="0 0 15 20"><style>use,:target~#blue{display:none}#blue,:target{display:inherit}#blue{fill:#00a9dc}#orange{fill:#f7a25e}</style><defs><g id="cursor"><path d="M3.387 12.783l2.833 5.009.469.829.851-.428 1.979-.995h-.001l.938-.472-.517-.914-2.553-4.513h5.244l-1.966-1.747-9-8-1.664-1.479v17.227-.001l1.8-2.4 1.587-2.116z" fill="#fff"/><path fill-rule="evenodd" d="M3.505 10.96l3.586 6.34 1.979-.995-3.397-6.005h4.327l-9-8v12l2.505-3.34z"/></g></defs><use id="orange" x:href="#cursor"/><use id="blue" x:href="#cursor"/></svg>

До

Ширина:  |  Высота:  |  Размер: 612 B

После

Ширина:  |  Высота:  |  Размер: 609 B

Просмотреть файл

@ -1 +1 @@
<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>
<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>

До

Ширина:  |  Высота:  |  Размер: 638 B

После

Ширина:  |  Высота:  |  Размер: 639 B

Просмотреть файл

@ -1 +1 @@
<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>
<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>

До

Ширина:  |  Высота:  |  Размер: 351 B

После

Ширина:  |  Высота:  |  Размер: 352 B

Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#00a9dc" 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>

После

Ширина:  |  Высота:  |  Размер: 411 B

Просмотреть файл

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="13px" height="10px" viewBox="0 0 13 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.5.1 (25234) - http://www.bohemiancoding.com/sketch -->
<title>Shape Copy 2</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Link-Clickers-Flow" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="LG-has-left" sketch:type="MSArtboardGroup" transform="translate(-1114.000000, -519.000000)" fill="#757575">
<g id="SideBar---Empty---Ad" sketch:type="MSLayerGroup" transform="translate(1093.000000, 78.000000)">
<g id="Panel" transform="translate(1.000000, 0.000000)" sketch:type="MSShapeGroup">
<g id="Notification-3" transform="translate(9.000000, 414.000000)">
<path d="M17.0190514,29.0316428 C17.0127791,29.0315277 17.0064926,29.03147 17.0001925,29.03147 L13.9998075,29.03147 C13.4437166,29.03147 13,29.479099 13,30.0312775 L13,33.0316625 C13,33.5877533 13.4476291,34.03147 13.9998075,34.03147 L17.0001925,34.03147 C17.0064934,34.03147 17.0127799,34.0314125 17.0190514,34.0312981 L17.0190514,35.5706518 C17.0190514,36.1103849 17.3755471,36.2960247 17.8153068,35.9629325 L22.8239665,32.1691662 C23.2653143,31.8348711 23.2637262,31.2960247 22.8239665,30.9629325 L17.8153068,27.1691662 C17.373959,26.8348711 17.0190514,27.011704 17.0190514,27.5614469 L17.0190514,29.0316428 Z M11,27.03147 L16,27.03147 L16,28.03147 L12,28.03147 L12,35.03147 L16,35.03147 L16,36.0414724 L11,36.0414721 L11,27.03147 Z" id="Shape-Copy-2"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.8 KiB

Просмотреть файл

@ -134,8 +134,8 @@ loop.shared.actions = (function() {
* Used to send cursor data to the other peer
*/
SendCursorData: Action.define("sendCursorData", {
ratioX: Number,
ratioY: Number,
// ratioX: Number (optional)
// ratioY: Number (optional)
type: String
}),
@ -143,8 +143,8 @@ loop.shared.actions = (function() {
* Notifies that cursor data has been received from the other peer.
*/
ReceivedCursorData: Action.define("receivedCursorData", {
ratioX: Number,
ratioY: Number,
// ratioX: Number (optional)
// ratioY: Number (optional)
type: String
}),
@ -282,7 +282,9 @@ loop.shared.actions = (function() {
* XXX: should move to some roomActions module - refs bug 1079284
*/
CreatedRoom: Action.define("createdRoom", {
roomToken: String
decryptedContext: Object,
roomToken: String,
roomUrl: String
}),
/**
@ -511,10 +513,22 @@ loop.shared.actions = (function() {
expires: Number
}),
/**
* Used to indicate the user wishes to leave the conversation. This is
* different to leaving the room, in that we might display the feedback
* view, or just close the window. Whereas, the leaveRoom action is for
* the action of leaving an activeRoomStore room.
*/
LeaveConversation: Action.define("leaveConversation", {
}),
/**
* Used to indicate the user wishes to leave the room.
*/
LeaveRoom: Action.define("leaveRoom", {
// Optional, Used to indicate that we know the window is staying open,
// and hence any messages to ensure the call is fully ended must be sent.
// windowStayingOpen: Boolean,
}),
/**

Просмотреть файл

@ -109,6 +109,7 @@ loop.store.ActiveRoomStore = (function(mozL10n) {
"localVideoDimensions",
"mediaConnected",
"receivingScreenShare",
"remotePeerDisconnected",
"remoteSrcMediaElement",
"remoteVideoDimensions",
"remoteVideoEnabled",
@ -144,7 +145,8 @@ loop.store.ActiveRoomStore = (function(mozL10n) {
screenSharingState: SCREEN_SHARE_STATES.INACTIVE,
sharingPaused: false,
receivingScreenShare: false,
// Any urls (aka context) associated with the room.
remotePeerDisconnected: false,
// Any urls (aka context) associated with the room. null if no context.
roomContextUrls: null,
// The description for a room as stored in the context data.
roomDescription: null,
@ -968,6 +970,11 @@ loop.store.ActiveRoomStore = (function(mozL10n) {
* @param {sharedActions.StartBrowserShare} actionData
*/
startBrowserShare: function() {
if (this._storeState.screenSharingState !== SCREEN_SHARE_STATES.INACTIVE) {
console.error("Attempting to start browser sharing when already running.");
return;
}
// For the unit test we already set the state here, instead of indirectly
// via an action, because actions are queued thus depending on the
// asynchronous nature of `loop.request`.
@ -1025,6 +1032,7 @@ loop.store.ActiveRoomStore = (function(mozL10n) {
*/
remotePeerConnected: function() {
this.setStoreState({
remotePeerDisconnected: false,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
used: true
});
@ -1048,7 +1056,9 @@ loop.store.ActiveRoomStore = (function(mozL10n) {
mediaConnected: false,
participants: participants,
roomState: ROOM_STATES.SESSION_CONNECTED,
remoteSrcMediaElement: null
remotePeerDisconnected: true,
remoteSrcMediaElement: null,
streamPaused: false
});
},
@ -1085,9 +1095,11 @@ loop.store.ActiveRoomStore = (function(mozL10n) {
/**
* Handles a room being left.
*
* @param {sharedActions.LeaveRoom} actionData
*/
leaveRoom: function() {
this._leaveRoom(ROOM_STATES.ENDED);
leaveRoom: function(actionData) {
this._leaveRoom(ROOM_STATES.ENDED, false, actionData && actionData.windowStayingOpen);
},
/**
@ -1131,8 +1143,11 @@ loop.store.ActiveRoomStore = (function(mozL10n) {
* @param {Boolean} failedJoinRequest Optional. Set to true if the join
* request to loop-server failed. It
* will skip the leave message.
* @param {Boolean} windowStayingOpen Optional. Set to true to ensure
* that messages relating to ending
* of the conversation are sent on desktop.
*/
_leaveRoom: function(nextState, failedJoinRequest) {
_leaveRoom: function(nextState, failedJoinRequest, windowStayingOpen) {
if (this._storeState.standalone && this._storeState.userAgentHandlesRoom) {
// If the user agent is handling the room, all we need to do is advance
// to the next state.
@ -1173,7 +1188,8 @@ loop.store.ActiveRoomStore = (function(mozL10n) {
// NOTE: when the window _is_ closed, hanging up the call is performed by
// MozLoopService, because we can't get a message across to LoopAPI
// in time whilst a window is closing.
if ((nextState === ROOM_STATES.FAILED || !this._isDesktop) && !failedJoinRequest) {
if ((nextState === ROOM_STATES.FAILED || windowStayingOpen || !this._isDesktop) &&
!failedJoinRequest) {
loop.request("HangupNow", this._storeState.roomToken,
this._storeState.sessionToken, this._storeState.windowId);
}

Просмотреть файл

@ -10,7 +10,6 @@ loop.OTSdkDriver = (function() {
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var STREAM_PROPERTIES = loop.shared.utils.STREAM_PROPERTIES;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var CURSOR_MESSAGE_TYPES = loop.shared.utils.CURSOR_MESSAGE_TYPES;
/**
* This is a wrapper for the OT sdk. It is used to translate the SDK events into
@ -55,6 +54,17 @@ loop.OTSdkDriver = (function() {
this._debugTwoWayMediaTelemetry = enabled;
}.bind(this));
// Set loop.debug.sdk to true in the browser, or in standalone:
// localStorage.setItem("debug.sdk", true);
loop.shared.utils.getBoolPreference("debug.sdk", function(enabled) {
// We don't bother with the else case - as we only create one instance of
// OTSdkDriver per window, and hence, we leave the sdk set to its default
// value.
if (enabled) {
this.sdk.setLogLevel(this.sdk.DEBUG);
}
}.bind(this));
/**
* XXX This is a workaround for desktop machines that do not have a
* camera installed. The SDK doesn't currently do use the new device
@ -547,7 +557,15 @@ loop.OTSdkDriver = (function() {
* https://tokbox.com/opentok/libraries/client/js/reference/Stream.html
*/
_handleRemoteScreenShareCreated: function(stream) {
// Let the stores know first so they can update the display.
// Let the stores know first if the screen sharing is paused or not so
// they can update the display properly
if (!stream[STREAM_PROPERTIES.HAS_VIDEO]) {
this.dispatcher.dispatch(new sharedActions.VideoScreenStreamChanged({
hasVideo: false
}));
}
// Let the stores know so they can update the display if needed.
this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
receiving: true
}));
@ -699,11 +717,7 @@ loop.OTSdkDriver = (function() {
}.bind(this)],
["cursor",
function(message) {
switch (message.type) {
case CURSOR_MESSAGE_TYPES.POSITION:
this.dispatcher.dispatch(new sharedActions.ReceivedCursorData(message));
break;
}
this.dispatcher.dispatch(new sharedActions.ReceivedCursorData(message));
}.bind(this),
function(channel) {
this._subscriberCursorChannel = channel;
@ -812,7 +826,7 @@ loop.OTSdkDriver = (function() {
},
/**
* Sends the cursor position on the data channel.
* Sends the cursor events through the data channel.
*
* @param {String} message The message to send.
*/

Просмотреть файл

@ -43,6 +43,7 @@ loop.store.RemoteCursorStore = (function() {
loop.subscribe("CursorPositionChange",
this._cursorPositionChangeListener.bind(this));
loop.subscribe("CursorClick", this._cursorClickListener.bind(this));
},
/**
@ -51,12 +52,22 @@ loop.store.RemoteCursorStore = (function() {
getInitialStoreState: function() {
return {
realVideoSize: null,
remoteCursorClick: null,
remoteCursorPosition: null
};
},
/**
* Sends cursor position through the sdk.
* Sends cursor click position through the sdk.
*/
_cursorClickListener: function() {
this.sendCursorData({
type: CURSOR_MESSAGE_TYPES.CLICK
});
},
/**
* Prepares the cursor position object to be sent.
*
* @param {Object} event An object containing the cursor position and
* stream dimensions. It should contain:
@ -79,12 +90,15 @@ loop.store.RemoteCursorStore = (function() {
* {
* 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
* type {String} Type of the data being sent. Could be of the type
* | CURSOR_MESSAGE_TYPES.POSITION
* | CURSOR_MESSAGE_TYPES.CLICK
* }
*/
sendCursorData: function(actionData) {
switch (actionData.type) {
case CURSOR_MESSAGE_TYPES.POSITION:
case CURSOR_MESSAGE_TYPES.CLICK:
this._sdkDriver.sendCursorMessage(actionData);
break;
}
@ -105,6 +119,11 @@ loop.store.RemoteCursorStore = (function() {
}
});
break;
case CURSOR_MESSAGE_TYPES.CLICK:
this.setStoreState({
remoteCursorClick: true
});
break;
}
},

Просмотреть файл

@ -26,7 +26,8 @@ loop.store.TextChatStore = (function() {
"receivedTextChatMessage",
"sendTextChatMessage",
"updateRoomInfo",
"updateRoomContext"
"updateRoomContext",
"remotePeerDisconnected"
],
/**
@ -118,7 +119,8 @@ loop.store.TextChatStore = (function() {
// Notify MozLoopService if appropriate that a message has been appended
// and it should therefore check if we need a different sized window or not.
if (message.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME &&
message.contentType !== CHAT_CONTENT_TYPES.CONTEXT) {
message.contentType !== CHAT_CONTENT_TYPES.CONTEXT &&
message.contentType !== CHAT_CONTENT_TYPES.NOTIFICATION) {
if (this._storeState.textChatEnabled) {
window.dispatchEvent(new CustomEvent("LoopChatMessageAppended"));
} else {
@ -136,7 +138,8 @@ loop.store.TextChatStore = (function() {
// If we don't know how to deal with this content, then skip it
// as this version doesn't support it.
if (actionData.contentType !== CHAT_CONTENT_TYPES.TEXT &&
actionData.contentType !== CHAT_CONTENT_TYPES.CONTEXT_TILE) {
actionData.contentType !== CHAT_CONTENT_TYPES.CONTEXT_TILE &&
actionData.contentType !== CHAT_CONTENT_TYPES.NOTIFICATION) {
return;
}
@ -222,6 +225,32 @@ loop.store.TextChatStore = (function() {
this._appendContextTileMessage(actionData);
},
/**
* Handles a remote peer disconnecting from the session.
* With specific to text chat area, we will put a notification
* when the peer has left the room or unexpectedly quit.
*
* @param {sharedActions.remotePeerDisconnected} actionData
*/
remotePeerDisconnected: function(actionData) {
var notificationTextKey;
if (actionData.peerHungup) {
notificationTextKey = "peer_left_session";
} else {
notificationTextKey = "peer_unexpected_quit";
}
var message = {
contentType: CHAT_CONTENT_TYPES.NOTIFICATION,
message: notificationTextKey,
receivedTimestamp: (new Date()).toISOString()
};
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.RECEIVED, message);
},
/**
* Appends a context tile message to the UI and sends it.
*

Просмотреть файл

@ -31,8 +31,7 @@ loop.shared.views.chat = function (mozL10n) {
message: React.PropTypes.string.isRequired,
showTimestamp: React.PropTypes.bool.isRequired,
timestamp: React.PropTypes.string.isRequired,
type: React.PropTypes.string.isRequired,
useDesktopPaths: React.PropTypes.bool
type: React.PropTypes.string.isRequired
},
/**
@ -53,11 +52,12 @@ loop.shared.views.chat = function (mozL10n) {
render: function () {
var classes = classNames({
"text-chat-entry": true,
"text-chat-entry": this.props.contentType !== CHAT_CONTENT_TYPES.NOTIFICATION,
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
"sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
"special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME,
"text-chat-notif": this.props.contentType === CHAT_CONTENT_TYPES.NOTIFICATION
});
var optionalProps = {};
@ -76,8 +76,25 @@ loop.shared.views.chat = function (mozL10n) {
description: this.props.message,
dispatcher: this.props.dispatcher,
thumbnail: this.props.extraData.newRoomThumbnail,
url: this.props.extraData.newRoomURL,
useDesktopPaths: this.props.useDesktopPaths }),
url: this.props.extraData.newRoomURL }),
this.props.showTimestamp ? this._renderTimestamp() : null
);
}
if (this.props.contentType === CHAT_CONTENT_TYPES.NOTIFICATION) {
return React.createElement(
"div",
{ className: classes },
React.createElement(
"div",
{ className: "content-wrapper" },
React.createElement("img", { className: "notification-icon", src: "shared/img/leave_notification.svg" }),
React.createElement(
"p",
null,
mozL10n.get(this.props.message)
)
),
this.props.showTimestamp ? this._renderTimestamp() : null
);
}
@ -132,8 +149,7 @@ loop.shared.views.chat = function (mozL10n) {
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
messageList: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
showInitialContext: React.PropTypes.bool.isRequired,
useDesktopPaths: React.PropTypes.bool.isRequired
showInitialContext: React.PropTypes.bool.isRequired
},
getInitialState: function () {
@ -144,7 +160,7 @@ loop.shared.views.chat = function (mozL10n) {
_hasChatMessages: function () {
return this.props.messageList.some(function (message) {
return message.contentType === CHAT_CONTENT_TYPES.TEXT;
return message.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME && message.contentType !== CHAT_CONTENT_TYPES.CONTEXT;
});
},
@ -219,8 +235,7 @@ loop.shared.views.chat = function (mozL10n) {
description: entry.message,
dispatcher: this.props.dispatcher,
thumbnail: entry.extraData.thumbnail,
url: entry.extraData.location,
useDesktopPaths: this.props.useDesktopPaths })
url: entry.extraData.location })
);
default:
console.error("Unsupported contentType", entry.contentType);
@ -245,8 +260,7 @@ loop.shared.views.chat = function (mozL10n) {
message: entry.message,
showTimestamp: shouldShowTimestamp,
timestamp: timestamp,
type: entry.type,
useDesktopPaths: this.props.useDesktopPaths });
type: entry.type });
}, this)
)
);
@ -393,7 +407,7 @@ loop.shared.views.chat = function (mozL10n) {
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
showInitialContext: React.PropTypes.bool.isRequired,
useDesktopPaths: React.PropTypes.bool.isRequired
showTile: React.PropTypes.bool.isRequired
},
getInitialState: function () {
@ -428,12 +442,14 @@ loop.shared.views.chat = function (mozL10n) {
React.createElement(TextChatEntriesView, {
dispatcher: this.props.dispatcher,
messageList: messageList,
showInitialContext: this.props.showInitialContext,
useDesktopPaths: this.props.useDesktopPaths }),
showInitialContext: this.props.showInitialContext }),
React.createElement(TextChatInputView, {
dispatcher: this.props.dispatcher,
showPlaceholder: !hasSentMessages,
textChatEnabled: this.state.textChatEnabled })
textChatEnabled: this.state.textChatEnabled }),
React.createElement(sharedViews.AdsTileView, {
dispatcher: this.props.dispatcher,
showTile: this.props.showTile })
);
}
});

Просмотреть файл

@ -118,11 +118,13 @@ if (inChrome) {
CONTEXT: "chat-context",
TEXT: "chat-text",
ROOM_NAME: "room-name",
CONTEXT_TILE: "context-tile"
CONTEXT_TILE: "context-tile",
NOTIFICATION: "chat-notification"
};
var CURSOR_MESSAGE_TYPES = {
POSITION: "cursor-position"
POSITION: "cursor-position",
CLICK: "cursor-click"
};
/**

Просмотреть файл

@ -572,8 +572,6 @@ loop.shared.views = function (_, mozL10n) {
* shown.
* @property {String} url The url to be displayed. If not present or invalid,
* then this view won't be displayed.
* @property {Boolean} useDesktopPaths Whether or not to use the desktop paths for for the
* fallback url.
*/
var ContextUrlView = React.createClass({
displayName: "ContextUrlView",
@ -585,8 +583,7 @@ loop.shared.views = function (_, mozL10n) {
description: React.PropTypes.string.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
thumbnail: React.PropTypes.string,
url: React.PropTypes.string,
useDesktopPaths: React.PropTypes.bool.isRequired
url: React.PropTypes.string
},
/**
@ -619,7 +616,7 @@ loop.shared.views = function (_, mozL10n) {
var thumbnail = this.props.thumbnail;
if (!thumbnail) {
thumbnail = this.props.useDesktopPaths ? "shared/img/icons-16x16.svg#globe" : "shared/img/icons-16x16.svg#globe";
thumbnail = "shared/img/icons-16x16.svg#globe";
}
var wrapperClasses = classNames({
@ -703,6 +700,7 @@ loop.shared.views = function (_, mozL10n) {
window.removeEventListener("resize", this.handleVideoDimensions);
videoElement.removeEventListener("loadeddata", this.handleVideoDimensions);
videoElement.removeEventListener("mousemove", this.handleMousemove);
videoElement.removeEventListener("click", this.handleMouseClick);
},
componentDidUpdate: function () {
@ -765,6 +763,12 @@ loop.shared.views = function (_, mozL10n) {
}));
},
handleMouseClick: function () {
this.props.dispatcher.dispatch(new sharedActions.SendCursorData({
type: loop.shared.utils.CURSOR_MESSAGE_TYPES.CLICK
}));
},
/**
* Attaches a video stream from a donor video element to this component's
* video element if the component is displaying one.
@ -790,6 +794,7 @@ loop.shared.views = function (_, mozL10n) {
if (this.props.shareCursor && !this.props.screenSharingPaused) {
videoElement.addEventListener("loadeddata", this.handleVideoDimensions);
videoElement.addEventListener("mousemove", this.handleMouseMove);
videoElement.addEventListener("click", this.handleMouseClick);
}
// Set the src of our video element
@ -880,7 +885,7 @@ loop.shared.views = function (_, mozL10n) {
screenSharePosterUrl: React.PropTypes.string,
screenSharingPaused: React.PropTypes.bool,
showInitialContext: React.PropTypes.bool.isRequired,
useDesktopPaths: React.PropTypes.bool.isRequired
showTile: React.PropTypes.bool.isRequired
},
isLocalMediaAbsolutelyPositioned: function (matchMedia) {
@ -997,7 +1002,7 @@ loop.shared.views = function (_, mozL10n) {
React.createElement(loop.shared.views.chat.TextChatView, {
dispatcher: this.props.dispatcher,
showInitialContext: this.props.showInitialContext,
useDesktopPaths: this.props.useDesktopPaths }),
showTile: this.props.showTile }),
this.state.localMediaAboslutelyPositioned ? null : this.renderLocalVideo()
)
);
@ -1007,6 +1012,10 @@ loop.shared.views = function (_, mozL10n) {
var RemoteCursorView = React.createClass({
displayName: "RemoteCursorView",
statics: {
TRIGGERED_RESET_DELAY: 1000
},
mixins: [React.addons.PureRenderMixin, loop.store.StoreMixin("remoteCursorStore")],
propTypes: {
@ -1098,16 +1107,99 @@ loop.shared.views = function (_, mozL10n) {
};
},
resetClickState: function () {
this.getStore().setStoreState({
remoteCursorClick: false
});
},
render: function () {
if (!this.state.remoteCursorPosition || !this.state.videoLetterboxing) {
return null;
}
return React.createElement("div", { className: "remote-cursor", style: this.calculateCursorPosition() });
var cx = classNames;
var cursorClasses = cx({
"remote-cursor-container": true,
"remote-cursor-clicked": this.state.remoteCursorClick
});
if (this.state.remoteCursorClick) {
setTimeout(this.resetClickState, this.constructor.TRIGGERED_RESET_DELAY);
}
return React.createElement(
"div",
{ className: cursorClasses, style: this.calculateCursorPosition() },
React.createElement("div", { className: "remote-cursor" })
);
}
});
var AdsTileView = React.createClass({
displayName: "AdsTileView",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
showTile: React.PropTypes.bool.isRequired
},
componentDidMount: function () {
// Watch for messages from the waiting-tile iframe
window.addEventListener("message", this.recordTileClick);
},
componentWillUnmount: function () {
window.removeEventListener("message", this.recordTileClick);
},
recordTileClick: function (event) {
if (event.data === "tile-click") {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: "Tiles iframe click"
}));
}
},
recordTilesSupport: function () {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: "Tiles support link click"
}));
},
render: function () {
if (!this.props.showTile) {
window.removeEventListener("message", this.recordTileClick);
return null;
}
return React.createElement(
"div",
{ className: "ads-tile" },
React.createElement(
"div",
{ className: "ads-wrapper" },
React.createElement(
"p",
null,
mozL10n.get("rooms_read_while_wait_offer2")
),
React.createElement(
"a",
{ href: loop.config.tilesSupportUrl,
onClick: this.recordTilesSupport,
rel: "noreferrer",
target: "_blank" },
React.createElement("i", { className: "room-waiting-help" })
),
React.createElement("iframe", { className: "room-waiting-tile", src: loop.config.tilesIframeUrl })
)
);
}
});
return {
AdsTileView: AdsTileView,
AudioMuteButton: AudioMuteButton,
AvatarView: AvatarView,
Button: Button,

Просмотреть файл

@ -88,6 +88,14 @@ describe("loop.store.ActiveRoomStore", function() {
});
});
describe("#getInitialStoreState", function() {
it("should return an object with roomContextUrls set to null", function() {
var initialState = store.getInitialStoreState();
expect(initialState).to.have.a.property("roomContextUrls", null);
});
});
describe("#roomFailure", function() {
var fakeError;
@ -1576,12 +1584,35 @@ describe("loop.store.ActiveRoomStore", function() {
owner: false
}]
});
sandbox.stub(console, "error");
});
afterEach(function() {
store.endScreenShare();
});
it("should log an error if the state is not inactive", function() {
store.setStoreState({
screenSharingState: SCREEN_SHARE_STATES.PENDING
});
store.startBrowserShare(new sharedActions.StartBrowserShare());
sinon.assert.calledOnce(console.error);
});
it("should not do anything if the state is not inactive", function() {
store.setStoreState({
screenSharingState: SCREEN_SHARE_STATES.PENDING
});
store.startBrowserShare(new sharedActions.StartBrowserShare());
sinon.assert.notCalled(requestStubs.AddBrowserSharingListener);
sinon.assert.notCalled(fakeSdkDriver.startScreenShare);
});
it("should set the state to 'pending'", function() {
store.startBrowserShare(new sharedActions.StartBrowserShare());
sinon.assert.calledOnce(dispatcher.dispatch);
@ -1856,6 +1887,26 @@ describe("loop.store.ActiveRoomStore", function() {
expect(participants).to.have.length.of(1);
expect(participants[0].owner).eql(true);
});
it("should clear the streamPaused state", function() {
store.setStoreState({
streamPaused: true
});
store.remotePeerDisconnected();
expect(store.getStoreState().streamPaused).eql(false);
});
it("should set the remotePeerDisconnected to `true", function() {
store.setStoreState({
remotePeerDisconnected: false
});
store.remotePeerDisconnected();
expect(store.getStoreState().remotePeerDisconnected).eql(true);
});
});
describe("#connectionStatus", function() {
@ -1997,6 +2048,16 @@ describe("loop.store.ActiveRoomStore", function() {
sinon.assert.calledWith(requestStubs["HangupNow"], "fakeToken", "1627384950");
});
it("should call 'HangupNow' when _isDesktop is true and windowStayingOpen", function() {
store._isDesktop = true;
store.leaveRoom({
windowStayingOpen: true
});
sinon.assert.calledOnce(requestStubs["HangupNow"]);
});
it("should not call 'HangupNow' Loop API when _isDesktop is true", function() {
store._isDesktop = true;
@ -2029,6 +2090,7 @@ describe("loop.store.ActiveRoomStore", function() {
audioMuted: true,
localVideoDimensions: { x: 10 },
receivingScreenShare: true,
remotePeerDisconnected: true,
remoteVideoDimensions: { y: 10 },
screenSharingState: true,
videoMuted: true,
@ -2040,6 +2102,7 @@ describe("loop.store.ActiveRoomStore", function() {
expect(store._storeState.audioMuted).eql(false);
expect(store._storeState.localVideoDimensions).eql({});
expect(store._storeState.receivingScreenShare).eql(false);
expect(store._storeState.remotePeerDisconnected).eql(false);
expect(store._storeState.remoteVideoDimensions).eql({});
expect(store._storeState.screenSharingState).eql(SCREEN_SHARE_STATES.INACTIVE);
expect(store._storeState.videoMuted).eql(false);

Просмотреть файл

@ -270,7 +270,7 @@ var LoopMochaUtils = (function(global, _) {
var consoleWarn = console.warn;
var consoleError = console.error;
console.warn = function() {
var args = Array.slice(arguments);
var args = Array.prototype.slice.call(arguments);
try {
throw new Error();
} catch (e) {
@ -279,7 +279,7 @@ var LoopMochaUtils = (function(global, _) {
consoleWarn.apply(console, args);
};
console.error = function() {
var args = Array.slice(arguments);
var args = Array.prototype.slice.call(arguments);
try {
throw new Error();
} catch (e) {

Просмотреть файл

@ -69,8 +69,10 @@ describe("loop.OTSdkDriver", function() {
});
sdk = _.extend({
DEBUG: "fakeDebug",
initPublisher: sinon.stub().returns(publisher),
initSession: sinon.stub().returns(session)
initSession: sinon.stub().returns(session),
setLogLevel: sinon.stub()
}, Backbone.Events);
window.OT = {
@ -141,6 +143,41 @@ describe("loop.OTSdkDriver", function() {
recvStreams: 0
});
});
it("should enable debug for two way media telemetry if required", function() {
// Simulate the pref being enabled.
sandbox.stub(loop.shared.utils, "getBoolPreference", function(prefName, callback) {
if (prefName === "debug.twoWayMediaTelemetry") {
callback(true);
}
});
driver = new loop.OTSdkDriver({
constants: constants,
dispatcher: dispatcher,
sdk: sdk
});
expect(driver._debugTwoWayMediaTelemetry).eql(true);
});
it("should enable debug on the sdk if required", function() {
// Simulate the pref being enabled.
sandbox.stub(loop.shared.utils, "getBoolPreference", function(prefName, callback) {
if (prefName === "debug.sdk") {
callback(true);
}
});
driver = new loop.OTSdkDriver({
constants: constants,
dispatcher: dispatcher,
sdk: sdk
});
sinon.assert.calledOnce(sdk.setLogLevel);
sinon.assert.calledWithExactly(sdk.setLogLevel, sdk.DEBUG);
});
});
describe("#setupStreamElements", function() {
@ -1327,6 +1364,18 @@ describe("loop.OTSdkDriver", function() {
new sharedActions.ReceivingScreenShare({ receiving: true }));
});
it("should dispatch a VideoScreenStreamChanged action for paused screen sharing streams", function() {
fakeStream.videoType = "screen";
fakeStream.hasVideo = false;
session.trigger("streamCreated", { stream: fakeStream });
// Called twice due to the VideoDimensionsChanged above.
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.VideoScreenStreamChanged({ hasVideo: false }));
});
it("should dispatch a ReceivingScreenShare action for screen sharing streams", function() {
fakeStream.videoType = "screen";

Просмотреть файл

@ -40,15 +40,16 @@ describe("loop.store.RemoteCursorStore", function() {
}).to.Throw(/sdkDriver/);
});
it("should add a CursorPositionChange event listener", function() {
it("should add a event listeners", function() {
sandbox.stub(loop, "subscribe");
new loop.store.RemoteCursorStore(dispatcher, { sdkDriver: fakeSdkDriver });
sinon.assert.calledOnce(loop.subscribe);
sinon.assert.calledTwice(loop.subscribe);
sinon.assert.calledWith(loop.subscribe, "CursorPositionChange");
sinon.assert.calledWith(loop.subscribe, "CursorClick");
});
});
describe("#_cursorPositionChangeListener", function() {
describe("#cursor position change", function() {
it("should send cursor data through the sdk", function() {
var fakeEvent = {
ratioX: 10,
@ -66,6 +67,19 @@ describe("loop.store.RemoteCursorStore", function() {
});
});
describe("#cursor click", function() {
it("should send cursor data through the sdk", function() {
var fakeClick = true;
LoopMochaUtils.publish("CursorClick", fakeClick);
sinon.assert.calledOnce(fakeSdkDriver.sendCursorMessage);
sinon.assert.calledWith(fakeSdkDriver.sendCursorMessage, {
type: CURSOR_MESSAGE_TYPES.CLICK
});
});
});
describe("#sendCursorData", function() {
it("should do nothing if not a proper event", function() {
var fakeData = {
@ -112,7 +126,7 @@ describe("loop.store.RemoteCursorStore", function() {
sinon.assert.notCalled(store.setStoreState);
});
it("should save the state", function() {
it("should save the state of the cursor position", function() {
store.receivedCursorData(new sharedActions.ReceivedCursorData({
type: CURSOR_MESSAGE_TYPES.POSITION,
ratioX: 10,
@ -124,6 +138,14 @@ describe("loop.store.RemoteCursorStore", function() {
ratioY: 10
});
});
it("should save the state of the cursor click", function() {
store.receivedCursorData(new sharedActions.ReceivedCursorData({
type: CURSOR_MESSAGE_TYPES.CLICK
}));
expect(store.getStoreState().remoteCursorClick).eql(true);
});
});
describe("#videoDimensionsChanged", function() {

Просмотреть файл

@ -18,5 +18,7 @@
return true;
};
window.OT.setLogLevel = function() {};
})(window);

Просмотреть файл

@ -334,4 +334,31 @@ describe("loop.store.TextChatStore", function() {
expect(store.getStoreState("messageList").length).eql(2);
});
});
describe("#remotePeerDisconnected", function() {
it("should append the right message when peer disconnected cleanly", function() {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: true
}));
expect(store.getStoreState("messageList").length).eql(1);
expect(store.getStoreState("messageList")[0].contentType).eql(
CHAT_CONTENT_TYPES.NOTIFICATION
);
expect(store.getStoreState("messageList")[0].message).eql("peer_left_session");
});
it("should append the right message when peer disconnected unexpectedly", function() {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: false
}));
expect(store.getStoreState("messageList").length).eql(1);
expect(store.getStoreState("messageList")[0].contentType).eql(
CHAT_CONTENT_TYPES.NOTIFICATION
);
expect(store.getStoreState("messageList")[0].message).eql("peer_unexpected_quit");
});
});
});

Просмотреть файл

@ -6,6 +6,7 @@ describe("loop.shared.views.TextChatView", function() {
var expect = chai.expect;
var sharedActions = loop.shared.actions;
var sharedViews = loop.shared.views;
var TestUtils = React.addons.TestUtils;
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
var CHAT_CONTENT_TYPES = loop.shared.utils.CHAT_CONTENT_TYPES;
@ -43,6 +44,11 @@ describe("loop.shared.views.TextChatView", function() {
sandbox.stub(mozL10n, "get", function(string) {
return string;
});
// Need to stub these methods because when mounting the AdsTileView we are
// attaching listeners to the window object and "AdsTileView" tests will failed
sandbox.stub(window, "addEventListener");
sandbox.stub(window, "removeEventListener");
});
afterEach(function() {
@ -58,8 +64,7 @@ describe("loop.shared.views.TextChatView", function() {
var basicProps = {
dispatcher: dispatcher,
messageList: [],
showInitialContext: true,
useDesktopPaths: false
showInitialContext: true
};
return TestUtils.renderIntoDocument(
@ -71,8 +76,7 @@ describe("loop.shared.views.TextChatView", function() {
var basicProps = {
dispatcher: dispatcher,
messageList: [],
showInitialContext: true,
useDesktopPaths: false
showInitialContext: true
};
return React.render(
@ -267,6 +271,40 @@ describe("loop.shared.views.TextChatView", function() {
expect(node.scrollTop).eql(node.scrollHeight - node.clientHeight);
});
it("should scroll when a notification is added", function() {
var messageList = [{
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.NOTIFICATION,
message: "Bye!",
receivedTimestamp: "2015-06-25T17:53:55.357Z"
}];
view.setProps({ messageList: messageList });
node = view.getDOMNode();
expect(node.scrollTop).eql(node.scrollHeight - node.clientHeight);
});
it("should scroll when a context tile is added", function() {
var messageList = [{
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: "A marvelous page!",
extraData: {
roomToken: "fake",
newRoomURL: "http://marvelous.invalid"
},
receivedTimestamp: "2015-06-25T17:53:55.357Z"
}];
view.setProps({ messageList: messageList });
node = view.getDOMNode();
expect(node.scrollTop).eql(node.scrollHeight - node.clientHeight);
});
it("should not scroll when a context tile is added", function() {
var messageList = [{
type: CHAT_MESSAGE_TYPES.SPECIAL,
@ -411,8 +449,8 @@ describe("loop.shared.views.TextChatView", function() {
var props = _.extend({
dispatcher: dispatcher,
showInitialContext: true,
useDesktopPaths: false,
showAlways: true
showAlways: true,
showTile: false
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(loop.shared.views.chat.TextChatView, props));
@ -581,6 +619,21 @@ describe("loop.shared.views.TextChatView", function() {
expect(node.querySelector(".context-url-view-wrapper")).to.not.eql(null);
});
it("should render a ContextUrlView for a CONTEXT_TILE", function() {
view = mountTestComponent();
store.updateRoomContext(new sharedActions.UpdateRoomContext({
newRoomDescription: "fake",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com",
roomToken: "fakeRoomToken"
}));
expect(function() {
TestUtils.findRenderedComponentWithType(view, sharedViews.ContextUrlView);
}).to.not.Throw();
});
it("should not render a room title and context url when show initial context is false", function() {
view = mountTestComponent({
showInitialContext: false
@ -667,5 +720,29 @@ describe("loop.shared.views.TextChatView", function() {
expect(textBox.placeholder).not.contain("placeholder");
});
it("should add `text-chat-notif` CSS class selector to msg of contentType NOTIFICATION",
function() {
view = mountTestComponent();
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: true
}));
var node = view.getDOMNode();
expect(node.querySelector(".text-chat-notif")).to.not.eql(null);
});
it("should render an icon for contentType NOTIFICATION",
function() {
view = mountTestComponent();
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: true
}));
var node = view.getDOMNode();
expect(node.querySelectorAll(".notification-icon").length).to.eql(1);
});
});
});

Просмотреть файл

@ -34,8 +34,11 @@
var Assertion = chai.Assertion;
var assert = chai.assert;
function isJQueryPromise(thenable) {
return typeof thenable.always === "function" &&
function isLegacyJQueryPromise(thenable) {
// jQuery promises are Promises/A+-compatible since 3.0.0. jQuery 3.0.0 is also the first version
// to define the catch method.
return typeof thenable.catch !== "function" &&
typeof thenable.always === "function" &&
typeof thenable.done === "function" &&
typeof thenable.fail === "function" &&
typeof thenable.pipe === "function" &&
@ -47,9 +50,10 @@
if (typeof assertion._obj.then !== "function") {
throw new TypeError(utils.inspect(assertion._obj) + " is not a thenable.");
}
if (isJQueryPromise(assertion._obj)) {
throw new TypeError("Chai as Promised is incompatible with jQuery's thenables, sorry! Please use a " +
"Promises/A+ compatible library (see http://promisesaplus.com/).");
if (isLegacyJQueryPromise(assertion._obj)) {
throw new TypeError("Chai as Promised is incompatible with thenables of jQuery<3.0.0, sorry! Please " +
"upgrade jQuery or use another Promises/A+ compatible library (see " +
"http://promisesaplus.com/).");
}
}
@ -233,8 +237,8 @@
doNotify(getBasePromise(this), done);
});
method("become", function (value) {
return this.eventually.deep.equal(value);
method("become", function (value, message) {
return this.eventually.deep.equal(value, message);
});
////////

Просмотреть файл

@ -35,6 +35,11 @@ describe("loop.shared.views", function() {
sandbox.stub(loop.shared.utils, "getOSVersion", function() {
return OSVersion;
});
loop.config = {
tilesIframeUrl: "",
tilesSupportUrl: ""
};
});
afterEach(function() {
@ -147,7 +152,7 @@ describe("loop.shared.views", function() {
});
});
describe("VideoMuteButton", function() {
describe("VideoMuteButton", function() {
it("should set the muted class when not enabled", function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.VideoMuteButton, {
@ -457,8 +462,7 @@ describe("VideoMuteButton", function() {
allowClick: false,
description: "test",
dispatcher: dispatcher,
showContextTitle: false,
useDesktopPaths: false
showContextTitle: false
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(sharedViews.ContextUrlView, props));
@ -494,6 +498,24 @@ describe("VideoMuteButton", function() {
expect(view.getDOMNode()).eql(null);
});
it("should display nothing if it is an about url", function() {
view = mountTestComponent({
url: "about:config"
});
expect(view.getDOMNode()).eql(null);
});
it("should display nothing if it is a javascript url", function() {
/* eslint-disable no-script-url */
view = mountTestComponent({
url: "javascript:alert('hello')"
});
expect(view.getDOMNode()).eql(null);
/* eslint-enable no-script-url */
});
it("should use a default thumbnail if one is not supplied", function() {
view = mountTestComponent({
url: "http://wonderful.invalid"
@ -505,7 +527,6 @@ describe("VideoMuteButton", function() {
it("should use a default thumbnail for desktop if one is not supplied", function() {
view = mountTestComponent({
useDesktopPaths: true,
url: "http://wonderful.invalid"
});
@ -703,9 +724,10 @@ describe("VideoMuteButton", function() {
src: { fake: 1 }
});
sinon.assert.calledTwice(fakeVideoElement.addEventListener);
sinon.assert.calledThrice(fakeVideoElement.addEventListener);
sinon.assert.calledWith(fakeVideoElement.addEventListener, "loadeddata");
sinon.assert.calledWith(fakeVideoElement.addEventListener, "mousemove");
sinon.assert.calledWith(fakeVideoElement.addEventListener, "click");
});
it("should attach a video object for Firefox", function() {
@ -772,6 +794,24 @@ describe("VideoMuteButton", function() {
});
});
});
describe("handleMouseClick", function() {
beforeEach(function() {
view = mountTestComponent({
dispatcher: dispatcher,
displayAvatar: false,
mediaType: "local",
srcMediaElement: {
fake: 1
}
});
});
it("should dispatch cursor click event when video element is clicked", function() {
view.handleMouseClick();
sinon.assert.calledOnce(dispatcher.dispatch);
});
});
});
describe("MediaLayoutView", function() {
@ -791,7 +831,7 @@ describe("VideoMuteButton", function() {
matchMedia: window.matchMedia,
renderRemoteVideo: false,
showInitialContext: false,
useDesktopPaths: false
showTile: false
};
return TestUtils.renderIntoDocument(
@ -800,6 +840,10 @@ describe("VideoMuteButton", function() {
}
beforeEach(function() {
loop.config = {
tilesIframeUrl: "",
tilesSupportUrl: ""
};
textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: {}
});
@ -808,6 +852,11 @@ describe("VideoMuteButton", function() {
});
loop.store.StoreMixin.register({ textChatStore: textChatStore });
// Need to stub these methods because when mounting the AdsTileView we are
// attaching listeners to the window object and "AdsTileView" tests will failed
sandbox.stub(window, "addEventListener");
sandbox.stub(window, "removeEventListener");
});
it("should mark the remote stream as the focus stream when not displaying screen share", function() {
@ -1048,5 +1097,82 @@ describe("VideoMuteButton", function() {
});
});
});
describe("resetClickState", function() {
beforeEach(function() {
remoteCursorStore.setStoreState({ remoteCursorClick: true });
view = mountTestComponent({
videoElementSize: fakeVideoElementSize
});
});
it("should restore the state to its default value", function() {
view.resetClickState();
expect(view.getStore().getStoreState().remoteCursorClick).eql(false);
});
});
describe("#render", function() {
beforeEach(function() {
remoteCursorStore.setStoreState({ remoteCursorClick: true });
view = mountTestComponent({
videoElementSize: fakeVideoElementSize
});
});
it("should add click class to the remote cursor", function() {
expect(view.getDOMNode().classList.contains("remote-cursor-clicked")).eql(true);
});
it("should remove the click class when the animation is completed", function() {
clock.tick(sharedViews.RemoteCursorView.TRIGGERED_RESET_DELAY);
expect(view.getDOMNode().classList.contains("remote-cursor-clicked")).eql(false);
});
});
});
describe("AdsTileView", function() {
it("should dispatch a RecordClick action when the tile is clicked", function(done) {
// Point the iframe to a page that will auto-"click"
loop.config.tilesIframeUrl = "data:text/html,<script>parent.postMessage('tile-click', '*');</script>";
// Render the iframe into the fixture to cause it to load
React.render(React.createElement(
sharedViews.AdsTileView, {
dispatcher: dispatcher,
showTile: true
}), document.querySelector("#fixtures"));
// Wait for the iframe to load and trigger a message that should also
// cause the RecordClick action
window.addEventListener("message", function onMessage() {
window.removeEventListener("message", onMessage);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RecordClick({
linkInfo: "Tiles iframe click"
}));
done();
});
});
it("should dispatch a RecordClick action when the support button is clicked", function() {
var view = TestUtils.renderIntoDocument(
React.createElement(sharedViews.AdsTileView, {
dispatcher: dispatcher,
showTile: true
}));
var node = view.getDOMNode().querySelector("a");
TestUtils.Simulate.click(node);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RecordClick({
linkInfo: "Tiles support link click"
}));
});
});
});

Просмотреть файл

@ -1,6 +1,6 @@
// Backbone.js 1.2.1
// Backbone.js 1.3.2
// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// (c) 2010-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
@ -9,8 +9,8 @@
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
// We use `self` instead of `window` for `WebWorker` support.
var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global);
var root = (typeof self == 'object' && self.self === self && self) ||
(typeof global == 'object' && global.global === global && global);
// Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {
@ -23,7 +23,7 @@
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore'), $;
try { $ = require('jquery'); } catch(e) {}
try { $ = require('jquery'); } catch (e) {}
factory(root, exports, _, $);
// Finally, as a browser global.
@ -31,7 +31,7 @@
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
}
}(function(root, Backbone, _, $) {
})(function(root, Backbone, _, $) {
// Initial Setup
// -------------
@ -41,10 +41,10 @@
var previousBackbone = root.Backbone;
// Create a local reference to a common array method we'll want to use later.
var slice = [].slice;
var slice = Array.prototype.slice;
// Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '1.2.1';
Backbone.VERSION = '1.3.2';
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable.
@ -68,8 +68,13 @@
// form param named `model`.
Backbone.emulateJSON = false;
// Proxy Underscore methods to a Backbone class' prototype using a
// particular attribute as the data argument
// Proxy Backbone class methods to Underscore functions, wrapping the model's
// `attributes` object or collection's `models` array behind the scenes.
//
// collection.filter(function(model) { return model.get('age') > 10 });
// collection.each(this.addView);
//
// `Function#apply` can be slow so we use the method's arg count, if we know it.
var addMethod = function(length, method, attribute) {
switch (length) {
case 1: return function() {
@ -79,10 +84,10 @@
return _[method](this[attribute], value);
};
case 3: return function(iteratee, context) {
return _[method](this[attribute], iteratee, context);
return _[method](this[attribute], cb(iteratee, this), context);
};
case 4: return function(iteratee, defaultVal, context) {
return _[method](this[attribute], iteratee, defaultVal, context);
return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
};
default: return function() {
var args = slice.call(arguments);
@ -97,12 +102,26 @@
});
};
// Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
var cb = function(iteratee, instance) {
if (_.isFunction(iteratee)) return iteratee;
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
return iteratee;
};
var modelMatcher = function(attrs) {
var matcher = _.matches(attrs);
return function(model) {
return matcher(model.attributes);
};
};
// Backbone.Events
// ---------------
// A module that can be mixed in to *any object* in order to provide it with
// custom events. You may bind with `on` or remove with `off` callback
// functions to an event; `trigger`-ing an event fires all callbacks in
// a custom event channel. You may bind a callback to an event with `on` or
// remove with `off`; `trigger`-ing an event fires all callbacks in
// succession.
//
// var object = {};
@ -117,26 +136,25 @@
// Iterates over the standard `event, callback` (as well as the fancy multiple
// space-separated events `"change blur", callback` and jQuery-style event
// maps `{event: callback}`), reducing them by manipulating `memo`.
// Passes a normalized single event name and callback, as well as any
// optional `opts`.
var eventsApi = function(iteratee, memo, name, callback, opts) {
// maps `{event: callback}`).
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0, names;
if (name && typeof name === 'object') {
// Handle event maps.
if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
for (names = _.keys(name); i < names.length ; i++) {
memo = iteratee(memo, names[i], name[names[i]], opts);
events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
}
} else if (name && eventSplitter.test(name)) {
// Handle space separated event names.
// Handle space-separated event names by delegating them individually.
for (names = name.split(eventSplitter); i < names.length; i++) {
memo = iteratee(memo, names[i], callback, opts);
events = iteratee(events, names[i], callback, opts);
}
} else {
memo = iteratee(memo, name, callback, opts);
// Finally, standard events.
events = iteratee(events, name, callback, opts);
}
return memo;
return events;
};
// Bind an event to a `callback` function. Passing `"all"` will bind
@ -145,13 +163,12 @@
return internalOn(this, name, callback, context);
};
// An internal use `on` function, used to guard the `listening` argument from
// the public API.
// Guard the `listening` argument from the public API.
var internalOn = function(obj, name, callback, context, listening) {
obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
context: context,
ctx: obj,
listening: listening
context: context,
ctx: obj,
listening: listening
});
if (listening) {
@ -163,8 +180,9 @@
};
// Inversion-of-control versions of `on`. Tell *this* object to listen to
// an event in another object... keeping track of what it's listening to.
Events.listenTo = function(obj, name, callback) {
// an event in another object... keeping track of what it's listening to
// for easier unbinding later.
Events.listenTo = function(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
@ -189,7 +207,7 @@
var context = options.context, ctx = options.ctx, listening = options.listening;
if (listening) listening.count++;
handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening });
handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
}
return events;
};
@ -198,18 +216,18 @@
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
Events.off = function(name, callback, context) {
Events.off = function(name, callback, context) {
if (!this._events) return this;
this._events = eventsApi(offApi, this._events, name, callback, {
context: context,
listeners: this._listeners
context: context,
listeners: this._listeners
});
return this;
};
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
Events.stopListening = function(obj, name, callback) {
Events.stopListening = function(obj, name, callback) {
var listeningTo = this._listeningTo;
if (!listeningTo) return this;
@ -224,14 +242,12 @@
listening.obj.off(name, callback, this);
}
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
return this;
};
// The reducing API that removes a callback from the `events` object.
var offApi = function(events, name, callback, options) {
// No events to consider.
if (!events) return;
var i = 0, listening;
@ -282,21 +298,22 @@
delete events[name];
}
}
if (_.size(events)) return events;
return events;
};
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, it will be removed. When multiple events are
// passed in using the space-separated syntax, the event will fire once for every
// event you passed in, not once for a combination of all events
Events.once = function(name, callback, context) {
// the callback is invoked, its listener will be removed. If multiple events
// are passed in using the space-separated syntax, the handler will fire
// once for each event, not once for a combination of all events.
Events.once = function(name, callback, context) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
return this.on(events, void 0, context);
if (typeof name === 'string' && context == null) callback = void 0;
return this.on(events, callback, context);
};
// Inversion-of-control versions of `once`.
Events.listenToOnce = function(obj, name, callback) {
Events.listenToOnce = function(obj, name, callback) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
return this.listenTo(obj, events);
@ -319,7 +336,7 @@
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
Events.trigger = function(name) {
Events.trigger = function(name) {
if (!this._events) return this;
var length = Math.max(0, arguments.length - 1);
@ -331,7 +348,7 @@
};
// Handles triggering the appropriate event callbacks.
var triggerApi = function(objEvents, name, cb, args) {
var triggerApi = function(objEvents, name, callback, args) {
if (objEvents) {
var events = objEvents[name];
var allEvents = objEvents.all;
@ -381,7 +398,8 @@
this.attributes = {};
if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {};
attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
var defaults = _.result(this, 'defaults');
attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
@ -476,9 +494,6 @@
var changed = this.changed;
var prev = this._previousAttributes;
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
// For each `set` attribute, update or delete the current value.
for (var attr in attrs) {
val = attrs[attr];
@ -491,6 +506,9 @@
unset ? delete current[attr] : current[attr] = val;
}
// Update the `id`.
if (this.idAttribute in attrs) this.id = this.get(this.idAttribute);
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = options;
@ -602,8 +620,8 @@
// the model will be valid when the attributes, if any, are set.
if (attrs && !wait) {
if (!this.set(attrs, options)) return false;
} else {
if (!this._validate(attrs, options)) return false;
} else if (!this._validate(attrs, options)) {
return false;
}
// After a successful server-side save, the client is (optionally)
@ -697,7 +715,7 @@
// Check if the model is currently in a valid state.
isValid: function(options) {
return this._validate({}, _.defaults({validate: true}, options));
return this._validate({}, _.extend({}, options, {validate: true}));
},
// Run validation against the next complete set of model attributes,
@ -713,9 +731,10 @@
});
// Underscore methods that we want to implement on the Model.
var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
omit: 0, chain: 1, isEmpty: 1 };
// Underscore methods that we want to implement on the Model, mapped to the
// number of arguments they take.
var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
omit: 0, chain: 1, isEmpty: 1};
// Mix in each Underscore method as a proxy to `Model#attributes`.
addUnderscoreMethods(Model, modelMethods, 'attributes');
@ -746,6 +765,17 @@
var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, remove: false};
// Splices `insert` into `array` at index `at`.
var splice = function(array, insert, at) {
at = Math.min(Math.max(at, 0), array.length);
var tail = Array(array.length - at);
var length = insert.length;
var i;
for (i = 0; i < tail.length; i++) tail[i] = array[i + at];
for (i = 0; i < length; i++) array[i + at] = insert[i];
for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
};
// Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, {
@ -768,7 +798,9 @@
return Backbone.sync.apply(this, arguments);
},
// Add a model, or list of models to the set.
// Add a model, or list of models to the set. `models` may be Backbone
// Models or raw JavaScript objects to be converted to Models, or any
// combination of the two.
add: function(models, options) {
return this.set(models, _.extend({merge: false}, options, addOptions));
},
@ -777,9 +809,12 @@
remove: function(models, options) {
options = _.extend({}, options);
var singular = !_.isArray(models);
models = singular ? [models] : _.clone(models);
models = singular ? [models] : models.slice();
var removed = this._removeModels(models, options);
if (!options.silent && removed) this.trigger('update', this, options);
if (!options.silent && removed.length) {
options.changes = {added: [], merged: [], removed: removed};
this.trigger('update', this, options);
}
return singular ? removed[0] : removed;
},
@ -788,97 +823,114 @@
// already exist in the collection, as necessary. Similar to **Model#set**,
// the core operation for updating the data contained by the collection.
set: function(models, options) {
options = _.defaults({}, options, setOptions);
if (options.parse && !this._isModel(models)) models = this.parse(models, options);
if (models == null) return;
options = _.extend({}, setOptions, options);
if (options.parse && !this._isModel(models)) {
models = this.parse(models, options) || [];
}
var singular = !_.isArray(models);
models = singular ? (models ? [models] : []) : models.slice();
var id, model, attrs, existing, sort;
models = singular ? [models] : models.slice();
var at = options.at;
if (at != null) at = +at;
if (at > this.length) at = this.length;
if (at < 0) at += this.length + 1;
var sortable = this.comparator && (at == null) && options.sort !== false;
var set = [];
var toAdd = [];
var toMerge = [];
var toRemove = [];
var modelMap = {};
var add = options.add;
var merge = options.merge;
var remove = options.remove;
var sort = false;
var sortable = this.comparator && at == null && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
var toAdd = [], toRemove = [], modelMap = {};
var add = options.add, merge = options.merge, remove = options.remove;
var order = !sortable && add && remove ? [] : false;
var orderChanged = false;
// Turn bare objects into model references, and prevent invalid models
// from being added.
for (var i = 0; i < models.length; i++) {
attrs = models[i];
var model, i;
for (i = 0; i < models.length; i++) {
model = models[i];
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
if (existing = this.get(attrs)) {
if (remove) modelMap[existing.cid] = true;
if (merge && attrs !== existing) {
attrs = this._isModel(attrs) ? attrs.attributes : attrs;
var existing = this.get(model);
if (existing) {
if (merge && model !== existing) {
var attrs = this._isModel(model) ? model.attributes : model;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
toMerge.push(existing);
if (sortable && !sort) sort = existing.hasChanged(sortAttr);
}
if (!modelMap[existing.cid]) {
modelMap[existing.cid] = true;
set.push(existing);
}
models[i] = existing;
// If this is a new, valid model, push it to the `toAdd` list.
} else if (add) {
model = models[i] = this._prepareModel(attrs, options);
if (!model) continue;
toAdd.push(model);
this._addReference(model, options);
model = models[i] = this._prepareModel(model, options);
if (model) {
toAdd.push(model);
this._addReference(model, options);
modelMap[model.cid] = true;
set.push(model);
}
}
// Do not add multiple models with the same `id`.
model = existing || model;
if (!model) continue;
id = this.modelId(model.attributes);
if (order && (model.isNew() || !modelMap[id])) {
order.push(model);
// Check to see if this is actually a new model at this index.
orderChanged = orderChanged || !this.models[i] || model.cid !== this.models[i].cid;
}
modelMap[id] = true;
}
// Remove nonexistent models if appropriate.
// Remove stale models.
if (remove) {
for (var i = 0; i < this.length; i++) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
for (i = 0; i < this.length; i++) {
model = this.models[i];
if (!modelMap[model.cid]) toRemove.push(model);
}
if (toRemove.length) this._removeModels(toRemove, options);
}
// See if sorting is needed, update `length` and splice in new models.
if (toAdd.length || orderChanged) {
var orderChanged = false;
var replace = !sortable && add && remove;
if (set.length && replace) {
orderChanged = this.length !== set.length || _.some(this.models, function(m, index) {
return m !== set[index];
});
this.models.length = 0;
splice(this.models, set, 0);
this.length = this.models.length;
} else if (toAdd.length) {
if (sortable) sort = true;
this.length += toAdd.length;
if (at != null) {
for (var i = 0; i < toAdd.length; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else {
if (order) this.models.length = 0;
var orderedModels = order || toAdd;
for (var i = 0; i < orderedModels.length; i++) {
this.models.push(orderedModels[i]);
}
}
splice(this.models, toAdd, at == null ? this.length : at);
this.length = this.models.length;
}
// Silently sort the collection if appropriate.
if (sort) this.sort({silent: true});
// Unless silenced, it's time to fire all appropriate add/sort events.
// Unless silenced, it's time to fire all appropriate add/sort/update events.
if (!options.silent) {
var addOpts = at != null ? _.clone(options) : options;
for (var i = 0; i < toAdd.length; i++) {
if (at != null) addOpts.index = at + i;
(model = toAdd[i]).trigger('add', model, this, addOpts);
for (i = 0; i < toAdd.length; i++) {
if (at != null) options.index = at + i;
model = toAdd[i];
model.trigger('add', model, this, options);
}
if (sort || orderChanged) this.trigger('sort', this, options);
if (toAdd.length || toRemove.length) this.trigger('update', this, options);
if (toAdd.length || toRemove.length || toMerge.length) {
options.changes = {
added: toAdd,
removed: toRemove,
merged: toMerge
};
this.trigger('update', this, options);
}
}
// Return the added (or merged) model (or models).
@ -928,11 +980,18 @@
return slice.apply(this.models, arguments);
},
// Get a model from the set by id.
// Get a model from the set by id, cid, model object with id or cid
// properties, or an attributes object that is transformed through modelId.
get: function(obj) {
if (obj == null) return void 0;
var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
return this._byId[obj] ||
this._byId[this.modelId(obj.attributes || obj)] ||
obj.cid && this._byId[obj.cid];
},
// Returns `true` if the model is in the collection.
has: function(obj) {
return this.get(obj) != null;
},
// Get the model at the given index.
@ -944,10 +1003,7 @@
// Return models with matching attributes. Useful for simple cases of
// `filter`.
where: function(attrs, first) {
var matches = _.matches(attrs);
return this[first ? 'find' : 'filter'](function(model) {
return matches(model.attributes);
});
return this[first ? 'find' : 'filter'](attrs);
},
// Return the first model with matching attributes. Useful for simple cases
@ -960,23 +1016,26 @@
// normal circumstances, as the set will maintain sort order as each item
// is added.
sort: function(options) {
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
var comparator = this.comparator;
if (!comparator) throw new Error('Cannot sort a set without a comparator');
options || (options = {});
// Run sort based on type of `comparator`.
if (_.isString(this.comparator) || this.comparator.length === 1) {
this.models = this.sortBy(this.comparator, this);
} else {
this.models.sort(_.bind(this.comparator, this));
}
var length = comparator.length;
if (_.isFunction(comparator)) comparator = _.bind(comparator, this);
// Run sort based on type of `comparator`.
if (length === 1 || _.isString(comparator)) {
this.models = this.sortBy(comparator);
} else {
this.models.sort(comparator);
}
if (!options.silent) this.trigger('sort', this, options);
return this;
},
// Pluck an attribute from each model in the collection.
pluck: function(attr) {
return _.invoke(this.models, 'get', attr);
return this.map(attr + '');
},
// Fetch the default set of models for this collection, resetting the
@ -1007,9 +1066,9 @@
if (!wait) this.add(model, options);
var collection = this;
var success = options.success;
options.success = function(model, resp, callbackOpts) {
if (wait) collection.add(model, callbackOpts);
if (success) success.call(callbackOpts.context, model, resp, callbackOpts);
options.success = function(m, resp, callbackOpts) {
if (wait) collection.add(m, callbackOpts);
if (success) success.call(callbackOpts.context, m, resp, callbackOpts);
};
model.save(null, options);
return model;
@ -1030,7 +1089,7 @@
},
// Define how to uniquely identify models in the collection.
modelId: function (attrs) {
modelId: function(attrs) {
return attrs[this.model.prototype.idAttribute || 'id'];
},
@ -1058,7 +1117,6 @@
},
// Internal method called by both remove and set.
// Returns removed models, or false if nothing is removed.
_removeModels: function(models, options) {
var removed = [];
for (var i = 0; i < models.length; i++) {
@ -1069,6 +1127,12 @@
this.models.splice(index, 1);
this.length--;
// Remove references before triggering 'remove' event to prevent an
// infinite loop. #3693
delete this._byId[model.cid];
var id = this.modelId(model.attributes);
if (id != null) delete this._byId[id];
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
@ -1077,12 +1141,12 @@
removed.push(model);
this._removeReference(model, options);
}
return removed.length ? removed : false;
return removed;
},
// Method for checking whether an object should be considered a model for
// the purposes of adding to the collection.
_isModel: function (model) {
_isModel: function(model) {
return model instanceof Model;
},
@ -1108,14 +1172,16 @@
// events simply proxy through. "add" and "remove" events that originate
// in other collections are ignored.
_onModelEvent: function(event, model, collection, options) {
if ((event === 'add' || event === 'remove') && collection !== this) return;
if (event === 'destroy') this.remove(model, options);
if (event === 'change') {
var prevId = this.modelId(model.previousAttributes());
var id = this.modelId(model.attributes);
if (prevId !== id) {
if (prevId != null) delete this._byId[prevId];
if (id != null) this._byId[id] = model;
if (model) {
if ((event === 'add' || event === 'remove') && collection !== this) return;
if (event === 'destroy') this.remove(model, options);
if (event === 'change') {
var prevId = this.modelId(model.previousAttributes());
var id = this.modelId(model.attributes);
if (prevId !== id) {
if (prevId != null) delete this._byId[prevId];
if (id != null) this._byId[id] = model;
}
}
}
this.trigger.apply(this, arguments);
@ -1126,31 +1192,18 @@
// Underscore methods that we want to implement on the Collection.
// 90% of the core usefulness of Backbone Collections is actually implemented
// right here:
var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4,
foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3,
select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 2,
contains: 2, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0,
foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3,
select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
isEmpty: 1, chain: 1, sample: 3, partition: 3 };
isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3};
// Mix in each Underscore method as a proxy to `Collection#models`.
addUnderscoreMethods(Collection, collectionMethods, 'models');
// Underscore methods that take a property name as an argument.
var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
// Use attributes instead of properties.
_.each(attributeMethods, function(method) {
if (!_[method]) return;
Collection.prototype[method] = function(value, context) {
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _[method](this.models, iterator, context);
};
});
// Backbone.View
// -------------
@ -1174,7 +1227,7 @@
// Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// List of view options to be merged as properties.
// List of view options to be set as properties.
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
// Set up all inheritable **Backbone.View** properties and methods.
@ -1396,9 +1449,9 @@
var methodMap = {
'create': 'POST',
'update': 'PUT',
'patch': 'PATCH',
'patch': 'PATCH',
'delete': 'DELETE',
'read': 'GET'
'read': 'GET'
};
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
@ -1518,7 +1571,7 @@
// falls back to polling.
var History = Backbone.History = function() {
this.handlers = [];
_.bindAll(this, 'checkUrl');
this.checkUrl = _.bind(this.checkUrl, this);
// Ensure that `History` can be used outside of the browser.
if (typeof window !== 'undefined') {
@ -1555,8 +1608,8 @@
// Does the pathname match the root?
matchRoot: function() {
var path = this.decodeFragment(this.location.pathname);
var root = path.slice(0, this.root.length - 1) + '/';
return root === this.root;
var rootPath = path.slice(0, this.root.length - 1) + '/';
return rootPath === this.root;
},
// Unicode characters in `location.pathname` are percent encoded so they're
@ -1611,7 +1664,7 @@
this.options = _.extend({root: '/'}, this.options, options);
this.root = this.options.root;
this._wantsHashChange = this.options.hashChange !== false;
this._hasHashChange = 'onhashchange' in window;
this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
this._useHashChange = this._wantsHashChange && this._hasHashChange;
this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.history && this.history.pushState);
@ -1628,8 +1681,8 @@
// If we've started off with a route from a `pushState`-enabled
// browser, but we're currently in a browser that doesn't support it...
if (!this._hasPushState && !this.atRoot()) {
var root = this.root.slice(0, -1) || '/';
this.location.replace(root + '#' + this.getPath());
var rootPath = this.root.slice(0, -1) || '/';
this.location.replace(rootPath + '#' + this.getPath());
// Return immediately as browser will do redirect to new url
return true;
@ -1658,7 +1711,7 @@
}
// Add a cross-platform `addEventListener` shim for older browsers.
var addEventListener = window.addEventListener || function (eventName, listener) {
var addEventListener = window.addEventListener || function(eventName, listener) {
return attachEvent('on' + eventName, listener);
};
@ -1679,7 +1732,7 @@
// but possibly useful for unit testing Routers.
stop: function() {
// Add a cross-platform `removeEventListener` shim for older browsers.
var removeEventListener = window.removeEventListener || function (eventName, listener) {
var removeEventListener = window.removeEventListener || function(eventName, listener) {
return detachEvent('on' + eventName, listener);
};
@ -1730,7 +1783,7 @@
// If the root doesn't match, no routes can match either.
if (!this.matchRoot()) return false;
fragment = this.fragment = this.getFragment(fragment);
return _.any(this.handlers, function(handler) {
return _.some(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
return true;
@ -1753,11 +1806,11 @@
fragment = this.getFragment(fragment || '');
// Don't include a trailing slash on the root.
var root = this.root;
var rootPath = this.root;
if (fragment === '' || fragment.charAt(0) === '?') {
root = root.slice(0, -1) || '/';
rootPath = rootPath.slice(0, -1) || '/';
}
var url = root + fragment;
var url = rootPath + fragment;
// Strip the hash and decode for matching.
fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
@ -1773,7 +1826,7 @@
// fragment to store history.
} else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace);
if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) {
if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
var iWindow = this.iframe.contentWindow;
// Opening and closing the iframe tricks IE7 and earlier to push a
@ -1835,14 +1888,9 @@
_.extend(child, parent, staticProps);
// Set the prototype chain to inherit from `parent`, without calling
// `parent` constructor function.
var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate;
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) _.extend(child.prototype, protoProps);
// `parent`'s constructor function and add the prototype properties.
child.prototype = _.create(parent.prototype, protoProps);
child.prototype.constructor = child;
// Set a convenience property in case the parent's prototype is needed
// later.
@ -1869,5 +1917,4 @@
};
return Backbone;
}));
});

Просмотреть файл

@ -1,5 +1,5 @@
/*!
Copyright (c) 2015 Jed Watson.
Copyright (c) 2016 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
@ -11,7 +11,7 @@
var hasOwn = {}.hasOwnProperty;
function classNames () {
var classes = '';
var classes = [];
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
@ -20,26 +20,26 @@
var argType = typeof arg;
if (argType === 'string' || argType === 'number') {
classes += ' ' + arg;
classes.push(arg);
} else if (Array.isArray(arg)) {
classes += ' ' + classNames.apply(null, arg);
classes.push(classNames.apply(null, arg));
} else if (argType === 'object') {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes += ' ' + key;
classes.push(key);
}
}
}
}
return classes.substr(1);
return classes.join(' ');
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// register as 'classnames', consistent with npm package name
define('classnames', function () {
define('classnames', [], function () {
return classNames;
});
} else {

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Web səhifələrini yoldaşlarınızla birlikdə gəzin
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=İstər səyahət planlayın, istərsə də hədiyyə alın, {{clientShortname2}} daha tez seçim etməyinizə kömək edəcək.
fte_slide_2_title=Eyni səhifədə olun
fte_slide_2_copy=Daxili yazı və video söhbəti işlədərək fikirləri paylaşın, seçimləri qarşılaşdırın və razılığa gəlin.
fte_slide_2_title2=Web-i paylaşmaq üçün yaradıldı
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Artıq yoldaşınızı sessiyaya dəvət etdiyinizdə {{clientShortname2}} avtomatik olaraq baxdığınız səhifəni paylaşacaq. Planlaşdırın. Alış-veriş edin. Qərar verin. Birlikdə.
fte_slide_3_title=Keçid göndərməklə yoldaşınızı dəvət edin
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Başlamaq üçün {{clientSuperShortname}} ikonunu tapın
## will be replaced by the brand short name.
fte_slide_4_copy=Müzakirə etmək istədiyiniz səhifəni tapdığınızda keçidi yaratmaq üçün {{brandShortname}} səyyahındakı ikona klikləyin. Daha sonra istədiyiniz yöntəmlə yoldaşınıza göndərin!
invite_header_text_bold=Birilərini bu səhifəyi sizinlə gəzməyə dəvət edin!
invite_header_text_bold2=Yoldaşınızı sizə qoşulmağa dəvət edin!
invite_header_text3=Firefox Hello-nu işlətmək 2 addımdan ibarətdir, yoldaşınıza keçidi göndərin və Web-də bərabər səyahət edin!
invite_header_text4=Keçidi paylaşın və Web-i birlikdə gəzməyə başlayın.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Xidmət Əlavə et
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Keçidi köçür
email_link_menuitem=Keçidi e-poçtla göndər
edit_name_menuitem=Adı dəyiş
delete_conversation_menuitem2=Sil
panel_footer_signin_or_signup_link=Daxil ol və ya Qeydiyyatdan keç

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Prohlížejte si web spolu s přítelem
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Ať už plánujete výlet nebo vybíráte dárek, {{clientShortname2}} vám pomůže urychlit společné rozhodování.
fte_slide_2_title=Buďte na stejné stránce
fte_slide_2_copy=Použijte vestavěný chat a video pro sdílení nápadů, srovnání možností a společnému rozhodnutí.
fte_slide_2_title2=Vytvořeno pro sdílení webu
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Nyní, když pozvete své přátele na společné sezení, {{clientShortname2}} bude automaticky sdílet všechny stránky, na které se podíváte. Plánujte. Nakupujte. Rozhodujte. Společně.
fte_slide_3_title=Pozvěte přátele posláním odkazu
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Najděte ikonu {{clientSuperShortname}} a začněte
## will be replaced by the brand short name.
fte_slide_4_copy=Jakmile najdete stránku, o které chcete mluvit, klepněte na ikonu {{brandShortname}} a vytvořte odkaz. Ten pošlete svému příteli jakýmkoliv způsobem se vám zrovna hodí!
invite_header_text_bold=Pozvěte někoho, kdo bude prohlížet tuto stránku s vámi!
invite_header_text_bold2=Pozvěte přítele na hovor!
invite_header_text3=K používání Firefox Hello jsou potřeba dva, takže pošlete kamarádovi odkaz, aby si prohlížel web s vámi!
invite_header_text4=Sdílejte tento odkaz a můžete začít prohlížeč web společně.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Přidat službu
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Kopírovat odkaz
email_link_menuitem=Poslat odkaz e-mailem
edit_name_menuitem=Upravit jméno
delete_conversation_menuitem2=Smazat
panel_footer_signin_or_signup_link=Přihlásit nebo registrovat

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -26,31 +24,50 @@ sign_in_again_button=Mewngofnodi
sign_in_again_use_as_guest_button2=Defnyddio {{clientSuperShortname}} fel Gwestai
panel_browse_with_friend_button=Pori'r dudalen gyda ffrind
panel_stop_sharing_tabs_button=Peidio rhannu eich tabiau
panel_disconnect_button=Datgysylltu
## LOCALIZATION_NOTE(first_time_experience_subheading2): Message inviting the
## LOCALIZATION_NOTE(first_time_experience_subheading2, first_time_experience_subheading_button_above): Message inviting the
## user to create his or her first conversation.
first_time_experience_subheading2=Cliciwch y botwm Helo i bori drwy'r tudalennau gwe gyda ffrind.
first_time_experience_subheading_button_above=Cliciwch ar y botwn uchod er mwyn pori tudalennau gwe gyda ffrind.
## LOCALIZATION_NOTE(first_time_experience_content): Message describing
## LOCALIZATION_NOTE(first_time_experience_content, first_time_experience_content2): Message describing
## ways to use Hello project.
first_time_experience_content=Ei ddefnyddio i gynllunio gyda'n gilydd, yn gweithio gyda'n gilydd, yn chwerthin gyda'n gilydd.
first_time_experience_content2=Defnyddiwch Hello i wneud pethau: cynllunio, chwerthin, gweithio gyda'ch gilydd.
first_time_experience_button_label2=Gweld sut mae'n gweithio
invite_header_text_bold=Gwahoddwch rywun i bori'r dudalen hon gyda chi!
invite_header_text3=Mae'n cymryd dau i ddefnyddio Firefox Hello, felly anfonwch ddolen at ffrind i bori'r we gyda chi!
## First Time Experience Slides
fte_slide_1_title=Porwch dudalennau Gwe gyda ffrind
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=P'un ai rydych yn cynllunio taith neu siopa am anrheg, mae {{clientShortname2}} yn caniatáu i chi wneud penderfyniadau'n gynt.
fte_slide_2_title2=Wedi ei wneud ar gyfer rhannu'r We
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Nawr pan fyddwch yn gwahodd ffrind i sesiwn, bydd {{clientShortname2}} yn rhannu unrhyw dudalen Gwe rydych yn edrych arni. Cynllunio. Siopa. Penderfynu. Gyda'ch Gilydd.
fte_slide_3_title=Gwahoddwch ffrind drwy anfon dolen
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
fte_slide_3_copy=Mae {{clientSuperShortname}} y gweithio gyda'r rhan fwyaf o borwyr. Does dim angen cyfrif ac mae pawb yn cysylltu am ddim.
## LOCALIZATION_NOTE(fte_slide_4_title): {{clientSuperShortname}}
## will be replaced by the super short brand name.
fte_slide_4_title=Chwiliwch am eicon {{clientSuperShortname}} er mwyn cychwyn arni
## LOCALIZATION_NOTE(fte_slide_4_copy): {{brandShortname}}
## will be replaced by the brand short name.
fte_slide_4_copy=Unwaith i chi ddarganfod tudalen rydych am ei thrafod, cliciwch eicon {{brandShortname}} er mwyn creu dolen. Yna gallwch ei anfon at eich ffrind yma ma bynnag ffordd ag yr hoffech chi!
invite_header_text_bold2=Gwahoddwch ffrind i ymuno â chi!
invite_header_text4=Rhannwch y ddolen fel bod modd i chi bori'r We gyda'ch gilydd.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
## an iconic button for the invite view.
invite_copy_link_button=Copïo'r Ddolen
invite_copied_link_button=Copïwyd!
invite_copied_link_button=Wedi ei chopïo!
invite_email_link_button=Dolen E-bost
invite_facebook_button3=Facebook
invite_your_link=Eich dolen:
# Status text
display_name_guest=Gwestai
# Error bars
## LOCALIZATION NOTE(session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
## These may be displayed at the top of the panel.
@ -66,9 +83,9 @@ problem_accessing_account=Bu Anhawster wrth Geisio Mynediad i'ch Cyfrif
## LOCALIZATION NOTE(retry_button): Displayed when there is an error to retry
## the appropriate action.
retry_button=Ceisio eto
retry_button=Ceisiwch eto
share_email_subject7=Eich gwahoddiad i bori'r We gyda'ch gilydd
share_email_subject7=Hwn yw eich gwahoddiad i bori'r We gyda'ch gilydd
## LOCALIZATION NOTE (share_email_body7): In this item, don't translate the
## part between {{..}} and leave the \n\n part alone
share_email_body7=Mae ffrind yn aros amdanoch ar Firefox Hello. Cliciwch y ddolen i gysylltu a phori'r We gyda'ch gilydd: {{callUrl}}
@ -76,7 +93,7 @@ share_email_body7=Mae ffrind yn aros amdanoch ar Firefox Hello. Cliciwch y ddole
## the part between {{..}} and leave the \n\n part alone.
share_email_body_context3=Mae ffrind yn aros amdanoch ar Firefox Hello. Cliciwch y ddolen i gysylltu a phori {{title}} gyda'ch gilydd: {{callUrl}}
## LOCALIZATION NOTE (share_email_footer2): Common footer content for both email types
share_email_footer2=Mae \n\n___\nFirefox Hello yn gadael i chi pori'r we gyda'ch ffrindiau. Ei defnyddio pan rydych eisiau gwneud pethau'n: cynllun gyda'i gilydd, yn gweithio gyda'i gilydd, yn chwerthin gyda'i gilydd. Dysgu mwy ar http://www.firefox.com/hello
share_email_footer2=Mae \n\n___\nFirefox Hello yn gadael i chi pori'r we gyda'ch ffrindiau. Gallwch ei defnyddio pan rydych eisiau gwneud pethau: cynllunio, gweithio a chwerthin gyda'i gilydd. Dysgwch ragor yn http://www.firefox.com/hello
## LOCALIZATION NOTE (share_tweeet): In this item, don't translate the part
## between {{..}}. Please keep the text below 117 characters to make sure it fits
## in a tweet.
@ -88,6 +105,7 @@ share_add_service_button=&Ychwanegu Gwasanaeth
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copïo'r Ddolen
email_link_menuitem=Dolen E-bost
edit_name_menuitem=Golygu enw
delete_conversation_menuitem2=Dileu
panel_footer_signin_or_signup_link=Mewngofnodi neu Ymuno
@ -123,7 +141,7 @@ initiate_audio_video_call_button2=Cychwyn
initiate_audio_video_call_tooltip2=Cychwyn sgwrs fideo
initiate_audio_call_button2=Sgwrs llais
peer_ended_conversation2=Mae'r person roeddech yn eu galw wedi gorffen y sgwrs.
peer_ended_conversation2=Mae'r person roeddech yn galw wedi dod a'r sgwr i ben.
restart_call=Ailymuno
## LOCALIZATION NOTE (contact_offline_title): Title which is displayed when the
@ -181,29 +199,16 @@ door_hanger_button=Iawn
# Infobar strings
infobar_screenshare_no_guest_message=Cyn gynted a bo'ch ffrind yn ymuno, bydd modd iddyn nhw weld unrhyw dab rydych yn clicio arno.
infobar_screenshare_browser_message2=Rydych yn rhannu eich tabiau. Mae modd i'ch ffrindiau weld unrhyw dab rydych yn clicio arno
infobar_screenshare_paused_browser_message=Mae rhannu tabiau wedi ei oedi
infobar_button_gotit_label=Iawn!
infobar_button_gotit_accesskey=I
infobar_button_pause_label=Oedi
infobar_button_pause_accesskey=O
infobar_button_restart_label=Ailgychwyn
infobar_screenshare_browser_message3=Rydych nawr yn rhannu eich tabiau. Bydd eich ffrind yn gweld unrhyw dab fyddwch chi'n clicio arno.
infobar_screenshare_stop_sharing_message=Nid ydych bellach yn rhannu eich tabiau
infobar_button_restart_label2=Ail gychwyn rhannu
infobar_button_restart_accesskey=A
infobar_button_resume_label=Ailgychwyn
infobar_button_resume_accesskey=i
infobar_button_stop_label=Atal
infobar_button_stop_accesskey=t
infobar_menuitem_dontshowagain_label=Peidio â dangos hwn eto
infobar_menuitem_dontshowagain_accesskey=P
# Context in conversation strings
## LOCALIZATION NOTE(no_conversations_message_heading2): Title shown when user
## has no conversations available.
no_conversations_message_heading2=Dim sgyrsiau eto.
## LOCALIZATION NOTE(no_conversations_start_message2): Subheading inviting the
## user to start a new conversation.
no_conversations_start_message2=Cychwyn un newydd nawr!
infobar_button_stop_label2=Peidio rhannu
infobar_button_stop_accesskey=P
infobar_button_disconnect_label=Datgysylltu
infobar_button_disconnect_accesskey=D
# E10s not supported strings
@ -223,7 +228,7 @@ chat_textbox_placeholder=Teipiwch yma…
clientShortname2=Firefox Hello
conversation_has_ended=Mae eich sgwrs wedi dod i ben.
generic_failure_message=Mae gennym anawsterau technegol…
generic_failure_message=Rydym yn profi anawsterau technegol…
generic_failure_no_reason2=Hoffech chi geisio eto?
@ -246,14 +251,20 @@ rooms_room_full_call_to_action_label=Dysgu rhagor am {{clientShortname}} »
rooms_room_full_call_to_action_nonFx_label=Llwytho {{brandShortname}} i lawr i gychwyn eich sgwrs eich hun
rooms_room_full_label=Mae eisioes dau berson yn y sgwrs.
rooms_room_join_label=Ymuno â'r sgwrs
rooms_room_joined_label=Mae rhywun wedi ymuno â'r sgwrs!
rooms_room_joined_owner_connected_label2=Mae eich ffrind nawr wedi cysylltu a bydd yn gallu gweld eich tabiau.
rooms_room_joined_owner_not_connected_label=Mae eich ffrind yn aros i gael pori {{roomURLHostname}} gyda chi.
self_view_hidden_message=Golwg o'ch hun yn cael ei guddio ond yn dal i gael ei anfon; newid maint ffenestr i'w dangos
peer_left_session=Mae eich ffrind wedi gadael.
peer_unexpected_quit=Mae eich ffrind wedi datgysylltu'n annisgwyl.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.
tos_failure_message=Nid yw {{clientShortname}} ar gael yn eich gwlad.
display_name_guest=Gwestai
## LOCALIZATION NOTE(clientSuperShortname): This should not be localized and
## should remain "Hello" for all locales.
clientSuperShortname=Hello

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Surf på internettet sammen med en ven
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Med {{clientShortname2}} kan du tage hurtigere beslutninger i realtid, uanset om du planlægger en rejse eller indkøb af en gave.
fte_slide_2_title=Surf sammen
fte_slide_2_copy=Brug den indbyggede tekst- eller video-chat til at dele ideér, sammenligne muligheder og blive enige.
fte_slide_2_title2=Lavet til deling af nettet
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Når du inviterer en ven til en session, vil {{clientShortname2}} automatisk dele de websider, du ser. Planlæg. Køb ind. Beslut. Sammen.
fte_slide_3_title=Invitér en ven ved at sende vedkommende et link
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Find ikonet {{clientSuperShortname}} for at komme i gang
## will be replaced by the brand short name.
fte_slide_4_copy=Når I har fundet en side, I vil diskutere, så klik på ikonet i {{brandShortname}} for at oprette et link. Send så linket til din ven.
invite_header_text_bold=Inviter nogen til at besøge siden sammen med dig.
invite_header_text_bold2=Invitèr en ven til at være med!
invite_header_text3=Det kræver selskab at bruge Firefox Hello, så send et link til én af dine venner sådan at I kan bruge nettet sammen.
invite_header_text4=Del dette link, så I kan begynde at surfe på internettet sammen.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Tilføj en tjeneste
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Kopier link
email_link_menuitem=Mail link
edit_name_menuitem=Rediger navn
delete_conversation_menuitem2=Slet
panel_footer_signin_or_signup_link=Log ind eller tilmeld dig

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Surfen Sie gemeinsam mit einem Freund
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Egal, ob Sie eine Reise planen oder ein Geschenk einkaufen, mit {{clientShortname2}} können Sie schnellere Entscheidungen in Echtzeit treffen.
fte_slide_2_title=Rufen Sie die gleiche Seite auf
fte_slide_2_copy=Verwenden Sie den integrierten Text- oder Videochat, um Ideen auszutauschen, Optionen zu vergleichen und zu einer Einigung zu kommen.
fte_slide_2_title2=Zum Teilen des Internets entwickelt
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Wenn Sie jetzt einen Freund zu einer Sitzung einladen teilt {{clientShortname2}} automatisch die aktuelle Webseite. Planen. Einkaufen. Entscheiden. Gemeinsam.
fte_slide_3_title=Laden Sie einen Freund ein, indem Sie ihm einen Link senden
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Über das {{clientSuperShortname}}-Symbol geht es los.
## will be replaced by the brand short name.
fte_slide_4_copy=Wenn Sie eine Seite gefunden haben, die Sie diskutieren möchten, klicken Sie auf das Symbol in {{brandShortname}}, um einen Link zu erstellen. Schicken Sie diesen dann auf beliebige Weise an Ihren Freund.
invite_header_text_bold=Laden Sie jemanden dazu ein, gemeinsam mit Ihnen auf der Seite zu surfen!
invite_header_text_bold2=Laden Sie einen Freund ein!
invite_header_text3=Firefox Hello ist für die Nutzung durch zwei Personen, also senden Sie einem Freund einen Link, um gemeinsam im Web zu surfen!
invite_header_text4=Teilen Sie diesen Link, damit Sie gemeinsam im Internet surfen können.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Dienst hinzufügen
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Link kopieren
email_link_menuitem=Link per E-Mail versenden
edit_name_menuitem=Name bearbeiten
delete_conversation_menuitem2=Entfernen
panel_footer_signin_or_signup_link=Anmelden oder registrieren

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Woglědajśo se webboki se z pśijaśelom
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Lěc drogowanje planujośo abo dar nakupujośo, {{clientShortname2}} wam pomaga, w napšawdnem casu malsnjej rozsuźiś.
fte_slide_2_title=Buźćo togo samskego měnjenja
fte_slide_2_copy=Wužywajśo zatwarjony tekst abo wideowy chat, aby ideje źělili, móžnosći pśirownali a něco dojadnali.
fte_slide_2_title2=Za zgromadne wužywanje weba wuwity
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Gaž něnto pśijaśela na pósejźenje pśepšosujośo, buźo {{clientShortname2}} awtomatiski webbok źěliś, kótaryž se woglědujośo. Planowaś. Nakupowaś. Rozsuźiś. Gromaźe.
fte_slide_3_title=Pósćelśo pśijaśeloju wótkaz, aby jogo pśepšosył
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Klikniśo na symbol {{clientSuperShortname}}, aby zachopił
## will be replaced by the brand short name.
fte_slide_4_copy=Gaž sćo bok namakał, wó kótaremž cośo diskutěrowaś, klikniśo na symbol {{brandShortname}}, aby wótkaz napórał. Pósćelśo jen pótom swójomu pśijaśeloju, wšojadno kak se wam spódoba!
invite_header_text_bold=Pśepšosćo někogo, aby toś ten bok z wami pśeglědował!
invite_header_text_bold2=Pśepšosćo pśijaśela, aby se wam pśizamknuł!
invite_header_text3=Stej dwě wósobje trěbnej, aby Firefox Hello wužywałej, pósćelśo pótakem pśijaśeloju wótkaz, aby z wami web pśeglědował!
invite_header_text4=Źělśo toś ten wótkaz, aby mógłej web zgromadnje pśeglědowaś.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Słužbu pśidaś
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Wótkaz kopěrowaś
email_link_menuitem=Wótkaz e-mailowaś
edit_name_menuitem=Mě wobźěłaś
delete_conversation_menuitem2=Lašowaś
panel_footer_signin_or_signup_link=Pśizjawiś abo registrěrowaś

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
clientSuperShortname=Hello
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Browse Web pages with a friend
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Whether youre planning a trip or shopping for a gift, {{clientShortname2}} lets you make faster decisions in real time.
fte_slide_2_title=Get on the same page
fte_slide_2_copy=Use the built-in text or video chat to share ideas, compare options and come to an agreement.
fte_slide_2_title2=Made for sharing the Web
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Now when you invite a friend to a session, {{clientShortname2}} will automatically share any Web page youre viewing. Plan. Shop. Decide. Together.
fte_slide_3_title=Invite a friend by sending a link
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Find the {{clientSuperShortname}} icon to get started
## will be replaced by the brand short name.
fte_slide_4_copy=Once youve found a page you want to discuss, click the icon in {{brandShortname}} to create a link. Then send it to your friend however you like!
invite_header_text_bold=Invite someone to browse this page with you!
invite_header_text_bold2=Invite a friend to join you!
invite_header_text3=It takes two to use Firefox Hello, so send a friend a link to browse the Web with you!
invite_header_text4=Share this link so you can start browsing the Web together.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Add a Service
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copy Link
email_link_menuitem=Email Link
edit_name_menuitem=Edit name
delete_conversation_menuitem2=Delete
panel_footer_signin_or_signup_link=Sign In or Sign Up
@ -257,8 +256,15 @@ rooms_room_joined_owner_not_connected_label=Your friend is waiting to browse {{r
self_view_hidden_message=Self-view hidden but still being sent; resize window to show
peer_left_session=Your friend has left.
peer_unexpected_quit=Your friend has unexpectedly disconnected.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.
tos_failure_message={{clientShortname}} is not available in your country.
display_name_guest=Guest
## LOCALIZATION NOTE(clientSuperShortname): This should not be localized and
## should remain "Hello" for all locales.
clientSuperShortname=Hello

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Navega por páginas Web con un amigo
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Si estás planeando un viaje o comprar un regalo, {{clientShortname2}} te permite tomar decisiones más rápido en tiempo real.
fte_slide_2_title=Usa la misma página
fte_slide_2_copy=Usa el chat de texto o video integrado para compartir ideas, comparar opiniones y llegar a un acuerdo.
fte_slide_2_title2=Hecho para compartir la Web
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Ahora, cuando invites a un amigo a una sesión, {{clientShortname2}} automáticamente compartir cualquier página Web que estés visitando. planear, comprar y decidir en conjunto.
fte_slide_3_title=Invita un amigo enviándole un enlace
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Encuentra el ícono de {{clientSuperShortname}} para empezar
## will be replaced by the brand short name.
fte_slide_4_copy=Una vez que has encontrado una página sobre la que quieras discutir, aprieta el ícono en {{brandShortname}} para crear un enlace. Luego, ¡envíalo a tu amigo de la forma en que desees!
invite_header_text_bold=¡Invita a alguien a navegar esta página contigo!
invite_header_text_bold2=¡Invita a un amigo a unirse!
invite_header_text3=¡Se requieren dos para usar Firefox Hello, así que envía un enlace a un amigo para que navegue la Web contigo!
invite_header_text4=Comparte este enlace para que puedan navegar la Web juntos.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Añadir un servicio
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copiar enlace
email_link_menuitem=Enviar enlace
edit_name_menuitem=Editar nombre
delete_conversation_menuitem2=Eliminar
panel_footer_signin_or_signup_link=Conectarse o registrarse

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Navega por páginas web con un amigo
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Tanto si estás planeando un viaje o buscando un regalo para comprarlo, {{clientShortname2}} te permite tomar decisiones más rápidas en tiempo real.
fte_slide_2_title=Reuníos en la misma página
fte_slide_2_copy=Usa los chats incluidos de texto o vídeo para compartir ideas, comparar opciones o llegar a acuerdos.
fte_slide_2_title2=Hecho para compartir la Web
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Ahora cuando invites a un amigo a una sesión, {{clientShortname2}} automáticamente compartirá cualquier página web que estés viendo. Planea. Compra. Decide. Entre amigos.
fte_slide_3_title=Invita a un amigo enviando un enlace
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Busca el icono de {{clientSuperShortname}} para comenzar
## will be replaced by the brand short name.
fte_slide_4_copy=Cuando encuentres una página sobre la que conversar, pulsa el icono en {{brandShortname}} para crear un enlace. ¡Luego envíala a tu amigo como mejor te parezca!
invite_header_text_bold=¡Invita a alguien a navegar por la página contigo!
invite_header_text_bold2=¡Invita a un amigo a unirse a ti!
invite_header_text3=Se necesitan dos personas para utilizar Firefox Hello. ¡Envíale el enlace a un amigo y navegad juntos!
invite_header_text4=Comparte este enlace para que podáis comenzar a navegar juntos por la web.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Añadir un servicio
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copiar enlace
email_link_menuitem=Enviar enlace
edit_name_menuitem=Editar nombre
delete_conversation_menuitem2=Eliminar
panel_footer_signin_or_signup_link=Inicia sesión o regístrate

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Explora páginas Web con un amigo
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Ya sea que estés planeando un viaje o comprando un regalo, {{clientShortname2}} te permite tomar decisiones más rápido en tiempo real.
fte_slide_2_title=Estar en la misma página
fte_slide_2_copy=Usa los chats incluidos de texto o video para compartir ideas, comparar opciones y lograr acuerdos.
fte_slide_2_title2=Hecho para compartir la Web
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Ahora, cuando invites a un amigo a la sesión, {{clientShortname2}} automáticamente se compartirá cualquier página Web que estés viendo. Planea, Compra, Decide, Juntos.
fte_slide_3_title=Invita a un amigo enviándole un enlace
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Busca el icono de {{clientSuperShortname}} para comenzar
## will be replaced by the brand short name.
fte_slide_4_copy=Una vez que encuentres una página que quieras discutir, haz clic en el icono de {{brandShortname}} para crear un enlace. ¡Luego envíaselo a tu amigo como mejor te parezca!
invite_header_text_bold=¡Invita a alguien para explorar esta página contigo!
invite_header_text_bold2=¡Invita a un amigo a unirse a ti!
invite_header_text3=Hacen falta dos para usar Firefox Hello, ¡así que envíale un enlace a tu amigo para navegar la Web juntos!
invite_header_text4=Comparte este enlace para que puedan iniciar a explorar juntos la Web.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Agregar un servicio
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copiar enlace
email_link_menuitem=Enviar enlace
edit_name_menuitem=Editar nombre
delete_conversation_menuitem2=Eliminar
panel_footer_signin_or_signup_link=Iniciar sesión o registrarse

Просмотреть файл

@ -42,8 +42,10 @@ fte_slide_1_title=Lehitse veebilehti koos sõbraga
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Kui plaanid reisi või otsid kingitust, siis võimaldab {{clientShortname2}} sul kiiremini otsustada.
fte_slide_2_title=Olge samal veebilehel.
fte_slide_2_copy=Kasuta sisseehitatud teksti- või videovestlust ideede jagamiseks, valikute võrdlemiseks ning ühisele otsusele jõudmiseks.
fte_slide_2_title2=Veebi jagamiseks loodud
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Kui kutsud sõbra ühinema, siis jagab {{clientShortname2}} avatud veebilehte. Saate teha plaane, oste ning otsuseid. Koos.
fte_slide_3_title=Kutsu sõber, saates talle lingi.
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -55,9 +57,7 @@ fte_slide_4_title=Leia {{clientSuperShortname}} ikoon ning tee algust.
## will be replaced by the brand short name.
fte_slide_4_copy=Kui oled leidnud lehe, mida soovid arutada, siis klõpsa ikoonil {{brandShortname}}s ning loo link. Seejärel saada see sõbrale, kuidas iganes soovid!
invite_header_text_bold=Kutsu keegi teine seda lehte vaatama!
invite_header_text_bold2=Kutsu sõber endaga ühinema!
invite_header_text3=Firefox Hello kasutamiseks on vaja kahte kasutajat. Saada sõbrale link!
invite_header_text4=Jaga seda linki, et saaksite koos veebilehitsemist alustada.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -105,6 +105,7 @@ share_add_service_button=Lisa teenus
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Kopeeri link
email_link_menuitem=Saada link e-postiga
edit_name_menuitem=Muuda nime
delete_conversation_menuitem2=Kustuta
panel_footer_signin_or_signup_link=Logi sisse või registreeru

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Naviguez sur le Web avec une autre personne
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Que ce soit pour planifier un voyage ou lachat dun cadeau, {{clientShortname2}} vous permet de prendre des décisions plus rapidement.
fte_slide_2_title=Sur la même page au même moment
fte_slide_2_copy=Utilisez la conversation texte ou vidéo pour partager vos idées, comparer vos choix et vous mettre daccord.
fte_slide_2_title2=Conçu pour partager le Web
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Désormais, lorsque vous invitez un ami à rejoindre une conversation, {{clientShortname2}} partagera automatiquement les onglets que vous consultez. Planifiez, achetez, décidez ensemble.
fte_slide_3_title=Invitez votre interlocuteur en lui envoyant un lien
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Cherchez licône {{clientSuperShortname}} pour commencer
## will be replaced by the brand short name.
fte_slide_4_copy=Une fois sur la page à propos de laquelle vous souhaitez discuter, cliquez sur licône dans {{brandShortname}} pour créer un lien. Vous pouvez alors lenvoyer à votre interlocuteur de la manière que vous voulez.
invite_header_text_bold=Invitez quelquun à consulter cette page avec vous !
invite_header_text_bold2=Invitez quelquun à vous rejoindre !
invite_header_text3=Utiliser Firefox Hello est très simple, envoyez simplement un lien à votre interlocuteur et vous pourrez naviguer sur le Web ensemble !
invite_header_text4=Partagez ce lien pour surfer sur le Web ensemble.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Ajouter un service
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copier le lien
email_link_menuitem=Envoyer le lien
edit_name_menuitem=Modifier le nom
delete_conversation_menuitem2=Supprimer
panel_footer_signin_or_signup_link=Sinscrire ou se connecter

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Besjoch websiden mei in freon
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Oft jo no in reis planne of in kado sykje, mei {{clientShortname2}} meitsje jo yn realtime fluggere beslissingen.
fte_slide_2_title=Besjoch deselde side
fte_slide_2_copy=Brûk de ynboude tekst- of fideochat om ideeën út te wikseljen, opsjes te fergelykjen en oerienstimming te berikjen.
fte_slide_2_title2=Makke om it web te dielen
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=As jo no in freon útnoegje foar in sesje, sil {{clientShortname2}} automatysk websiden dy't jo besjen diele. Plan. Winkelje. Beslis. Tegearre.
fte_slide_3_title=Noegje in freon út troch in keppeling te dielen
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Sykje it {{clientSuperShortname}}-piktogram om te begjinnen
## will be replaced by the brand short name.
fte_slide_4_copy=As jo in side fûn hawwe dy't jo besprekke wolle, klikke jo op it piktogram yn {{brandShortname}} om in keppeling te meitsjen. Stjoer dizze dêrnei nei jo freon lykas jo wolle!
invite_header_text_bold=Noegje ien út om tegearre dizze website te besjen!
invite_header_text_bold2=Noegje in freon út om diel te nimmen!
invite_header_text3=Der binne twa nedich om Firefox Hello te brûken, dus stjoer in freon in keppeling om tegearre op it web te sneupjen!
invite_header_text4=Diel dizze keppeling, sadat jo tegearre it web besjen kinne.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=In service tafoegje
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Keppeling kopiearje
email_link_menuitem=Keppeling e-maile
edit_name_menuitem=Namme bewurkje
delete_conversation_menuitem2=Fuortsmite
panel_footer_signin_or_signup_link=Loch yn of meld jo oan

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Besjoch websiden mei in freon
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Oft jo no in reis planne of in kado sykje, mei {{clientShortname2}} meitsje jo yn realtime fluggere beslissingen.
fte_slide_2_title=Besjoch deselde side
fte_slide_2_copy=Brûk de ynboude tekst- of fideochat om ideeën út te wikseljen, opsjes te fergelykjen en oerienstimming te berikjen.
fte_slide_2_title2=Makke om it web te dielen
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=As jo no in freon útnoegje foar in sesje, sil {{clientShortname2}} automatysk websiden dy't jo besjen diele. Plan. Winkelje. Beslis. Tegearre.
fte_slide_3_title=Noegje in freon út troch in keppeling te dielen
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Sykje it {{clientSuperShortname}}-piktogram om te begjinnen
## will be replaced by the brand short name.
fte_slide_4_copy=As jo in side fûn hawwe dy't jo besprekke wolle, klikke jo op it piktogram yn {{brandShortname}} om in keppeling te meitsjen. Stjoer dizze dêrnei nei jo freon lykas jo wolle!
invite_header_text_bold=Noegje ien út om tegearre dizze website te besjen!
invite_header_text_bold2=Noegje in freon út om diel te nimmen!
invite_header_text3=Der binne twa nedich om Firefox Hello te brûken, dus stjoer in freon in keppeling om tegearre op it web te sneupjen!
invite_header_text4=Diel dizze keppeling, sadat jo tegearre it web besjen kinne.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=In service tafoegje
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Keppeling kopiearje
email_link_menuitem=Keppeling e-maile
edit_name_menuitem=Namme bewurkje
delete_conversation_menuitem2=Fuortsmite
panel_footer_signin_or_signup_link=Loch yn of meld jo oan

Просмотреть файл

@ -42,8 +42,10 @@ fte_slide_1_title=Tutu stronu z přećelom přehladować
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Hač pućowanje planujeće abo dar nakupujeće, {{clientShortname2}} wam pomha, we woprawdźitym času spěšnišo rozsudźić.
fte_slide_2_title=Budźće samsneho měnjenja
fte_slide_2_copy=Wužiwajće zatwarjeny tekst abo widejowy chat, zo byšće ideje dźělili, móžnosće přirunali a něšto dojednali.
fte_slide_2_title2=Za zhromadne wužiwanje weba wuwity
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Hdyž nětko přećela na posedźenje přeprošeće, budźe {{clientShortname2}} awtomatisce webstronu dźělić, kotruž sej wobhladujeće. Planować. Nakupować. Rozsudźić. Hromadźe.
fte_slide_3_title=Pósćelće přećelej wotkaz, zo byšće jeho přeprosył
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -55,9 +57,7 @@ fte_slide_4_title=Klikńće na symbol {{clientSuperShortname}}, zo byšće zapo
## will be replaced by the brand short name.
fte_slide_4_copy=Hdyž sće stronu namakał, wo kotrejž chceće diskutować, klikńće na symbol {{brandShortname}}, zo byšće wotkaz wutworił. Pósćelće jón potom swojemu přećelej, wšojedne kak so wam spodoba!
invite_header_text_bold=Přeprošće někoho, zo by z wami tutu stronu přehladował!
invite_header_text_bold2=Přeprošće přećela, zo by so wam přidružił!
invite_header_text3=Stej dwě wosobje trěbnej, zo byštej Firefox Hello wužiwałoj, pósćelće tuž přećelej wotkaz, zo by z wami web přehladował!
invite_header_text4=Dźělće tutón wotkaz, zo byštaj móhłoj web zhromadnje přehladować.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -105,6 +105,7 @@ share_add_service_button=Słužbu přidać
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Wotkaz kopěrować
email_link_menuitem=Wotkaz e-mejlować
edit_name_menuitem=Mjeno wobdźěłać
delete_conversation_menuitem2=Zhašeć
panel_footer_signin_or_signup_link=Přizjewić abo registrować

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Weboldalak böngészése ismerősével
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Ha utazást tervez vagy ajándékot vásárol, a {{clientShortname2}} segít gyorsabb döntést hozni valós időben.
fte_slide_2_title=Kerüljenek egy lapra
fte_slide_2_copy=Használja a beépített szöveges vagy videócsevegést ötletek megosztására, lehetőségek összehasonlítására és megegyezésre.
fte_slide_2_title2=A web megosztására készítve
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Most, ha meghívja egy ismerősét csevegésre, a {{clientShortname2}} automatikusan megosztja a weblapot amit Ön néz. Tervezzen. Vásároljon. Döntsön. Együtt.
fte_slide_3_title=Hívja meg ismerősét egy hivatkozás küldésével
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Keresse a {{clientSuperShortname}} ikont a kezdéshez
## will be replaced by the brand short name.
fte_slide_4_copy=Amint olyan oldalt talált, amit megosztana, hivatkozás létrehozásához kattintson a {{brandShortname}} ikonra. Majd küldje el ismerősének ahogy szeretné!
invite_header_text_bold=Hívjon meg valakit az oldal közös böngészésére!
invite_header_text_bold2=Hívja meg ismerősét, hogy csatlakozzon!
invite_header_text3=A Firefox Hello használatához két ember kell: küldjön egy hivatkozást ismerősének, hogy közösen böngésszék a webet!
invite_header_text4=Ossza meg ezt a hivatkozást, hogy együtt böngészhessék a webet.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Szolgáltatás hozzáadása
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Hivatkozás másolása
email_link_menuitem=Hivatkozás küldése
edit_name_menuitem=Név szerkesztése
delete_conversation_menuitem2=Törlés
panel_footer_signin_or_signup_link=Bejelentkezés vagy regisztráció

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Դիտարկել վեբ էջերը ընկերների հետ
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Անկախ նրանից, թե պլանավորում եք ուղևորություն կամ գնումներ նվերի համար՝ {{clientShortname2}}-ը կօգնի արագ որոշումներ կայացնել իրական ժամանակում:
fte_slide_2_title=Ստանալ նույն էջում
fte_slide_2_copy=Օգտագործեք ներկառուցված տեքստային զրույց կամ տեսազանգ՝ մտքեր փոխանակելու, ընտրանքներ համեմատելու և համաձայնության գալու համար:
fte_slide_2_title2=Ստեղծված է վեբը համաօգտագործելու համար
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Այժմ, երբ հրավիրել եք ձեր ընկերոջը զրույցի՝ {{clientShortname2}}-ը ինքնաբար կհամաօգտագործի ցանկացած վեբ էջ, որը դիտում եք: Պլանավորեք: Կատարեք գնումներ: Կայացրեք որոշումներ: Միասին:
fte_slide_3_title=Հրավիրեք ընկերոջը՝ ուղարկելով հղումը
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Գտեք {{clientSuperShortname}} պատկերակը՝ սկս
## will be replaced by the brand short name.
fte_slide_4_copy=Երբ գտնեք այն էջը, որ ցանկանում եք քննարկել՝ սեղմեք {{brandShortname}}-ի պատկերակին՝ հղում ստեղծելու համար: Ուղարկեք այն ձեր ընկերներին:
invite_header_text_bold=Հրավիրել որևէ մեկին՝ դիտարկելու էջը ձեզ հետ:
invite_header_text_bold2=Հրավիրել ընկերոջը միանալու ձեզ:
invite_header_text3=Այն զբաղեցնում է երկու՝ օգտագործելու Firefox Hello-ն և ուղարկելու ընկերներին հղում` վեբը ձեզ հետ դիտարկելու համար:
invite_header_text_bold2=Հրավիրել ընկերոջը՝ միանալու ձեզ:
invite_header_text4=Համաօգտագործեք այս հղումը, որպեսզի կարողանաք դիտարկել այս էջը միասին:
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Ավելացնել ծառայություն
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Պատճենել հղումը
email_link_menuitem=Ուղարկել հղումը
edit_name_menuitem=Խմբագրել անունը
delete_conversation_menuitem2=Ջնջել
panel_footer_signin_or_signup_link=Մուտք գործել կամ գրանցվել
@ -257,6 +256,9 @@ rooms_room_joined_owner_not_connected_label=Ձեր ընկերը սպասում
self_view_hidden_message=Ինքնադիտումը թաքցված է, բայց դեռ ուղարկվում է. չափափոխել պատուհանը՝ ցուցադրելու համար
peer_left_session=Ձեր ընկերը հեռացել է:
peer_unexpected_quit=Ձեր ընկերը անսպասելի կապախզվել է:
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.
tos_failure_message={{clientShortname}}-ը հասանելի չէ ձեր երկրում:

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Naviga sul Web insieme a un amico
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Quando organizzi un viaggio o acquisti un regalo in comune con gli amici, {{clientShortname2}} vi aiuta a prendere decisioni più rapide in tempo reale.
fte_slide_2_title=Condividi le tue vedute
fte_slide_2_copy=Grazie alla chat e alla video chat integrate puoi condividere idee, confrontare opzioni e raggiungere un accordo facilmente.
fte_slide_2_title2=Realizzato per condividere il Web
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Adesso ogni volta che inviti un utente a una sessione di chat, {clientShortname2} condividerà automaticamente la pagina web che stai visitando. Organizza. Fai acquisti. Decidi. In compagnia.
fte_slide_3_title=Invita un link a un altro utente per invitarlo
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Per iniziare individua licona di {{clientSuperShortname}}
## will be replaced by the brand short name.
fte_slide_4_copy=Quando ti trovi su una pagina web che vuoi mostrare a qualcuno, genera un link facendo clic sullicona di {{brandShortname}}, poi invialo allinteressato con il metodo che preferisci.
invite_header_text_bold=Invita un amico e visita questa pagina insieme a lui.
invite_header_text_bold2=Invita chi vuoi tu a unirsi alla conversazione!
invite_header_text3=Servono due persone per utilizzare Firefox Hello: invita un amico e naviga sul Web insieme a lui!
invite_header_text4=Condividi questo link per navigare sul Web in compagnia.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Aggiungi un servizio
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copia link
email_link_menuitem=Invia link per email
edit_name_menuitem=Modifica il nome
delete_conversation_menuitem2=Elimina
panel_footer_signin_or_signup_link=Accedi o registrati

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -25,13 +23,13 @@ sign_in_again_button=ログイン
## will be replaced by the super short brandname.
sign_in_again_use_as_guest_button2=ゲストとして {{clientSuperShortname}} を使用
panel_browse_with_friend_button=このページを友だちと一緒に見る
panel_browse_with_friend_button=このページを友と一緒に見る
panel_disconnect_button=切断
## LOCALIZATION_NOTE(first_time_experience_subheading2, first_time_experience_subheading_button_above): Message inviting the
## user to create his or her first conversation.
first_time_experience_subheading2=Hello ボタンをクリックして Web ページを友だちと一緒に見ましょう。
first_time_experience_subheading_button_above=上のボタンをクリックして、友だちと Web ページをブラウズしましょう。
first_time_experience_subheading2=Hello ボタンをクリックして Web ページを友と一緒に見ましょう。
first_time_experience_subheading_button_above=上のボタンをクリックして、友達と Web ページをブラウジングしましょう。
## LOCALIZATION_NOTE(first_time_experience_content, first_time_experience_content2): Message describing
## ways to use Hello project.
@ -40,13 +38,15 @@ first_time_experience_content2=このアドオンは、一緒に計画を立て
first_time_experience_button_label2=使い方を見る
## First Time Experience Slides
fte_slide_1_title=だちと Web ページをブラウズ
fte_slide_1_title=達と Web ページをブラウジング
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=旅行の計画を立てるときも、何かプレゼントを買うときも、{{clientShortname2}} を使えばリアルタイムにより速く物事を決められます。
fte_slide_2_title=同じページを見る
fte_slide_2_copy=組み込みのテキスト・動画チャットを使って、アイデアを共有したり、選択肢を比べたり、話し合いをまとめましょう。
fte_slide_3_title=リンクを送って友だちを招待
fte_slide_2_title2=Web ページの共有に最適です
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=今後、友達をセッションに招待する際、{{clientShortname2}} はあなたが見ている Web ページを自動的に共有します。計画を立てたり。買い物をしたり。何かを決めたり。いつでも一緒に。
fte_slide_3_title=リンクを送って友達を招待
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
fte_slide_3_copy={{clientSuperShortname}} は、ほとんどのデスクトップ向けブラウザに対応しています。アカウント不要で、誰とでも無料で会話できます。
@ -55,11 +55,9 @@ fte_slide_3_copy={{clientSuperShortname}} は、ほとんどのデスクトッ
fte_slide_4_title=始めるには {{clientSuperShortname}} アイコンを見つけてください
## LOCALIZATION_NOTE(fte_slide_4_copy): {{brandShortname}}
## will be replaced by the brand short name.
fte_slide_4_copy=話し合いたいページを見つけたら、{{brandShortname}} 内のアイコンをクリックしてリンクを作成します。それからそのリンクを好きな友だちに送りましょう。
fte_slide_4_copy=話し合いたいページを見つけたら、{{brandShortname}} 内のアイコンをクリックしてリンクを作成します。それからそのリンクを好きな友に送りましょう。
invite_header_text_bold=このページを一緒に見る友だちを招待しましょう!
invite_header_text_bold2=会話に参加する友だちを招待しましょう!
invite_header_text3=Firefox Hello は 2 人で使うので、友だちにリンクを送って一緒に Web をブラウズしましょう!
invite_header_text_bold2=会話に参加する友達を招待しましょう!
invite_header_text4=このリンクを送って、一緒に Web ブラウジングを始めましょう。
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -87,15 +85,15 @@ problem_accessing_account=アカウントへアクセスする際に問題が発
## the appropriate action.
retry_button=再試行
share_email_subject7=Web を一緒にブラウするための招待状
share_email_subject7=Web を一緒にブラウジングするための招待状
## LOCALIZATION NOTE (share_email_body7): In this item, don't translate the
## part between {{..}} and leave the \n\n part alone
share_email_body7=だちが Firefox Hello 上であなたを待っています。このリンクをクリックして接続し、一緒に Web をブラウしましょう: {{callUrl}}
share_email_body7=が Firefox Hello 上であなたを待っています。このリンクをクリックして接続し、一緒に Web をブラウジングしましょう: {{callUrl}}
## LOCALIZATION NOTE (share_email_body_context3): In this item, don't translate
## the part between {{..}} and leave the \n\n part alone.
share_email_body_context3=だちが Firefox Hello 上であなたを待っています。このリンクをクリックして接続し、一緒に {{title}} をブラウしましょう: {{callUrl}}
share_email_body_context3=が Firefox Hello 上であなたを待っています。このリンクをクリックして接続し、一緒に {{title}} をブラウジングしましょう: {{callUrl}}
## LOCALIZATION NOTE (share_email_footer2): Common footer content for both email types
share_email_footer2=\n\n____________\nFirefox Hello を使うと、友だちと共に Web をブラウズできます。一緒に計画を立てたり、作業をしたり、おしゃべりをしたいときに使ってください。詳しくは http://www.firefox.com/hello をご覧ください。
share_email_footer2=\n\n____________\nFirefox Hello を使うと、友達と共に Web をブラウジングできます。一緒に計画を立てたり、作業をしたり、おしゃべりをしたいときに使ってください。詳しくは http://www.firefox.com/hello をご覧ください。
## LOCALIZATION NOTE (share_tweeet): In this item, don't translate the part
## between {{..}}. Please keep the text below 117 characters to make sure it fits
## in a tweet.
@ -107,6 +105,7 @@ share_add_service_button=サービスを追加
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=リンクをコピー
email_link_menuitem=リンクをメールで送る
edit_name_menuitem=名前を編集
delete_conversation_menuitem2=削除
panel_footer_signin_or_signup_link=ログインまたはアカウント登録
@ -200,9 +199,9 @@ door_hanger_button=OK
# Infobar strings
infobar_screenshare_no_guest_message=だちが参加すると、その人はあなたがクリックしたタブを見ることができます。
infobar_screenshare_browser_message2=あなたはウィンドウを共有しています。開いているタブはすべて友だちによって見られます
infobar_screenshare_browser_message3=あなたはタブを共有しています。友だちはあなたがクリックしたタブを見ることができます。
infobar_screenshare_no_guest_message=が参加すると、その人はあなたがクリックしたタブを見ることができます。
infobar_screenshare_browser_message2=あなたはウィンドウを共有しています。開いているタブはすべて友によって見られます
infobar_screenshare_browser_message3=あなたはタブを共有しています。友はあなたがクリックしたタブを見ることができます。
infobar_screenshare_stop_sharing_message=タブの共有を中止しました
infobar_button_restart_label2=共有を再開
infobar_button_restart_accesskey=R
@ -252,13 +251,13 @@ rooms_room_full_call_to_action_label={{clientShortname}} の詳細 »
rooms_room_full_call_to_action_nonFx_label={{brandShortname}} をダウンロードして会話を始めましょう
rooms_room_full_label=この会話には既に 2 名が参加しています。
rooms_room_join_label=会話に参加
rooms_room_joined_owner_connected_label2=だちが接続し、あなたのタブを見られるようになりました。
rooms_room_joined_owner_not_connected_label=だちがあなたと {{roomURLHostname}} を見るために待っています。
rooms_room_joined_owner_connected_label2=が接続し、あなたのタブを見られるようになりました。
rooms_room_joined_owner_not_connected_label=があなたと {{roomURLHostname}} を見るために待っています。
self_view_hidden_message=セルフビューは隠れていますが送信されています。表示するにはウィンドウの大きさを変更してください
peer_left_session=だちが退出しました。
peer_unexpected_quit=だちの接続が予期せず終了しました。
peer_left_session=が退出しました。
peer_unexpected_quit=の接続が予期せず終了しました。
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Bekijk webpaginas met een vriend
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Of u nu een reis plant of een cadeau zoekt, met {{clientShortname2}} maakt u in realtime snellere beslissingen.
fte_slide_2_title=Bekijk dezelfde pagina
fte_slide_2_copy=Gebruik de ingebouwde tekst- of videochat om ideeën uit te wisselen, opties te vergelijken en overeenstemming te bereiken.
fte_slide_2_title2=Gemaakt om het web te delen
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Als u nu een vriend uitnodigt voor een sessie, zal {{clientShortname2}} automatisch webpaginas die u bekijkt delen. Plan. Winkel. Beslis. Samen.
fte_slide_3_title=Nodig een vriend uit door een koppeling te delen
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Zoek het {{clientSuperShortname}}-pictogram om te beginnen
## will be replaced by the brand short name.
fte_slide_4_copy=Als u een pagina hebt gevonden die u wilt bespreken, klikt u op het pictogram in {{brandShortname}} om een koppeling te maken. Stuur deze daarna naar uw vriend zoals u wilt!
invite_header_text_bold=Nodig iemand uit om deze pagina met u te bekijken!
invite_header_text_bold2=Nodig een vriend uit om deel te nemen!
invite_header_text3=Er zijn twee personen nodig om Firefox Hello te gebruiken, dus stuur een koppeling naar een vriend om samen op het web te bladeren!
invite_header_text4=Deel deze koppeling, zodat u samen het web kunt bekijken.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Een service toevoegen
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Koppeling kopiëren
email_link_menuitem=Koppeling e-mailen
edit_name_menuitem=Naam bewerken
delete_conversation_menuitem2=Verwijderen
panel_footer_signin_or_signup_link=Aanmelden of registreren

Просмотреть файл

@ -22,16 +22,14 @@ first_time_experience_button_label2=Przekonaj się, jak to działa
fte_slide_1_title=Przeglądaj strony WWW wspólnie ze znajomymi
fte_slide_1_copy=Niezależnie czy planujesz wycieczkę, zakup prezentu, {{clientShortname2}} pozwala podejmować decyzje szybciej.
fte_slide_2_title=Przeglądaj tę samą stronę
fte_slide_2_copy=Wbudowany czat tekstowy i wideo umożliwia łatwe dzielenie się pomysłami, porównywanie opcji i podejmowanie decyzji.
fte_slide_2_title2=Stworzony, by dzielić się siecią
fte_slide_2_copy2=Zapraszając znajomego do czatu, {{clientShortname2}} automatycznie udostępni stronę którą oglądasz. Planuj. Rób zakupy. Decyduj. Wspólnie.
fte_slide_3_title=Zaproś znajomego wysyłając odnośnik
fte_slide_3_copy={{clientSuperShortname}} działa w większości przeglądarek na komputery. Nie jest wymagane zakładnie kont i wszyscy łączą się bez opłat.
fte_slide_4_title=Odszukaj ikonę {{clientSuperShortname}}, aby rozpocząć
fte_slide_4_copy=Po znalezieniu strony do omówienia, kliknij ikonę w {{brandShortname}}, aby utworzyć odnośnik. Wyślij go znajomemu jakkolwiek chcesz!
invite_header_text_bold=Zaproś kogoś do wspólnego przeglądania strony
invite_header_text_bold2=Zaproś znajomego, aby dołączył do Ciebie!
invite_header_text3=Do używania Firefox Hello potrzeba dwóch osób, prześlij znajomemu odnośnik do wspólnego przeglądania sieci.
invite_header_text4=Udostępnij ten odnośnik, aby przeglądać wspólnie sieć.
invite_copy_link_button=Kopiuj odnośnik
invite_copied_link_button=Skopiowano
@ -59,6 +57,7 @@ share_tweet=Dołącz do rozmowy wideo ze mną, używając {{clientShortname2}}!
share_add_service_button=Dodaj serwis
copy_link_menuitem=Kopiuj odnośnik
email_link_menuitem=Wyślij odnośnik
edit_name_menuitem=Zmień nazwę
delete_conversation_menuitem2=Usuń
panel_footer_signin_or_signup_link=Zaloguj się lub utwórz konto

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Navegue na web com um amigo
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Se está planejando uma viagem ou comprando um presente, {{clientShortname2}} te deixa tomar decisões mais rápidas em tempo real.
fte_slide_2_title=Use a mesma página
fte_slide_2_copy=Use o chat embutido de texto ou vídeo para compartilhar ideias, comparar opções e chegar a um acordo.
fte_slide_2_title2=Feito para compartilhar a Web
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Agora quando convida um amigo para uma sessão, o {{clientShortname2}} automaticamente compartilhará qualquer página Web que você estiver vendo. Planejem. Comprem. Decidam. Juntos.
fte_slide_3_title=Convide um amigo enviando um link
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Encontre o ícone {{clientSuperShortname}} para começar
## will be replaced by the brand short name.
fte_slide_4_copy=Assim que encontrar uma página que quer comentar, clique no ícone em {{brandShortname}} para criar um link. Então, envie-a para um amigo como quiser!
invite_header_text_bold=Convide alguém para navegar nessa página com você!
invite_header_text_bold2=Convide um amigo para juntar-se a você!
invite_header_text3=É preciso de dois para usar o Firefox Hello, então envie um link para um amigo navegar na web com você!
invite_header_text4=Compartilhe este link para poder iniciar a navegação em conjunto.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Adicionar um serviço
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copiar link
email_link_menuitem=Enviar link por email
edit_name_menuitem=Editar nome
delete_conversation_menuitem2=Apagar
panel_footer_signin_or_signup_link=Entre ou registre-se

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Navegue em páginas Web com um amigo
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Se estiver a planear uma viagem ou a fazer compras para um presente, o {{clientShortname2}} deixa-o fazer decisões mais rápidas em tempo real.
fte_slide_2_title=Esteja na mesma página
fte_slide_2_copy=Utilize o texto embutido ou conversa por vídeo para partilhar ideias, comparar opções e chegar a um acordo.
fte_slide_2_title2=Feito para partilhar a Web
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Agora, quando convida um amigo para uma sessão, o {{clientShortname2}} irá partilhar automaticamente qualquer página Web que estiver a ver. Planeie. Faça compras. Decida. Em conjunto.
fte_slide_3_title=Convide um amigo ao enviar esta ligação
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Encontre o ícone {{clientSuperShortname}} para começar
## will be replaced by the brand short name.
fte_slide_4_copy=Assim que encontrar uma página que queira discutir, clique no ícone no {{brandShortname}} para criar uma ligação. Depois, envie-a a um amigo como quiser!
invite_header_text_bold=Convide alguém para navegar nesta página consigo!
invite_header_text_bold2=Convide um amigo para se juntar a si!
invite_header_text3=São necessárias duas pessoas para utilizar o Firefox Hello, pelo que, envie uma ligação a um amigo para navegar na Web consigo!
invite_header_text4=Partilhe esta ligação para que possa começar a navegar na Web em conjunto.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Adicionar um serviço
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copiar ligação
email_link_menuitem=Enviar por email
edit_name_menuitem=Editar nome
delete_conversation_menuitem2=Apagar
panel_footer_signin_or_signup_link=Iniciar sessão ou Criar conta

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Просматривайте веб-страницы с дру
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Бу то планирование поездки или покупка подарка, {{clientShortname2}} позволяет вам принимать решения быстрее в реальном времени.
fte_slide_2_title=Смотрите одну страницу
fte_slide_2_copy=Используйте встроенные текстовые и видео чаты для обмена идеями, сравнения мнений и прихода к соглашению.
fte_slide_2_title2=Создан для совместной работы в Интернете
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Теперь, когда вы пригласите друга в сессию, {{clientShortname2}} автоматически поделится с ним любой веб-страницей, которую вы просматриваете. Планируйте. Покупайте. Решайте. Вместе.
fte_slide_3_title=Пригласите друга, отправив ему ссылку
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Найдите иконку {{clientSuperShortname}}, чтоб
## will be replaced by the brand short name.
fte_slide_4_copy=Когда вы найдете страницу, которую захотите обсудить, нажмите иконку {{brandShortname}}, чтобы создать ссылку. Затем отправьте её другу, любым удобным для вас способом!
invite_header_text_bold=Пригласите кого-нибудь, чтобы посмотреть эту страницу вместе!
invite_header_text_bold2=Пригласить друга присоединиться к вам!
invite_header_text3=Firefox Hello создан для общения, так что отправьте другу ссылку, чтобы вместе посёрфить по Интернету!
invite_header_text4=Поделитесь этой ссылкой и вы сможете просматривать Веб вместе.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Добавить службу
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Скопировать ссылку
email_link_menuitem=Послать ссылку по эл. почте
edit_name_menuitem=Изменить имя
delete_conversation_menuitem2=Удалить
panel_footer_signin_or_signup_link=Вход или Регистрация

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Prehliadajte webové stránky s priateľom
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Či plánujete cestu alebo nakupujete darček, {{clientShortname2}} vám umožní sa rýchlejšie rozhodnúť.
fte_slide_2_title=Ujasnite si to
fte_slide_2_copy=Použite zabudovaný textový alebo video rozhovor a zdieľajte svoje nápady, porovnávajte možnosti a dospejte k dohode.
fte_slide_2_title2=Vytvorené pre zdieľanie webu
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Teraz, keď pozvete svojich priateľov na spoločnú reláciu, {{clientShortname2}} bude automaticky zdieľať všetky stránky, na ktoré sa pozriete. Plánujte. Nakupujte. Rozhodujte. Spoločne.
fte_slide_3_title=Pozvite priateľa odoslaním odkazu
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Nájdite ikonu {{clientSuperShortname}} a začnite
## will be replaced by the brand short name.
fte_slide_4_copy=Keď nájdete stránku, o ktorej sa chcete porozprávať, kliknite na ikonu v prehliadači {{brandShortname}} a vytvorí sa odkaz. Tento následne doručte vášmu priateľovi.
invite_header_text_bold=Pozvite niekoho na spoločné prehliadanie tejto stránky!
invite_header_text_bold2=Pozvite priateľa, aby sa k vám pripojil
invite_header_text3=Na používanie Firefox Hello treba dvoch, takže pošlite kamarátovi odkaz, aby si web prehliadal s vami!
invite_header_text4=Zdieľajte tento odkaz, aby ste mohli začať prehliadať web spolu.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Pridať službu
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Skopírovať odkaz
email_link_menuitem=Odoslať odkaz
edit_name_menuitem=Upraviť názov
delete_conversation_menuitem2=Odstrániť
panel_footer_signin_or_signup_link=Zaregistrujte sa alebo sa prihláste

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Brskajte po spletu s prijateljem
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Ne glede na to, ali načrtujete izlet ali kupujete darilo - {{clientShortname2}} omogoča hitrejše odločanje v resničnem času.
fte_slide_2_title=Obiščite isto stran
fte_slide_2_copy=Uporabite vgrajeni besedilni ali videoklepet za deljenje idej, primerjavo možnosti in dogovarjanje.
fte_slide_2_title2=Narejen za deljenje spleta
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Ko boste odslej v sejo povabili prijatelja, bo {{clientShortname2}} samodejno delil vsako stran, ki si jo ogledujete. Načrtujte. Nakupujte. Odločajte se. Skupaj.
fte_slide_3_title=Povabite prijatelja s pošiljanjem povezave
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Za začetek poiščite ikono {{clientSuperShortname}}
## will be replaced by the brand short name.
fte_slide_4_copy=Ko najdete stran, o kateri želite razpravljati, kliknite ikono v {{brandShortname}}, da ustvarite povezavo. Potem jo pošljite prijatelju na kateri koli način!
invite_header_text_bold=Povabite nekoga, da bo brskal po tej strani z vami!
invite_header_text_bold2=Povabite prijatelja, da se vam pridruži!
invite_header_text3=Za uporabo Firefox Hello sta potrebna dva. Pošljite povezavo prijatelju in brskajte skupaj!
invite_header_text4=Delite to povezavo za začetek skupnega brskanja po spletu.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Dodaj storitev
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Kopiraj povezavo
email_link_menuitem=Pošlji povezavo
edit_name_menuitem=Uredi ime
delete_conversation_menuitem2=Izbriši
panel_footer_signin_or_signup_link=Prijava ali registracija

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Shfletoni faqe në Internet me një shok
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Qoftë kur bëni plane për një udhëtim apo për të blerë një dhuratë, {{clientShortname2}} ju lejon të merrni vendime më shpejt, aty për aty.
fte_slide_2_title=Merruni me të njëjtën faqe
fte_slide_2_copy=Përdorni fjalosjen me tekst ose video, të vetë programit, për të ndarë me të tjerët ide, për të krahasuar mundësi të ndryshme dhe për të arritur në të njëjtin mendim.
fte_slide_2_title2=Krijuar për të ndarë Web-in me të tjerët
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Tanimë, kur ftoni një shok në një sesion, {{clientShortname2}} do të ndajë me të vetvetiu çfarëdo faqe Web që po shihni. Planifikoni. Blini. Vendosni. Tok.
fte_slide_3_title=Ftoni një shok duke i dërguar një lidhje
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Që tia filloni, gjeni ikonën {{clientSuperShortname}}
## will be replaced by the brand short name.
fte_slide_4_copy=Pasi të keni gjetur një faqe mbi të cilën doni të diskutoni, klikoni mbi ikonën te {{brandShortname}} që të krijoni një lidhje. Mandej dërgojani shokut tuaj kur të doni!
invite_header_text_bold=Ftoni dikë ta shfletojë këtë faqe tok me ju!
invite_header_text_bold2=Ftoni të vijë një shok!
invite_header_text3=Duhen dy vetë që të përdorni Firefox Hello-në, ndaj dërgojini një shoku një lidhje që të shfletoni Web-in së bashku!
invite_header_text4=Ndajeni me ta këtë lidhje që të filloni të shfletoni në internet tok.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Shtoni një Shërbim
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Kpjoje Lidhjen
email_link_menuitem=Dërgojeni Lidhjen Me Email
edit_name_menuitem=Përpunoni emrin
delete_conversation_menuitem2=Fshije
panel_footer_signin_or_signup_link=Hyni ose Regjistrohuni

Просмотреть файл

@ -42,8 +42,10 @@ fte_slide_1_title=Surfa på hemsidor med en vän
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Vare sig du planerar en resa eller letar efter en present, låter {{clientShortname2}} dig fatta snabbare beslut i realtid.
fte_slide_2_title=Hamna på samma sida
fte_slide_2_copy=Använd den inbyggda text eller videochatten för att utbyta idéer, jämföra olika alternativ och komma överens.
fte_slide_2_title2=Gjord för att dela webben
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Nu när du bjuder in en vän till en session, kommer {{clientShortname2}} automatiskt dela alla webbsidor du visar. Planera. Handla. Besluta. Tillsammans.
fte_slide_3_title=Bjuda in en vän genom att skicka en länk
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -55,9 +57,7 @@ fte_slide_4_title=Hitta ikonen för {{clientSuperShortname}} för att komma igå
## will be replaced by the brand short name.
fte_slide_4_copy=När du har hittat en sida som du vill diskutera, klicka på ikonen för {{brandShortname}} för att skapa en länk. Skicka det sen till din vän hur du vill!
invite_header_text_bold=Bjud in någon att surfa på sidan med dig!
invite_header_text_bold2=Bjud in en kompis att delta!
invite_header_text3=Det krävs två för att använda Firefox Hello, så skicka en länk till en vän för att surfa på webben med dig!
invite_header_text4=Dela denna länken så att ni kan börja surfa på Webben tillsammans.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -105,6 +105,7 @@ share_add_service_button=Lägga till en tjänst
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Kopiera länk
email_link_menuitem=E-posta länk
edit_name_menuitem=Redigera namn
delete_conversation_menuitem2=Ta bort
panel_footer_signin_or_signup_link=Logga in eller registrera dig

Просмотреть файл

@ -4,10 +4,10 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
loopMenuItem_label=ஒரு உரையாடலை தொடங்கு...
## LOCALIZATION_NOTE(sign_in_again_title_line_one, sign_in_again_title_line_two2):
## These are displayed together at the top of the panel when a user is needed to
@ -19,18 +19,28 @@
## will be replaced by the super short brandname.
## LOCALIZATION_NOTE(first_time_experience_subheading2): Message inviting the
## LOCALIZATION_NOTE(first_time_experience_subheading2, first_time_experience_subheading_button_above): Message inviting the
## user to create his or her first conversation.
## LOCALIZATION_NOTE(first_time_experience_content): Message describing
## LOCALIZATION_NOTE(first_time_experience_content, first_time_experience_content2): Message describing
## ways to use Hello project.
## First Time Experience Slides
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
## LOCALIZATION_NOTE(fte_slide_4_title): {{clientSuperShortname}}
## will be replaced by the super short brand name.
## LOCALIZATION_NOTE(fte_slide_4_copy): {{brandShortname}}
## will be replaced by the brand short name.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
## an iconic button for the invite view.
# Status text
# Error bars
## LOCALIZATION NOTE(session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
## These may be displayed at the top of the panel.
@ -112,13 +122,6 @@ tour_label=சுற்றுலா
# Infobar strings
# Context in conversation strings
## LOCALIZATION NOTE(no_conversations_message_heading2): Title shown when user
## has no conversations available.
## LOCALIZATION NOTE(no_conversations_start_message2): Subheading inviting the
## user to start a new conversation.
# E10s not supported strings
# This Source Code Form is subject to the terms of the Mozilla Public

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Web sayfalarını bir arkadaşınızla gezin
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=İster bir seyahat planlıyor olun ister hediye seçiyor, {{clientShortname2}} daha çabuk karar vermenize yardımcı olacak.
fte_slide_2_title=Aynı sayfada buluşun
fte_slide_2_copy=Fikirlerinizi paylaşmak, seçenekleri karşılaştırmak ve karara varmak için yazılı ve görüntülü sohbet edebilirsiniz.
fte_slide_2_title2=Webi paylaşmanın en iyi yolu
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Artık bir arkadaşınızı oturumu davet ettiğinizde {{clientShortname2}} baktığınız web sayfasını otomatik olarak paylaşacaktır. Birlikte planlayın, alışveriş yapın, karar verin.
fte_slide_3_title=Bir bağlantı göndererek arkadaşınızı davet edin
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Başlamak için {{clientSuperShortname}} simgesini bulun
## will be replaced by the brand short name.
fte_slide_4_copy=Üzerinde konuşmak istediğiniz sayfayı bulunca bir bağlantı oluşturmak için {{brandShortname}}'taki simgeye tıklayın. Ardından bu bağlantıyı arkadaşınızı gönderin.
invite_header_text_bold=Bu sayfayı birlikte gezmek için birini davet edin!
invite_header_text_bold2=Size katılması için arkadaşınızı davet edin!
invite_header_text3=Firefox Hello'yu kullanmak için iki kişi gerekiyor. Web'i birlikte gezmek istediğiniz arkadaşınıza bir bağlantı gönderin!
invite_header_text4=Web'de birlikte dolaşmaya başlamak için bu bağlantıyı paylaşın.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Servis ekle
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Bağlantıyı kopyala
email_link_menuitem=Bağlantıyı e-postala
edit_name_menuitem=Adı düzenle
delete_conversation_menuitem2=Sil
panel_footer_signin_or_signup_link=Giriş yap veya kaydol
@ -257,6 +256,9 @@ rooms_room_joined_owner_not_connected_label=Arkadaşınız sizinle birlikte {{ro
self_view_hidden_message=Kendi görünümünüz gizlendi ama hâlâ gönderiliyor. Görmek için pencereyi boyutlandırın
peer_left_session=Arkadaşınız çıktı.
peer_unexpected_quit=Arkadaşınızın bağlantısı koptu.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.
tos_failure_message={{clientShortname}} ülkenizde kullanılamıyor.

Просмотреть файл

@ -4,8 +4,6 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
@ -44,8 +42,10 @@ fte_slide_1_title=Переглядайте веб-сторінки з друзя
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Плануєте ви подорож чи покупку подарунку, {{clientShortname2}} дозволяє вам приймати швидші рішення в реальному часі.
fte_slide_2_title=Переглядайте одну й ту саму сторінку
fte_slide_2_copy=Використовуйте вбудований текстовий чи відео чат для обміну ідеями, порівнянням можливостей і спільного узгодження.
fte_slide_2_title2=Створений для спільного користування Інтернетом
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Тепер, коли ви запрошуєте друзів на сеанс, {{clientShortname2}} буде автоматично ділитися будь-якою веб-сторінкою, яку ви переглядаєте. Плануйте. Купуйте. Вирішуйте. Разом.
fte_slide_3_title=Запрошуйте друзів, надсилаючи їм посилання
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
@ -57,9 +57,7 @@ fte_slide_4_title=Знайдіть піктограму {{clientSuperShortname}}
## will be replaced by the brand short name.
fte_slide_4_copy=Як тільки ви знайшли сторінку, яку хочете обговорити, натисніть піктограму в {{brandShortname}}, щоб створити посилання. Потім надішліть його друзям будь-яким бажаним способом!
invite_header_text_bold=Запросіть когось для перегляду цієї сторінки разом з вами!
invite_header_text_bold2=Запросіть друзів приєднатися!
invite_header_text3=Для користування Firefox Hello потрібно двоє осіб, тож надішліть посилання другу для спільного перегляду Інтернету!
invite_header_text4=Поділіться цим посиланням, щоб ви змогли почати перегляд в Інтернеті разом.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
@ -107,6 +105,7 @@ share_add_service_button=Додати службу
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Копіювати посилання
email_link_menuitem=Надіслати посилання
edit_name_menuitem=Змінити ім’я
delete_conversation_menuitem2=Видалити
panel_footer_signin_or_signup_link=Вхід або реєстрація

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше