Merge mozilla-central to mozilla-inbound
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 you’re 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 you’re 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 you’ve 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 l’achat d’un 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 d’accord.
|
||||
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 l’icô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 l’icône dans {{brandShortname}} pour créer un lien. Vous pouvez alors l’envoyer à votre interlocuteur de la manière que vous voulez.
|
||||
|
||||
invite_header_text_bold=Invitez quelqu’un à consulter cette page avec vous !
|
||||
invite_header_text_bold2=Invitez quelqu’un à 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=S’inscrire 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 l’icona 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 sull’icona di {{brandShortname}}, poi invialo all’interessato 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 webpagina’s 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 webpagina’s 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ë t’ia 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=Web’i 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=Вхід або реєстрація
|
||||
|
|