mirror of
https://github.com/roytam1/palemoon27.git
synced 2026-05-26 14:30:27 +00:00
import changes from `dev' branch of rmottola/Arctic-Fox:
- Bug 1193050 - Update the copy of GentiumPlus used in font-inspector test. r=jdaggett (253fce5764)
- align tests (5b817116a0)
- Bug 1246106 - pass aStyleBorder as reference to ComputeBorderOverflow. r=mats (53ccaa1282)
- align tests (81b3943058)
- Bug 1269080 - Convert NS_ERROR to NS_WARNING when setting an invalid scheme in nsSimpleURI. r=mcmanus (cad86c963d)
- Bug 1248558 - "[Static Analysis][Unintentional integer overflow][CacheFile.cpp::PreloadChunk, CacheFile.cpp::GetChunkLocked]". r=michal (6a9e34dfbe)
- Bug 1121672 - Make CacheFile::IsDoomed() protect mHandle with lock, r=michal (9e925b903a)
- Bug 1253040 - Do not write metadata if CacheFile is killed, r=honzab (c0378088b2)
- grammar as 1268922 (c801618b1d)
- Bug 1247548 - Changed nsCookieService::EnsureReadComplete and nsCookieService::PurgeCookeis to allocate nsTArray instead of AutoTArray. r=jdm (0e2bb0465e)
- var-let (1702e664dc)
- Bug 1233813 - Fix mDNS bug in resolving services with no TXT records. r=schien (4ce7e5fca0)
- Bug 1266632 - Add a null check in nsHostResolver::SizeOfIncludingThis. r=sworkman. (035d4a7130)
- Bug 1267000 - null deref with spdy proxy r=hurley (099db40d0f)
- Bug 1240932 - figure out 'network id' on Linux. r=mcmanus (47ceb12a70)
- Bug 1240932: add Telemetry to record network id successes, r=mcmanus (6985d1a4ce)
- Bug 1205158 - Use channel->Open2() netwerk/test/ (r=sicking) (64de01cda3)
- Bug 1240932 - figure out 'network id' on OS X, r=mcmanus (7ee4875835)
- Bug 1240932 - figure out 'network id' on Windows, r=mcmanus (3227a81c14)
- Bug 1188644 - Use channel->ascynOpen2 in netwerk/test (r=mcmanus) (7097897cdc)
- Bug 1264887: |nsWifiScannerDBus|: Hold DBusMessage references in |RefPtr|, r=josh (7e68e8663f)
- Bug 1260718 - use plain promise in CustomizableUI.jsm and ScrollbarSampler.jsm; r=Gijs (275c4f9efa)
- Bug 1254268 - Distribution should use default prefs, regression from 1252466; r=mconnor (79503a338d)
- Bug 1239949 - Add a pref for showing bookmarks toolbar and menubar; r=mixedpuppy (a2538235e6)
- Bug 1261657 - Don't record SSTabRestored events in StartupPerformance that are the result of a remoteness flip. r=Yoric (1016be766a)
- Bug 1170166, add a capture flag to BrowserTestUtils.waitForEvent, r=paolo (639c68fd65)
- Bug 1245723 - Make browser_bug680727 e10s compatible. r=felipe (6f29088ba2)
- Bug 1233497 - Allow CPOW usage in BrowserTestUtils testing module. r=felipe (29f9ca39d2)
- Bug 1243928 - Make these tests work in e10s. r=felipe dd (2fe22b5802)
- Bug 1147700 - Part 1: Add aDragEvent parameter to ChromeUtils.synthesizeDrop to modify DragEvent, and set default position to center of destElement. r=enndeakin (e8fb568796)
- Bug 1196638 - Infer the proper button for synthesized mouse clicks on all callsites. r=jmaher (838230baaa)
- Bug 1241672 - Add synthesizeWheelAtPoint to avoid styling flush. r=smaug (35a1bff9f9)
- Bug 1245153 - Make EventUtils.js use aWindow argument for sub-calls consistently; r=jmaher (ee20272638)
- Bug 1093153: introduce 'BrowserTestUtils#synthesizeKey', 'BrowserTestUtils#synthesizeComposition' and 'BrowserTestUtils#synthesizeCompositionChange' to allow mochitests to remotely invoke EventUtils' text composition utilities. Changes to EventUtils include 1) removed dependency on the 'navigator' object when 'nsIXULRuntime' is available and 2) make '_getKeyboardEvent' more robust when used in frame scripts. r=Enn (010ff131a7)
- Bug 1267289 - add more URL bar tests and fix issue with error pages, r=mikedeboer,mconley (2fa5b4cd18)
- more hack since CustomizableUI isn't loaded (482d4c7054)
- add PanelFrame (dbc45cdfde)
- add ContentObservers (f697c17a3f)
- add some CustomizableUI stuff (7b352953a8)
- Bug 1260148 - Make sendWheelAndPaint wait for the system wheel event. r=masayuki (89de140823)
- Bug 1258532 - Port test_bug574596.html to mochitest-plain in order to enable it on e10s; r=mrbkap (ae5693ab54)
- no more shumway (4ec4c5929d)
- Bug 1208145 - Clear XUL persistence data for passwordManager.xul passwordCol.hidden. r=dolske (286326c6d4)
- Bug 1252855 - allow setting a specific list of prefs from the content process, r=mrbkap,margaret,haik (b13645826a)
- Bug 1260718 - use plain promise in panelUI.js; r=Gijs (5b2d20e6d8)
- Bug 1256085 - fix hamburger menu losing pressed state when a context menu inside the panel closes, r=jaws (8074b072f3)
- de-palemoonize slwo startup URL for now (22cd2896f1)
- minor inits, but PanelUI still doesn't init (0a982d67f4)
- Bug 690307 - make trimURL not generate URLs that parse back into search queries, r=mak (9d488702f4)
- Bug 1094179 - use uriFixup for trimURL, r=dao (7a5e6ea7c8)
- Bug 1254503 - ignore exceptions from trying to fix up obviously broken URIs, r=mak (8a7b5e08dc)
- Bug 1240169 - Revert to returning a dynamic newtab URL for BROWSER_NEW_TAB_URL r=mconley (f9426cd705)
- small bits (f667b5b40a)
- Bug 1207490 - Part 14: Remove use of expression closure from browser/modules/WindowsJumpLists.jsm. r=Gijs (f5ffeb762d)
- Bug 957585 - Make jumplists listen for "active" idle topic instead of the defunct "back". r=jimm (21faffc952)
- bug 1223573 and other move loop (b6f11f9b65)
- remove fuel (d231f1c07f)
- package some stuff that is there and wasn't (3df3a2461d)
- Bug 1249845 - bootstrap.js code to manage the e10s staged rollout. r=Mossop (93fdf6ddd6)
- Bug 1255013 - Tag disqualified cohorts in e10s staged rollout. r=Mossop (2a1bf3eb57)
- Bug 1257251 - Annotate e10s cohort in crash report. r=Mossop (eaa77c89bd)
- Bug 1257972 - Put users currently running an experiment into the disqualified cohort of the e10s rollout system add-on. r=Mossop (63cfa4e9bd)
- Bug 1257972 - Follow up, use getActiveExperimentID instead. r=me (062b8db4b0)
- Bug 1264345 - Remove restriction to disqualify from e10s the users who are running an experiment, by backing out bug 1257972. r=backout (ca4edc1985)
- Bug 1264437 - Manage browser.tabs.remote.autostart.2 pref even for disqualified users. r=rvitillo (20f34e41b6)
- Bug 1268921 - Allow non-integer values to be specified for e10srollout cohort samples. r=mconley (5d084d8a4c)
- Bug 1249845 - Set up structure for e10srollout system add-on. r=Standard8,glandium (653ffeb6f9)
- Bug 1268197 - stub for webcompat fix system add-on r=kmag (a6f25edf7e)
- missing pocket locales (453efccf16)
- Bug 1215965 - Remove use of non-standard features from toolkit/components/social/SocialService.jsm. r=mixedpuppy (a1d79f295c)
- Bug 898706 remove frameworker and all associated code and tests, r=markh (7dd5241c68)
- ship cmpiler for XP (ac60884173)
- convenience file (fedc58984f)
- Bug 1261045: remove unused MOZ_SYMBOLS_EXTRA_BUILDID; r=ted.mielczarek (96c4acfc47)
- Bug 1262814 - Warn on Wlanapi.dll load or init fail, don't fail assertion, r=mcmanus (aa1335320a)
- Bug 1137151: Marked destructors of ref-counted time-zone classes as protected, r=dhylands (5668226442)
- Bug 908038 - Move worker_buf.js installation into moz.build; r=mshal (5977402229)
- Bug 1225549 - Modify the data type of mThreshold of NetworkParams to long long from long. r=ettseng (7164a517ac)
- Bug 1139805 - B2G NetworkManager: move NetworkManager.js out of EXTRA_PP_COMPONENTS in moz.build. r=echen (e857f53cfc)
- Bug 1256246 - Improve prompt for loading an add-on from directory. r=ochameau (e4eaf98420)
- Bug 1231128 - Display errors when temporary add-on install fails. r=ochameau (7866ee6157)
- Bug 1249088 - Add eslint rules for React. r=pbrosset (43ccb89844)
- Bug 1264063 - 1 - Disable prop-types eslint rule and fix sort-comp errors; r=ochameau (a765af076a)
- Bug 1264063 - 2 - Make the CPOW rule log errors and ignore ContentTask.spawn; r=Mossop (d5a6de53ba)
- Bug 1217769 - aboutdebugging: default to addons tab without navigating if hash is empty;r=janx (d821504376)
- Bug 1229859 - Massively reduce the number of eslint errors in devtools by ignoring lib files, adding missing .eslintrc files and making some rules be warnings; r=Mossop (e6fdb3043a)
- Bug 1210778 - improving accessibility for about:debugging. r=janx (e9d1c80835)
- Bug 1087608 - ensuring multitap gestures do not resolve to explore. r=eeejay (6c4b603738)
- bug 1259023 - make nsIAccessible.{Next,Prev}Sibling work with proxied accessibles r=yzen (84b47a82e0)
- bug 1259023 - make nsIAccessible.indexInParent work on proxied accessibles r=yzen (3531b45e35)
- bug 1250882 - implement xpcAccessible::GetState() for proxied accessibles r=davidb (5e58214441)
- bug 1250882 - implement xpcAccessible::Name() for proxied accessibles r=davidb (4b11af6ecc)
- bug 1250882 - implement xpcAccessible::GetDescription() for proxied accessibles r=davidb (d0d0e133fb)
- bug 1250882 - implement xpcAccessible::GetLanguage() for proxied accessibles r=davidb (88e98a5b24)
- bug 1250882 - implement xpcAccessible::GetValue() for proxied accessibles r=davidb (9ad4347d45)
- bug 1250882 - implement xpcAccessible::GetBounds() for proxied accessibles r=davidb (e67f7c551b)
- Bug 1249930 - menupoup shouldn't look for children in XBL anonymous content, r=davidb (14b3e39954)
- Bug 1261473 - Remove INSTALL_TARGETS from addon-sdk/Makefile.in; r=chmanchester (8a65454da5)
- Bug 1148200 - Move isJSONable to sdk/lang/type. r=Mossop (df9e4a3e64)
- Bug 1026614 - SDK ui/toolbar not working in permanent private browsing. r=rpl (589c0eb203)
- Bug 1263077 - Stop using parseInt in the Add-on SDK when specifying octal numbers now that JS has the new octal notation. r=dietrich (9a82b90c79)
- Bug 1225800 - only import items that have valid URLs, r=MattN (b1609de6b5)
- Bug 1194692 - fix some cookie parsing issues in the IE/Edge cookie importer code, r=MattN (ce08d5a913)
- Bug 1255526 - fix import of typed URLs on versions of windows that do not store timestamps, r=MattN (abff778842)
- Bug 1236551 - Update the profile migrator to the Unified Telemetry changes. r=MattN (3fc75f60c8)
- Bug 1239085 - Remove Deprecation warning from NewTabUrl.jsm (r=olivier) (ef09dd61fd)
- Bug 1201977 - Replace usage of nsINavHistoryQuery with a string query to nsPIPlacesDatabase.asyncStatement r=oyiptong (105178d138)
- Bug 1258728 - remote newtab path starts with /newtab/ r=ursula (575bccc434)
- Bug 1262781 - Use the host compiler for the ASTMatcher check. r=froydnj (d5b04fc0f3)
- Followup for bug 1262781: change ASTMatcher check name to force override config.cache on a CLOSED TREE. r=me (7cda382eac)
- Bug 1268617 - Pass -g to rustc on debug builds. r=ted (f280e86566)
- Bug 1243233 - Test ALLOW_COMPILER_WARNINGS instead of WARNINGS_AS_ERRORS, and move it to after it is set, r=glandium (216c407d20)
- Bug 1245992 - Update the Safe Browsing phishing interstitial page. r=flod,past (80afbfc6c9)
- fix some elements (dac19cdc1a)
- Bug 1245260 - Ignore redundant calls to RestyleManager::IncrementAnimationGeneration; r=dbaron (e35571b613)
- bump overrides (90b8e83afd)
- Bug 1182778, r=margaret,f=bz (b379c97f8e)
- Bug 1244887: Fixing userContext label on awesomebar, r=baku (063a8e8263)
- Bug 1255499 - Remove SEC_NORMAL from browser/. r=sicking (12a374232b)
- Bug 1258212 - Hook the parent up to the cild for registerContentHandler. r=gwright (039c75d4d5)
- Bug 1233895 - Make Feeds.jsm properly handle principal origin attributes when loading subresources. r=sicking (6b4544effe)
- Bug 1234398 - declare |item|. r=dolske (fa9a5ff39c)
- Bug 1244684 - Make FX_TAB_SWITCH_TOTAL_MS work for non-e10s in an OMTC world. r=mstange (05cb04c474)
- Bug 1261738: Try to avoid overlapping FX_TAB_SWITCH_TOTAL_MS stopwatches. r=mconley (5d4bae300c)
- Bug 747338 - Set last-accessed timestamp when deselecting tabs rather than when selecting them. r=ttaubert (a1514ad247)
- Bug 1248302 - We should not show any decoration for tab with usercontextid=0, r=gijs (29c5addab7)
- specific google font overrides (03db96e884)
- add bits of CustomizableUI (8a67508a56)
- add some more customizeui but keep the button hidden, or it messes up the toolbar (b9787fc5bd)
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
# This Makefile is used as a shim to aid people with muscle memory
|
||||
# so that they can type "make".
|
||||
#
|
||||
# This file and all of its targets should not be used by anything important.
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
./mach build
|
||||
|
||||
clean:
|
||||
./mach clobber
|
||||
|
||||
.PHONY: all build clean
|
||||
+1
-5
@@ -288,12 +288,8 @@ SYM_STORE_SOURCE_DIRS := $(topsrcdir)
|
||||
|
||||
include $(topsrcdir)/toolkit/mozapps/installer/package-name.mk
|
||||
|
||||
ifdef MOZ_SYMBOLS_EXTRA_BUILDID
|
||||
EXTRA_BUILDID := -$(MOZ_SYMBOLS_EXTRA_BUILDID)
|
||||
endif
|
||||
|
||||
SYMBOL_INDEX_NAME = \
|
||||
$(MOZ_APP_NAME)-$(MOZ_APP_VERSION)-$(OS_TARGET)-$(BUILDID)-$(CPU_ARCH)$(EXTRA_BUILDID)-symbols.txt
|
||||
$(MOZ_APP_NAME)-$(MOZ_APP_VERSION)-$(OS_TARGET)-$(BUILDID)-$(CPU_ARCH)-symbols.txt
|
||||
|
||||
buildsymbols:
|
||||
ifdef MOZ_CRASHREPORTER
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
|
||||
DoubleTap -> TripleTap (x)
|
||||
-> TapHold (x)
|
||||
-> Explore (x)
|
||||
|
||||
TripleTap -> DoubleTapHold (x)
|
||||
-> Explore (x)
|
||||
|
||||
Dwell -> DwellEnd (v)
|
||||
|
||||
@@ -39,7 +37,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
this.EXPORTED_SYMBOLS = ['GestureSettings', 'GestureTracker']; // jshint ignore:line
|
||||
@@ -73,8 +70,6 @@ const TAP_MAX_RADIUS = 0.2;
|
||||
// Directness coefficient. It is based on the maximum 15 degree angle between
|
||||
// consequent pointer move lines.
|
||||
const DIRECTNESS_COEFF = 1.44;
|
||||
// The virtual touch ID generated by a mouse event.
|
||||
const MOUSE_ID = 'mouse';
|
||||
// Amount in inches from the edges of the screen for it to be an edge swipe
|
||||
const EDGE = 0.1;
|
||||
// Multiply timeouts by this constant, x2 works great too for slower users.
|
||||
@@ -211,7 +206,6 @@ this.GestureTracker = { // jshint ignore:line
|
||||
if (aDetail.type !== 'pointerdown') {
|
||||
return;
|
||||
}
|
||||
let points = aDetail.points;
|
||||
let GestureConstructor = aGesture || (IS_ANDROID ? DoubleTap : Tap);
|
||||
this._create(GestureConstructor);
|
||||
this._update(aDetail, aTimeStamp);
|
||||
@@ -270,6 +264,7 @@ this.GestureTracker = { // jshint ignore:line
|
||||
this._create(gestureType, current.startTime, current.points,
|
||||
current.lastEvent);
|
||||
} else {
|
||||
this.current.clearTimer();
|
||||
delete this.current;
|
||||
}
|
||||
}
|
||||
@@ -608,6 +603,9 @@ TravelGesture.prototype = Object.create(Gesture.prototype);
|
||||
* this._travelTo gesture iff at least one point crosses this._threshold.
|
||||
*/
|
||||
TravelGesture.prototype.test = function TravelGesture_test() {
|
||||
if (!this._travelTo) {
|
||||
return;
|
||||
}
|
||||
for (let identifier in this.points) {
|
||||
let point = this.points[identifier];
|
||||
if (point.totalDistanceTraveled / Utils.dpi > this._threshold) {
|
||||
@@ -680,10 +678,10 @@ DoubleTapHoldEnd.prototype.type = 'doubletapholdend';
|
||||
* @param {Function} aRejectToOnWait A constructor for the next gesture to
|
||||
* reject to in case no pointermove or pointerup happens within the
|
||||
* GestureSettings.dwellThreshold.
|
||||
* @param {Function} aRejectToOnPointerDown A constructor for the gesture to
|
||||
* reject to if a finger comes down immediately after the tap.
|
||||
* @param {Function} aTravelTo An optional constuctor for the next gesture to
|
||||
* reject to in case the the TravelGesture test fails.
|
||||
* @param {Function} aRejectToOnPointerDown A constructor for the gesture to
|
||||
* reject to if a finger comes down immediately after the tap.
|
||||
*/
|
||||
function TapGesture(aTimeStamp, aPoints, aLastEvent, aRejectToOnWait, aTravelTo, aRejectToOnPointerDown) {
|
||||
this._rejectToOnWait = aRejectToOnWait;
|
||||
@@ -722,11 +720,12 @@ TapGesture.prototype.pointerup = function TapGesture_pointerup(aPoints) {
|
||||
};
|
||||
|
||||
TapGesture.prototype.pointerdown = function TapGesture_pointerdown(aPoints, aTimeStamp) {
|
||||
TravelGesture.prototype.pointerdown.call(this, aPoints, aTimeStamp);
|
||||
if (this._pointerUpTimer) {
|
||||
clearTimeout(this._pointerUpTimer);
|
||||
delete this._pointerUpTimer;
|
||||
this._deferred.reject(this._rejectToOnPointerDown);
|
||||
} else {
|
||||
TravelGesture.prototype.pointerdown.call(this, aPoints, aTimeStamp);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -771,7 +770,7 @@ DoubleTap.prototype.type = 'doubletap';
|
||||
*/
|
||||
function TripleTap(aTimeStamp, aPoints, aLastEvent) {
|
||||
this._inProgress = true;
|
||||
TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, DoubleTapHold);
|
||||
TapGesture.call(this, aTimeStamp, aPoints, aLastEvent, DoubleTapHold, null, null);
|
||||
}
|
||||
|
||||
TripleTap.prototype = Object.create(TapGesture.prototype);
|
||||
|
||||
@@ -41,12 +41,17 @@ xpcAccessible::GetNextSibling(nsIAccessible** aNextSibling)
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
if (!Intl())
|
||||
return NS_ERROR_FAILURE;
|
||||
if (IntlGeneric().IsAccessible()) {
|
||||
nsresult rv = NS_OK;
|
||||
NS_IF_ADDREF(*aNextSibling = ToXPC(Intl()->GetSiblingAtOffset(1, &rv)));
|
||||
return rv;
|
||||
}
|
||||
|
||||
nsresult rv = NS_OK;
|
||||
NS_IF_ADDREF(*aNextSibling = ToXPC(Intl()->GetSiblingAtOffset(1, &rv)));
|
||||
return rv;
|
||||
ProxyAccessible* proxy = IntlGeneric().AsProxy();
|
||||
NS_ENSURE_STATE(proxy);
|
||||
|
||||
NS_IF_ADDREF(*aNextSibling = ToXPC(proxy->NextSibling()));
|
||||
return *aNextSibling ? NS_OK : NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
@@ -57,12 +62,17 @@ xpcAccessible::GetPreviousSibling(nsIAccessible** aPreviousSibling)
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
if (!Intl())
|
||||
return NS_ERROR_FAILURE;
|
||||
if (IntlGeneric().IsAccessible()) {
|
||||
nsresult rv = NS_OK;
|
||||
NS_IF_ADDREF(*aPreviousSibling = ToXPC(Intl()->GetSiblingAtOffset(-1, &rv)));
|
||||
return rv;
|
||||
}
|
||||
|
||||
nsresult rv = NS_OK;
|
||||
NS_IF_ADDREF(*aPreviousSibling = ToXPC(Intl()->GetSiblingAtOffset(-1, &rv)));
|
||||
return rv;
|
||||
ProxyAccessible* proxy = IntlGeneric().AsProxy();
|
||||
NS_ENSURE_STATE(proxy);
|
||||
|
||||
NS_IF_ADDREF(*aPreviousSibling = ToXPC(proxy->PrevSibling()));
|
||||
return *aPreviousSibling ? NS_OK : NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
@@ -157,10 +167,12 @@ xpcAccessible::GetIndexInParent(int32_t* aIndexInParent)
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
if (!Intl())
|
||||
return NS_ERROR_FAILURE;
|
||||
if (IntlGeneric().IsAccessible()) {
|
||||
*aIndexInParent = Intl()->IndexInParent();
|
||||
} else if (IntlGeneric().IsProxy()) {
|
||||
*aIndexInParent = IntlGeneric().AsProxy()->IndexInParent();
|
||||
}
|
||||
|
||||
*aIndexInParent = Intl()->IndexInParent();
|
||||
return *aIndexInParent != -1 ? NS_OK : NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
@@ -239,10 +251,13 @@ xpcAccessible::GetState(uint32_t* aState, uint32_t* aExtraState)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aState);
|
||||
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
nsAccUtils::To32States(states::DEFUNCT, aState, aExtraState);
|
||||
else
|
||||
else if (Intl())
|
||||
nsAccUtils::To32States(Intl()->State(), aState, aExtraState);
|
||||
else
|
||||
nsAccUtils::To32States(IntlGeneric().AsProxy()->State(), aState,
|
||||
aExtraState);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
@@ -252,11 +267,16 @@ xpcAccessible::GetName(nsAString& aName)
|
||||
{
|
||||
aName.Truncate();
|
||||
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
nsAutoString name;
|
||||
Intl()->Name(name);
|
||||
if (ProxyAccessible* proxy = IntlGeneric().AsProxy()) {
|
||||
proxy->Name(name);
|
||||
} else {
|
||||
Intl()->Name(name);
|
||||
}
|
||||
|
||||
aName.Assign(name);
|
||||
|
||||
return NS_OK;
|
||||
@@ -265,11 +285,16 @@ xpcAccessible::GetName(nsAString& aName)
|
||||
NS_IMETHODIMP
|
||||
xpcAccessible::GetDescription(nsAString& aDescription)
|
||||
{
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
nsAutoString desc;
|
||||
Intl()->Description(desc);
|
||||
if (ProxyAccessible* proxy = IntlGeneric().AsProxy()) {
|
||||
proxy->Description(desc);
|
||||
} else {
|
||||
Intl()->Description(desc);
|
||||
}
|
||||
|
||||
aDescription.Assign(desc);
|
||||
|
||||
return NS_OK;
|
||||
@@ -278,21 +303,33 @@ xpcAccessible::GetDescription(nsAString& aDescription)
|
||||
NS_IMETHODIMP
|
||||
xpcAccessible::GetLanguage(nsAString& aLanguage)
|
||||
{
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
Intl()->Language(aLanguage);
|
||||
nsAutoString lang;
|
||||
if (ProxyAccessible* proxy = IntlGeneric().AsProxy()) {
|
||||
proxy->Language(lang);
|
||||
} else {
|
||||
Intl()->Language(lang);
|
||||
}
|
||||
|
||||
aLanguage.Assign(lang);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
xpcAccessible::GetValue(nsAString& aValue)
|
||||
{
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
nsAutoString value;
|
||||
Intl()->Value(value);
|
||||
if (ProxyAccessible* proxy = IntlGeneric().AsProxy()) {
|
||||
proxy->Value(value);
|
||||
} else {
|
||||
Intl()->Value(value);
|
||||
}
|
||||
|
||||
aValue.Assign(value);
|
||||
|
||||
return NS_OK;
|
||||
@@ -379,10 +416,16 @@ xpcAccessible::GetBounds(int32_t* aX, int32_t* aY,
|
||||
NS_ENSURE_ARG_POINTER(aHeight);
|
||||
*aHeight = 0;
|
||||
|
||||
if (!Intl())
|
||||
if (IntlGeneric().IsNull())
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
nsIntRect rect = Intl()->Bounds();
|
||||
nsIntRect rect;
|
||||
if (Accessible* acc = IntlGeneric().AsAccessible()) {
|
||||
rect = acc->Bounds();
|
||||
} else {
|
||||
rect = IntlGeneric().AsProxy()->Bounds();
|
||||
}
|
||||
|
||||
*aX = rect.x;
|
||||
*aY = rect.y;
|
||||
*aWidth = rect.width;
|
||||
|
||||
@@ -407,6 +407,8 @@ XULMenupopupAccessible::
|
||||
mSelectControl = do_QueryInterface(mContent->GetFlattenedTreeParent());
|
||||
if (!mSelectControl)
|
||||
mGenericTypes &= ~eSelect;
|
||||
|
||||
mStateFlags |= eNoXBLKids;
|
||||
}
|
||||
|
||||
uint64_t
|
||||
|
||||
@@ -4,15 +4,6 @@
|
||||
|
||||
TESTADDONS = source/test/addons
|
||||
ADDONSRC = $(srcdir)/$(TESTADDONS)
|
||||
TESTROOT = $(CURDIR)/$(DEPTH)/_tests/testing/mochitest/jetpack-addon/$(relativesrcdir)/$(TESTADDONS)
|
||||
|
||||
# Build a list of the test add-ons
|
||||
ADDONS = $(patsubst $(ADDONSRC)/%/package.json,$(TESTADDONS)/%.xpi,$(wildcard $(ADDONSRC)/*/package.json))
|
||||
|
||||
INSTALL_TARGETS += test_addons
|
||||
test_addons_FILES = $(ADDONS)
|
||||
test_addons_DEST = $(TESTROOT)
|
||||
test_addons_TARGET := misc
|
||||
|
||||
sinclude $(topsrcdir)/config/rules.mk
|
||||
|
||||
@@ -20,8 +11,6 @@ sinclude $(topsrcdir)/config/rules.mk
|
||||
$(TESTADDONS)/%.xpi: FORCE $(call mkdir_deps,$(CURDIR)/$(TESTADDONS)) $(ADDONSRC)/%
|
||||
$(PYTHON) $(srcdir)/source/bin/cfx xpi --no-strip-xpi --pkgdir=$(lastword $^) --output-file=$@
|
||||
|
||||
#libs:: $(ADDONS)
|
||||
|
||||
TEST_FILES = \
|
||||
$(srcdir)/source/app-extension \
|
||||
$(srcdir)/source/bin \
|
||||
|
||||
@@ -16,6 +16,55 @@ BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
|
||||
JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini']
|
||||
JETPACK_ADDON_MANIFESTS += ['source/test/addons/jetpack-addon.ini']
|
||||
|
||||
addons = [
|
||||
'addon-manager',
|
||||
'author-email',
|
||||
'child_process',
|
||||
'chrome',
|
||||
'content-permissions',
|
||||
'contributors',
|
||||
'curly-id',
|
||||
'developers',
|
||||
'e10s-content',
|
||||
'e10s-l10n',
|
||||
'e10s-remote',
|
||||
'e10s-tabs',
|
||||
'e10s',
|
||||
'l10n-properties',
|
||||
'l10n',
|
||||
'layout-change',
|
||||
'main',
|
||||
'name-in-numbers-plus',
|
||||
'name-in-numbers',
|
||||
'packaging',
|
||||
'packed',
|
||||
'page-mod-debugger-post',
|
||||
'page-mod-debugger-pre',
|
||||
'page-worker',
|
||||
'places',
|
||||
'predefined-id-with-at',
|
||||
'preferences-branch',
|
||||
'private-browsing-supported',
|
||||
'remote',
|
||||
'require',
|
||||
'self',
|
||||
'simple-prefs-l10n',
|
||||
'simple-prefs-regression',
|
||||
'simple-prefs',
|
||||
'standard-id',
|
||||
'tab-close-on-startup',
|
||||
'toolkit-require-reload',
|
||||
'translators',
|
||||
'unsafe-content-script',
|
||||
]
|
||||
|
||||
addons = ['source/test/addons/%s.xpi' % f for f in addons]
|
||||
GENERATED_FILES += addons
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest['jetpack-addon']['addon-sdk'].source.test.addons += [
|
||||
'!%s' % f for f in addons
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.sdk += [
|
||||
'source/app-extension/bootstrap.js',
|
||||
]
|
||||
|
||||
@@ -9,20 +9,10 @@ module.metadata = {
|
||||
|
||||
const { isValidURI, isLocalURL, URL } = require('../url');
|
||||
const { contract } = require('../util/contract');
|
||||
const { isString, isNil, instanceOf } = require('../lang/type');
|
||||
const { isString, isNil, instanceOf, isJSONable } = require('../lang/type');
|
||||
const { validateOptions,
|
||||
string, array, object, either, required } = require('../deprecated/api-utils');
|
||||
|
||||
const isJSONable = (value) => {
|
||||
try {
|
||||
JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isValidScriptFile = (value) =>
|
||||
(isString(value) || instanceOf(value, URL)) && isLocalURL(value);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const { Sequence, seq, filter, object, pairs } = require("../util/sequence");
|
||||
// When iterated over belowe sequences items will represent
|
||||
// state of windows at the time of iteration.
|
||||
const opened = seq(function*() {
|
||||
const items = windows("navigator:browser", {includePrivates: true});
|
||||
const items = windows("navigator:browser", {includePrivate: true});
|
||||
for (let item of items) {
|
||||
yield [getOuterId(item), item];
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ exports.open = function open(filename, mode) {
|
||||
var openFlags = OPEN_FLAGS.WRONLY |
|
||||
OPEN_FLAGS.CREATE_FILE |
|
||||
OPEN_FLAGS.TRUNCATE;
|
||||
var permFlags = parseInt("0644", 8); // u+rw go+r
|
||||
var permFlags = 0o644; // u+rw go+r
|
||||
try {
|
||||
stream.init(file, openFlags, permFlags, 0);
|
||||
}
|
||||
@@ -178,7 +178,7 @@ exports.remove = function remove(path) {
|
||||
exports.mkpath = function mkpath(path) {
|
||||
var file = MozFile(path);
|
||||
if (!file.exists())
|
||||
file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); // u+rwx go+rx
|
||||
file.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); // u+rwx go+rx
|
||||
else if (!file.isDirectory())
|
||||
throw new Error("The path already exists and is not a directory: " + path);
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ const { REOPEN_ON_REWIND, DEFER_OPEN } = Ci.nsIFileInputStream;
|
||||
const { DIRECTORY_TYPE, NORMAL_FILE_TYPE } = Ci.nsIFile;
|
||||
const { NS_SEEK_SET, NS_SEEK_CUR, NS_SEEK_END } = Ci.nsISeekableStream;
|
||||
|
||||
const FILE_PERMISSION = parseInt("0666", 8);
|
||||
const FILE_PERMISSION = 0o666;
|
||||
const PR_UINT32_MAX = 0xfffffff;
|
||||
// Values taken from:
|
||||
// http://mxr.mozilla.org/mozilla-central/source/nsprpub/pr/include/prio.h#615
|
||||
|
||||
@@ -215,6 +215,20 @@ exports.isJSON = function (value) {
|
||||
return isJSON(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns `true` if `value` is JSONable
|
||||
*/
|
||||
const isJSONable = (value) => {
|
||||
try {
|
||||
JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
exports.isJSONable = isJSONable;
|
||||
|
||||
/**
|
||||
* Returns if `value` is an instance of a given `Type`. This is exactly same as
|
||||
* `value instanceof Type` with a difference that `Type` can be from a scope
|
||||
|
||||
@@ -23,6 +23,7 @@ const { curry, flip } = require("../../lang/functional");
|
||||
const { patch, diff } = require("diffpatcher/index");
|
||||
const prefs = require("../../preferences/service");
|
||||
const { getByOuterId } = require("../../window/utils");
|
||||
const { ignoreWindow } = require('../../private-browsing/utils');
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const PREF_ROOT = "extensions.sdk-toolbar-collapsed.";
|
||||
@@ -85,7 +86,10 @@ const attributesChanged = mutations => {
|
||||
// Utility function creates `toolbar` with a "close" button and returns
|
||||
// it back. In addition it set's up a listener and observer to communicate
|
||||
// state changes.
|
||||
const addView = curry((options, {document}) => {
|
||||
const addView = curry((options, {document, window}) => {
|
||||
if (ignoreWindow(window))
|
||||
return;
|
||||
|
||||
let view = document.createElementNS(XUL_NS, "toolbar");
|
||||
view.setAttribute("id", options.id);
|
||||
view.setAttribute("collapsed", options.collapsed);
|
||||
|
||||
@@ -2046,7 +2046,7 @@ function maybeAddHeaders(file, metadata, response)
|
||||
return;
|
||||
|
||||
const PR_RDONLY = 0x01;
|
||||
var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(headerFile, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
try
|
||||
@@ -2572,7 +2572,7 @@ ServerHandler.prototype =
|
||||
var type = this._getTypeFromFile(file);
|
||||
if (type === SJS_TYPE)
|
||||
{
|
||||
var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(file, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
try
|
||||
@@ -2666,7 +2666,7 @@ ServerHandler.prototype =
|
||||
maybeAddHeaders(file, metadata, response);
|
||||
response.setHeader("Content-Length", "" + count, false);
|
||||
|
||||
var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(file, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
offset = offset || 0;
|
||||
|
||||
@@ -2047,7 +2047,7 @@ function maybeAddHeaders(file, metadata, response)
|
||||
return;
|
||||
|
||||
const PR_RDONLY = 0x01;
|
||||
var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(headerFile, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
try
|
||||
@@ -2573,7 +2573,7 @@ ServerHandler.prototype =
|
||||
var type = this._getTypeFromFile(file);
|
||||
if (type === SJS_TYPE)
|
||||
{
|
||||
var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(file, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
try
|
||||
@@ -2667,7 +2667,7 @@ ServerHandler.prototype =
|
||||
maybeAddHeaders(file, metadata, response);
|
||||
response.setHeader("Content-Length", "" + count, false);
|
||||
|
||||
var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(file, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
offset = offset || 0;
|
||||
|
||||
@@ -2046,7 +2046,7 @@ function maybeAddHeaders(file, metadata, response)
|
||||
return;
|
||||
|
||||
const PR_RDONLY = 0x01;
|
||||
var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(headerFile, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
try
|
||||
@@ -2572,7 +2572,7 @@ ServerHandler.prototype =
|
||||
var type = this._getTypeFromFile(file);
|
||||
if (type === SJS_TYPE)
|
||||
{
|
||||
var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(file, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
try
|
||||
@@ -2666,7 +2666,7 @@ ServerHandler.prototype =
|
||||
maybeAddHeaders(file, metadata, response);
|
||||
response.setHeader("Content-Length", "" + count, false);
|
||||
|
||||
var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(file, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
offset = offset || 0;
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ function makeExecutable (name) {
|
||||
let { CC } = require('chrome');
|
||||
let nsILocalFile = CC('@mozilla.org/file/local;1', 'nsILocalFile', 'initWithPath');
|
||||
let file = nsILocalFile(name);
|
||||
file.permissions = parseInt('0777', 8);
|
||||
file.permissions = 0o777;
|
||||
}
|
||||
|
||||
function deleteFile (name) {
|
||||
|
||||
@@ -2047,7 +2047,7 @@ function maybeAddHeaders(file, metadata, response)
|
||||
return;
|
||||
|
||||
const PR_RDONLY = 0x01;
|
||||
var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(headerFile, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
try
|
||||
@@ -2573,7 +2573,7 @@ ServerHandler.prototype =
|
||||
var type = this._getTypeFromFile(file);
|
||||
if (type === SJS_TYPE)
|
||||
{
|
||||
var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(file, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
try
|
||||
@@ -2667,7 +2667,7 @@ ServerHandler.prototype =
|
||||
maybeAddHeaders(file, metadata, response);
|
||||
response.setHeader("Content-Length", "" + count, false);
|
||||
|
||||
var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
|
||||
var fis = new FileInputStream(file, PR_RDONLY, 0o444,
|
||||
Ci.nsIFileInputStream.CLOSE_ON_EOF);
|
||||
|
||||
offset = offset || 0;
|
||||
|
||||
@@ -510,12 +510,12 @@ exports["test fs.chmod"] = function (assert, done) {
|
||||
testPerm("0755")()
|
||||
.then(testPerm("0777"))
|
||||
.then(testPerm("0666"))
|
||||
.then(testPerm(parseInt("0511", 8)))
|
||||
.then(testPerm(parseInt("0200", 8)))
|
||||
.then(testPerm(0o511))
|
||||
.then(testPerm(0o200))
|
||||
.then(testPerm("0040"))
|
||||
.then(testPerm("0000"))
|
||||
.then(testPermSync(parseInt("0777", 8)))
|
||||
.then(testPermSync(parseInt("0666", 8)))
|
||||
.then(testPermSync(0o777))
|
||||
.then(testPermSync(0o666))
|
||||
.then(testPermSync("0511"))
|
||||
.then(testPermSync("0200"))
|
||||
.then(testPermSync("0040"))
|
||||
@@ -524,20 +524,20 @@ exports["test fs.chmod"] = function (assert, done) {
|
||||
assert.pass("Successful chmod passes");
|
||||
}, assert.fail)
|
||||
// Test invalid paths
|
||||
.then(() => chmod("not-a-valid-file", parseInt("0755", 8)))
|
||||
.then(() => chmod("not-a-valid-file", 0o755))
|
||||
.then(assert.fail, (err) => {
|
||||
checkPermError(err, "not-a-valid-file");
|
||||
})
|
||||
.then(() => chmod("not-a-valid-file", parseInt("0755", 8), "sync"))
|
||||
.then(() => chmod("not-a-valid-file", 0o755, "sync"))
|
||||
.then(assert.fail, (err) => {
|
||||
checkPermError(err, "not-a-valid-file");
|
||||
})
|
||||
// Test invalid files
|
||||
.then(() => chmod("resource://not-a-real-file", parseInt("0755", 8)))
|
||||
.then(() => chmod("resource://not-a-real-file", 0o755))
|
||||
.then(assert.fail, (err) => {
|
||||
checkPermError(err, "resource://not-a-real-file");
|
||||
})
|
||||
.then(() => chmod("resource://not-a-real-file", parseInt("0755", 8), 'sync'))
|
||||
.then(() => chmod("resource://not-a-real-file", 0o755, 'sync'))
|
||||
.then(assert.fail, (err) => {
|
||||
checkPermError(err, "resource://not-a-real-file");
|
||||
})
|
||||
@@ -603,8 +603,8 @@ exports["test fs.chmod"] = function (assert, done) {
|
||||
if (!isWindows)
|
||||
return mode;
|
||||
|
||||
var ANY_READ = parseInt("0444", 8);
|
||||
var ANY_WRITE = parseInt("0222", 8);
|
||||
var ANY_READ = 0o444;
|
||||
var ANY_WRITE = 0o222;
|
||||
var winMode = 0;
|
||||
|
||||
// On Windows, if WRITE is true, then READ is also true
|
||||
|
||||
@@ -101,6 +101,27 @@ exports["test json atoms"] = function (assert) {
|
||||
assert.ok(utils.isJSON("foo bar"), "strings are JSON");
|
||||
};
|
||||
|
||||
exports["test jsonable values"] = function (assert) {
|
||||
assert.ok(utils.isJSONable(null), "`null` is JSONable");
|
||||
assert.ok(!utils.isJSONable(undefined), "`undefined` is not JSONable");
|
||||
assert.ok(utils.isJSONable(NaN), "`NaN` is JSONable");
|
||||
assert.ok(utils.isJSONable(Infinity), "`Infinity` is JSONable");
|
||||
assert.ok(utils.isJSONable(true) && utils.isJSONable(false), "booleans are JSONable");
|
||||
assert.ok(utils.isJSONable(0), "numbers are JSONable");
|
||||
assert.ok(utils.isJSONable("foo bar"), "strings are JSONable");
|
||||
assert.ok(!utils.isJSONable(function(){}), "functions are not JSONable");
|
||||
|
||||
const functionWithToJSON = function(){};
|
||||
functionWithToJSON.toJSON = function() { return "foo bar"; };
|
||||
assert.ok(utils.isJSONable(functionWithToJSON), "functions with toJSON() are JSONable");
|
||||
|
||||
assert.ok(utils.isJSONable({}), "`{}` is JSONable");
|
||||
|
||||
const foo = {};
|
||||
foo.bar = foo;
|
||||
assert.ok(!utils.isJSONable(foo), "recursive objects are not JSONable");
|
||||
};
|
||||
|
||||
exports["test instanceOf"] = function (assert) {
|
||||
assert.ok(utils.instanceOf(assert, Object),
|
||||
"assert is object from other sandbox");
|
||||
|
||||
@@ -492,4 +492,20 @@ exports["test button are attached to toolbar"] = function*(assert) {
|
||||
yield cleanUI();
|
||||
};
|
||||
|
||||
exports["test toolbar are not in private windows"] = function*(assert) {
|
||||
const w = open(null, {features: {toolbar: true, private: true}});
|
||||
|
||||
yield ready(w);
|
||||
|
||||
const t = new Toolbar({title: "foo"});
|
||||
|
||||
yield wait(t, "attach");
|
||||
|
||||
assert.ok(!isAttached(t), "toolbar wasn't actually attached");
|
||||
|
||||
t.destroy();
|
||||
|
||||
yield cleanUI();
|
||||
}
|
||||
|
||||
require("sdk/test").run(module.exports);
|
||||
|
||||
@@ -251,7 +251,6 @@
|
||||
@RESPATH@/components/filepicker.xpt
|
||||
#endif
|
||||
@RESPATH@/components/find.xpt
|
||||
@RESPATH@/components/fuel.xpt
|
||||
@RESPATH@/components/gfx.xpt
|
||||
@RESPATH@/components/hal.xpt
|
||||
@RESPATH@/components/html5.xpt
|
||||
|
||||
@@ -31,7 +31,7 @@ externalProtocolChkMsg=Remember my choice for all links of this type.
|
||||
externalProtocolLaunchBtn=Launch application
|
||||
malwareBlocked=The site at %S has been reported as an attack site and has been blocked based on your security preferences.
|
||||
unwantedBlocked=The site at %S has been reported as serving unwanted software and has been blocked based on your security preferences.
|
||||
phishingBlocked=The website at %S has been reported as a web forgery designed to trick users into sharing personal or financial information.
|
||||
deceptiveBlocked=This web page at %S has been reported as a deceptive site and has been blocked based on your security preferences.
|
||||
forbiddenBlocked=The site at %S has been blocked by your browser configuration.
|
||||
cspBlocked=This page has a content security policy that prevents it from being loaded in this way.
|
||||
corruptedContentError=The page you are trying to view cannot be shown because an error in the data transmission was detected.
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
case "malwareBlocked" :
|
||||
error = "malware";
|
||||
break;
|
||||
case "phishingBlocked" :
|
||||
case "deceptiveBlocked" :
|
||||
error = "phishing";
|
||||
break;
|
||||
case "unwantedBlocked" :
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
<!-- Error Title -->
|
||||
<div id="errorTitle">
|
||||
<h1 id="errorTitleText_phishing">&safeb.blocked.phishingPage.title;</h1>
|
||||
<h1 id="errorTitleText_phishing">&safeb.blocked.phishingPage.title2;</h1>
|
||||
<h1 id="errorTitleText_malware">&safeb.blocked.malwarePage.title;</h1>
|
||||
<h1 id="errorTitleText_unwanted">&safeb.blocked.unwantedPage.title;</h1>
|
||||
<h1 id="errorTitleText_forbidden">&safeb.blocked.forbiddenPage.title2;</h1>
|
||||
@@ -186,7 +186,7 @@
|
||||
|
||||
<!-- Short Description -->
|
||||
<div id="errorShortDesc">
|
||||
<p id="errorShortDescText_phishing">&safeb.blocked.phishingPage.shortDesc;</p>
|
||||
<p id="errorShortDescText_phishing">&safeb.blocked.phishingPage.shortDesc2;</p>
|
||||
<p id="errorShortDescText_malware">&safeb.blocked.malwarePage.shortDesc;</p>
|
||||
<p id="errorShortDescText_unwanted">&safeb.blocked.unwantedPage.shortDesc;</p>
|
||||
<p id="errorShortDescText_forbidden">&safeb.blocked.forbiddenPage.shortDesc2;</p>
|
||||
@@ -194,7 +194,7 @@
|
||||
|
||||
<!-- Long Description -->
|
||||
<div id="errorLongDesc">
|
||||
<p id="errorLongDescText_phishing">&safeb.blocked.phishingPage.longDesc;</p>
|
||||
<p id="errorLongDescText_phishing">&safeb.blocked.phishingPage.longDesc2;</p>
|
||||
<p id="errorLongDescText_malware">&safeb.blocked.malwarePage.longDesc;</p>
|
||||
<p id="errorLongDescText_unwanted">&safeb.blocked.unwantedPage.longDesc;</p>
|
||||
</div>
|
||||
|
||||
@@ -1080,7 +1080,7 @@ var PlacesToolbarHelper = {
|
||||
|
||||
// CustomizableUI.addListener is idempotent, so we can safely
|
||||
// call this multiple times.
|
||||
//FIXME CustomizableUI.addListener(this);
|
||||
CustomizableUI.addListener(this);
|
||||
|
||||
// If the bookmarks toolbar item is:
|
||||
// - not in a toolbar, or;
|
||||
@@ -1297,8 +1297,7 @@ var BookmarkingUI = {
|
||||
*/
|
||||
_currentAreaType: null,
|
||||
_shouldUpdateStarState: function() {
|
||||
// return this._currentAreaType == CustomizableUI.TYPE_TOOLBAR;
|
||||
return;
|
||||
return this._currentAreaType == CustomizableUI.TYPE_TOOLBAR;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ var gSafeBrowsing = {
|
||||
// will point to the internal error page we loaded instead.
|
||||
var docURI = gBrowser.selectedBrowser.documentURI;
|
||||
var isPhishingPage =
|
||||
docURI && docURI.spec.startsWith("about:blocked?e=phishingBlocked");
|
||||
docURI && docURI.spec.startsWith("about:blocked?e=deceptiveBlocked");
|
||||
|
||||
// Show/hide the appropriate menu item.
|
||||
document.getElementById("menu_HelpPopup_reportPhishingtoolmenu")
|
||||
|
||||
@@ -1285,6 +1285,10 @@ var gBrowserInit = {
|
||||
// We do this before the session restore service gets initialized so we can
|
||||
// apply full zoom settings to tabs restored by the session restore service.
|
||||
FullZoom.init();
|
||||
PanelUI.init();
|
||||
LightweightThemeListener.init();
|
||||
|
||||
Services.telemetry.getHistogramById("E10S_WINDOW").add(gMultiProcessBrowser);
|
||||
|
||||
SidebarUI.startDelayedLoad();
|
||||
|
||||
@@ -1612,6 +1616,8 @@ var gBrowserInit = {
|
||||
|
||||
BrowserOffline.uninit();
|
||||
IndexedDBPromptHelper.uninit();
|
||||
LightweightThemeListener.uninit();
|
||||
PanelUI.uninit();
|
||||
AddonManager.removeAddonListener(AddonsMgrListener);
|
||||
}
|
||||
|
||||
@@ -2974,10 +2980,10 @@ var BrowserOnClick = {
|
||||
}
|
||||
};
|
||||
} else if (reason === 'phishing') {
|
||||
title = gNavigatorBundle.getString("safebrowsing.reportedWebForgery");
|
||||
title = gNavigatorBundle.getString("safebrowsing.deceptiveSite");
|
||||
buttons[1] = {
|
||||
label: gNavigatorBundle.getString("safebrowsing.notAForgeryButton.label"),
|
||||
accessKey: gNavigatorBundle.getString("safebrowsing.notAForgeryButton.accessKey"),
|
||||
label: gNavigatorBundle.getString("safebrowsing.notADeceptiveSiteButton.label"),
|
||||
accessKey: gNavigatorBundle.getString("safebrowsing.notADeceptiveSiteButton.accessKey"),
|
||||
callback: function() {
|
||||
openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab');
|
||||
}
|
||||
|
||||
@@ -781,6 +781,17 @@
|
||||
</menupopup>
|
||||
</toolbarbutton>
|
||||
|
||||
<toolbaritem id="PanelUI-button"
|
||||
hidden="true"
|
||||
class="chromeclass-toolbar-additional"
|
||||
removable="false">
|
||||
<toolbarbutton id="PanelUI-menu-button"
|
||||
class="toolbarbutton-1 badged-button"
|
||||
consumeanchor="PanelUI-button"
|
||||
label="&brandShortName;"
|
||||
tooltiptext="&appmenu.tooltip;"/>
|
||||
</toolbaritem>
|
||||
|
||||
<hbox id="window-controls" hidden="true" pack="end">
|
||||
<toolbarbutton id="minimize-button"
|
||||
tooltiptext="&fullScreenMinimize.tooltip;"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/places/browserPlacesViews.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/browser.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/customizableui/panelUI.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/downloads/downloads.js"/>
|
||||
<script type="application/javascript" src="chrome://browser/content/downloads/indicator.js"/>
|
||||
<script type="application/javascript" src="chrome://global/content/inlineSpellCheckUI.js"/>
|
||||
|
||||
@@ -18,15 +18,15 @@
|
||||
</broadcasterset>
|
||||
<menupopup id="menu_HelpPopup">
|
||||
<menuitem id="menu_HelpPopup_reportPhishingtoolmenu"
|
||||
label="&reportPhishSiteMenu.title2;"
|
||||
accesskey="&reportPhishSiteMenu.accesskey;"
|
||||
label="&reportDeceptiveSiteMenu.title;"
|
||||
accesskey="&reportDeceptiveSiteMenu.accesskey;"
|
||||
insertbefore="aboutSeparator"
|
||||
observes="reportPhishingBroadcaster"
|
||||
oncommand="openUILink(gSafeBrowsing.getReportURL('Phish'), event);"
|
||||
onclick="checkForMiddleClick(this, event);"/>
|
||||
<menuitem id="menu_HelpPopup_reportPhishingErrortoolmenu"
|
||||
label="&safeb.palm.notforgery.label2;"
|
||||
accesskey="&reportPhishSiteMenu.accesskey;"
|
||||
label="&safeb.palm.notdeceptive.label;"
|
||||
accesskey="&reportDeceptiveSiteMenu.accesskey;"
|
||||
insertbefore="aboutSeparator"
|
||||
observes="reportPhishingErrorBroadcaster"
|
||||
oncommand="openUILinkIn(gSafeBrowsing.getReportURL('Error'), 'tab');"
|
||||
|
||||
@@ -1053,6 +1053,11 @@
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<!-- Holds a unique ID for the tab change that's currently being timed.
|
||||
Used to make sure that multiple, rapid tab switches do not try to
|
||||
create overlapping timers. -->
|
||||
<field name="_tabSwitchID">null</field>
|
||||
|
||||
<method name="updateCurrentBrowser">
|
||||
<parameter name="aForceUpdate"/>
|
||||
<body>
|
||||
@@ -1063,11 +1068,28 @@
|
||||
|
||||
if (!aForceUpdate) {
|
||||
//TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
|
||||
if (!Services.appinfo.browserTabsRemoteAutostart) {
|
||||
// old way of measuring tab paint which is not
|
||||
// valid with e10s.
|
||||
window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
|
||||
.beginTabSwitch();
|
||||
if (!gMultiProcessBrowser) {
|
||||
// old way of measuring tab paint which is not valid with e10s.
|
||||
// Waiting until the next MozAfterPaint ensures that we capture
|
||||
// the time it takes to paint, upload the textures to the compositor,
|
||||
// and then composite.
|
||||
if (this._tabSwitchID) {
|
||||
//TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_MS");
|
||||
}
|
||||
|
||||
let tabSwitchID = Symbol();
|
||||
|
||||
//TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_MS");
|
||||
this._tabSwitchID = tabSwitchID;
|
||||
|
||||
let onMozAfterPaint = () => {
|
||||
if (this._tabSwitchID === tabSwitchID) {
|
||||
TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_MS");
|
||||
this._tabSwitchID = null;
|
||||
}
|
||||
window.removeEventListener("MozAfterPaint", onMozAfterPaint);
|
||||
}
|
||||
window.addEventListener("MozAfterPaint", onMozAfterPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1150,7 +1172,7 @@
|
||||
|
||||
this.mCurrentTab.lastAccessed = Infinity;
|
||||
this.mCurrentTab.removeAttribute("unread");
|
||||
this.selectedTab.lastAccessed = Date.now();
|
||||
oldTab.lastAccessed = Date.now();
|
||||
|
||||
let oldFindBar = oldTab._findBar;
|
||||
if (oldFindBar &&
|
||||
@@ -1590,7 +1612,7 @@
|
||||
// Unhook our progress listener.
|
||||
let tab = this.getTabForBrowser(aBrowser);
|
||||
let filter = this._tabFilters.get(tab);
|
||||
let listener = this._tabListeners.get(tab);
|
||||
let listener = this._tabListeners.get(tab);
|
||||
aBrowser.webProgress.removeProgressListener(filter);
|
||||
filter.removeProgressListener(listener);
|
||||
|
||||
@@ -1830,6 +1852,7 @@
|
||||
var notificationbox = document.createElementNS(NS_XUL,
|
||||
"notificationbox");
|
||||
notificationbox.setAttribute("flex", "1");
|
||||
notificationbox.setAttribute("notificationside", "top");
|
||||
notificationbox.appendChild(browserSidebarContainer);
|
||||
|
||||
// Prevent the superfluous initial load of a blank document
|
||||
@@ -6263,7 +6286,6 @@
|
||||
<field name="_overPlayingIcon">false</field>
|
||||
<field name="mCorrespondingMenuitem">null</field>
|
||||
<field name="closing">false</field>
|
||||
<field name="lastAccessed">0</field>
|
||||
|
||||
<method name="_mouseenter">
|
||||
<body><![CDATA[
|
||||
@@ -6322,9 +6344,11 @@
|
||||
if (browser.audioMuted) {
|
||||
browser.unmute();
|
||||
this.removeAttribute("muted");
|
||||
BrowserUITelemetry.countTabMutingEvent("unmute", aMuteReason);
|
||||
} else {
|
||||
browser.mute();
|
||||
this.setAttribute("muted", "true");
|
||||
BrowserUITelemetry.countTabMutingEvent("mute", aMuteReason);
|
||||
}
|
||||
this.muteReason = aMuteReason || null;
|
||||
tabContainer.tabbrowser._tabAttrModified(this, ["muted"]);
|
||||
@@ -6336,8 +6360,13 @@
|
||||
<parameter name="aUserContextId"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.linkedBrowser.setAttribute("usercontextid", aUserContextId);
|
||||
this.setAttribute("usercontextid", aUserContextId);
|
||||
if (aUserContextId) {
|
||||
this.linkedBrowser.setAttribute("usercontextid", aUserContextId);
|
||||
this.setAttribute("usercontextid", aUserContextId);
|
||||
} else {
|
||||
this.linkedBrowser.removeAttribute("usercontextid");
|
||||
this.removeAttribute("usercontextid");
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// gBrowser.selectedTab.lastAccessed and Date.now() called from this test can't
|
||||
// run concurrently, and therefore don't always match exactly.
|
||||
const CURRENT_TIME_TOLERANCE_MS = 15;
|
||||
|
||||
function isCurrent(tab, msg) {
|
||||
const DIFF = Math.abs(Date.now() - tab.lastAccessed);
|
||||
ok(DIFF <= CURRENT_TIME_TOLERANCE_MS, msg + " (difference: " + DIFF + ")");
|
||||
}
|
||||
|
||||
function nextStep(fn) {
|
||||
setTimeout(fn, CURRENT_TIME_TOLERANCE_MS + 10);
|
||||
}
|
||||
|
||||
var originalTab;
|
||||
var newTab;
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
originalTab = gBrowser.selectedTab;
|
||||
nextStep(step2);
|
||||
}
|
||||
|
||||
function step2() {
|
||||
isCurrent(originalTab, "selected tab has the current timestamp");
|
||||
newTab = gBrowser.addTab("about:blank", {skipAnimation: true});
|
||||
nextStep(step3);
|
||||
}
|
||||
|
||||
function step3() {
|
||||
ok(newTab.lastAccessed < Date.now(), "new tab hasn't been selected so far");
|
||||
gBrowser.selectedTab = newTab;
|
||||
isCurrent(newTab, "new tab has the current timestamp after being selected");
|
||||
nextStep(step4);
|
||||
}
|
||||
|
||||
function step4() {
|
||||
ok(originalTab.lastAccessed < Date.now(),
|
||||
"original tab has old timestamp after being deselected");
|
||||
isCurrent(newTab, "new tab has the current timestamp since it's still selected");
|
||||
|
||||
gBrowser.removeTab(newTab);
|
||||
finish();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
registerCleanupFunction(function* cleanup() {
|
||||
while (gBrowser.tabs.length > 1) {
|
||||
yield BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
|
||||
}
|
||||
Services.search.currentEngine = originalEngine;
|
||||
let engine = Services.search.getEngineByName("MozSearch");
|
||||
Services.search.removeEngine(engine);
|
||||
});
|
||||
|
||||
let originalEngine;
|
||||
add_task(function* test_setup() {
|
||||
// Stop search-engine loads from hitting the network
|
||||
Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
|
||||
"http://example.com/?q={searchTerms}");
|
||||
let engine = Services.search.getEngineByName("MozSearch");
|
||||
originalEngine = Services.search.currentEngine;
|
||||
Services.search.currentEngine = engine;
|
||||
});
|
||||
|
||||
add_task(function*() { yield drop("mochi.test/first", true); });
|
||||
add_task(function*() { yield drop("javascript:'bad'"); });
|
||||
add_task(function*() { yield drop("jAvascript:'bad'"); });
|
||||
add_task(function*() { yield drop("search this", true); });
|
||||
add_task(function*() { yield drop("mochi.test/second", true); });
|
||||
add_task(function*() { yield drop("data:text/html,bad"); });
|
||||
add_task(function*() { yield drop("mochi.test/third", true); });
|
||||
|
||||
function* drop(text, valid = false) {
|
||||
info(`Starting test for text:${text}; valid:${valid}`);
|
||||
let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
|
||||
getService(Ci.mozIJSSubScriptLoader);
|
||||
let ChromeUtils = {};
|
||||
scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils);
|
||||
|
||||
let awaitDrop = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "drop");
|
||||
let awaitTabOpen = valid && BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
|
||||
// A drop type of "link" onto an existing tab would normally trigger a
|
||||
// load in that same tab, but tabbrowser code in _getDragTargetTab treats
|
||||
// drops on the outer edges of a tab differently (loading a new tab
|
||||
// instead). Make events created by synthesizeDrop have all of their
|
||||
// coordinates set to 0 (screenX/screenY), so they're treated as drops
|
||||
// on the outer edge of the tab, thus they open new tabs.
|
||||
var event = {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
screenX: 0,
|
||||
screenY: 0,
|
||||
};
|
||||
ChromeUtils.synthesizeDrop(gBrowser.selectedTab, gBrowser.selectedTab, [[{type: "text/plain", data: text}]], "link", window, undefined, event);
|
||||
let tabOpened = false;
|
||||
if (awaitTabOpen) {
|
||||
let tabOpenEvent = yield awaitTabOpen;
|
||||
info("Got TabOpen event");
|
||||
tabOpened = true;
|
||||
yield BrowserTestUtils.removeTab(tabOpenEvent.target);
|
||||
}
|
||||
is(tabOpened, valid, `Tab for ${text} should only open if valid`);
|
||||
|
||||
yield awaitDrop;
|
||||
ok(true, "Got drop event");
|
||||
}
|
||||
@@ -71,6 +71,18 @@ function test() {
|
||||
testVal("http://localhost/ foo bar baz");
|
||||
testVal("http://localhost.localdomain/ foo bar baz", "localhost.localdomain/ foo bar baz");
|
||||
|
||||
// Behaviour for hosts with no dots depends on the whitelist:
|
||||
let fixupWhitelistPref = "browser.fixup.domainwhitelist.localhost";
|
||||
Services.prefs.setBoolPref(fixupWhitelistPref, false);
|
||||
testVal("http://localhost");
|
||||
Services.prefs.setBoolPref(fixupWhitelistPref, true);
|
||||
testVal("http://localhost", "localhost");
|
||||
Services.prefs.clearUserPref(fixupWhitelistPref);
|
||||
|
||||
testVal("http:// invalid url");
|
||||
|
||||
testVal("http://someotherhostwithnodots");
|
||||
|
||||
Services.prefs.setBoolPref(prefname, false);
|
||||
|
||||
testVal("http://mozilla.org/");
|
||||
|
||||
@@ -18,7 +18,6 @@ var manifestUpgrade = { // used for testing install
|
||||
name: "provider 3",
|
||||
origin: "https://test2.example.com",
|
||||
sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html",
|
||||
workerURL: "https://test2.example.com/browser/browser/base/content/test/social/social_worker.js",
|
||||
iconURL: "https://test2.example.com/browser/browser/base/content/test/general/moz.png",
|
||||
version: "1.0"
|
||||
};
|
||||
@@ -211,72 +210,5 @@ var tests = {
|
||||
Social.uninstallProvider(addonManifest.origin);
|
||||
});
|
||||
});
|
||||
},
|
||||
testUpgradeProviderFromWorker: function(next) {
|
||||
// add the provider, change the pref, add it again. The provider at that
|
||||
// point should be upgraded
|
||||
let activationURL = manifestUpgrade.origin + "/browser/browser/base/content/test/social/social_activate.html"
|
||||
ensureEventFired(PopupNotifications.panel, "popupshown").then(() => {
|
||||
let panel = document.getElementById("servicesInstall-notification");
|
||||
info("servicesInstall-notification panel opened");
|
||||
panel.button.click();
|
||||
});
|
||||
|
||||
addTab(activationURL, function(tab) {
|
||||
let doc = tab.linkedBrowser.contentDocument;
|
||||
let installFrom = doc.nodePrincipal.origin;
|
||||
Services.prefs.setCharPref("social.whitelist", installFrom);
|
||||
let data = {
|
||||
origin: installFrom,
|
||||
url: doc.location.href,
|
||||
manifest: manifestUpgrade,
|
||||
window: window
|
||||
}
|
||||
Social.installProvider(data, function(addonManifest) {
|
||||
SocialService.enableProvider(addonManifest.origin, function(provider) {
|
||||
is(provider.manifest.version, 1, "manifest version is 1");
|
||||
|
||||
// watch for the provider-update and test the new version
|
||||
SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
|
||||
if (topic != "provider-update")
|
||||
return;
|
||||
// The worker will have reloaded and the current provider instance
|
||||
// disabled, removed from the provider list. We have a reference
|
||||
// here, check it is is disabled.
|
||||
is(provider.enabled, false, "old provider instance is disabled")
|
||||
is(origin, addonManifest.origin, "provider manifest updated")
|
||||
SocialService.unregisterProviderListener(providerListener);
|
||||
|
||||
// Get the new provider instance, fetch the manifest via workerapi
|
||||
// and validate that data as well.
|
||||
let p = Social._getProviderFromOrigin(origin);
|
||||
is(p.manifest.version, 2, "manifest version is 2");
|
||||
let port = p.getWorkerPort();
|
||||
ok(port, "got a new port");
|
||||
port.onmessage = function (e) {
|
||||
let topic = e.data.topic;
|
||||
switch (topic) {
|
||||
case "social.manifest":
|
||||
let manifest = e.data.data;
|
||||
is(manifest.version, 2, "manifest version is 2");
|
||||
port.close();
|
||||
Social.uninstallProvider(origin, function() {
|
||||
Services.prefs.clearUserPref("social.whitelist");
|
||||
ensureBrowserTabClosed(tab).then(next);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
port.postMessage({topic: "test-init"});
|
||||
port.postMessage({topic: "manifest-get"});
|
||||
|
||||
});
|
||||
|
||||
let port = provider.getWorkerPort();
|
||||
port.postMessage({topic: "worker.update", data: true});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
[browser_moz_action_link.js]
|
||||
[browser_urlbarHashChangeProxyState.js]
|
||||
[browser_urlbarKeepStateAcrossTabSwitches.js]
|
||||
[browser_urlbarUpdateForDomainCompletion.js]
|
||||
[browser_urlbar_blanking.js]
|
||||
support-files =
|
||||
file_blank_but_not_blank.html
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Check that navigating through both the URL bar and using in-page hash- or ref-
|
||||
* based links and back or forward navigation updates the URL bar and identity block correctly.
|
||||
*/
|
||||
add_task(function* () {
|
||||
let baseURL = "https://example.org/browser/browser/base/content/test/urlbar/dummy_page.html";
|
||||
let url = baseURL + "#foo";
|
||||
yield BrowserTestUtils.withNewTab({ gBrowser, url }, function(browser) {
|
||||
let identityBox = document.getElementById("identity-box");
|
||||
let expectedURL = url;
|
||||
|
||||
let verifyURLBarState = testType => {
|
||||
is(gURLBar.textValue, expectedURL, "URL bar visible value should be correct " + testType);
|
||||
is(gURLBar.value, expectedURL, "URL bar value should be correct " + testType);
|
||||
ok(identityBox.classList.contains("verifiedDomain"), "Identity box should know we're doing SSL " + testType);
|
||||
is(gURLBar.getAttribute("pageproxystate"), "valid", "URL bar is in valid page proxy state");
|
||||
};
|
||||
|
||||
verifyURLBarState("at the beginning");
|
||||
|
||||
let locationChangePromise;
|
||||
let resolveLocationChangePromise;
|
||||
let expectURL = url => {
|
||||
expectedURL = url;
|
||||
locationChangePromise = new Promise(r => resolveLocationChangePromise = r);
|
||||
};
|
||||
let wpl = {
|
||||
onLocationChange(wpl, request, location, flags) {
|
||||
is(location.spec, expectedURL, "Got the expected URL");
|
||||
resolveLocationChangePromise();
|
||||
},
|
||||
};
|
||||
gBrowser.addProgressListener(wpl);
|
||||
|
||||
expectURL(baseURL + "#foo");
|
||||
gURLBar.select();
|
||||
EventUtils.sendKey("return");
|
||||
|
||||
yield locationChangePromise;
|
||||
verifyURLBarState("after hitting enter on the same URL a second time");
|
||||
|
||||
expectURL(baseURL + "#bar");
|
||||
gURLBar.value = expectedURL;
|
||||
gURLBar.select();
|
||||
EventUtils.sendKey("return");
|
||||
|
||||
yield locationChangePromise;
|
||||
verifyURLBarState("after a URL bar hash navigation");
|
||||
|
||||
expectURL(baseURL + "#foo");
|
||||
yield ContentTask.spawn(browser, null, function() {
|
||||
let a = content.document.createElement("a");
|
||||
a.href = "#foo";
|
||||
a.textContent = "Foo Link";
|
||||
content.document.body.appendChild(a);
|
||||
a.click();
|
||||
});
|
||||
|
||||
yield locationChangePromise;
|
||||
verifyURLBarState("after a page link hash navigation");
|
||||
|
||||
expectURL(baseURL + "#bar");
|
||||
gBrowser.goBack();
|
||||
|
||||
yield locationChangePromise;
|
||||
verifyURLBarState("after going back");
|
||||
|
||||
expectURL(baseURL + "#foo");
|
||||
gBrowser.goForward();
|
||||
|
||||
yield locationChangePromise;
|
||||
verifyURLBarState("after going forward");
|
||||
|
||||
expectURL(baseURL + "#foo");
|
||||
gURLBar.select();
|
||||
EventUtils.sendKey("return");
|
||||
|
||||
yield locationChangePromise;
|
||||
verifyURLBarState("after hitting enter on the same URL");
|
||||
|
||||
gBrowser.removeProgressListener(wpl);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Check that initial secure loads that swap remoteness
|
||||
* get the correct page icon when finished.
|
||||
*/
|
||||
add_task(function* () {
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
|
||||
// NB: CPOW usage because new tab pages can be preloaded, in which case no
|
||||
// load events fire.
|
||||
yield BrowserTestUtils.waitForCondition(() => !tab.linkedBrowser.contentDocument.hidden)
|
||||
let url = "https://example.org/browser/browser/base/content/test/urlbar/dummy_page.html#foo";
|
||||
gURLBar.value = url;
|
||||
gURLBar.select();
|
||||
EventUtils.sendKey("return");
|
||||
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||
|
||||
is(gURLBar.textValue, url, "URL bar visible value should be correct when the page loads from about:newtab");
|
||||
is(gURLBar.value, url, "URL bar value should be correct when the page loads from about:newtab");
|
||||
let identityBox = document.getElementById("identity-box");
|
||||
ok(identityBox.classList.contains("verifiedDomain"),
|
||||
"Identity box should know we're doing SSL when the page loads from about:newtab");
|
||||
is(gURLBar.getAttribute("pageproxystate"), "valid",
|
||||
"URL bar is in valid page proxy state when SSL page with hash loads from about:newtab");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Verify user typed text remains in the URL bar when tab switching, even when
|
||||
* loads fail.
|
||||
*/
|
||||
add_task(function* () {
|
||||
let input = "i-definitely-dont-exist.example.com";
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
|
||||
// NB: CPOW usage because new tab pages can be preloaded, in which case no
|
||||
// load events fire.
|
||||
yield BrowserTestUtils.waitForCondition(() => !tab.linkedBrowser.contentDocument.hidden)
|
||||
let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
|
||||
gURLBar.value = input;
|
||||
gURLBar.select();
|
||||
EventUtils.sendKey("return");
|
||||
yield errorPageLoaded;
|
||||
is(gURLBar.textValue, input, "Text is still in URL bar");
|
||||
yield BrowserTestUtils.switchTab(gBrowser, tab.previousSibling);
|
||||
yield BrowserTestUtils.switchTab(gBrowser, tab);
|
||||
is(gURLBar.textValue, input, "Text is still in URL bar after tab switch");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
/**
|
||||
* Invalid URIs fail differently (that is, immediately, in the loadURI call)
|
||||
* if keyword searches are turned off. Test that this works, too.
|
||||
*/
|
||||
add_task(function* () {
|
||||
let input = "To be or not to be-that is the question";
|
||||
yield new Promise(resolve => SpecialPowers.pushPrefEnv({set: [["keyword.enabled", false]]}, resolve));
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
|
||||
// NB: CPOW usage because new tab pages can be preloaded, in which case no
|
||||
// load events fire.
|
||||
yield BrowserTestUtils.waitForCondition(() => !tab.linkedBrowser.contentDocument.hidden)
|
||||
let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser);
|
||||
gURLBar.value = input;
|
||||
gURLBar.select();
|
||||
EventUtils.sendKey("return");
|
||||
yield errorPageLoaded;
|
||||
is(gURLBar.textValue, input, "Text is still in URL bar");
|
||||
is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser");
|
||||
yield BrowserTestUtils.switchTab(gBrowser, tab.previousSibling);
|
||||
yield BrowserTestUtils.switchTab(gBrowser, tab);
|
||||
is(gURLBar.textValue, input, "Text is still in URL bar after tab switch");
|
||||
is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Disable keyword.enabled (so no keyword search), and check that when you type in
|
||||
* "example" and hit enter, the browser loads and the URL bar is updated accordingly.
|
||||
*/
|
||||
add_task(function* () {
|
||||
yield new Promise(resolve => SpecialPowers.pushPrefEnv({set: [["keyword.enabled", false]]}, resolve));
|
||||
yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (browser) {
|
||||
gURLBar.value = "example";
|
||||
gURLBar.select();
|
||||
let loadPromise = BrowserTestUtils.browserLoaded(browser, false, url => url == "http://www.example.com/");
|
||||
EventUtils.sendKey("return");
|
||||
yield loadPromise;
|
||||
is(gURLBar.textValue, "www.example.com");
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,7 @@ this.__defineGetter__("BROWSER_NEW_TAB_URL", () => {
|
||||
!aboutNewTabService.overridden) {
|
||||
return "about:privatebrowsing";
|
||||
}
|
||||
return "about:newtab";
|
||||
return aboutNewTabService.newTabURL;
|
||||
});
|
||||
|
||||
var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
|
||||
@@ -686,8 +686,27 @@ function openPrefsHelp() {
|
||||
function trimURL(aURL) {
|
||||
// This function must not modify the given URL such that calling
|
||||
// nsIURIFixup::createFixupURI with the result will produce a different URI.
|
||||
return aURL /* remove single trailing slash for http/https/ftp URLs */
|
||||
.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1")
|
||||
/* remove http:// unless the host starts with "ftp\d*\." or contains "@" */
|
||||
.replace(/^http:\/\/((?!ftp\d*\.)[^\/@]+(?:\/|$))/, "$1");
|
||||
|
||||
// remove single trailing slash for http/https/ftp URLs
|
||||
let url = aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1");
|
||||
|
||||
// remove http://
|
||||
if (!url.startsWith("http://")) {
|
||||
return url;
|
||||
}
|
||||
let urlWithoutProtocol = url.substring(7);
|
||||
|
||||
let flags = Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP |
|
||||
Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
|
||||
let fixedUpURL, expectedURLSpec;
|
||||
try {
|
||||
fixedUpURL = Services.uriFixup.createFixupURI(urlWithoutProtocol, flags);
|
||||
expectedURLSpec = makeURI(aURL).spec;
|
||||
} catch (ex) {
|
||||
return url;
|
||||
}
|
||||
if (fixedUpURL.spec == expectedURLSpec) {
|
||||
return urlWithoutProtocol;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ pref("@GUAO_PREF@.calendar.yahoo.com","Mozilla/5.0 (@OS_SLICE@ rv:@GK_VERSION@)
|
||||
pref("@GUAO_PREF@.google.com","Mozilla/5.0 (@OS_SLICE@ rv:52.0) Gecko/20010101 @GRE_VERSION_SLICE@ Firefox/52.0 @PM_SLICE@");
|
||||
pref("@GUAO_PREF@.googlevideos.com","Mozilla/5.0 (@OS_SLICE@ rv:52.0) Gecko/20010101 @GRE_VERSION_SLICE@ Firefox/52.0 @PM_SLICE@");
|
||||
pref("@GUAO_PREF@.gstatic.com","Mozilla/5.0 (@OS_SLICE@ rv:52.0) Gecko/20010101 @GRE_VERSION_SLICE@ Firefox/52.0 @PM_SLICE@");
|
||||
|
||||
// Google fonts serves physically different fonts to later Firefox versions that render incorrectly unless on Gecko
|
||||
pref("@GUAO_PREF@.fonts.googleapis.com", "Mozilla/5.0 (%OS_SLICE% rv:52.0) Gecko/20100101 Firefox/52.0 @PM_SLICE@"");
|
||||
pref("@GUAO_PREF@.fonts-api.wp.com", "Mozilla/5.0 (%OS_SLICE% rv:52.0) Gecko/20100101 Firefox/52.0 @PM_SLICE@"");
|
||||
|
||||
// now YouTube needs to be fooled too
|
||||
pref("@GUAO_PREF@.youtube.com","Mozilla/5.0 (Linux; U; Android 4.4.2) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30");
|
||||
// also GMail
|
||||
|
||||
@@ -19,8 +19,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
|
||||
"resource://gre/modules/DeferredTask.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
|
||||
const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties";
|
||||
return Services.strings.createBundle(kUrl);
|
||||
@@ -4104,28 +4102,26 @@ OverflowableToolbar.prototype = {
|
||||
},
|
||||
|
||||
show: function() {
|
||||
let deferred = Promise.defer();
|
||||
if (this._panel.state == "open") {
|
||||
deferred.resolve();
|
||||
return deferred.promise;
|
||||
return Promise.resolve();
|
||||
}
|
||||
let doc = this._panel.ownerDocument;
|
||||
this._panel.hidden = false;
|
||||
let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
|
||||
gELS.addSystemEventListener(contextMenu, 'command', this, true);
|
||||
let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
|
||||
this._panel.openPopup(anchor || this._chevron);
|
||||
this._chevron.open = true;
|
||||
return new Promise(resolve => {
|
||||
let doc = this._panel.ownerDocument;
|
||||
this._panel.hidden = false;
|
||||
let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
|
||||
gELS.addSystemEventListener(contextMenu, 'command', this, true);
|
||||
let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
|
||||
this._panel.openPopup(anchor || this._chevron);
|
||||
this._chevron.open = true;
|
||||
|
||||
let overflowableToolbarInstance = this;
|
||||
this._panel.addEventListener("popupshown", function onPopupShown(aEvent) {
|
||||
this.removeEventListener("popupshown", onPopupShown);
|
||||
this.addEventListener("dragover", overflowableToolbarInstance);
|
||||
this.addEventListener("dragend", overflowableToolbarInstance);
|
||||
deferred.resolve();
|
||||
let overflowableToolbarInstance = this;
|
||||
this._panel.addEventListener("popupshown", function onPopupShown(aEvent) {
|
||||
this.removeEventListener("popupshown", onPopupShown);
|
||||
this.addEventListener("dragover", overflowableToolbarInstance);
|
||||
this.addEventListener("dragend", overflowableToolbarInstance);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
_onClickChevron: function(aEvent) {
|
||||
|
||||
@@ -10,25 +10,21 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
|
||||
var gSystemScrollbarWidth = null;
|
||||
|
||||
this.ScrollbarSampler = {
|
||||
getSystemScrollbarWidth: function() {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
if (gSystemScrollbarWidth !== null) {
|
||||
deferred.resolve(gSystemScrollbarWidth);
|
||||
return deferred.promise;
|
||||
return Promise.resolve(gSystemScrollbarWidth);
|
||||
}
|
||||
|
||||
this._sampleSystemScrollbarWidth().then(function(systemScrollbarWidth) {
|
||||
gSystemScrollbarWidth = systemScrollbarWidth;
|
||||
deferred.resolve(gSystemScrollbarWidth);
|
||||
return new Promise(resolve => {
|
||||
this._sampleSystemScrollbarWidth().then(function(systemScrollbarWidth) {
|
||||
gSystemScrollbarWidth = systemScrollbarWidth;
|
||||
resolve(gSystemScrollbarWidth);
|
||||
});
|
||||
});
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
resetSystemScrollbarWidth: function() {
|
||||
@@ -36,7 +32,6 @@ this.ScrollbarSampler = {
|
||||
},
|
||||
|
||||
_sampleSystemScrollbarWidth: function() {
|
||||
let deferred = Promise.defer();
|
||||
let hwin = Services.appShell.hiddenDOMWindow;
|
||||
let hdoc = hwin.document.documentElement;
|
||||
let iframe = hwin.document.createElementNS("http://www.w3.org/1999/xhtml",
|
||||
@@ -48,23 +43,23 @@ this.ScrollbarSampler = {
|
||||
let utils = cwindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
|
||||
cwindow.addEventListener("load", function onLoad(aEvent) {
|
||||
cwindow.removeEventListener("load", onLoad);
|
||||
let sbWidth = {};
|
||||
try {
|
||||
utils.getScrollbarSize(true, sbWidth, {});
|
||||
} catch(e) {
|
||||
Cu.reportError("Could not sample scrollbar size: " + e + " -- " +
|
||||
e.stack);
|
||||
sbWidth.value = 0;
|
||||
}
|
||||
// Minimum width of 10 so that we have enough padding:
|
||||
sbWidth.value = Math.max(sbWidth.value, 10);
|
||||
deferred.resolve(sbWidth.value);
|
||||
iframe.remove();
|
||||
return new Promise(resolve => {
|
||||
cwindow.addEventListener("load", function onLoad(aEvent) {
|
||||
cwindow.removeEventListener("load", onLoad);
|
||||
let sbWidth = {};
|
||||
try {
|
||||
utils.getScrollbarSize(true, sbWidth, {});
|
||||
} catch(e) {
|
||||
Cu.reportError("Could not sample scrollbar size: " + e + " -- " +
|
||||
e.stack);
|
||||
sbWidth.value = 0;
|
||||
}
|
||||
// Minimum width of 10 so that we have enough padding:
|
||||
sbWidth.value = Math.max(sbWidth.value, 10);
|
||||
resolve(sbWidth.value);
|
||||
iframe.remove();
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
Object.freeze(this.ScrollbarSampler);
|
||||
|
||||
@@ -6,8 +6,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
|
||||
"resource:///modules/CustomizableUI.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler",
|
||||
"resource:///modules/ScrollbarSampler.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
|
||||
"resource://gre/modules/ShortcutUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
||||
@@ -127,48 +125,46 @@ const PanelUI = {
|
||||
* @param aEvent the event (if any) that triggers showing the menu.
|
||||
*/
|
||||
show: function(aEvent) {
|
||||
let deferred = Promise.defer();
|
||||
return new Promise(resolve => {
|
||||
this.ensureReady().then(() => {
|
||||
if (this.panel.state == "open" ||
|
||||
document.documentElement.hasAttribute("customizing")) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensureReady().then(() => {
|
||||
if (this.panel.state == "open" ||
|
||||
document.documentElement.hasAttribute("customizing")) {
|
||||
deferred.resolve();
|
||||
return;
|
||||
}
|
||||
let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls");
|
||||
if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) {
|
||||
updateEditUIVisibility();
|
||||
}
|
||||
|
||||
let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls");
|
||||
if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) {
|
||||
updateEditUIVisibility();
|
||||
}
|
||||
let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
|
||||
if (personalBookmarksPlacement &&
|
||||
personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) {
|
||||
PlacesToolbarHelper.customizeChange();
|
||||
}
|
||||
|
||||
let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
|
||||
if (personalBookmarksPlacement &&
|
||||
personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) {
|
||||
PlacesToolbarHelper.customizeChange();
|
||||
}
|
||||
let anchor;
|
||||
if (!aEvent ||
|
||||
aEvent.type == "command") {
|
||||
anchor = this.menuButton;
|
||||
} else {
|
||||
anchor = aEvent.target;
|
||||
}
|
||||
|
||||
let anchor;
|
||||
if (!aEvent ||
|
||||
aEvent.type == "command") {
|
||||
anchor = this.menuButton;
|
||||
} else {
|
||||
anchor = aEvent.target;
|
||||
}
|
||||
this.panel.addEventListener("popupshown", function onPopupShown() {
|
||||
this.removeEventListener("popupshown", onPopupShown);
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.panel.addEventListener("popupshown", function onPopupShown() {
|
||||
this.removeEventListener("popupshown", onPopupShown);
|
||||
deferred.resolve();
|
||||
let iconAnchor =
|
||||
document.getAnonymousElementByAttribute(anchor, "class",
|
||||
"toolbarbutton-icon");
|
||||
this.panel.openPopup(iconAnchor || anchor);
|
||||
}, (reason) => {
|
||||
console.error("Error showing the PanelUI menu", reason);
|
||||
});
|
||||
|
||||
let iconAnchor =
|
||||
document.getAnonymousElementByAttribute(anchor, "class",
|
||||
"toolbarbutton-icon");
|
||||
this.panel.openPopup(iconAnchor || anchor);
|
||||
}, (reason) => {
|
||||
console.error("Error showing the PanelUI menu", reason);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -183,6 +179,11 @@ const PanelUI = {
|
||||
},
|
||||
|
||||
handleEvent: function(aEvent) {
|
||||
// Ignore context menus and menu button menus showing and hiding:
|
||||
if (aEvent.type.startsWith("popup") &&
|
||||
aEvent.target != this.panel) {
|
||||
return;
|
||||
}
|
||||
switch (aEvent.type) {
|
||||
case "popupshowing":
|
||||
this._adjustLabelsForAutoHyphens();
|
||||
@@ -226,15 +227,15 @@ const PanelUI = {
|
||||
}
|
||||
this._readyPromise = Task.spawn(function*() {
|
||||
if (!this._initialized) {
|
||||
let delayedStartupDeferred = Promise.defer();
|
||||
let delayedStartupObserver = (aSubject, aTopic, aData) => {
|
||||
if (aSubject == window) {
|
||||
Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
|
||||
delayedStartupDeferred.resolve();
|
||||
}
|
||||
};
|
||||
Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
|
||||
yield delayedStartupDeferred.promise;
|
||||
yield new Promise(resolve => {
|
||||
let delayedStartupObserver = (aSubject, aTopic, aData) => {
|
||||
if (aSubject == window) {
|
||||
Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
|
||||
});
|
||||
}
|
||||
|
||||
this.contents.setAttributeNS("http://www.w3.org/XML/1998/namespace", "lang",
|
||||
|
||||
@@ -149,4 +149,5 @@ skip-if = os == "mac"
|
||||
[browser_1096763_seen_widgets_post_reset.js]
|
||||
[browser_1161838_inserted_new_default_buttons.js]
|
||||
[browser_bootstrapped_custom_toolbar.js]
|
||||
[browser_customizemode_contextmenu_menubuttonstate.js]
|
||||
[browser_panel_toggle.js]
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
|
||||
add_task(function*() {
|
||||
ok(!PanelUI.menuButton.hasAttribute("open"), "Menu button should not be 'pressed' outside customize mode");
|
||||
yield startCustomizing();
|
||||
|
||||
is(PanelUI.menuButton.getAttribute("open"), "true", "Menu button should be 'pressed' when in customize mode");
|
||||
|
||||
let contextMenu = document.getElementById("customizationPanelItemContextMenu");
|
||||
let shownPromise = popupShown(contextMenu);
|
||||
let newWindowButton = document.getElementById("wrapper-new-window-button");
|
||||
EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2});
|
||||
yield shownPromise;
|
||||
is(PanelUI.menuButton.getAttribute("open"), "true", "Menu button should be 'pressed' when in customize mode after opening a context menu");
|
||||
|
||||
let hiddenContextPromise = popupHidden(contextMenu);
|
||||
contextMenu.hidePopup();
|
||||
yield hiddenContextPromise;
|
||||
is(PanelUI.menuButton.getAttribute("open"), "true", "Menu button should be 'pressed' when in customize mode after hiding a context menu");
|
||||
yield endCustomizing();
|
||||
|
||||
ok(!PanelUI.menuButton.hasAttribute("open"), "Menu button should not be 'pressed' after ending customize mode");
|
||||
});
|
||||
|
||||
@@ -241,9 +241,14 @@ DistributionCustomizer.prototype = {
|
||||
}
|
||||
}),
|
||||
|
||||
_newProfile: false,
|
||||
_customizationsApplied: false,
|
||||
applyCustomizations: function DIST_applyCustomizations() {
|
||||
this._customizationsApplied = true;
|
||||
|
||||
if (!Services.prefs.prefHasUserValue("browser.migration.version"))
|
||||
this._newProfile = true;
|
||||
|
||||
if (!this._ini)
|
||||
return this._checkCustomizationComplete();
|
||||
|
||||
@@ -348,7 +353,7 @@ DistributionCustomizer.prototype = {
|
||||
try {
|
||||
let value = this._ini.getString("Preferences-" + this._locale, key);
|
||||
if (value) {
|
||||
Preferences.set(key, parseValue(value));
|
||||
defaults.set(key, parseValue(value));
|
||||
}
|
||||
usedPreferences.push(key);
|
||||
} catch (e) { /* ignore bad prefs and move on */ }
|
||||
@@ -363,7 +368,7 @@ DistributionCustomizer.prototype = {
|
||||
try {
|
||||
let value = this._ini.getString("Preferences-" + this._language, key);
|
||||
if (value) {
|
||||
Preferences.set(key, parseValue(value));
|
||||
defaults.set(key, parseValue(value));
|
||||
}
|
||||
usedPreferences.push(key);
|
||||
} catch (e) { /* ignore bad prefs and move on */ }
|
||||
@@ -380,7 +385,7 @@ DistributionCustomizer.prototype = {
|
||||
if (value) {
|
||||
value = value.replace(/%LOCALE%/g, this._locale);
|
||||
value = value.replace(/%LANGUAGE%/g, this._language);
|
||||
Preferences.set(key, parseValue(value));
|
||||
defaults.set(key, parseValue(value));
|
||||
}
|
||||
} catch (e) { /* ignore bad prefs and move on */ }
|
||||
}
|
||||
@@ -444,6 +449,25 @@ DistributionCustomizer.prototype = {
|
||||
},
|
||||
|
||||
_checkCustomizationComplete: function DIST__checkCustomizationComplete() {
|
||||
const BROWSER_DOCURL = "chrome://browser/content/browser.xul";
|
||||
|
||||
if (this._newProfile) {
|
||||
let xulStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
|
||||
|
||||
try {
|
||||
var showPersonalToolbar = Services.prefs.getBoolPref("browser.showPersonalToolbar");
|
||||
if (showPersonalToolbar) {
|
||||
xulStore.setValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed", "false");
|
||||
}
|
||||
} catch(e) {}
|
||||
try {
|
||||
var showMenubar = Services.prefs.getBoolPref("browser.showMenubar");
|
||||
if (showMenubar) {
|
||||
xulStore.setValue(BROWSER_DOCURL, "toolbar-menubar", "collapsed", "false");
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
let prefDefaultsApplied = this._prefDefaultsApplied || !this._ini;
|
||||
if (this._customizationsApplied && this._bookmarksApplied &&
|
||||
prefDefaultsApplied) {
|
||||
|
||||
@@ -1,823 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
|
||||
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
|
||||
"resource://gre/modules/Deprecated.jsm");
|
||||
|
||||
const APPLICATION_CID = Components.ID("fe74cf80-aa2d-11db-abbd-0800200c9a66");
|
||||
const APPLICATION_CONTRACTID = "@mozilla.org/fuel/application;1";
|
||||
|
||||
//=================================================
|
||||
// Singleton that holds services and utilities
|
||||
var Utilities = {
|
||||
get bookmarks() {
|
||||
let bookmarks = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||||
getService(Ci.nsINavBookmarksService);
|
||||
this.__defineGetter__("bookmarks", function() bookmarks);
|
||||
return this.bookmarks;
|
||||
},
|
||||
|
||||
get bookmarksObserver() {
|
||||
let bookmarksObserver = new BookmarksObserver();
|
||||
this.__defineGetter__("bookmarksObserver", function() bookmarksObserver);
|
||||
return this.bookmarksObserver;
|
||||
},
|
||||
|
||||
get annotations() {
|
||||
let annotations = Cc["@mozilla.org/browser/annotation-service;1"].
|
||||
getService(Ci.nsIAnnotationService);
|
||||
this.__defineGetter__("annotations", function() annotations);
|
||||
return this.annotations;
|
||||
},
|
||||
|
||||
get history() {
|
||||
let history = Cc["@mozilla.org/browser/nav-history-service;1"].
|
||||
getService(Ci.nsINavHistoryService);
|
||||
this.__defineGetter__("history", function() history);
|
||||
return this.history;
|
||||
},
|
||||
|
||||
get windowMediator() {
|
||||
let windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"].
|
||||
getService(Ci.nsIWindowMediator);
|
||||
this.__defineGetter__("windowMediator", function() windowMediator);
|
||||
return this.windowMediator;
|
||||
},
|
||||
|
||||
makeURI: function fuelutil_makeURI(aSpec) {
|
||||
if (!aSpec)
|
||||
return null;
|
||||
var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
|
||||
return ios.newURI(aSpec, null, null);
|
||||
},
|
||||
|
||||
free: function fuelutil_free() {
|
||||
delete this.bookmarks;
|
||||
delete this.bookmarksObserver;
|
||||
delete this.annotations;
|
||||
delete this.history;
|
||||
delete this.windowMediator;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//=================================================
|
||||
// Window implementation
|
||||
|
||||
var fuelWindowMap = new WeakMap();
|
||||
function getWindow(aWindow) {
|
||||
let fuelWindow = fuelWindowMap.get(aWindow);
|
||||
if (!fuelWindow) {
|
||||
fuelWindow = new Window(aWindow);
|
||||
fuelWindowMap.set(aWindow, fuelWindow);
|
||||
}
|
||||
return fuelWindow;
|
||||
}
|
||||
|
||||
// Don't call new Window() directly; use getWindow instead.
|
||||
function Window(aWindow) {
|
||||
this._window = aWindow;
|
||||
this._events = new Events();
|
||||
|
||||
this._watch("TabOpen");
|
||||
this._watch("TabMove");
|
||||
this._watch("TabClose");
|
||||
this._watch("TabSelect");
|
||||
}
|
||||
|
||||
Window.prototype = {
|
||||
get events() {
|
||||
return this._events;
|
||||
},
|
||||
|
||||
get _tabbrowser() {
|
||||
return this._window.getBrowser();
|
||||
},
|
||||
|
||||
/*
|
||||
* Helper used to setup event handlers on the XBL element. Note that the events
|
||||
* are actually dispatched to tabs, so we capture them.
|
||||
*/
|
||||
_watch: function win_watch(aType) {
|
||||
this._tabbrowser.tabContainer.addEventListener(aType, this,
|
||||
/* useCapture = */ true);
|
||||
},
|
||||
|
||||
handleEvent: function win_handleEvent(aEvent) {
|
||||
this._events.dispatch(aEvent.type, getBrowserTab(this, aEvent.originalTarget.linkedBrowser));
|
||||
},
|
||||
|
||||
get tabs() {
|
||||
var tabs = [];
|
||||
var browsers = this._tabbrowser.browsers;
|
||||
for (var i=0; i<browsers.length; i++)
|
||||
tabs.push(getBrowserTab(this, browsers[i]));
|
||||
return tabs;
|
||||
},
|
||||
|
||||
get activeTab() {
|
||||
return getBrowserTab(this, this._tabbrowser.selectedBrowser);
|
||||
},
|
||||
|
||||
open: function win_open(aURI) {
|
||||
return getBrowserTab(this, this._tabbrowser.addTab(aURI.spec).linkedBrowser);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.fuelIWindow])
|
||||
};
|
||||
|
||||
//=================================================
|
||||
// BrowserTab implementation
|
||||
|
||||
var fuelBrowserTabMap = new WeakMap();
|
||||
function getBrowserTab(aFUELWindow, aBrowser) {
|
||||
let fuelBrowserTab = fuelBrowserTabMap.get(aBrowser);
|
||||
if (!fuelBrowserTab) {
|
||||
fuelBrowserTab = new BrowserTab(aFUELWindow, aBrowser);
|
||||
fuelBrowserTabMap.set(aBrowser, fuelBrowserTab);
|
||||
}
|
||||
else {
|
||||
// This tab may have moved to another window, so make sure its cached
|
||||
// window is up-to-date.
|
||||
fuelBrowserTab._window = aFUELWindow;
|
||||
}
|
||||
|
||||
return fuelBrowserTab;
|
||||
}
|
||||
|
||||
// Don't call new BrowserTab() directly; call getBrowserTab instead.
|
||||
function BrowserTab(aFUELWindow, aBrowser) {
|
||||
this._window = aFUELWindow;
|
||||
this._browser = aBrowser;
|
||||
this._events = new Events();
|
||||
|
||||
this._watch("load");
|
||||
}
|
||||
|
||||
BrowserTab.prototype = {
|
||||
get _tabbrowser() {
|
||||
return this._window._tabbrowser;
|
||||
},
|
||||
|
||||
get uri() {
|
||||
return this._browser.currentURI;
|
||||
},
|
||||
|
||||
get index() {
|
||||
var tabs = this._tabbrowser.tabs;
|
||||
for (var i=0; i<tabs.length; i++) {
|
||||
if (tabs[i].linkedBrowser == this._browser)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
|
||||
get events() {
|
||||
return this._events;
|
||||
},
|
||||
|
||||
get window() {
|
||||
return this._window;
|
||||
},
|
||||
|
||||
get document() {
|
||||
return this._browser.contentDocument;
|
||||
},
|
||||
|
||||
/*
|
||||
* Helper used to setup event handlers on the XBL element
|
||||
*/
|
||||
_watch: function bt_watch(aType) {
|
||||
this._browser.addEventListener(aType, this,
|
||||
/* useCapture = */ true);
|
||||
},
|
||||
|
||||
handleEvent: function bt_handleEvent(aEvent) {
|
||||
if (aEvent.type == "load") {
|
||||
if (!(aEvent.originalTarget instanceof Ci.nsIDOMDocument))
|
||||
return;
|
||||
|
||||
if (aEvent.originalTarget.defaultView instanceof Ci.nsIDOMWindow &&
|
||||
aEvent.originalTarget.defaultView.frameElement)
|
||||
return;
|
||||
}
|
||||
this._events.dispatch(aEvent.type, this);
|
||||
},
|
||||
/*
|
||||
* Helper used to determine the index offset of the browsertab
|
||||
*/
|
||||
_getTab: function bt_gettab() {
|
||||
var tabs = this._tabbrowser.tabs;
|
||||
return tabs[this.index] || null;
|
||||
},
|
||||
|
||||
load: function bt_load(aURI) {
|
||||
this._browser.loadURI(aURI.spec, null, null);
|
||||
},
|
||||
|
||||
focus: function bt_focus() {
|
||||
this._tabbrowser.selectedTab = this._getTab();
|
||||
this._tabbrowser.focus();
|
||||
},
|
||||
|
||||
close: function bt_close() {
|
||||
this._tabbrowser.removeTab(this._getTab());
|
||||
},
|
||||
|
||||
moveBefore: function bt_movebefore(aBefore) {
|
||||
this._tabbrowser.moveTabTo(this._getTab(), aBefore.index);
|
||||
},
|
||||
|
||||
moveToEnd: function bt_moveend() {
|
||||
this._tabbrowser.moveTabTo(this._getTab(), this._tabbrowser.browsers.length);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.fuelIBrowserTab])
|
||||
};
|
||||
|
||||
|
||||
//=================================================
|
||||
// Annotations implementation
|
||||
function Annotations(aId) {
|
||||
this._id = aId;
|
||||
}
|
||||
|
||||
Annotations.prototype = {
|
||||
get names() {
|
||||
return Utilities.annotations.getItemAnnotationNames(this._id);
|
||||
},
|
||||
|
||||
has: function ann_has(aName) {
|
||||
return Utilities.annotations.itemHasAnnotation(this._id, aName);
|
||||
},
|
||||
|
||||
get: function ann_get(aName) {
|
||||
if (this.has(aName))
|
||||
return Utilities.annotations.getItemAnnotation(this._id, aName);
|
||||
return null;
|
||||
},
|
||||
|
||||
set: function ann_set(aName, aValue, aExpiration) {
|
||||
Utilities.annotations.setItemAnnotation(this._id, aName, aValue, 0, aExpiration);
|
||||
},
|
||||
|
||||
remove: function ann_remove(aName) {
|
||||
if (aName)
|
||||
Utilities.annotations.removeItemAnnotation(this._id, aName);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.fuelIAnnotations])
|
||||
};
|
||||
|
||||
|
||||
//=================================================
|
||||
// BookmarksObserver implementation (internal class)
|
||||
//
|
||||
// BookmarksObserver is a global singleton which watches the browser's
|
||||
// bookmarks and sends you events when things change.
|
||||
//
|
||||
// You can register three different kinds of event listeners on
|
||||
// BookmarksObserver, using addListener, addFolderListener, and
|
||||
// addRootlistener.
|
||||
//
|
||||
// - addListener(aId, aEvent, aListener) lets you listen to a specific
|
||||
// bookmark. You can listen to the "change", "move", and "remove" events.
|
||||
//
|
||||
// - addFolderListener(aId, aEvent, aListener) lets you listen to a specific
|
||||
// bookmark folder. You can listen to "addchild" and "removechild".
|
||||
//
|
||||
// - addRootListener(aEvent, aListener) lets you listen to the root bookmark
|
||||
// node. This lets you hear "add", "remove", and "change" events on all
|
||||
// bookmarks.
|
||||
//
|
||||
|
||||
function BookmarksObserver() {
|
||||
this._eventsDict = {};
|
||||
this._folderEventsDict = {};
|
||||
this._rootEvents = new Events();
|
||||
Utilities.bookmarks.addObserver(this, /* ownsWeak = */ true);
|
||||
}
|
||||
|
||||
BookmarksObserver.prototype = {
|
||||
onBeginUpdateBatch: function () {},
|
||||
onEndUpdateBatch: function () {},
|
||||
onItemVisited: function () {},
|
||||
|
||||
onItemAdded: function bo_onItemAdded(aId, aFolder, aIndex, aItemType, aURI) {
|
||||
this._rootEvents.dispatch("add", aId);
|
||||
this._dispatchToEvents("addchild", aId, this._folderEventsDict[aFolder]);
|
||||
},
|
||||
|
||||
onItemRemoved: function bo_onItemRemoved(aId, aFolder, aIndex) {
|
||||
this._rootEvents.dispatch("remove", aId);
|
||||
this._dispatchToEvents("remove", aId, this._eventsDict[aId]);
|
||||
this._dispatchToEvents("removechild", aId, this._folderEventsDict[aFolder]);
|
||||
},
|
||||
|
||||
onItemChanged: function bo_onItemChanged(aId, aProperty, aIsAnnotationProperty, aValue) {
|
||||
this._rootEvents.dispatch("change", aProperty);
|
||||
this._dispatchToEvents("change", aProperty, this._eventsDict[aId]);
|
||||
},
|
||||
|
||||
onItemMoved: function bo_onItemMoved(aId, aOldParent, aOldIndex, aNewParent, aNewIndex) {
|
||||
this._dispatchToEvents("move", aId, this._eventsDict[aId]);
|
||||
},
|
||||
|
||||
_dispatchToEvents: function bo_dispatchToEvents(aEvent, aData, aEvents) {
|
||||
if (aEvents) {
|
||||
aEvents.dispatch(aEvent, aData);
|
||||
}
|
||||
},
|
||||
|
||||
_addListenerToDict: function bo_addListenerToDict(aId, aEvent, aListener, aDict) {
|
||||
var events = aDict[aId];
|
||||
if (!events) {
|
||||
events = new Events();
|
||||
aDict[aId] = events;
|
||||
}
|
||||
events.addListener(aEvent, aListener);
|
||||
},
|
||||
|
||||
_removeListenerFromDict: function bo_removeListenerFromDict(aId, aEvent, aListener, aDict) {
|
||||
var events = aDict[aId];
|
||||
if (!events) {
|
||||
return;
|
||||
}
|
||||
events.removeListener(aEvent, aListener);
|
||||
if (events._listeners.length == 0) {
|
||||
delete aDict[aId];
|
||||
}
|
||||
},
|
||||
|
||||
addListener: function bo_addListener(aId, aEvent, aListener) {
|
||||
this._addListenerToDict(aId, aEvent, aListener, this._eventsDict);
|
||||
},
|
||||
|
||||
removeListener: function bo_removeListener(aId, aEvent, aListener) {
|
||||
this._removeListenerFromDict(aId, aEvent, aListener, this._eventsDict);
|
||||
},
|
||||
|
||||
addFolderListener: function addFolderListener(aId, aEvent, aListener) {
|
||||
this._addListenerToDict(aId, aEvent, aListener, this._folderEventsDict);
|
||||
},
|
||||
|
||||
removeFolderListener: function removeFolderListener(aId, aEvent, aListener) {
|
||||
this._removeListenerFromDict(aId, aEvent, aListener, this._folderEventsDict);
|
||||
},
|
||||
|
||||
addRootListener: function addRootListener(aEvent, aListener) {
|
||||
this._rootEvents.addListener(aEvent, aListener);
|
||||
},
|
||||
|
||||
removeRootListener: function removeRootListener(aEvent, aListener) {
|
||||
this._rootEvents.removeListener(aEvent, aListener);
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarksObserver,
|
||||
Ci.nsISupportsWeakReference])
|
||||
};
|
||||
|
||||
//=================================================
|
||||
// Bookmark implementation
|
||||
//
|
||||
// Bookmark event listeners are stored in BookmarksObserver, not in the
|
||||
// Bookmark objects themselves. Thus, you don't have to hold on to a Bookmark
|
||||
// object in order for your event listener to stay valid, and Bookmark objects
|
||||
// not kept alive by the extension can be GC'ed.
|
||||
//
|
||||
// A consequence of this is that if you have two different Bookmark objects x
|
||||
// and y for the same bookmark (i.e., x != y but x.id == y.id), and you do
|
||||
//
|
||||
// x.addListener("foo", fun);
|
||||
// y.removeListener("foo", fun);
|
||||
//
|
||||
// the second line will in fact remove the listener added in the first line.
|
||||
//
|
||||
|
||||
function Bookmark(aId, aParent, aType) {
|
||||
this._id = aId;
|
||||
this._parent = aParent;
|
||||
this._type = aType || "bookmark";
|
||||
this._annotations = new Annotations(this._id);
|
||||
|
||||
// Our _events object forwards to bookmarksObserver.
|
||||
var self = this;
|
||||
this._events = {
|
||||
addListener: function bookmarkevents_al(aEvent, aListener) {
|
||||
Utilities.bookmarksObserver.addListener(self._id, aEvent, aListener);
|
||||
},
|
||||
removeListener: function bookmarkevents_rl(aEvent, aListener) {
|
||||
Utilities.bookmarksObserver.removeListener(self._id, aEvent, aListener);
|
||||
},
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.extIEvents])
|
||||
};
|
||||
|
||||
// For our onItemMoved listener, which updates this._parent.
|
||||
Utilities.bookmarks.addObserver(this, /* ownsWeak = */ true);
|
||||
}
|
||||
|
||||
Bookmark.prototype = {
|
||||
get id() {
|
||||
return this._id;
|
||||
},
|
||||
|
||||
get title() {
|
||||
return Utilities.bookmarks.getItemTitle(this._id);
|
||||
},
|
||||
|
||||
set title(aTitle) {
|
||||
Utilities.bookmarks.setItemTitle(this._id, aTitle);
|
||||
},
|
||||
|
||||
get uri() {
|
||||
return Utilities.bookmarks.getBookmarkURI(this._id);
|
||||
},
|
||||
|
||||
set uri(aURI) {
|
||||
return Utilities.bookmarks.changeBookmarkURI(this._id, aURI);
|
||||
},
|
||||
|
||||
get description() {
|
||||
return this._annotations.get("bookmarkProperties/description");
|
||||
},
|
||||
|
||||
set description(aDesc) {
|
||||
this._annotations.set("bookmarkProperties/description", aDesc, Ci.nsIAnnotationService.EXPIRE_NEVER);
|
||||
},
|
||||
|
||||
get keyword() {
|
||||
return Utilities.bookmarks.getKeywordForBookmark(this._id);
|
||||
},
|
||||
|
||||
set keyword(aKeyword) {
|
||||
Utilities.bookmarks.setKeywordForBookmark(this._id, aKeyword);
|
||||
},
|
||||
|
||||
get type() {
|
||||
return this._type;
|
||||
},
|
||||
|
||||
get parent() {
|
||||
return this._parent;
|
||||
},
|
||||
|
||||
set parent(aFolder) {
|
||||
Utilities.bookmarks.moveItem(this._id, aFolder.id, Utilities.bookmarks.DEFAULT_INDEX);
|
||||
// this._parent is updated in onItemMoved
|
||||
},
|
||||
|
||||
get annotations() {
|
||||
return this._annotations;
|
||||
},
|
||||
|
||||
get events() {
|
||||
return this._events;
|
||||
},
|
||||
|
||||
remove : function bm_remove() {
|
||||
Utilities.bookmarks.removeItem(this._id);
|
||||
},
|
||||
|
||||
onBeginUpdateBatch: function () {},
|
||||
onEndUpdateBatch: function () {},
|
||||
onItemAdded: function () {},
|
||||
onItemVisited: function () {},
|
||||
onItemRemoved: function () {},
|
||||
onItemChanged: function () {},
|
||||
|
||||
onItemMoved: function(aId, aOldParent, aOldIndex, aNewParent, aNewIndex) {
|
||||
if (aId == this._id) {
|
||||
this._parent = new BookmarkFolder(aNewParent, Utilities.bookmarks.getFolderIdForItem(aNewParent));
|
||||
}
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.fuelIBookmark,
|
||||
Ci.nsINavBookmarksObserver,
|
||||
Ci.nsISupportsWeakReference])
|
||||
};
|
||||
|
||||
|
||||
//=================================================
|
||||
// BookmarkFolder implementation
|
||||
//
|
||||
// As with Bookmark, events on BookmarkFolder are handled by the
|
||||
// BookmarksObserver singleton.
|
||||
//
|
||||
|
||||
function BookmarkFolder(aId, aParent) {
|
||||
this._id = aId;
|
||||
this._parent = aParent;
|
||||
this._annotations = new Annotations(this._id);
|
||||
|
||||
// Our event listeners are handled by the BookmarksObserver singleton. This
|
||||
// is a bit complicated because there are three different kinds of events we
|
||||
// might want to listen to here:
|
||||
//
|
||||
// - If this._parent is null, we're the root bookmark folder, and all our
|
||||
// listeners should be root listeners.
|
||||
//
|
||||
// - Otherwise, events ending with "child" (addchild, removechild) are
|
||||
// handled by a folder listener.
|
||||
//
|
||||
// - Other events are handled by a vanilla bookmark listener.
|
||||
|
||||
var self = this;
|
||||
this._events = {
|
||||
addListener: function bmfevents_al(aEvent, aListener) {
|
||||
if (self._parent) {
|
||||
if (/child$/.test(aEvent)) {
|
||||
Utilities.bookmarksObserver.addFolderListener(self._id, aEvent, aListener);
|
||||
}
|
||||
else {
|
||||
Utilities.bookmarksObserver.addListener(self._id, aEvent, aListener);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Utilities.bookmarksObserver.addRootListener(aEvent, aListener);
|
||||
}
|
||||
},
|
||||
removeListener: function bmfevents_rl(aEvent, aListener) {
|
||||
if (self._parent) {
|
||||
if (/child$/.test(aEvent)) {
|
||||
Utilities.bookmarksObserver.removeFolderListener(self._id, aEvent, aListener);
|
||||
}
|
||||
else {
|
||||
Utilities.bookmarksObserver.removeListener(self._id, aEvent, aListener);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Utilities.bookmarksObserver.removeRootListener(aEvent, aListener);
|
||||
}
|
||||
},
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.extIEvents])
|
||||
};
|
||||
|
||||
// For our onItemMoved listener, which updates this._parent.
|
||||
Utilities.bookmarks.addObserver(this, /* ownsWeak = */ true);
|
||||
}
|
||||
|
||||
BookmarkFolder.prototype = {
|
||||
get id() {
|
||||
return this._id;
|
||||
},
|
||||
|
||||
get title() {
|
||||
return Utilities.bookmarks.getItemTitle(this._id);
|
||||
},
|
||||
|
||||
set title(aTitle) {
|
||||
Utilities.bookmarks.setItemTitle(this._id, aTitle);
|
||||
},
|
||||
|
||||
get description() {
|
||||
return this._annotations.get("bookmarkProperties/description");
|
||||
},
|
||||
|
||||
set description(aDesc) {
|
||||
this._annotations.set("bookmarkProperties/description", aDesc, Ci.nsIAnnotationService.EXPIRE_NEVER);
|
||||
},
|
||||
|
||||
get type() {
|
||||
return "folder";
|
||||
},
|
||||
|
||||
get parent() {
|
||||
return this._parent;
|
||||
},
|
||||
|
||||
set parent(aFolder) {
|
||||
Utilities.bookmarks.moveItem(this._id, aFolder.id, Utilities.bookmarks.DEFAULT_INDEX);
|
||||
// this._parent is updated in onItemMoved
|
||||
},
|
||||
|
||||
get annotations() {
|
||||
return this._annotations;
|
||||
},
|
||||
|
||||
get events() {
|
||||
return this._events;
|
||||
},
|
||||
|
||||
get children() {
|
||||
var items = [];
|
||||
|
||||
var options = Utilities.history.getNewQueryOptions();
|
||||
var query = Utilities.history.getNewQuery();
|
||||
query.setFolders([this._id], 1);
|
||||
var result = Utilities.history.executeQuery(query, options);
|
||||
var rootNode = result.root;
|
||||
rootNode.containerOpen = true;
|
||||
var cc = rootNode.childCount;
|
||||
for (var i=0; i<cc; ++i) {
|
||||
var node = rootNode.getChild(i);
|
||||
if (node.type == node.RESULT_TYPE_FOLDER) {
|
||||
var folder = new BookmarkFolder(node.itemId, this._id);
|
||||
items.push(folder);
|
||||
}
|
||||
else if (node.type == node.RESULT_TYPE_SEPARATOR) {
|
||||
var separator = new Bookmark(node.itemId, this._id, "separator");
|
||||
items.push(separator);
|
||||
}
|
||||
else {
|
||||
var bookmark = new Bookmark(node.itemId, this._id, "bookmark");
|
||||
items.push(bookmark);
|
||||
}
|
||||
}
|
||||
rootNode.containerOpen = false;
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
addBookmark: function bmf_addbm(aTitle, aUri) {
|
||||
var newBookmarkID = Utilities.bookmarks.insertBookmark(this._id, aUri, Utilities.bookmarks.DEFAULT_INDEX, aTitle);
|
||||
var newBookmark = new Bookmark(newBookmarkID, this, "bookmark");
|
||||
return newBookmark;
|
||||
},
|
||||
|
||||
addSeparator: function bmf_addsep() {
|
||||
var newBookmarkID = Utilities.bookmarks.insertSeparator(this._id, Utilities.bookmarks.DEFAULT_INDEX);
|
||||
var newBookmark = new Bookmark(newBookmarkID, this, "separator");
|
||||
return newBookmark;
|
||||
},
|
||||
|
||||
addFolder: function bmf_addfolder(aTitle) {
|
||||
var newFolderID = Utilities.bookmarks.createFolder(this._id, aTitle, Utilities.bookmarks.DEFAULT_INDEX);
|
||||
var newFolder = new BookmarkFolder(newFolderID, this);
|
||||
return newFolder;
|
||||
},
|
||||
|
||||
remove: function bmf_remove() {
|
||||
Utilities.bookmarks.removeItem(this._id);
|
||||
},
|
||||
|
||||
// observer
|
||||
onBeginUpdateBatch: function () {},
|
||||
onEndUpdateBatch : function () {},
|
||||
onItemAdded : function () {},
|
||||
onItemRemoved : function () {},
|
||||
onItemChanged : function () {},
|
||||
|
||||
onItemMoved: function bf_onItemMoved(aId, aOldParent, aOldIndex, aNewParent, aNewIndex) {
|
||||
if (this._id == aId) {
|
||||
this._parent = new BookmarkFolder(aNewParent, Utilities.bookmarks.getFolderIdForItem(aNewParent));
|
||||
}
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.fuelIBookmarkFolder,
|
||||
Ci.nsINavBookmarksObserver,
|
||||
Ci.nsISupportsWeakReference])
|
||||
};
|
||||
|
||||
//=================================================
|
||||
// BookmarkRoots implementation
|
||||
function BookmarkRoots() {
|
||||
}
|
||||
|
||||
BookmarkRoots.prototype = {
|
||||
get menu() {
|
||||
if (!this._menu)
|
||||
this._menu = new BookmarkFolder(Utilities.bookmarks.bookmarksMenuFolder, null);
|
||||
|
||||
return this._menu;
|
||||
},
|
||||
|
||||
get toolbar() {
|
||||
if (!this._toolbar)
|
||||
this._toolbar = new BookmarkFolder(Utilities.bookmarks.toolbarFolder, null);
|
||||
|
||||
return this._toolbar;
|
||||
},
|
||||
|
||||
get tags() {
|
||||
if (!this._tags)
|
||||
this._tags = new BookmarkFolder(Utilities.bookmarks.tagsFolder, null);
|
||||
|
||||
return this._tags;
|
||||
},
|
||||
|
||||
get unfiled() {
|
||||
if (!this._unfiled)
|
||||
this._unfiled = new BookmarkFolder(Utilities.bookmarks.unfiledBookmarksFolder, null);
|
||||
|
||||
return this._unfiled;
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.fuelIBookmarkRoots])
|
||||
};
|
||||
|
||||
|
||||
//=================================================
|
||||
// Factory - Treat Application as a singleton
|
||||
// XXX This is required, because we're registered for the 'JavaScript global
|
||||
// privileged property' category, whose handler always calls createInstance.
|
||||
// See bug 386535.
|
||||
var gSingleton = null;
|
||||
var ApplicationFactory = {
|
||||
createInstance: function af_ci(aOuter, aIID) {
|
||||
if (aOuter != null)
|
||||
throw Components.results.NS_ERROR_NO_AGGREGATION;
|
||||
|
||||
if (gSingleton == null) {
|
||||
gSingleton = new Application();
|
||||
}
|
||||
|
||||
return gSingleton.QueryInterface(aIID);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#include ../../../toolkit/components/exthelper/extApplication.js
|
||||
|
||||
//=================================================
|
||||
// Application constructor
|
||||
function Application() {
|
||||
Deprecated.warning("FUEL is deprecated, you should use the add-on SDK instead.",
|
||||
"https://developer.mozilla.org/Add-ons/SDK/");
|
||||
|
||||
this.initToolkitHelpers();
|
||||
}
|
||||
|
||||
//=================================================
|
||||
// Application implementation
|
||||
function ApplicationPrototype() {
|
||||
// for nsIClassInfo + XPCOMUtils
|
||||
this.classID = APPLICATION_CID;
|
||||
|
||||
// redefine the default factory for XPCOMUtils
|
||||
this._xpcom_factory = ApplicationFactory;
|
||||
|
||||
// for nsISupports
|
||||
this.QueryInterface = XPCOMUtils.generateQI([
|
||||
Ci.fuelIApplication,
|
||||
Ci.extIApplication,
|
||||
Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference
|
||||
]);
|
||||
|
||||
// for nsIClassInfo
|
||||
this.classInfo = XPCOMUtils.generateCI({
|
||||
classID: APPLICATION_CID,
|
||||
contractID: APPLICATION_CONTRACTID,
|
||||
interfaces: [
|
||||
Ci.fuelIApplication,
|
||||
Ci.extIApplication,
|
||||
Ci.nsIObserver
|
||||
],
|
||||
flags: Ci.nsIClassInfo.SINGLETON
|
||||
});
|
||||
|
||||
// for nsIObserver
|
||||
this.observe = function (aSubject, aTopic, aData) {
|
||||
// Call the extApplication version of this function first
|
||||
var superPrototype = Object.getPrototypeOf(Object.getPrototypeOf(this));
|
||||
superPrototype.observe.call(this, aSubject, aTopic, aData);
|
||||
if (aTopic == "xpcom-shutdown") {
|
||||
this._obs.removeObserver(this, "xpcom-shutdown");
|
||||
Utilities.free();
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(this, "bookmarks", {
|
||||
get: function bookmarks () {
|
||||
let bookmarks = new BookmarkRoots();
|
||||
Object.defineProperty(this, "bookmarks", { value: bookmarks });
|
||||
return this.bookmarks;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "windows", {
|
||||
get: function windows() {
|
||||
var win = [];
|
||||
var browserEnum = Utilities.windowMediator.getEnumerator("navigator:browser");
|
||||
|
||||
while (browserEnum.hasMoreElements())
|
||||
win.push(getWindow(browserEnum.getNext()));
|
||||
|
||||
return win;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "activeWindow", {
|
||||
get: () => getWindow(Utilities.windowMediator.getMostRecentWindow("navigator:browser")),
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// set the proto, defined in extApplication.js
|
||||
ApplicationPrototype.prototype = extApplication.prototype;
|
||||
|
||||
Application.prototype = new ApplicationPrototype();
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Application]);
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
component {fe74cf80-aa2d-11db-abbd-0800200c9a66} fuelApplication.js
|
||||
contract @mozilla.org/fuel/application;1 {fe74cf80-aa2d-11db-abbd-0800200c9a66}
|
||||
category JavaScript-global-privileged-property Application @mozilla.org/fuel/application;1
|
||||
@@ -1,347 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
#include "extIApplication.idl"
|
||||
|
||||
interface nsIVariant;
|
||||
interface nsIURI;
|
||||
interface nsIDOMHTMLDocument;
|
||||
|
||||
interface fuelIBookmarkFolder;
|
||||
interface fuelIBrowserTab;
|
||||
|
||||
/**
|
||||
* Interface representing a collection of annotations associated
|
||||
* with a bookmark or bookmark folder.
|
||||
*/
|
||||
[scriptable, uuid(335c9292-91a1-4ca0-ad0b-07d5f63ed6cd)]
|
||||
interface fuelIAnnotations : nsISupports
|
||||
{
|
||||
/**
|
||||
* Array of the annotation names associated with the owning item
|
||||
*/
|
||||
readonly attribute nsIVariant names;
|
||||
|
||||
/**
|
||||
* Determines if an annotation exists with the given name.
|
||||
* @param aName
|
||||
* The name of the annotation
|
||||
* @returns true if an annotation exists with the given name,
|
||||
* false otherwise.
|
||||
*/
|
||||
boolean has(in AString aName);
|
||||
|
||||
/**
|
||||
* Gets the value of an annotation with the given name.
|
||||
* @param aName
|
||||
* The name of the annotation
|
||||
* @returns A variant containing the value of the annotation. Supports
|
||||
* string, boolean and number.
|
||||
*/
|
||||
nsIVariant get(in AString aName);
|
||||
|
||||
/**
|
||||
* Sets the value of an annotation with the given name.
|
||||
* @param aName
|
||||
* The name of the annotation
|
||||
* @param aValue
|
||||
* The new value of the annotation. Supports string, boolean
|
||||
* and number
|
||||
* @param aExpiration
|
||||
* The expiration policy for the annotation.
|
||||
* See nsIAnnotationService.
|
||||
*/
|
||||
void set(in AString aName, in nsIVariant aValue, in int32_t aExpiration);
|
||||
|
||||
/**
|
||||
* Removes the named annotation from the owner item.
|
||||
* @param aName
|
||||
* The name of annotation.
|
||||
*/
|
||||
void remove(in AString aName);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Interface representing a bookmark item.
|
||||
*/
|
||||
[scriptable, uuid(808585b6-7568-4b26-8c62-545221bf2b8c)]
|
||||
interface fuelIBookmark : nsISupports
|
||||
{
|
||||
/**
|
||||
* The id of the bookmark.
|
||||
*/
|
||||
readonly attribute long long id;
|
||||
|
||||
/**
|
||||
* The title of the bookmark.
|
||||
*/
|
||||
attribute AString title;
|
||||
|
||||
/**
|
||||
* The uri of the bookmark.
|
||||
*/
|
||||
attribute nsIURI uri;
|
||||
|
||||
/**
|
||||
* The description of the bookmark.
|
||||
*/
|
||||
attribute AString description;
|
||||
|
||||
/**
|
||||
* The keyword associated with the bookmark.
|
||||
*/
|
||||
attribute AString keyword;
|
||||
|
||||
/**
|
||||
* The type of the bookmark.
|
||||
* values: "bookmark", "separator"
|
||||
*/
|
||||
readonly attribute AString type;
|
||||
|
||||
/**
|
||||
* The parent folder of the bookmark.
|
||||
*/
|
||||
attribute fuelIBookmarkFolder parent;
|
||||
|
||||
/**
|
||||
* The annotations object for the bookmark.
|
||||
*/
|
||||
readonly attribute fuelIAnnotations annotations;
|
||||
|
||||
/**
|
||||
* The events object for the bookmark.
|
||||
* supports: "remove", "change", "visit", "move"
|
||||
*/
|
||||
readonly attribute extIEvents events;
|
||||
|
||||
/**
|
||||
* Removes the item from the parent folder. Used to
|
||||
* delete a bookmark or separator
|
||||
*/
|
||||
void remove();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Interface representing a bookmark folder. Folders
|
||||
* can hold bookmarks, separators and other folders.
|
||||
*/
|
||||
[scriptable, uuid(9f42fe20-52de-4a55-8632-a459c7716aa0)]
|
||||
interface fuelIBookmarkFolder : nsISupports
|
||||
{
|
||||
/**
|
||||
* The id of the folder.
|
||||
*/
|
||||
readonly attribute long long id;
|
||||
|
||||
/**
|
||||
* The title of the folder.
|
||||
*/
|
||||
attribute AString title;
|
||||
|
||||
/**
|
||||
* The description of the folder.
|
||||
*/
|
||||
attribute AString description;
|
||||
|
||||
/**
|
||||
* The type of the folder.
|
||||
* values: "folder"
|
||||
*/
|
||||
readonly attribute AString type;
|
||||
|
||||
/**
|
||||
* The parent folder of the folder.
|
||||
*/
|
||||
attribute fuelIBookmarkFolder parent;
|
||||
|
||||
/**
|
||||
* The annotations object for the folder.
|
||||
*/
|
||||
readonly attribute fuelIAnnotations annotations;
|
||||
|
||||
/**
|
||||
* The events object for the folder.
|
||||
* supports: "add", "addchild", "remove", "removechild", "change", "move"
|
||||
*/
|
||||
readonly attribute extIEvents events;
|
||||
|
||||
/**
|
||||
* Array of all bookmarks, separators and folders contained
|
||||
* in this folder.
|
||||
*/
|
||||
readonly attribute nsIVariant children;
|
||||
|
||||
/**
|
||||
* Adds a new child bookmark to this folder.
|
||||
* @param aTitle
|
||||
* The title of bookmark.
|
||||
* @param aURI
|
||||
* The uri of bookmark.
|
||||
*/
|
||||
fuelIBookmark addBookmark(in AString aTitle, in nsIURI aURI);
|
||||
|
||||
/**
|
||||
* Adds a new child separator to this folder.
|
||||
*/
|
||||
fuelIBookmark addSeparator();
|
||||
|
||||
/**
|
||||
* Adds a new child folder to this folder.
|
||||
* @param aTitle
|
||||
* The title of folder.
|
||||
*/
|
||||
fuelIBookmarkFolder addFolder(in AString aTitle);
|
||||
|
||||
/**
|
||||
* Removes the folder from the parent folder.
|
||||
*/
|
||||
void remove();
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface representing a container for bookmark roots. Roots
|
||||
* are the top level parents for the various types of bookmarks in the system.
|
||||
*/
|
||||
[scriptable, uuid(c9a80870-eb3c-11dc-95ff-0800200c9a66)]
|
||||
interface fuelIBookmarkRoots : nsISupports
|
||||
{
|
||||
/**
|
||||
* The folder for the 'bookmarks menu' root.
|
||||
*/
|
||||
readonly attribute fuelIBookmarkFolder menu;
|
||||
|
||||
/**
|
||||
* The folder for the 'personal toolbar' root.
|
||||
*/
|
||||
readonly attribute fuelIBookmarkFolder toolbar;
|
||||
|
||||
/**
|
||||
* The folder for the 'tags' root.
|
||||
*/
|
||||
readonly attribute fuelIBookmarkFolder tags;
|
||||
|
||||
/**
|
||||
* The folder for the 'unfiled bookmarks' root.
|
||||
*/
|
||||
readonly attribute fuelIBookmarkFolder unfiled;
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface representing a browser window.
|
||||
*/
|
||||
[scriptable, uuid(207edb28-eb5e-424e-a862-b0e97C8de866)]
|
||||
interface fuelIWindow : nsISupports
|
||||
{
|
||||
/**
|
||||
* A collection of browser tabs within the browser window.
|
||||
*/
|
||||
readonly attribute nsIVariant tabs;
|
||||
|
||||
/**
|
||||
* The currently-active tab within the browser window.
|
||||
*/
|
||||
readonly attribute fuelIBrowserTab activeTab;
|
||||
|
||||
/**
|
||||
* Open a new browser tab, pointing to the specified URI.
|
||||
* @param aURI
|
||||
* The uri to open the browser tab to
|
||||
*/
|
||||
fuelIBrowserTab open(in nsIURI aURI);
|
||||
|
||||
/**
|
||||
* The events object for the browser window.
|
||||
* supports: "TabOpen", "TabClose", "TabMove", "TabSelect"
|
||||
*/
|
||||
readonly attribute extIEvents events;
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface representing a browser tab.
|
||||
*/
|
||||
[scriptable, uuid(3073ceff-777c-41ce-9ace-ab37268147c1)]
|
||||
interface fuelIBrowserTab : nsISupports
|
||||
{
|
||||
/**
|
||||
* The current uri of this tab.
|
||||
*/
|
||||
readonly attribute nsIURI uri;
|
||||
|
||||
/**
|
||||
* The current index of this tab in the browser window.
|
||||
*/
|
||||
readonly attribute int32_t index;
|
||||
|
||||
/**
|
||||
* The browser window that is holding the tab.
|
||||
*/
|
||||
readonly attribute fuelIWindow window;
|
||||
|
||||
/**
|
||||
* The content document of the browser tab.
|
||||
*/
|
||||
readonly attribute nsIDOMHTMLDocument document;
|
||||
|
||||
/**
|
||||
* The events object for the browser tab.
|
||||
* supports: "load"
|
||||
*/
|
||||
readonly attribute extIEvents events;
|
||||
|
||||
/**
|
||||
* Load a new URI into this browser tab.
|
||||
* @param aURI
|
||||
* The uri to load into the browser tab
|
||||
*/
|
||||
void load(in nsIURI aURI);
|
||||
|
||||
/**
|
||||
* Give focus to this browser tab, and bring it to the front.
|
||||
*/
|
||||
void focus();
|
||||
|
||||
/**
|
||||
* Close the browser tab. This may not actually close the tab
|
||||
* as script may abort the close operation.
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Moves this browser tab before another browser tab within the window.
|
||||
* @param aBefore
|
||||
* The tab before which the target tab will be moved
|
||||
*/
|
||||
void moveBefore(in fuelIBrowserTab aBefore);
|
||||
|
||||
/**
|
||||
* Move this browser tab to the last tab within the window.
|
||||
*/
|
||||
void moveToEnd();
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface for managing and accessing the applications systems
|
||||
*/
|
||||
[scriptable, uuid(fe74cf80-aa2d-11db-abbd-0800200c9a66)]
|
||||
interface fuelIApplication : extIApplication
|
||||
{
|
||||
/**
|
||||
* The root bookmarks object for the application.
|
||||
* Contains all the bookmark roots in the system.
|
||||
*/
|
||||
readonly attribute fuelIBookmarkRoots bookmarks;
|
||||
|
||||
/**
|
||||
* An array of browser windows within the application.
|
||||
*/
|
||||
readonly attribute nsIVariant windows;
|
||||
|
||||
/**
|
||||
* The currently active browser window.
|
||||
*/
|
||||
readonly attribute fuelIWindow activeWindow;
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
# This file currently uses a non-standard (and not on a standards track)
|
||||
# if statement within catch.
|
||||
modules/MozLoopWorker.js
|
||||
# This file currently uses es7 features eslint issue:
|
||||
# https://github.com/eslint/espree/issues/125
|
||||
modules/MozLoopAPI.jsm
|
||||
# Libs we don't need to check
|
||||
content/libs
|
||||
content/shared/libs
|
||||
standalone/content/libs
|
||||
standalone/node_modules
|
||||
# We should look at turning these on when we fix the warnings
|
||||
test/xpcshell
|
||||
test/mochitest
|
||||
# Libs we don't need to check
|
||||
test/shared/vendor
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
// Note: there are extra allowances for files used solely in Firefox desktop,
|
||||
// see content/js/.eslintrc and modules/.eslintrc
|
||||
{
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"ecmaFeatures": {
|
||||
"forOf": true,
|
||||
"jsx": true,
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"mocha": true
|
||||
},
|
||||
"globals": {
|
||||
"_": false,
|
||||
"$": false,
|
||||
"Backbone": false,
|
||||
"chai": false,
|
||||
"console": false,
|
||||
"jQuery": false,
|
||||
"loop": false,
|
||||
"MozActivity": false,
|
||||
"OT": false,
|
||||
"Promise": false,
|
||||
"React": false,
|
||||
"sinon": false
|
||||
},
|
||||
"rules": {
|
||||
// turn off all kinds of stuff that we actually do want, because
|
||||
// right now, we're bootstrapping the linting infrastructure. We'll
|
||||
// want to audit these rules, and start turning them on and fixing the
|
||||
// problems they find, one at a time.
|
||||
|
||||
// Eslint built-in rules are documented at <http://eslint.org/docs/rules/>
|
||||
"camelcase": 0, // TODO: Remove (use default)
|
||||
"comma-dangle": 0, // TODO: Remove (use default)
|
||||
"comma-spacing": 0, // TODO: Remove (use default)
|
||||
"consistent-return": 0, // TODO: Remove (use default)
|
||||
"curly": 0, // TODO: Remove (use default)
|
||||
"dot-notation": 0, // TODO: Remove (use default)
|
||||
"eol-last": 0, // TODO: Remove (use default)
|
||||
"eqeqeq": 0, // TBD. Might need to be separate for content & chrome
|
||||
"global-strict": 0, // Leave as zero (this will be unsupported in eslint 1.0.0)
|
||||
"key-spacing": 0, // TODO: Remove (use default)
|
||||
"new-cap": 0, // TODO: Remove (use default)
|
||||
"no-catch-shadow": 0, // TODO: Remove (use default)
|
||||
"no-console": 0, // Leave as 0. We use console logging in content code.
|
||||
"no-empty": 0, // TODO: Remove (use default)
|
||||
"no-extra-bind": 0, // Leave as 0
|
||||
"no-extra-boolean-cast": 0, // TODO: Remove (use default)
|
||||
"no-multi-spaces": 0, // TBD.
|
||||
"no-new": 0, // TODO: Remove (use default)
|
||||
"no-redeclare": 0, // TODO: Remove (use default)
|
||||
"no-return-assign": 0, // TODO: Remove (use default)
|
||||
"no-shadow": 0, // TODO: Remove (use default)
|
||||
"no-spaced-func": 0, // TODO: Remove (use default)
|
||||
"no-trailing-spaces": 0, // TODO: Remove (use default)
|
||||
"no-undef": 0, // TODO: Remove (use default)
|
||||
"no-underscore-dangle": 0, // Leave as 0. Commonly used for private variables.
|
||||
"no-unused-expressions": 0, // TODO: Remove (use default)
|
||||
"no-unused-vars": 0, // TODO: Remove (use default)
|
||||
"no-use-before-define": 0, // TODO: Remove (use default)
|
||||
"no-wrap-func": 0, // TODO: Remove (use default)
|
||||
"quotes": 0, // [2, "double", "avoid-escape"],
|
||||
"space-infix-ops": 0, // TODO: Remove (use default)
|
||||
"space-return-throw-case": 0, // TODO: Remove (use default)
|
||||
"strict": 0, // [2, "function"],
|
||||
"yoda": 0, // [2, "never"],
|
||||
// eslint-plugin-react rules. These are documented at
|
||||
// <https://github.com/yannickcr/eslint-plugin-react#list-of-supported-rules>
|
||||
"react/jsx-quotes": [2, "double", "avoid-escape"],
|
||||
"react/jsx-no-undef": 2,
|
||||
// Need to fix instances where this is failing.
|
||||
"react/jsx-sort-props": 0,
|
||||
"react/jsx-sort-prop-types": 0,
|
||||
"react/jsx-uses-vars": 2,
|
||||
// Need to fix the couple of instances which don't
|
||||
// currently pass this rule.
|
||||
"react/no-did-mount-set-state": 0,
|
||||
"react/no-did-update-set-state": 2,
|
||||
"react/no-unknown-property": 2,
|
||||
// Need to fix instances where this is currently failing
|
||||
"react/prop-types": 0,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/wrap-multilines": 2,
|
||||
// Not worth it: React is defined globally
|
||||
"react/jsx-uses-react": 0,
|
||||
"react/react-in-jsx-scope": 0,
|
||||
// These ones we don't want to ever enable
|
||||
"react/display-name": 0,
|
||||
"react/jsx-boolean-value": 0,
|
||||
"react/no-multi-comp": 0
|
||||
}
|
||||
}
|
||||
@@ -1,839 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource:///modules/loop/LoopCalls.jsm");
|
||||
Cu.import("resource:///modules/loop/MozLoopService.jsm");
|
||||
Cu.import("resource:///modules/loop/LoopRooms.jsm");
|
||||
Cu.import("resource:///modules/loop/LoopContacts.jsm");
|
||||
Cu.importGlobalProperties(["Blob"]);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
|
||||
"resource:///modules/loop/LoopContacts.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
|
||||
"resource:///modules/loop/LoopStorage.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
|
||||
"resource://gre/modules/MozSocialAPI.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
|
||||
"resource://gre/modules/PluralForm.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "UITour",
|
||||
"resource:///modules/UITour.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
|
||||
return Cc["@mozilla.org/xre/app-info;1"]
|
||||
.getService(Ci.nsIXULAppInfo)
|
||||
.QueryInterface(Ci.nsIXULRuntime);
|
||||
});
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
|
||||
"@mozilla.org/widget/clipboardhelper;1",
|
||||
"nsIClipboardHelper");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "extProtocolSvc",
|
||||
"@mozilla.org/uriloader/external-protocol-service;1",
|
||||
"nsIExternalProtocolService");
|
||||
this.EXPORTED_SYMBOLS = ["injectLoopAPI"];
|
||||
|
||||
/**
|
||||
* Trying to clone an Error object into a different container will yield an error.
|
||||
* We can work around this by copying the properties we care about onto a regular
|
||||
* object.
|
||||
*
|
||||
* @param {Error|nsIException} error Error object to copy
|
||||
* @param {nsIDOMWindow} targetWindow The content window to clone into
|
||||
*/
|
||||
const cloneErrorObject = function(error, targetWindow) {
|
||||
let obj = new targetWindow.Error();
|
||||
let props = Object.getOwnPropertyNames(error);
|
||||
// nsIException properties are not enumerable, so we'll try to copy the most
|
||||
// common and useful ones.
|
||||
if (!props.length) {
|
||||
props.push("message", "filename", "lineNumber", "columnNumber", "stack");
|
||||
}
|
||||
for (let prop of props) {
|
||||
let value = error[prop];
|
||||
// for nsIException objects, the property may not be defined.
|
||||
if (typeof value == "undefined") {
|
||||
continue;
|
||||
}
|
||||
if (typeof value != "string" && typeof value != "number") {
|
||||
value = String(value);
|
||||
}
|
||||
|
||||
Object.defineProperty(Cu.waiveXrays(obj), prop, {
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
value: value,
|
||||
writable: false
|
||||
});
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes an object or value available to an unprivileged target window.
|
||||
*
|
||||
* Primitives are returned as they are, while objects are cloned into the
|
||||
* specified target. Error objects are also handled correctly.
|
||||
*
|
||||
* @param {any} value Value or object to copy
|
||||
* @param {nsIDOMWindow} targetWindow The content window to copy to
|
||||
*/
|
||||
const cloneValueInto = function(value, targetWindow) {
|
||||
if (!value || typeof value != "object") {
|
||||
return value;
|
||||
}
|
||||
|
||||
// HAWK request errors contain an nsIException object inside `value`.
|
||||
if (("error" in value) && (value.error instanceof Ci.nsIException)) {
|
||||
value = value.error;
|
||||
}
|
||||
|
||||
// Strip Function properties, since they can not be cloned across boundaries
|
||||
// like this.
|
||||
for (let prop of Object.getOwnPropertyNames(value)) {
|
||||
if (typeof value[prop] == "function") {
|
||||
delete value[prop];
|
||||
}
|
||||
}
|
||||
|
||||
// Inspect for an error this way, because the Error object is special.
|
||||
if (value.constructor.name == "Error" || value instanceof Ci.nsIException) {
|
||||
return cloneErrorObject(value, targetWindow);
|
||||
}
|
||||
|
||||
let clone;
|
||||
try {
|
||||
clone = Cu.cloneInto(value, targetWindow);
|
||||
} catch (ex) {
|
||||
MozLoopService.log.debug("Failed to clone value:", value);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
return clone;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inject any API containing _only_ function properties into the given window.
|
||||
*
|
||||
* @param {Object} api Object containing functions that need to
|
||||
* be exposed to content
|
||||
* @param {nsIDOMWindow} targetWindow The content window to attach the API
|
||||
*/
|
||||
const injectObjectAPI = function(api, targetWindow) {
|
||||
let injectedAPI = {};
|
||||
// Wrap all the methods in `api` to help results passed to callbacks get
|
||||
// through the priv => unpriv barrier with `Cu.cloneInto()`.
|
||||
Object.keys(api).forEach(func => {
|
||||
injectedAPI[func] = function(...params) {
|
||||
let lastParam = params.pop();
|
||||
let callbackIsFunction = (typeof lastParam == "function");
|
||||
// Clone params coming from content to the current scope.
|
||||
params = [cloneValueInto(p, api) for (p of params)];
|
||||
|
||||
// If the last parameter is a function, assume its a callback
|
||||
// and wrap it differently.
|
||||
if (callbackIsFunction) {
|
||||
api[func](...params, function(...results) {
|
||||
// When the function was garbage collected due to async events, like
|
||||
// closing a window, we want to circumvent a JS error.
|
||||
if (callbackIsFunction && typeof lastParam != "function") {
|
||||
MozLoopService.log.debug(func + ": callback function was lost.");
|
||||
// Assume the presence of a first result argument to be an error.
|
||||
if (results[0]) {
|
||||
MozLoopService.log.error(func + " error:", results[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
lastParam(...[cloneValueInto(r, targetWindow) for (r of results)]);
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
lastParam = cloneValueInto(lastParam, api);
|
||||
return cloneValueInto(api[func](...params, lastParam), targetWindow);
|
||||
} catch (ex) {
|
||||
return cloneValueInto(ex, targetWindow);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
let contentObj = Cu.cloneInto(injectedAPI, targetWindow, {cloneFunctions: true});
|
||||
// Since we deny preventExtensions on XrayWrappers, because Xray semantics make
|
||||
// it difficult to act like an object has actually been frozen, we try to seal
|
||||
// the `contentObj` without Xrays.
|
||||
try {
|
||||
Object.seal(Cu.waiveXrays(contentObj));
|
||||
} catch (ex) {}
|
||||
return contentObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inject the loop API into the given window. The caller must be sure the
|
||||
* window is a loop content window (eg, a panel, chatwindow, or similar).
|
||||
*
|
||||
* See the documentation on the individual functions for details of the API.
|
||||
*
|
||||
* @param {nsIDOMWindow} targetWindow The content window to attach the API.
|
||||
*/
|
||||
function injectLoopAPI(targetWindow) {
|
||||
let ringer;
|
||||
let ringerStopper;
|
||||
let appVersionInfo;
|
||||
let contactsAPI;
|
||||
let roomsAPI;
|
||||
let callsAPI;
|
||||
|
||||
let api = {
|
||||
/**
|
||||
* Gets an object with data that represents the currently
|
||||
* authenticated user's identity.
|
||||
*
|
||||
* @return null if user not logged in; profile object otherwise
|
||||
*/
|
||||
userProfile: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
if (!MozLoopService.userProfile)
|
||||
return null;
|
||||
let userProfile = Cu.cloneInto({
|
||||
email: MozLoopService.userProfile.email,
|
||||
uid: MozLoopService.userProfile.uid
|
||||
}, targetWindow);
|
||||
return userProfile;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets and gets the "do not disturb" mode activation flag.
|
||||
*/
|
||||
doNotDisturb: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return MozLoopService.doNotDisturb;
|
||||
},
|
||||
set: function(aFlag) {
|
||||
MozLoopService.doNotDisturb = aFlag;
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
let errors = {};
|
||||
for (let [type, error] of MozLoopService.errors) {
|
||||
// if error.error is an nsIException, just delete it since it's hard
|
||||
// to clone across the boundary.
|
||||
if (error.error instanceof Ci.nsIException) {
|
||||
MozLoopService.log.debug("Warning: Some errors were omitted from MozLoopAPI.errors " +
|
||||
"due to issues copying nsIException across boundaries.",
|
||||
error.error);
|
||||
delete error.error;
|
||||
}
|
||||
|
||||
// We have to clone the error property since it may be an Error object.
|
||||
if (error.hasOwnProperty("toString")) {
|
||||
delete error.toString;
|
||||
}
|
||||
errors[type] = Cu.waiveXrays(Cu.cloneInto(error, targetWindow, { cloneFunctions: true }));
|
||||
}
|
||||
return Cu.cloneInto(errors, targetWindow, { cloneFunctions: true });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current locale of the browser.
|
||||
*
|
||||
* @returns {String} The locale string
|
||||
*/
|
||||
locale: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return MozLoopService.locale;
|
||||
}
|
||||
},
|
||||
|
||||
getActiveTabWindowId: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(callback) {
|
||||
let win = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
let browser = win && win.gBrowser.selectedTab.linkedBrowser;
|
||||
if (!win || !browser) {
|
||||
// This may happen when an undocked conversation window is the only
|
||||
// window left.
|
||||
let err = new Error("No tabs available to share.");
|
||||
MozLoopService.log.error(err);
|
||||
callback(cloneValueInto(err, targetWindow));
|
||||
return;
|
||||
}
|
||||
|
||||
let mm = browser.messageManager;
|
||||
mm.addMessageListener("webrtc:response:StartBrowserSharing", function listener(message) {
|
||||
mm.removeMessageListener("webrtc:response:StartBrowserSharing", listener);
|
||||
callback(null, message.data.windowID);
|
||||
});
|
||||
mm.sendAsyncMessage("webrtc:StartBrowserSharing");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the window data for a specific conversation window id.
|
||||
*
|
||||
* This data will be relevant to the type of window, e.g. rooms or calls.
|
||||
* See LoopRooms or LoopCalls for more information.
|
||||
*
|
||||
* @param {String} conversationWindowId
|
||||
* @returns {Object} The window data or null if error.
|
||||
*/
|
||||
getConversationWindowData: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(conversationWindowId) {
|
||||
return cloneValueInto(MozLoopService.getConversationWindowData(conversationWindowId),
|
||||
targetWindow);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the contacts API.
|
||||
*
|
||||
* @returns {Object} The contacts API object
|
||||
*/
|
||||
contacts: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
if (contactsAPI) {
|
||||
return contactsAPI;
|
||||
}
|
||||
|
||||
// Make a database switch when a userProfile is active already.
|
||||
let profile = MozLoopService.userProfile;
|
||||
if (profile) {
|
||||
LoopStorage.switchDatabase(profile.uid);
|
||||
}
|
||||
return contactsAPI = injectObjectAPI(LoopContacts, targetWindow);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the rooms API.
|
||||
*
|
||||
* @returns {Object} The rooms API object
|
||||
*/
|
||||
rooms: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
if (roomsAPI) {
|
||||
return roomsAPI;
|
||||
}
|
||||
return roomsAPI = injectObjectAPI(LoopRooms, targetWindow);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the calls API.
|
||||
*
|
||||
* @returns {Object} The rooms API object
|
||||
*/
|
||||
calls: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
if (callsAPI) {
|
||||
return callsAPI;
|
||||
}
|
||||
|
||||
return callsAPI = injectObjectAPI(LoopCalls, targetWindow);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Import a list of (new) contacts from an external data source.
|
||||
*
|
||||
* @param {Object} options Property bag of options for the importer
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the result of the operation, if successfull.
|
||||
*/
|
||||
startImport: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(options, callback) {
|
||||
LoopContacts.startImport(options, getChromeWindow(targetWindow), function(...results) {
|
||||
callback(...[cloneValueInto(r, targetWindow) for (r of results)]);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns translated strings associated with an element. Designed
|
||||
* for use with l10n.js
|
||||
*
|
||||
* @param {String} key The element id
|
||||
* @returns {Object} A JSON string containing the localized
|
||||
* attribute/value pairs for the element.
|
||||
*/
|
||||
getStrings: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(key) {
|
||||
return MozLoopService.getStrings(key);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the correct form of a semi-colon separated string
|
||||
* based on the value of the `num` argument and the current locale.
|
||||
*
|
||||
* @param {Integer} num The number used to find the plural form.
|
||||
* @param {String} str The semi-colon separated string of word forms.
|
||||
* @returns {String} The correct word form based on the value of the number
|
||||
* and the current locale.
|
||||
*/
|
||||
getPluralForm: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(num, str) {
|
||||
return PluralForm.get(num, str);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a confirmation dialog using the specified strings.
|
||||
*
|
||||
* @param {Object} options Confirm dialog options
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument
|
||||
* will be the result of the operation, TRUE if
|
||||
* the user chose the OK button.
|
||||
*/
|
||||
confirm: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(options, callback) {
|
||||
let buttonFlags;
|
||||
if (options.okButton && options.cancelButton) {
|
||||
buttonFlags =
|
||||
(Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING) +
|
||||
(Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING);
|
||||
} else if (!options.okButton && !options.cancelButton) {
|
||||
buttonFlags = Services.prompt.STD_YES_NO_BUTTONS;
|
||||
} else {
|
||||
callback(cloneValueInto(new Error("confirm: missing button options"), targetWindow));
|
||||
}
|
||||
|
||||
try {
|
||||
let chosenButton = Services.prompt.confirmEx(null, "",
|
||||
options.message, buttonFlags, options.okButton, options.cancelButton,
|
||||
null, null, {});
|
||||
|
||||
callback(null, chosenButton == 0);
|
||||
} catch (ex) {
|
||||
callback(cloneValueInto(ex, targetWindow));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set any preference under "loop."
|
||||
*
|
||||
* @param {String} prefName The name of the pref without the preceding "loop."
|
||||
* @param {*} value The value to set.
|
||||
* @param {Enum} prefType Type of preference, defined at Ci.nsIPrefBranch. Optional.
|
||||
*
|
||||
* Any errors thrown by the Mozilla pref API are logged to the console
|
||||
* and cause false to be returned.
|
||||
*/
|
||||
setLoopPref: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(prefName, value, prefType) {
|
||||
MozLoopService.setLoopPref(prefName, value, prefType);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return any preference under "loop.".
|
||||
*
|
||||
* @param {String} prefName The name of the pref without the preceding
|
||||
* "loop."
|
||||
* @param {Enum} prefType Type of preference, defined at Ci.nsIPrefBranch. Optional.
|
||||
*
|
||||
* Any errors thrown by the Mozilla pref API are logged to the console
|
||||
* and cause null to be returned. This includes the case of the preference
|
||||
* not being found.
|
||||
*
|
||||
* @return {*} on success, null on error
|
||||
*/
|
||||
getLoopPref: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(prefName, prefType) {
|
||||
return MozLoopService.getLoopPref(prefName);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts alerting the user about an incoming call
|
||||
*/
|
||||
startAlerting: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function() {
|
||||
let chromeWindow = getChromeWindow(targetWindow);
|
||||
chromeWindow.getAttention();
|
||||
ringer = new chromeWindow.Audio();
|
||||
ringer.src = Services.prefs.getCharPref("loop.ringtone");
|
||||
ringer.loop = true;
|
||||
ringer.load();
|
||||
ringer.play();
|
||||
targetWindow.document.addEventListener("visibilitychange",
|
||||
ringerStopper = function(event) {
|
||||
if (event.currentTarget.hidden) {
|
||||
api.stopAlerting.value();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops alerting the user about an incoming call
|
||||
*/
|
||||
stopAlerting: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function() {
|
||||
if (ringerStopper) {
|
||||
targetWindow.document.removeEventListener("visibilitychange",
|
||||
ringerStopper);
|
||||
ringerStopper = null;
|
||||
}
|
||||
if (ringer) {
|
||||
ringer.pause();
|
||||
ringer = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs a hawk based request to the loop server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - {Object|null} null if success. Otherwise an object:
|
||||
* {
|
||||
* code: 401,
|
||||
* errno: 401,
|
||||
* error: "Request failed",
|
||||
* message: "invalid token"
|
||||
* }
|
||||
* - {String} The body of the response.
|
||||
*
|
||||
* @param {LOOP_SESSION_TYPE} sessionType The type of session to use for
|
||||
* the request. This is one of the
|
||||
* LOOP_SESSION_TYPE members
|
||||
* @param {String} path The path to make the request to.
|
||||
* @param {String} method The request method, e.g. 'POST', 'GET'.
|
||||
* @param {Object} payloadObj An object which is converted to JSON and
|
||||
* transmitted with the request.
|
||||
* @param {Function} callback Called when the request completes.
|
||||
*/
|
||||
hawkRequest: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(sessionType, path, method, payloadObj, callback) {
|
||||
// XXX Should really return a DOM promise here.
|
||||
let callbackIsFunction = (typeof callback == "function");
|
||||
MozLoopService.hawkRequest(sessionType, path, method, payloadObj).then((response) => {
|
||||
callback(null, response.body);
|
||||
}, hawkError => {
|
||||
// When the function was garbage collected due to async events, like
|
||||
// closing a window, we want to circumvent a JS error.
|
||||
if (callbackIsFunction && typeof callback != "function") {
|
||||
MozLoopService.log.error("hawkRequest: callback function was lost.", hawkError);
|
||||
return;
|
||||
}
|
||||
// The hawkError.error property, while usually a string representing
|
||||
// an HTTP response status message, may also incorrectly be a native
|
||||
// error object that will cause the cloning function to fail.
|
||||
callback(Cu.cloneInto({
|
||||
error: (hawkError.error && typeof hawkError.error == "string")
|
||||
? hawkError.error : "Unexpected exception",
|
||||
message: hawkError.message,
|
||||
code: hawkError.code,
|
||||
errno: hawkError.errno,
|
||||
}, targetWindow));
|
||||
}).catch(Cu.reportError);
|
||||
}
|
||||
},
|
||||
|
||||
LOOP_SESSION_TYPE: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return Cu.cloneInto(LOOP_SESSION_TYPE, targetWindow);
|
||||
}
|
||||
},
|
||||
|
||||
fxAEnabled: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return MozLoopService.fxAEnabled;
|
||||
},
|
||||
},
|
||||
|
||||
logInToFxA: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function() {
|
||||
return MozLoopService.logInToFxA();
|
||||
}
|
||||
},
|
||||
|
||||
logOutFromFxA: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function() {
|
||||
return MozLoopService.logOutFromFxA();
|
||||
}
|
||||
},
|
||||
|
||||
openFxASettings: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function() {
|
||||
return MozLoopService.openFxASettings();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the Getting Started tour in the browser.
|
||||
*
|
||||
* @param {String} aSrc
|
||||
* - The UI element that the user used to begin the tour, optional.
|
||||
*/
|
||||
openGettingStartedTour: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(aSrc) {
|
||||
return MozLoopService.openGettingStartedTour(aSrc);
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Copies passed string onto the system clipboard.
|
||||
*
|
||||
* @param {String} str The string to copy
|
||||
*/
|
||||
copyString: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(str) {
|
||||
clipboardHelper.copyString(str);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the app version information for use during feedback.
|
||||
*
|
||||
* @return {Object} An object containing:
|
||||
* - channel: The update channel the application is on
|
||||
* - version: The application version
|
||||
* - OS: The operating system the application is running on
|
||||
*/
|
||||
appVersionInfo: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
if (!appVersionInfo) {
|
||||
let defaults = Services.prefs.getDefaultBranch(null);
|
||||
|
||||
// If the lazy getter explodes, we're probably loaded in xpcshell,
|
||||
// which doesn't have what we need, so log an error.
|
||||
try {
|
||||
appVersionInfo = Cu.cloneInto({
|
||||
channel: defaults.getCharPref("app.update.channel"),
|
||||
version: appInfo.version,
|
||||
OS: appInfo.OS
|
||||
}, targetWindow);
|
||||
} catch (ex) {
|
||||
// only log outside of xpcshell to avoid extra message noise
|
||||
if (typeof targetWindow !== 'undefined' && "console" in targetWindow) {
|
||||
MozLoopService.log.error("Failed to construct appVersionInfo; if this isn't " +
|
||||
"an xpcshell unit test, something is wrong", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
return appVersionInfo;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Composes an email via the external protocol service.
|
||||
*
|
||||
* @param {String} subject Subject of the email to send
|
||||
* @param {String} body Body message of the email to send
|
||||
* @param {String} recipient Recipient email address (optional)
|
||||
*/
|
||||
composeEmail: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(subject, body, recipient) {
|
||||
recipient = recipient || "";
|
||||
let mailtoURL = "mailto:" + encodeURIComponent(recipient) +
|
||||
"?subject=" + encodeURIComponent(subject) +
|
||||
"&body=" + encodeURIComponent(body);
|
||||
extProtocolSvc.loadURI(CommonUtils.makeURI(mailtoURL));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a value to a telemetry histogram.
|
||||
*
|
||||
* @param {string} histogramId Name of the telemetry histogram to update.
|
||||
* @param {integer} value Value to add to the histogram.
|
||||
*/
|
||||
telemetryAdd: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(histogramId, value) {
|
||||
Services.telemetry.getHistogramById(histogramId).add(value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a new GUID (UUID) in curly braces format.
|
||||
*/
|
||||
generateUUID: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function() {
|
||||
return MozLoopService.generateUUID();
|
||||
}
|
||||
},
|
||||
|
||||
getAudioBlob: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(name, callback) {
|
||||
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
||||
.createInstance(Ci.nsIXMLHttpRequest);
|
||||
let url = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
|
||||
|
||||
request.open("GET", url, true);
|
||||
request.responseType = "arraybuffer";
|
||||
request.onload = () => {
|
||||
if (request.status < 200 || request.status >= 300) {
|
||||
let error = new Error(request.status + " " + request.statusText);
|
||||
callback(cloneValueInto(error, targetWindow));
|
||||
return;
|
||||
}
|
||||
|
||||
let blob = new Blob([request.response], {type: "audio/ogg"});
|
||||
callback(null, cloneValueInto(blob, targetWindow));
|
||||
};
|
||||
|
||||
request.send();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Associates a session-id and a call-id with a window for debugging.
|
||||
*
|
||||
* @param {string} windowId The window id.
|
||||
* @param {string} sessionId OT session id.
|
||||
* @param {string} callId The callId on the server.
|
||||
*/
|
||||
addConversationContext: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(windowId, sessionId, callid) {
|
||||
MozLoopService.addConversationContext(windowId, {
|
||||
sessionId: sessionId,
|
||||
callId: callid
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies the UITour module that an event occurred that it might be
|
||||
* interested in.
|
||||
*
|
||||
* @param {String} subject Subject of the notification
|
||||
* @param {mixed} [params] Optional parameters, providing more details to
|
||||
* the notification subject
|
||||
*/
|
||||
notifyUITour: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(subject, params) {
|
||||
UITour.notify(subject, params);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to record the screen sharing state for a window so that it can
|
||||
* be reflected on the toolbar button.
|
||||
*
|
||||
* @param {String} windowId The id of the conversation window the state
|
||||
* is being changed for.
|
||||
* @param {Boolean} active Whether or not screen sharing is now active.
|
||||
*/
|
||||
setScreenShareState: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(windowId, active) {
|
||||
MozLoopService.setScreenShareState(windowId, active);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function onStatusChanged(aSubject, aTopic, aData) {
|
||||
let event = new targetWindow.CustomEvent("LoopStatusChanged");
|
||||
targetWindow.dispatchEvent(event);
|
||||
};
|
||||
|
||||
function onDOMWindowDestroyed(aSubject, aTopic, aData) {
|
||||
if (targetWindow && aSubject != targetWindow)
|
||||
return;
|
||||
Services.obs.removeObserver(onDOMWindowDestroyed, "dom-window-destroyed");
|
||||
Services.obs.removeObserver(onStatusChanged, "loop-status-changed");
|
||||
};
|
||||
|
||||
let contentObj = Cu.createObjectIn(targetWindow);
|
||||
Object.defineProperties(contentObj, api);
|
||||
Object.seal(contentObj);
|
||||
Cu.makeObjectPropsNormal(contentObj);
|
||||
Services.obs.addObserver(onStatusChanged, "loop-status-changed", false);
|
||||
Services.obs.addObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
|
||||
|
||||
if ("navigator" in targetWindow) {
|
||||
targetWindow.navigator.wrappedJSObject.__defineGetter__("mozLoop", function () {
|
||||
// We do this in a getter, so that we create these objects
|
||||
// only on demand (this is a potential concern, since
|
||||
// otherwise we might add one per iframe, and keep them
|
||||
// alive for as long as the window is alive).
|
||||
delete targetWindow.navigator.wrappedJSObject.mozLoop;
|
||||
return targetWindow.navigator.wrappedJSObject.mozLoop = contentObj;
|
||||
});
|
||||
|
||||
// Handle window.close correctly on the panel and chatbox.
|
||||
hookWindowCloseForPanelClose(targetWindow);
|
||||
} else {
|
||||
// This isn't a window; but it should be a JS scope; used for testing
|
||||
return targetWindow.mozLoop = contentObj;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getChromeWindow(contentWin) {
|
||||
return contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShellTreeItem)
|
||||
.rootTreeItem
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"ecmaFeatures": {
|
||||
// These are on for this directory for .jsm and content/js files.
|
||||
"blockBindings": true,
|
||||
"arrowFunctions": true,
|
||||
"destructuring": true,
|
||||
"generators": true,
|
||||
"spread": true,
|
||||
"restParams": true,
|
||||
"objectLiteralShorthandMethods": true
|
||||
},
|
||||
"rules": {
|
||||
"generator-star-spacing": [2, "after"]
|
||||
}
|
||||
}
|
||||
@@ -1,966 +0,0 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
/* 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/. */
|
||||
|
||||
/*jshint newcap:false*/
|
||||
/*global loop:true, React */
|
||||
|
||||
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 sharedUtils = loop.shared.utils;
|
||||
var Button = sharedViews.Button;
|
||||
var ButtonGroup = sharedViews.ButtonGroup;
|
||||
var ContactsList = loop.contacts.ContactsList;
|
||||
var ContactDetailsForm = loop.contacts.ContactDetailsForm;
|
||||
|
||||
var TabView = React.createClass({displayName: "TabView",
|
||||
propTypes: {
|
||||
buttonsHidden: React.PropTypes.array,
|
||||
// The selectedTab prop is used by the UI showcase.
|
||||
selectedTab: React.PropTypes.string,
|
||||
mozLoop: React.PropTypes.object
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
buttonsHidden: []
|
||||
};
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
var tabChange = this.state.selectedTab !== nextState.selectedTab;
|
||||
if (tabChange) {
|
||||
this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab);
|
||||
}
|
||||
|
||||
if (!tabChange && nextProps.buttonsHidden) {
|
||||
if (nextProps.buttonsHidden.length !== this.props.buttonsHidden.length) {
|
||||
tabChange = true;
|
||||
} else {
|
||||
for (var i = 0, l = nextProps.buttonsHidden.length; i < l && !tabChange; ++i) {
|
||||
if (this.props.buttonsHidden.indexOf(nextProps.buttonsHidden[i]) === -1) {
|
||||
tabChange = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tabChange;
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
// XXX Work around props.selectedTab being undefined initially.
|
||||
// When we don't need to rely on the pref, this can move back to
|
||||
// getDefaultProps (bug 1100258).
|
||||
return {
|
||||
selectedTab: this.props.selectedTab || "rooms"
|
||||
};
|
||||
},
|
||||
|
||||
handleSelectTab: function(event) {
|
||||
var tabName = event.target.dataset.tabName;
|
||||
this.setState({selectedTab: tabName});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
var tabButtons = [];
|
||||
var tabs = [];
|
||||
React.Children.forEach(this.props.children, function(tab, i) {
|
||||
// Filter out null tabs (eg. rooms when the feature is disabled)
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
var tabName = tab.props.name;
|
||||
if (this.props.buttonsHidden.indexOf(tabName) > -1) {
|
||||
return;
|
||||
}
|
||||
var isSelected = (this.state.selectedTab == tabName);
|
||||
if (!tab.props.hidden) {
|
||||
tabButtons.push(
|
||||
React.createElement("li", {className: cx({selected: isSelected}),
|
||||
key: i,
|
||||
"data-tab-name": tabName,
|
||||
title: mozL10n.get(tabName + "_tab_button_tooltip"),
|
||||
onClick: this.handleSelectTab})
|
||||
);
|
||||
}
|
||||
tabs.push(
|
||||
React.createElement("div", {key: i, className: cx({tab: true, selected: isSelected})},
|
||||
tab.props.children
|
||||
)
|
||||
);
|
||||
}, this);
|
||||
return (
|
||||
React.createElement("div", {className: "tab-view-container"},
|
||||
React.createElement("ul", {className: "tab-view"}, tabButtons),
|
||||
tabs
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Tab = React.createClass({displayName: "Tab",
|
||||
render: function() {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Availability drop down menu subview.
|
||||
*/
|
||||
var AvailabilityDropdown = React.createClass({displayName: "AvailabilityDropdown",
|
||||
mixins: [sharedMixins.DropdownMenuMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
doNotDisturb: navigator.mozLoop.doNotDisturb
|
||||
};
|
||||
},
|
||||
|
||||
// XXX target event can either be the li, the span or the i tag
|
||||
// this makes it easier to figure out the target by making a
|
||||
// closure with the desired status already passed in.
|
||||
changeAvailability: function(newAvailabilty) {
|
||||
return function(event) {
|
||||
// Note: side effect!
|
||||
switch (newAvailabilty) {
|
||||
case 'available':
|
||||
this.setState({doNotDisturb: false});
|
||||
navigator.mozLoop.doNotDisturb = false;
|
||||
break;
|
||||
case 'do-not-disturb':
|
||||
this.setState({doNotDisturb: true});
|
||||
navigator.mozLoop.doNotDisturb = true;
|
||||
break;
|
||||
}
|
||||
this.hideDropdownMenu();
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// XXX https://github.com/facebook/react/issues/310 for === htmlFor
|
||||
var cx = React.addons.classSet;
|
||||
var availabilityStatus = cx({
|
||||
'status': true,
|
||||
'status-dnd': this.state.doNotDisturb,
|
||||
'status-available': !this.state.doNotDisturb
|
||||
});
|
||||
var availabilityDropdown = cx({
|
||||
'dropdown-menu': true,
|
||||
'hide': !this.state.showMenu
|
||||
});
|
||||
var availabilityText = this.state.doNotDisturb ?
|
||||
mozL10n.get("display_name_dnd_status") :
|
||||
mozL10n.get("display_name_available_status");
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "dropdown"},
|
||||
React.createElement("p", {className: "dnd-status", onClick: this.toggleDropdownMenu, ref: "menu-button"},
|
||||
React.createElement("span", null, availabilityText),
|
||||
React.createElement("i", {className: availabilityStatus})
|
||||
),
|
||||
React.createElement("ul", {className: availabilityDropdown},
|
||||
React.createElement("li", {onClick: this.changeAvailability("available"),
|
||||
className: "dropdown-menu-item dnd-make-available"},
|
||||
React.createElement("i", {className: "status status-available"}),
|
||||
React.createElement("span", null, mozL10n.get("display_name_available_status"))
|
||||
),
|
||||
React.createElement("li", {onClick: this.changeAvailability("do-not-disturb"),
|
||||
className: "dropdown-menu-item dnd-make-unavailable"},
|
||||
React.createElement("i", {className: "status status-dnd"}),
|
||||
React.createElement("span", null, mozL10n.get("display_name_dnd_status"))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var GettingStartedView = React.createClass({displayName: "GettingStartedView",
|
||||
mixins: [sharedMixins.WindowCloseMixin],
|
||||
|
||||
handleButtonClick: function() {
|
||||
navigator.mozLoop.openGettingStartedTour("getting-started");
|
||||
navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
|
||||
var event = new CustomEvent("GettingStartedSeen");
|
||||
window.dispatchEvent(event);
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (navigator.mozLoop.getLoopPref("gettingStarted.seen")) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
React.createElement("div", {id: "fte-getstarted"},
|
||||
React.createElement("header", {id: "fte-title"},
|
||||
mozL10n.get("first_time_experience_title", {
|
||||
"clientShortname": mozL10n.get("clientShortname2")
|
||||
})
|
||||
),
|
||||
React.createElement(Button, {htmlId: "fte-button",
|
||||
onClick: this.handleButtonClick,
|
||||
caption: mozL10n.get("first_time_experience_button_label")})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ToSView = React.createClass({displayName: "ToSView",
|
||||
mixins: [sharedMixins.WindowCloseMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
var getPref = navigator.mozLoop.getLoopPref.bind(navigator.mozLoop);
|
||||
|
||||
return {
|
||||
seenToS: getPref("seenToS"),
|
||||
gettingStartedSeen: getPref("gettingStarted.seen"),
|
||||
showPartnerLogo: getPref("showPartnerLogo")
|
||||
};
|
||||
},
|
||||
|
||||
handleLinkClick: function(event) {
|
||||
if (!event.target || !event.target.href) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
navigator.mozLoop.openURL(event.target.href);
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
renderPartnerLogo: function() {
|
||||
if (!this.state.showPartnerLogo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var locale = mozL10n.getLanguage();
|
||||
navigator.mozLoop.setLoopPref('showPartnerLogo', false);
|
||||
return (
|
||||
React.createElement("p", {id: "powered-by", className: "powered-by"},
|
||||
mozL10n.get("powered_by_beforeLogo"),
|
||||
React.createElement("img", {id: "powered-by-logo", className: locale}),
|
||||
mozL10n.get("powered_by_afterLogo")
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.state.gettingStartedSeen || this.state.seenToS == "unseen") {
|
||||
var terms_of_use_url = navigator.mozLoop.getLoopPref('legal.ToS_url');
|
||||
var privacy_notice_url = navigator.mozLoop.getLoopPref('legal.privacy_url');
|
||||
var tosHTML = mozL10n.get("legal_text_and_links3", {
|
||||
"clientShortname": mozL10n.get("clientShortname2"),
|
||||
"terms_of_use": React.renderToStaticMarkup(
|
||||
React.createElement("a", {href: terms_of_use_url, target: "_blank"},
|
||||
mozL10n.get("legal_text_tos")
|
||||
)
|
||||
),
|
||||
"privacy_notice": React.renderToStaticMarkup(
|
||||
React.createElement("a", {href: privacy_notice_url, target: "_blank"},
|
||||
mozL10n.get("legal_text_privacy")
|
||||
)
|
||||
)
|
||||
});
|
||||
return (
|
||||
React.createElement("div", {id: "powered-by-wrapper"},
|
||||
this.renderPartnerLogo(),
|
||||
React.createElement("p", {className: "terms-service",
|
||||
dangerouslySetInnerHTML: {__html: tosHTML},
|
||||
onClick: this.handleLinkClick})
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return React.createElement("div", null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel settings (gear) menu entry.
|
||||
*/
|
||||
var SettingsDropdownEntry = React.createClass({displayName: "SettingsDropdownEntry",
|
||||
propTypes: {
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
label: React.PropTypes.string.isRequired,
|
||||
icon: React.PropTypes.string,
|
||||
displayed: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {displayed: true};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.props.displayed) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
React.createElement("li", {onClick: this.props.onClick, className: "dropdown-menu-item"},
|
||||
this.props.icon ?
|
||||
React.createElement("i", {className: "icon icon-" + this.props.icon}) :
|
||||
null,
|
||||
React.createElement("span", null, this.props.label)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel settings (gear) menu.
|
||||
*/
|
||||
var SettingsDropdown = React.createClass({displayName: "SettingsDropdown",
|
||||
propTypes: {
|
||||
mozLoop: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
|
||||
|
||||
handleClickSettingsEntry: function() {
|
||||
// XXX to be implemented at the same time as unhiding the entry
|
||||
},
|
||||
|
||||
handleClickAccountEntry: function() {
|
||||
this.props.mozLoop.openFxASettings();
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
handleClickAuthEntry: function() {
|
||||
if (this._isSignedIn()) {
|
||||
this.props.mozLoop.logOutFromFxA();
|
||||
} else {
|
||||
this.props.mozLoop.logInToFxA();
|
||||
}
|
||||
},
|
||||
|
||||
handleHelpEntry: function(event) {
|
||||
event.preventDefault();
|
||||
var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url");
|
||||
this.props.mozLoop.openURL(helloSupportUrl);
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
_isSignedIn: function() {
|
||||
return !!this.props.mozLoop.userProfile;
|
||||
},
|
||||
|
||||
openGettingStartedTour: function() {
|
||||
this.props.mozLoop.openGettingStartedTour("settings-menu");
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "settings-menu dropdown"},
|
||||
React.createElement("a", {className: "button-settings",
|
||||
onClick: this.toggleDropdownMenu,
|
||||
title: mozL10n.get("settings_menu_button_tooltip"),
|
||||
ref: "menu-button"}),
|
||||
React.createElement("ul", {className: cx({"dropdown-menu": true, hide: !this.state.showMenu})},
|
||||
React.createElement(SettingsDropdownEntry, {label: mozL10n.get("settings_menu_item_settings"),
|
||||
onClick: this.handleClickSettingsEntry,
|
||||
displayed: false,
|
||||
icon: "settings"}),
|
||||
React.createElement(SettingsDropdownEntry, {label: mozL10n.get("settings_menu_item_account"),
|
||||
onClick: this.handleClickAccountEntry,
|
||||
icon: "account",
|
||||
displayed: this._isSignedIn() && this.props.mozLoop.fxAEnabled}),
|
||||
React.createElement(SettingsDropdownEntry, {icon: "tour",
|
||||
label: mozL10n.get("tour_label"),
|
||||
onClick: this.openGettingStartedTour}),
|
||||
React.createElement(SettingsDropdownEntry, {label: this._isSignedIn() ?
|
||||
mozL10n.get("settings_menu_item_signout") :
|
||||
mozL10n.get("settings_menu_item_signin"),
|
||||
onClick: this.handleClickAuthEntry,
|
||||
displayed: this.props.mozLoop.fxAEnabled,
|
||||
icon: this._isSignedIn() ? "signout" : "signin"}),
|
||||
React.createElement(SettingsDropdownEntry, {label: mozL10n.get("help_label"),
|
||||
onClick: this.handleHelpEntry,
|
||||
icon: "help"})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* FxA sign in/up link component.
|
||||
*/
|
||||
var AuthLink = React.createClass({displayName: "AuthLink",
|
||||
mixins: [sharedMixins.WindowCloseMixin],
|
||||
|
||||
handleSignUpLinkClick: function() {
|
||||
navigator.mozLoop.logInToFxA();
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!navigator.mozLoop.fxAEnabled || navigator.mozLoop.userProfile) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
React.createElement("p", {className: "signin-link"},
|
||||
React.createElement("a", {href: "#", onClick: this.handleSignUpLinkClick},
|
||||
mozL10n.get("panel_footer_signin_or_signup_link")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* FxA user identity (guest/authenticated) component.
|
||||
*/
|
||||
var UserIdentity = React.createClass({displayName: "UserIdentity",
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("p", {className: "user-identity"},
|
||||
this.props.displayName
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var RoomEntryContextItem = React.createClass({displayName: "RoomEntryContextItem",
|
||||
mixins: [loop.shared.mixins.WindowCloseMixin],
|
||||
|
||||
propTypes: {
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
roomUrls: React.PropTypes.array
|
||||
},
|
||||
|
||||
handleClick: function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.props.mozLoop.openURL(event.currentTarget.href);
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var roomUrl = this.props.roomUrls && this.props.roomUrls[0];
|
||||
if (!roomUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "room-entry-context-item"},
|
||||
React.createElement("a", {href: roomUrl.location, onClick: this.handleClick},
|
||||
React.createElement("img", {title: roomUrl.description,
|
||||
src: roomUrl.thumbnail})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Room list entry.
|
||||
*/
|
||||
var RoomEntry = React.createClass({displayName: "RoomEntry",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
room: React.PropTypes.instanceOf(loop.store.Room).isRequired
|
||||
},
|
||||
|
||||
mixins: [loop.shared.mixins.WindowCloseMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return { urlCopied: false };
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
return (nextProps.room.ctime > this.props.room.ctime) ||
|
||||
(nextState.urlCopied !== this.state.urlCopied);
|
||||
},
|
||||
|
||||
handleClickEntry: function(event) {
|
||||
event.preventDefault();
|
||||
this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
|
||||
roomToken: this.props.room.roomToken
|
||||
}));
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
handleCopyButtonClick: function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
|
||||
roomUrl: this.props.room.roomUrl
|
||||
}));
|
||||
this.setState({urlCopied: true});
|
||||
},
|
||||
|
||||
handleDeleteButtonClick: function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.props.mozLoop.confirm({
|
||||
message: mozL10n.get("rooms_list_deleteConfirmation_label"),
|
||||
okButton: null,
|
||||
cancelButton: null
|
||||
}, function(err, result) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({
|
||||
roomToken: this.props.room.roomToken
|
||||
}));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
handleMouseLeave: function(event) {
|
||||
this.setState({urlCopied: false});
|
||||
},
|
||||
|
||||
_isActive: function() {
|
||||
return this.props.room.participants.length > 0;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var roomClasses = React.addons.classSet({
|
||||
"room-entry": true,
|
||||
"room-active": this._isActive()
|
||||
});
|
||||
var copyButtonClasses = React.addons.classSet({
|
||||
"copy-link": true,
|
||||
"checked": this.state.urlCopied
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: roomClasses, onMouseLeave: this.handleMouseLeave,
|
||||
onClick: this.handleClickEntry},
|
||||
React.createElement("h2", null,
|
||||
React.createElement("span", {className: "room-notification"}),
|
||||
this.props.room.decryptedContext.roomName,
|
||||
React.createElement("button", {className: copyButtonClasses,
|
||||
title: mozL10n.get("rooms_list_copy_url_tooltip"),
|
||||
onClick: this.handleCopyButtonClick}),
|
||||
React.createElement("button", {className: "delete-link",
|
||||
title: mozL10n.get("rooms_list_delete_tooltip"),
|
||||
onClick: this.handleDeleteButtonClick})
|
||||
),
|
||||
React.createElement(RoomEntryContextItem, {mozLoop: this.props.mozLoop,
|
||||
roomUrls: this.props.room.decryptedContext.urls})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Room list.
|
||||
*/
|
||||
var RoomList = React.createClass({displayName: "RoomList",
|
||||
mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
|
||||
|
||||
propTypes: {
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
userDisplayName: React.PropTypes.string.isRequired // for room creation
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this.props.store.getStoreState();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.listenTo(this.props.store, "change", this._onStoreStateChanged);
|
||||
|
||||
// XXX this should no longer be necessary once have a better mechanism
|
||||
// for updating the list (possibly as part of the content side of bug
|
||||
// 1074665.
|
||||
this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
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());
|
||||
},
|
||||
|
||||
_getListHeading: function() {
|
||||
var numRooms = this.state.rooms.length;
|
||||
if (numRooms === 0) {
|
||||
return mozL10n.get("rooms_list_no_current_conversations");
|
||||
}
|
||||
return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.error) {
|
||||
// XXX Better end user reporting of errors.
|
||||
console.error("RoomList error", this.state.error);
|
||||
}
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "rooms"},
|
||||
React.createElement("h1", null, this._getListHeading()),
|
||||
React.createElement("div", {className: "room-list"},
|
||||
this.state.rooms.map(function(room, i) {
|
||||
return (
|
||||
React.createElement(RoomEntry, {
|
||||
key: room.roomToken,
|
||||
dispatcher: this.props.dispatcher,
|
||||
mozLoop: this.props.mozLoop,
|
||||
room: room}
|
||||
)
|
||||
);
|
||||
}, this)
|
||||
),
|
||||
React.createElement(NewRoomView, {dispatcher: this.props.dispatcher,
|
||||
mozLoop: this.props.mozLoop,
|
||||
pendingOperation: this.state.pendingCreation ||
|
||||
this.state.pendingInitialRetrieval,
|
||||
userDisplayName: this.props.userDisplayName})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Used for creating a new room with or without context.
|
||||
*/
|
||||
var NewRoomView = React.createClass({displayName: "NewRoomView",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
pendingOperation: React.PropTypes.bool.isRequired,
|
||||
userDisplayName: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
mixins: [sharedMixins.DocumentVisibilityMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
checked: false,
|
||||
previewImage: "",
|
||||
description: "",
|
||||
url: ""
|
||||
};
|
||||
},
|
||||
|
||||
onDocumentVisible: function() {
|
||||
this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) {
|
||||
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
|
||||
var description = metadata.description || metadata.title;
|
||||
var url = metadata.url;
|
||||
this.setState({previewImage: previewImage,
|
||||
description: description,
|
||||
url: url});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
onDocumentHidden: function() {
|
||||
this.setState({previewImage: "",
|
||||
description: "",
|
||||
url: ""});
|
||||
},
|
||||
|
||||
onCheckboxChange: function(event) {
|
||||
this.setState({checked: event.target.checked});
|
||||
},
|
||||
|
||||
handleCreateButtonClick: function() {
|
||||
var createRoomAction = new sharedActions.CreateRoom({
|
||||
nameTemplate: mozL10n.get("rooms_default_room_name_template"),
|
||||
roomOwner: this.props.userDisplayName
|
||||
});
|
||||
|
||||
if (this.state.checked) {
|
||||
createRoomAction.urls = [{
|
||||
location: this.state.url,
|
||||
description: this.state.description,
|
||||
thumbnail: this.state.previewImage
|
||||
}];
|
||||
}
|
||||
this.props.dispatcher.dispatch(createRoomAction);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var hostname;
|
||||
|
||||
try {
|
||||
hostname = new URL(this.state.url).hostname;
|
||||
} catch (ex) {
|
||||
// Empty catch - if there's an error, then we won't show the context.
|
||||
}
|
||||
|
||||
var contextClasses = React.addons.classSet({
|
||||
context: true,
|
||||
hide: !hostname ||
|
||||
!this.props.mozLoop.getLoopPref("contextInConversations.enabled")
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "new-room-view"},
|
||||
React.createElement("div", {className: contextClasses},
|
||||
React.createElement("label", {className: "context-enabled"},
|
||||
React.createElement("input", {className: "context-checkbox",
|
||||
type: "checkbox", onChange: this.onCheckboxChange}),
|
||||
mozL10n.get("context_offer_label")
|
||||
),
|
||||
React.createElement("img", {className: "context-preview", src: this.state.previewImage}),
|
||||
React.createElement("span", {className: "context-description"}, this.state.description),
|
||||
React.createElement("span", {className: "context-url"}, hostname)
|
||||
),
|
||||
React.createElement("button", {className: "btn btn-info new-room-button",
|
||||
onClick: this.handleCreateButtonClick,
|
||||
disabled: this.props.pendingOperation},
|
||||
mozL10n.get("rooms_new_room_button_label")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel view.
|
||||
*/
|
||||
var PanelView = React.createClass({displayName: "PanelView",
|
||||
propTypes: {
|
||||
notifications: React.PropTypes.object.isRequired,
|
||||
// Mostly used for UI components showcase and unit tests
|
||||
userProfile: React.PropTypes.object,
|
||||
// Used only for unit tests.
|
||||
showTabButtons: React.PropTypes.bool,
|
||||
selectedTab: React.PropTypes.string,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
roomStore:
|
||||
React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
userProfile: this.props.userProfile || this.props.mozLoop.userProfile,
|
||||
gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen")
|
||||
};
|
||||
},
|
||||
|
||||
_serviceErrorToShow: function() {
|
||||
if (!this.props.mozLoop.errors ||
|
||||
!Object.keys(this.props.mozLoop.errors).length) {
|
||||
return null;
|
||||
}
|
||||
// Just get the first error for now since more than one should be rare.
|
||||
var firstErrorKey = Object.keys(this.props.mozLoop.errors)[0];
|
||||
return {
|
||||
type: firstErrorKey,
|
||||
error: this.props.mozLoop.errors[firstErrorKey]
|
||||
};
|
||||
},
|
||||
|
||||
updateServiceErrors: function() {
|
||||
var serviceError = this._serviceErrorToShow();
|
||||
if (serviceError) {
|
||||
this.props.notifications.set({
|
||||
id: "service-error",
|
||||
level: "error",
|
||||
message: serviceError.error.friendlyMessage,
|
||||
details: serviceError.error.friendlyDetails,
|
||||
detailsButtonLabel: serviceError.error.friendlyDetailsButtonLabel,
|
||||
detailsButtonCallback: serviceError.error.friendlyDetailsButtonCallback
|
||||
});
|
||||
} else {
|
||||
this.props.notifications.remove(this.props.notifications.get("service-error"));
|
||||
}
|
||||
},
|
||||
|
||||
_onStatusChanged: function() {
|
||||
var profile = this.props.mozLoop.userProfile;
|
||||
var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
|
||||
var newUid = profile ? profile.uid : null;
|
||||
if (currUid != newUid) {
|
||||
// On profile change (login, logout), switch back to the default tab.
|
||||
this.selectTab("rooms");
|
||||
this.setState({userProfile: profile});
|
||||
}
|
||||
this.updateServiceErrors();
|
||||
},
|
||||
|
||||
_gettingStartedSeen: function() {
|
||||
this.setState({
|
||||
gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen")
|
||||
});
|
||||
},
|
||||
|
||||
_UIActionHandler: function(e) {
|
||||
switch (e.detail.action) {
|
||||
case "selectTab":
|
||||
this.selectTab(e.detail.tab);
|
||||
break;
|
||||
default:
|
||||
console.error("Invalid action", e.detail.action);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
startForm: function(name, contact) {
|
||||
this.refs[name].initForm(contact);
|
||||
this.selectTab(name);
|
||||
},
|
||||
|
||||
selectTab: function(name) {
|
||||
this.refs.tabView.setState({ selectedTab: name });
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.updateServiceErrors();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
window.addEventListener("LoopStatusChanged", this._onStatusChanged);
|
||||
window.addEventListener("GettingStartedSeen", this._gettingStartedSeen);
|
||||
window.addEventListener("UIAction", this._UIActionHandler);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
|
||||
window.removeEventListener("GettingStartedSeen", this._gettingStartedSeen);
|
||||
window.removeEventListener("UIAction", this._UIActionHandler);
|
||||
},
|
||||
|
||||
_getUserDisplayName: function() {
|
||||
return this.state.userProfile && this.state.userProfile.email ||
|
||||
mozL10n.get("display_name_guest");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var NotificationListView = sharedViews.NotificationListView;
|
||||
|
||||
if (!this.state.gettingStartedSeen) {
|
||||
return (
|
||||
React.createElement("div", null,
|
||||
React.createElement(NotificationListView, {notifications: this.props.notifications,
|
||||
clearOnDocumentHidden: true}),
|
||||
React.createElement(GettingStartedView, null),
|
||||
React.createElement(ToSView, null)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Determine which buttons to NOT show.
|
||||
var hideButtons = [];
|
||||
if (!this.state.userProfile && !this.props.showTabButtons) {
|
||||
hideButtons.push("contacts");
|
||||
}
|
||||
|
||||
return (
|
||||
React.createElement("div", null,
|
||||
React.createElement(NotificationListView, {notifications: this.props.notifications,
|
||||
clearOnDocumentHidden: true}),
|
||||
React.createElement(TabView, {ref: "tabView", selectedTab: this.props.selectedTab,
|
||||
buttonsHidden: hideButtons, mozLoop: this.props.mozLoop},
|
||||
React.createElement(Tab, {name: "rooms"},
|
||||
React.createElement(RoomList, {dispatcher: this.props.dispatcher,
|
||||
store: this.props.roomStore,
|
||||
userDisplayName: this._getUserDisplayName(),
|
||||
mozLoop: this.props.mozLoop}),
|
||||
React.createElement(ToSView, null)
|
||||
),
|
||||
React.createElement(Tab, {name: "contacts"},
|
||||
React.createElement(ContactsList, {selectTab: this.selectTab,
|
||||
startForm: this.startForm,
|
||||
notifications: this.props.notifications})
|
||||
),
|
||||
React.createElement(Tab, {name: "contacts_add", hidden: true},
|
||||
React.createElement(ContactDetailsForm, {ref: "contacts_add", mode: "add",
|
||||
selectTab: this.selectTab})
|
||||
),
|
||||
React.createElement(Tab, {name: "contacts_edit", hidden: true},
|
||||
React.createElement(ContactDetailsForm, {ref: "contacts_edit", mode: "edit",
|
||||
selectTab: this.selectTab})
|
||||
),
|
||||
React.createElement(Tab, {name: "contacts_import", hidden: true},
|
||||
React.createElement(ContactDetailsForm, {ref: "contacts_import", mode: "import",
|
||||
selectTab: this.selectTab})
|
||||
)
|
||||
),
|
||||
React.createElement("div", {className: "footer"},
|
||||
React.createElement("div", {className: "user-details"},
|
||||
React.createElement(UserIdentity, {displayName: this._getUserDisplayName()}),
|
||||
React.createElement(AvailabilityDropdown, null)
|
||||
),
|
||||
React.createElement("div", {className: "signin-details"},
|
||||
React.createElement(AuthLink, null),
|
||||
React.createElement("div", {className: "footer-signin-separator"}),
|
||||
React.createElement(SettingsDropdown, {mozLoop: this.props.mozLoop})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel initialisation.
|
||||
*/
|
||||
function init() {
|
||||
// Do the initial L10n setup, we do this before anything
|
||||
// else to ensure the L10n environment is setup correctly.
|
||||
mozL10n.initialize(navigator.mozLoop);
|
||||
|
||||
var notifications = new sharedModels.NotificationCollection();
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
var roomStore = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop,
|
||||
notifications: notifications
|
||||
});
|
||||
|
||||
React.render(React.createElement(PanelView, {
|
||||
notifications: notifications,
|
||||
roomStore: roomStore,
|
||||
mozLoop: navigator.mozLoop,
|
||||
dispatcher: dispatcher}
|
||||
), document.querySelector("#main"));
|
||||
|
||||
document.body.setAttribute("dir", mozL10n.getDirection());
|
||||
|
||||
// Notify the window that we've finished initalization and initial layout
|
||||
var evtObject = document.createEvent('Event');
|
||||
evtObject.initEvent('loopPanelInitialized', true, false);
|
||||
window.dispatchEvent(evtObject);
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
AuthLink: AuthLink,
|
||||
AvailabilityDropdown: AvailabilityDropdown,
|
||||
GettingStartedView: GettingStartedView,
|
||||
NewRoomView: NewRoomView,
|
||||
PanelView: PanelView,
|
||||
RoomEntry: RoomEntry,
|
||||
RoomList: RoomList,
|
||||
SettingsDropdown: SettingsDropdown,
|
||||
ToSView: ToSView,
|
||||
UserIdentity: UserIdentity
|
||||
};
|
||||
})(_, document.mozL10n);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loop.panel.init);
|
||||
@@ -1,966 +0,0 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
/* 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/. */
|
||||
|
||||
/*jshint newcap:false*/
|
||||
/*global loop:true, React */
|
||||
|
||||
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 sharedUtils = loop.shared.utils;
|
||||
var Button = sharedViews.Button;
|
||||
var ButtonGroup = sharedViews.ButtonGroup;
|
||||
var ContactsList = loop.contacts.ContactsList;
|
||||
var ContactDetailsForm = loop.contacts.ContactDetailsForm;
|
||||
|
||||
var TabView = React.createClass({
|
||||
propTypes: {
|
||||
buttonsHidden: React.PropTypes.array,
|
||||
// The selectedTab prop is used by the UI showcase.
|
||||
selectedTab: React.PropTypes.string,
|
||||
mozLoop: React.PropTypes.object
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
buttonsHidden: []
|
||||
};
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
var tabChange = this.state.selectedTab !== nextState.selectedTab;
|
||||
if (tabChange) {
|
||||
this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab);
|
||||
}
|
||||
|
||||
if (!tabChange && nextProps.buttonsHidden) {
|
||||
if (nextProps.buttonsHidden.length !== this.props.buttonsHidden.length) {
|
||||
tabChange = true;
|
||||
} else {
|
||||
for (var i = 0, l = nextProps.buttonsHidden.length; i < l && !tabChange; ++i) {
|
||||
if (this.props.buttonsHidden.indexOf(nextProps.buttonsHidden[i]) === -1) {
|
||||
tabChange = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tabChange;
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
// XXX Work around props.selectedTab being undefined initially.
|
||||
// When we don't need to rely on the pref, this can move back to
|
||||
// getDefaultProps (bug 1100258).
|
||||
return {
|
||||
selectedTab: this.props.selectedTab || "rooms"
|
||||
};
|
||||
},
|
||||
|
||||
handleSelectTab: function(event) {
|
||||
var tabName = event.target.dataset.tabName;
|
||||
this.setState({selectedTab: tabName});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
var tabButtons = [];
|
||||
var tabs = [];
|
||||
React.Children.forEach(this.props.children, function(tab, i) {
|
||||
// Filter out null tabs (eg. rooms when the feature is disabled)
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
var tabName = tab.props.name;
|
||||
if (this.props.buttonsHidden.indexOf(tabName) > -1) {
|
||||
return;
|
||||
}
|
||||
var isSelected = (this.state.selectedTab == tabName);
|
||||
if (!tab.props.hidden) {
|
||||
tabButtons.push(
|
||||
<li className={cx({selected: isSelected})}
|
||||
key={i}
|
||||
data-tab-name={tabName}
|
||||
title={mozL10n.get(tabName + "_tab_button_tooltip")}
|
||||
onClick={this.handleSelectTab} />
|
||||
);
|
||||
}
|
||||
tabs.push(
|
||||
<div key={i} className={cx({tab: true, selected: isSelected})}>
|
||||
{tab.props.children}
|
||||
</div>
|
||||
);
|
||||
}, this);
|
||||
return (
|
||||
<div className="tab-view-container">
|
||||
<ul className="tab-view">{tabButtons}</ul>
|
||||
{tabs}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Tab = React.createClass({
|
||||
render: function() {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Availability drop down menu subview.
|
||||
*/
|
||||
var AvailabilityDropdown = React.createClass({
|
||||
mixins: [sharedMixins.DropdownMenuMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
doNotDisturb: navigator.mozLoop.doNotDisturb
|
||||
};
|
||||
},
|
||||
|
||||
// XXX target event can either be the li, the span or the i tag
|
||||
// this makes it easier to figure out the target by making a
|
||||
// closure with the desired status already passed in.
|
||||
changeAvailability: function(newAvailabilty) {
|
||||
return function(event) {
|
||||
// Note: side effect!
|
||||
switch (newAvailabilty) {
|
||||
case 'available':
|
||||
this.setState({doNotDisturb: false});
|
||||
navigator.mozLoop.doNotDisturb = false;
|
||||
break;
|
||||
case 'do-not-disturb':
|
||||
this.setState({doNotDisturb: true});
|
||||
navigator.mozLoop.doNotDisturb = true;
|
||||
break;
|
||||
}
|
||||
this.hideDropdownMenu();
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// XXX https://github.com/facebook/react/issues/310 for === htmlFor
|
||||
var cx = React.addons.classSet;
|
||||
var availabilityStatus = cx({
|
||||
'status': true,
|
||||
'status-dnd': this.state.doNotDisturb,
|
||||
'status-available': !this.state.doNotDisturb
|
||||
});
|
||||
var availabilityDropdown = cx({
|
||||
'dropdown-menu': true,
|
||||
'hide': !this.state.showMenu
|
||||
});
|
||||
var availabilityText = this.state.doNotDisturb ?
|
||||
mozL10n.get("display_name_dnd_status") :
|
||||
mozL10n.get("display_name_available_status");
|
||||
|
||||
return (
|
||||
<div className="dropdown">
|
||||
<p className="dnd-status" onClick={this.toggleDropdownMenu} ref="menu-button">
|
||||
<span>{availabilityText}</span>
|
||||
<i className={availabilityStatus}></i>
|
||||
</p>
|
||||
<ul className={availabilityDropdown}>
|
||||
<li onClick={this.changeAvailability("available")}
|
||||
className="dropdown-menu-item dnd-make-available">
|
||||
<i className="status status-available"></i>
|
||||
<span>{mozL10n.get("display_name_available_status")}</span>
|
||||
</li>
|
||||
<li onClick={this.changeAvailability("do-not-disturb")}
|
||||
className="dropdown-menu-item dnd-make-unavailable">
|
||||
<i className="status status-dnd"></i>
|
||||
<span>{mozL10n.get("display_name_dnd_status")}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var GettingStartedView = React.createClass({
|
||||
mixins: [sharedMixins.WindowCloseMixin],
|
||||
|
||||
handleButtonClick: function() {
|
||||
navigator.mozLoop.openGettingStartedTour("getting-started");
|
||||
navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
|
||||
var event = new CustomEvent("GettingStartedSeen");
|
||||
window.dispatchEvent(event);
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (navigator.mozLoop.getLoopPref("gettingStarted.seen")) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div id="fte-getstarted">
|
||||
<header id="fte-title">
|
||||
{mozL10n.get("first_time_experience_title", {
|
||||
"clientShortname": mozL10n.get("clientShortname2")
|
||||
})}
|
||||
</header>
|
||||
<Button htmlId="fte-button"
|
||||
onClick={this.handleButtonClick}
|
||||
caption={mozL10n.get("first_time_experience_button_label")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ToSView = React.createClass({
|
||||
mixins: [sharedMixins.WindowCloseMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
var getPref = navigator.mozLoop.getLoopPref.bind(navigator.mozLoop);
|
||||
|
||||
return {
|
||||
seenToS: getPref("seenToS"),
|
||||
gettingStartedSeen: getPref("gettingStarted.seen"),
|
||||
showPartnerLogo: getPref("showPartnerLogo")
|
||||
};
|
||||
},
|
||||
|
||||
handleLinkClick: function(event) {
|
||||
if (!event.target || !event.target.href) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
navigator.mozLoop.openURL(event.target.href);
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
renderPartnerLogo: function() {
|
||||
if (!this.state.showPartnerLogo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var locale = mozL10n.getLanguage();
|
||||
navigator.mozLoop.setLoopPref('showPartnerLogo', false);
|
||||
return (
|
||||
<p id="powered-by" className="powered-by">
|
||||
{mozL10n.get("powered_by_beforeLogo")}
|
||||
<img id="powered-by-logo" className={locale} />
|
||||
{mozL10n.get("powered_by_afterLogo")}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.state.gettingStartedSeen || this.state.seenToS == "unseen") {
|
||||
var terms_of_use_url = navigator.mozLoop.getLoopPref('legal.ToS_url');
|
||||
var privacy_notice_url = navigator.mozLoop.getLoopPref('legal.privacy_url');
|
||||
var tosHTML = mozL10n.get("legal_text_and_links3", {
|
||||
"clientShortname": mozL10n.get("clientShortname2"),
|
||||
"terms_of_use": React.renderToStaticMarkup(
|
||||
<a href={terms_of_use_url} target="_blank">
|
||||
{mozL10n.get("legal_text_tos")}
|
||||
</a>
|
||||
),
|
||||
"privacy_notice": React.renderToStaticMarkup(
|
||||
<a href={privacy_notice_url} target="_blank">
|
||||
{mozL10n.get("legal_text_privacy")}
|
||||
</a>
|
||||
)
|
||||
});
|
||||
return (
|
||||
<div id="powered-by-wrapper">
|
||||
{this.renderPartnerLogo()}
|
||||
<p className="terms-service"
|
||||
dangerouslySetInnerHTML={{__html: tosHTML}}
|
||||
onClick={this.handleLinkClick}></p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel settings (gear) menu entry.
|
||||
*/
|
||||
var SettingsDropdownEntry = React.createClass({
|
||||
propTypes: {
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
label: React.PropTypes.string.isRequired,
|
||||
icon: React.PropTypes.string,
|
||||
displayed: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {displayed: true};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.props.displayed) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<li onClick={this.props.onClick} className="dropdown-menu-item">
|
||||
{this.props.icon ?
|
||||
<i className={"icon icon-" + this.props.icon}></i> :
|
||||
null}
|
||||
<span>{this.props.label}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel settings (gear) menu.
|
||||
*/
|
||||
var SettingsDropdown = React.createClass({
|
||||
propTypes: {
|
||||
mozLoop: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
|
||||
|
||||
handleClickSettingsEntry: function() {
|
||||
// XXX to be implemented at the same time as unhiding the entry
|
||||
},
|
||||
|
||||
handleClickAccountEntry: function() {
|
||||
this.props.mozLoop.openFxASettings();
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
handleClickAuthEntry: function() {
|
||||
if (this._isSignedIn()) {
|
||||
this.props.mozLoop.logOutFromFxA();
|
||||
} else {
|
||||
this.props.mozLoop.logInToFxA();
|
||||
}
|
||||
},
|
||||
|
||||
handleHelpEntry: function(event) {
|
||||
event.preventDefault();
|
||||
var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url");
|
||||
this.props.mozLoop.openURL(helloSupportUrl);
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
_isSignedIn: function() {
|
||||
return !!this.props.mozLoop.userProfile;
|
||||
},
|
||||
|
||||
openGettingStartedTour: function() {
|
||||
this.props.mozLoop.openGettingStartedTour("settings-menu");
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
|
||||
return (
|
||||
<div className="settings-menu dropdown">
|
||||
<a className="button-settings"
|
||||
onClick={this.toggleDropdownMenu}
|
||||
title={mozL10n.get("settings_menu_button_tooltip")}
|
||||
ref="menu-button" />
|
||||
<ul className={cx({"dropdown-menu": true, hide: !this.state.showMenu})}>
|
||||
<SettingsDropdownEntry label={mozL10n.get("settings_menu_item_settings")}
|
||||
onClick={this.handleClickSettingsEntry}
|
||||
displayed={false}
|
||||
icon="settings" />
|
||||
<SettingsDropdownEntry label={mozL10n.get("settings_menu_item_account")}
|
||||
onClick={this.handleClickAccountEntry}
|
||||
icon="account"
|
||||
displayed={this._isSignedIn() && this.props.mozLoop.fxAEnabled} />
|
||||
<SettingsDropdownEntry icon="tour"
|
||||
label={mozL10n.get("tour_label")}
|
||||
onClick={this.openGettingStartedTour} />
|
||||
<SettingsDropdownEntry label={this._isSignedIn() ?
|
||||
mozL10n.get("settings_menu_item_signout") :
|
||||
mozL10n.get("settings_menu_item_signin")}
|
||||
onClick={this.handleClickAuthEntry}
|
||||
displayed={this.props.mozLoop.fxAEnabled}
|
||||
icon={this._isSignedIn() ? "signout" : "signin"} />
|
||||
<SettingsDropdownEntry label={mozL10n.get("help_label")}
|
||||
onClick={this.handleHelpEntry}
|
||||
icon="help" />
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* FxA sign in/up link component.
|
||||
*/
|
||||
var AuthLink = React.createClass({
|
||||
mixins: [sharedMixins.WindowCloseMixin],
|
||||
|
||||
handleSignUpLinkClick: function() {
|
||||
navigator.mozLoop.logInToFxA();
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!navigator.mozLoop.fxAEnabled || navigator.mozLoop.userProfile) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<p className="signin-link">
|
||||
<a href="#" onClick={this.handleSignUpLinkClick}>
|
||||
{mozL10n.get("panel_footer_signin_or_signup_link")}
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* FxA user identity (guest/authenticated) component.
|
||||
*/
|
||||
var UserIdentity = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<p className="user-identity">
|
||||
{this.props.displayName}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var RoomEntryContextItem = React.createClass({
|
||||
mixins: [loop.shared.mixins.WindowCloseMixin],
|
||||
|
||||
propTypes: {
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
roomUrls: React.PropTypes.array
|
||||
},
|
||||
|
||||
handleClick: function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.props.mozLoop.openURL(event.currentTarget.href);
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var roomUrl = this.props.roomUrls && this.props.roomUrls[0];
|
||||
if (!roomUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="room-entry-context-item">
|
||||
<a href={roomUrl.location} onClick={this.handleClick}>
|
||||
<img title={roomUrl.description}
|
||||
src={roomUrl.thumbnail} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Room list entry.
|
||||
*/
|
||||
var RoomEntry = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
room: React.PropTypes.instanceOf(loop.store.Room).isRequired
|
||||
},
|
||||
|
||||
mixins: [loop.shared.mixins.WindowCloseMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return { urlCopied: false };
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
return (nextProps.room.ctime > this.props.room.ctime) ||
|
||||
(nextState.urlCopied !== this.state.urlCopied);
|
||||
},
|
||||
|
||||
handleClickEntry: function(event) {
|
||||
event.preventDefault();
|
||||
this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
|
||||
roomToken: this.props.room.roomToken
|
||||
}));
|
||||
this.closeWindow();
|
||||
},
|
||||
|
||||
handleCopyButtonClick: function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
|
||||
roomUrl: this.props.room.roomUrl
|
||||
}));
|
||||
this.setState({urlCopied: true});
|
||||
},
|
||||
|
||||
handleDeleteButtonClick: function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.props.mozLoop.confirm({
|
||||
message: mozL10n.get("rooms_list_deleteConfirmation_label"),
|
||||
okButton: null,
|
||||
cancelButton: null
|
||||
}, function(err, result) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({
|
||||
roomToken: this.props.room.roomToken
|
||||
}));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
handleMouseLeave: function(event) {
|
||||
this.setState({urlCopied: false});
|
||||
},
|
||||
|
||||
_isActive: function() {
|
||||
return this.props.room.participants.length > 0;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var roomClasses = React.addons.classSet({
|
||||
"room-entry": true,
|
||||
"room-active": this._isActive()
|
||||
});
|
||||
var copyButtonClasses = React.addons.classSet({
|
||||
"copy-link": true,
|
||||
"checked": this.state.urlCopied
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={roomClasses} onMouseLeave={this.handleMouseLeave}
|
||||
onClick={this.handleClickEntry}>
|
||||
<h2>
|
||||
<span className="room-notification" />
|
||||
{this.props.room.decryptedContext.roomName}
|
||||
<button className={copyButtonClasses}
|
||||
title={mozL10n.get("rooms_list_copy_url_tooltip")}
|
||||
onClick={this.handleCopyButtonClick} />
|
||||
<button className="delete-link"
|
||||
title={mozL10n.get("rooms_list_delete_tooltip")}
|
||||
onClick={this.handleDeleteButtonClick} />
|
||||
</h2>
|
||||
<RoomEntryContextItem mozLoop={this.props.mozLoop}
|
||||
roomUrls={this.props.room.decryptedContext.urls} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Room list.
|
||||
*/
|
||||
var RoomList = React.createClass({
|
||||
mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
|
||||
|
||||
propTypes: {
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
userDisplayName: React.PropTypes.string.isRequired // for room creation
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this.props.store.getStoreState();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.listenTo(this.props.store, "change", this._onStoreStateChanged);
|
||||
|
||||
// XXX this should no longer be necessary once have a better mechanism
|
||||
// for updating the list (possibly as part of the content side of bug
|
||||
// 1074665.
|
||||
this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
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());
|
||||
},
|
||||
|
||||
_getListHeading: function() {
|
||||
var numRooms = this.state.rooms.length;
|
||||
if (numRooms === 0) {
|
||||
return mozL10n.get("rooms_list_no_current_conversations");
|
||||
}
|
||||
return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.error) {
|
||||
// XXX Better end user reporting of errors.
|
||||
console.error("RoomList error", this.state.error);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rooms">
|
||||
<h1>{this._getListHeading()}</h1>
|
||||
<div className="room-list">{
|
||||
this.state.rooms.map(function(room, i) {
|
||||
return (
|
||||
<RoomEntry
|
||||
key={room.roomToken}
|
||||
dispatcher={this.props.dispatcher}
|
||||
mozLoop={this.props.mozLoop}
|
||||
room={room}
|
||||
/>
|
||||
);
|
||||
}, this)
|
||||
}</div>
|
||||
<NewRoomView dispatcher={this.props.dispatcher}
|
||||
mozLoop={this.props.mozLoop}
|
||||
pendingOperation={this.state.pendingCreation ||
|
||||
this.state.pendingInitialRetrieval}
|
||||
userDisplayName={this.props.userDisplayName} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Used for creating a new room with or without context.
|
||||
*/
|
||||
var NewRoomView = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
pendingOperation: React.PropTypes.bool.isRequired,
|
||||
userDisplayName: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
mixins: [sharedMixins.DocumentVisibilityMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
checked: false,
|
||||
previewImage: "",
|
||||
description: "",
|
||||
url: ""
|
||||
};
|
||||
},
|
||||
|
||||
onDocumentVisible: function() {
|
||||
this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) {
|
||||
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
|
||||
var description = metadata.description || metadata.title;
|
||||
var url = metadata.url;
|
||||
this.setState({previewImage: previewImage,
|
||||
description: description,
|
||||
url: url});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
onDocumentHidden: function() {
|
||||
this.setState({previewImage: "",
|
||||
description: "",
|
||||
url: ""});
|
||||
},
|
||||
|
||||
onCheckboxChange: function(event) {
|
||||
this.setState({checked: event.target.checked});
|
||||
},
|
||||
|
||||
handleCreateButtonClick: function() {
|
||||
var createRoomAction = new sharedActions.CreateRoom({
|
||||
nameTemplate: mozL10n.get("rooms_default_room_name_template"),
|
||||
roomOwner: this.props.userDisplayName
|
||||
});
|
||||
|
||||
if (this.state.checked) {
|
||||
createRoomAction.urls = [{
|
||||
location: this.state.url,
|
||||
description: this.state.description,
|
||||
thumbnail: this.state.previewImage
|
||||
}];
|
||||
}
|
||||
this.props.dispatcher.dispatch(createRoomAction);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var hostname;
|
||||
|
||||
try {
|
||||
hostname = new URL(this.state.url).hostname;
|
||||
} catch (ex) {
|
||||
// Empty catch - if there's an error, then we won't show the context.
|
||||
}
|
||||
|
||||
var contextClasses = React.addons.classSet({
|
||||
context: true,
|
||||
hide: !hostname ||
|
||||
!this.props.mozLoop.getLoopPref("contextInConversations.enabled")
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="new-room-view">
|
||||
<div className={contextClasses}>
|
||||
<label className="context-enabled">
|
||||
<input className="context-checkbox"
|
||||
type="checkbox" onChange={this.onCheckboxChange}/>
|
||||
{mozL10n.get("context_offer_label")}
|
||||
</label>
|
||||
<img className="context-preview" src={this.state.previewImage}/>
|
||||
<span className="context-description">{this.state.description}</span>
|
||||
<span className="context-url">{hostname}</span>
|
||||
</div>
|
||||
<button className="btn btn-info new-room-button"
|
||||
onClick={this.handleCreateButtonClick}
|
||||
disabled={this.props.pendingOperation}>
|
||||
{mozL10n.get("rooms_new_room_button_label")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel view.
|
||||
*/
|
||||
var PanelView = React.createClass({
|
||||
propTypes: {
|
||||
notifications: React.PropTypes.object.isRequired,
|
||||
// Mostly used for UI components showcase and unit tests
|
||||
userProfile: React.PropTypes.object,
|
||||
// Used only for unit tests.
|
||||
showTabButtons: React.PropTypes.bool,
|
||||
selectedTab: React.PropTypes.string,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
roomStore:
|
||||
React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
userProfile: this.props.userProfile || this.props.mozLoop.userProfile,
|
||||
gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen")
|
||||
};
|
||||
},
|
||||
|
||||
_serviceErrorToShow: function() {
|
||||
if (!this.props.mozLoop.errors ||
|
||||
!Object.keys(this.props.mozLoop.errors).length) {
|
||||
return null;
|
||||
}
|
||||
// Just get the first error for now since more than one should be rare.
|
||||
var firstErrorKey = Object.keys(this.props.mozLoop.errors)[0];
|
||||
return {
|
||||
type: firstErrorKey,
|
||||
error: this.props.mozLoop.errors[firstErrorKey]
|
||||
};
|
||||
},
|
||||
|
||||
updateServiceErrors: function() {
|
||||
var serviceError = this._serviceErrorToShow();
|
||||
if (serviceError) {
|
||||
this.props.notifications.set({
|
||||
id: "service-error",
|
||||
level: "error",
|
||||
message: serviceError.error.friendlyMessage,
|
||||
details: serviceError.error.friendlyDetails,
|
||||
detailsButtonLabel: serviceError.error.friendlyDetailsButtonLabel,
|
||||
detailsButtonCallback: serviceError.error.friendlyDetailsButtonCallback
|
||||
});
|
||||
} else {
|
||||
this.props.notifications.remove(this.props.notifications.get("service-error"));
|
||||
}
|
||||
},
|
||||
|
||||
_onStatusChanged: function() {
|
||||
var profile = this.props.mozLoop.userProfile;
|
||||
var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
|
||||
var newUid = profile ? profile.uid : null;
|
||||
if (currUid != newUid) {
|
||||
// On profile change (login, logout), switch back to the default tab.
|
||||
this.selectTab("rooms");
|
||||
this.setState({userProfile: profile});
|
||||
}
|
||||
this.updateServiceErrors();
|
||||
},
|
||||
|
||||
_gettingStartedSeen: function() {
|
||||
this.setState({
|
||||
gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen")
|
||||
});
|
||||
},
|
||||
|
||||
_UIActionHandler: function(e) {
|
||||
switch (e.detail.action) {
|
||||
case "selectTab":
|
||||
this.selectTab(e.detail.tab);
|
||||
break;
|
||||
default:
|
||||
console.error("Invalid action", e.detail.action);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
startForm: function(name, contact) {
|
||||
this.refs[name].initForm(contact);
|
||||
this.selectTab(name);
|
||||
},
|
||||
|
||||
selectTab: function(name) {
|
||||
this.refs.tabView.setState({ selectedTab: name });
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.updateServiceErrors();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
window.addEventListener("LoopStatusChanged", this._onStatusChanged);
|
||||
window.addEventListener("GettingStartedSeen", this._gettingStartedSeen);
|
||||
window.addEventListener("UIAction", this._UIActionHandler);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
|
||||
window.removeEventListener("GettingStartedSeen", this._gettingStartedSeen);
|
||||
window.removeEventListener("UIAction", this._UIActionHandler);
|
||||
},
|
||||
|
||||
_getUserDisplayName: function() {
|
||||
return this.state.userProfile && this.state.userProfile.email ||
|
||||
mozL10n.get("display_name_guest");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var NotificationListView = sharedViews.NotificationListView;
|
||||
|
||||
if (!this.state.gettingStartedSeen) {
|
||||
return (
|
||||
<div>
|
||||
<NotificationListView notifications={this.props.notifications}
|
||||
clearOnDocumentHidden={true} />
|
||||
<GettingStartedView />
|
||||
<ToSView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Determine which buttons to NOT show.
|
||||
var hideButtons = [];
|
||||
if (!this.state.userProfile && !this.props.showTabButtons) {
|
||||
hideButtons.push("contacts");
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NotificationListView notifications={this.props.notifications}
|
||||
clearOnDocumentHidden={true} />
|
||||
<TabView ref="tabView" selectedTab={this.props.selectedTab}
|
||||
buttonsHidden={hideButtons} mozLoop={this.props.mozLoop}>
|
||||
<Tab name="rooms">
|
||||
<RoomList dispatcher={this.props.dispatcher}
|
||||
store={this.props.roomStore}
|
||||
userDisplayName={this._getUserDisplayName()}
|
||||
mozLoop={this.props.mozLoop}/>
|
||||
<ToSView />
|
||||
</Tab>
|
||||
<Tab name="contacts">
|
||||
<ContactsList selectTab={this.selectTab}
|
||||
startForm={this.startForm}
|
||||
notifications={this.props.notifications} />
|
||||
</Tab>
|
||||
<Tab name="contacts_add" hidden={true}>
|
||||
<ContactDetailsForm ref="contacts_add" mode="add"
|
||||
selectTab={this.selectTab} />
|
||||
</Tab>
|
||||
<Tab name="contacts_edit" hidden={true}>
|
||||
<ContactDetailsForm ref="contacts_edit" mode="edit"
|
||||
selectTab={this.selectTab} />
|
||||
</Tab>
|
||||
<Tab name="contacts_import" hidden={true}>
|
||||
<ContactDetailsForm ref="contacts_import" mode="import"
|
||||
selectTab={this.selectTab}/>
|
||||
</Tab>
|
||||
</TabView>
|
||||
<div className="footer">
|
||||
<div className="user-details">
|
||||
<UserIdentity displayName={this._getUserDisplayName()} />
|
||||
<AvailabilityDropdown />
|
||||
</div>
|
||||
<div className="signin-details">
|
||||
<AuthLink />
|
||||
<div className="footer-signin-separator" />
|
||||
<SettingsDropdown mozLoop={this.props.mozLoop}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel initialisation.
|
||||
*/
|
||||
function init() {
|
||||
// Do the initial L10n setup, we do this before anything
|
||||
// else to ensure the L10n environment is setup correctly.
|
||||
mozL10n.initialize(navigator.mozLoop);
|
||||
|
||||
var notifications = new sharedModels.NotificationCollection();
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
var roomStore = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop,
|
||||
notifications: notifications
|
||||
});
|
||||
|
||||
React.render(<PanelView
|
||||
notifications={notifications}
|
||||
roomStore={roomStore}
|
||||
mozLoop={navigator.mozLoop}
|
||||
dispatcher={dispatcher}
|
||||
/>, document.querySelector("#main"));
|
||||
|
||||
document.body.setAttribute("dir", mozL10n.getDirection());
|
||||
|
||||
// Notify the window that we've finished initalization and initial layout
|
||||
var evtObject = document.createEvent('Event');
|
||||
evtObject.initEvent('loopPanelInitialized', true, false);
|
||||
window.dispatchEvent(evtObject);
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
AuthLink: AuthLink,
|
||||
AvailabilityDropdown: AvailabilityDropdown,
|
||||
GettingStartedView: GettingStartedView,
|
||||
NewRoomView: NewRoomView,
|
||||
PanelView: PanelView,
|
||||
RoomEntry: RoomEntry,
|
||||
RoomList: RoomList,
|
||||
SettingsDropdown: SettingsDropdown,
|
||||
ToSView: ToSView,
|
||||
UserIdentity: UserIdentity
|
||||
};
|
||||
})(_, document.mozL10n);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loop.panel.init);
|
||||
@@ -1,708 +0,0 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
/* 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/. */
|
||||
|
||||
/* jshint newcap:false */
|
||||
/* global loop:true, React */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.roomViews = (function(mozL10n) {
|
||||
"use strict";
|
||||
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var sharedUtils = loop.shared.utils;
|
||||
var sharedViews = loop.shared.views;
|
||||
|
||||
/**
|
||||
* ActiveRoomStore mixin.
|
||||
* @type {Object}
|
||||
*/
|
||||
var ActiveRoomStoreMixin = {
|
||||
mixins: [Backbone.Events],
|
||||
|
||||
propTypes: {
|
||||
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.listenTo(this.props.roomStore, "change:activeRoom",
|
||||
this._onActiveRoomStateChanged);
|
||||
this.listenTo(this.props.roomStore, "change:error",
|
||||
this._onRoomError);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.stopListening(this.props.roomStore);
|
||||
},
|
||||
|
||||
_onActiveRoomStateChanged: function() {
|
||||
// Only update the state if we're mounted, to avoid the problem where
|
||||
// stopListening doesn't nuke the active listeners during a event
|
||||
// processing.
|
||||
if (this.isMounted()) {
|
||||
this.setState(this.props.roomStore.getStoreState("activeRoom"));
|
||||
}
|
||||
},
|
||||
|
||||
_onRoomError: function() {
|
||||
// Only update the state if we're mounted, to avoid the problem where
|
||||
// stopListening doesn't nuke the active listeners during a event
|
||||
// processing.
|
||||
if (this.isMounted()) {
|
||||
this.setState({error: this.props.roomStore.getStoreState("error")});
|
||||
}
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var storeState = this.props.roomStore.getStoreState("activeRoom");
|
||||
return _.extend({
|
||||
// Used by the UI showcase.
|
||||
roomState: this.props.roomState || storeState.roomState
|
||||
}, storeState);
|
||||
}
|
||||
};
|
||||
|
||||
var SocialShareDropdown = React.createClass({displayName: "SocialShareDropdown",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
roomUrl: React.PropTypes.string,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
socialShareButtonAvailable: React.PropTypes.bool,
|
||||
socialShareProviders: React.PropTypes.array
|
||||
},
|
||||
|
||||
handleToolbarAddButtonClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatcher.dispatch(new sharedActions.AddSocialShareButton());
|
||||
},
|
||||
|
||||
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(provider) {
|
||||
return provider.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 = React.addons.classSet;
|
||||
var shareDropdown = cx({
|
||||
"share-service-dropdown": true,
|
||||
"dropdown-menu": true,
|
||||
"share-button-unavailable": !this.props.socialShareButtonAvailable,
|
||||
"hide": !this.props.show
|
||||
});
|
||||
|
||||
// When the button is not yet available, we offer to put it in the navbar
|
||||
// for the user.
|
||||
if (!this.props.socialShareButtonAvailable) {
|
||||
return (
|
||||
React.createElement("div", {className: shareDropdown},
|
||||
React.createElement("div", {className: "share-panel-header"},
|
||||
mozL10n.get("share_panel_header")
|
||||
),
|
||||
React.createElement("div", {className: "share-panel-body"},
|
||||
|
||||
mozL10n.get("share_panel_body", {
|
||||
brandShortname: mozL10n.get("brandShortname"),
|
||||
clientSuperShortname: mozL10n.get("clientSuperShortname")
|
||||
})
|
||||
|
||||
),
|
||||
React.createElement("button", {className: "btn btn-info btn-toolbar-add",
|
||||
onClick: this.handleToolbarAddButtonClick},
|
||||
mozL10n.get("add_to_toolbar_button")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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",
|
||||
key: "provider-" + idx,
|
||||
"data-provider": provider.origin,
|
||||
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",
|
||||
mixins: [sharedMixins.DropdownMenuMixin],
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
error: React.PropTypes.object,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
// This data is supplied by the activeRoomStore.
|
||||
roomData: React.PropTypes.object.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
showContext: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
copiedUrl: false,
|
||||
editMode: false
|
||||
};
|
||||
},
|
||||
|
||||
handleEmailButtonClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatcher.dispatch(
|
||||
new sharedActions.EmailRoomUrl({roomUrl: this.props.roomData.roomUrl}));
|
||||
},
|
||||
|
||||
handleCopyButtonClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatcher.dispatch(
|
||||
new sharedActions.CopyRoomUrl({roomUrl: this.props.roomData.roomUrl}));
|
||||
|
||||
this.setState({copiedUrl: true});
|
||||
},
|
||||
|
||||
handleShareButtonClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.toggleDropdownMenu();
|
||||
},
|
||||
|
||||
handleAddContextClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.handleEditModeChange(true);
|
||||
},
|
||||
|
||||
handleEditModeChange: function(newEditMode) {
|
||||
this.setState({ editMode: newEditMode });
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.props.show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var canAddContext = this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
|
||||
!this.props.showContext && !this.state.editMode;
|
||||
|
||||
var cx = React.addons.classSet;
|
||||
return (
|
||||
React.createElement("div", {className: "room-invitation-overlay"},
|
||||
React.createElement("div", {className: "room-invitation-content"},
|
||||
React.createElement("p", {className: cx({hide: this.state.editMode})},
|
||||
mozL10n.get("invite_header_text")
|
||||
),
|
||||
React.createElement("a", {className: cx({hide: !canAddContext, "room-invitation-addcontext": true}),
|
||||
onClick: this.handleAddContextClick},
|
||||
mozL10n.get("context_add_some_label")
|
||||
),
|
||||
React.createElement("div", {className: "btn-group call-action-group"},
|
||||
React.createElement("button", {className: "btn btn-info btn-email",
|
||||
onClick: this.handleEmailButtonClick},
|
||||
mozL10n.get("email_link_button")
|
||||
),
|
||||
React.createElement("button", {className: "btn btn-info btn-copy",
|
||||
onClick: this.handleCopyButtonClick},
|
||||
this.state.copiedUrl ? mozL10n.get("copied_url_button") :
|
||||
mozL10n.get("copy_url_button2")
|
||||
),
|
||||
React.createElement("button", {className: "btn btn-info btn-share",
|
||||
ref: "anchor",
|
||||
onClick: this.handleShareButtonClick},
|
||||
mozL10n.get("share_button3")
|
||||
)
|
||||
),
|
||||
React.createElement(SocialShareDropdown, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
roomUrl: this.props.roomData.roomUrl,
|
||||
show: this.state.showMenu,
|
||||
socialShareButtonAvailable: this.props.socialShareButtonAvailable,
|
||||
socialShareProviders: this.props.socialShareProviders,
|
||||
ref: "menu"})
|
||||
),
|
||||
React.createElement(DesktopRoomContextView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
editMode: this.state.editMode,
|
||||
error: this.props.error,
|
||||
mozLoop: this.props.mozLoop,
|
||||
onEditModeChange: this.handleEditModeChange,
|
||||
roomData: this.props.roomData,
|
||||
show: this.props.showContext || this.state.editMode})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var DesktopRoomContextView = React.createClass({displayName: "DesktopRoomContextView",
|
||||
mixins: [React.addons.LinkedStateMixin],
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
editMode: React.PropTypes.bool,
|
||||
error: React.PropTypes.object,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
onEditModeChange: React.PropTypes.func,
|
||||
// This data is supplied by the activeRoomStore.
|
||||
roomData: React.PropTypes.object.isRequired,
|
||||
show: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
var newState = {};
|
||||
// When the 'show' prop is changed from outside this component, we do need
|
||||
// to update the state.
|
||||
if (("show" in nextProps) && nextProps.show !== this.props.show) {
|
||||
newState.show = nextProps.show;
|
||||
}
|
||||
if (("editMode" in nextProps && nextProps.editMode !== this.props.editMode)) {
|
||||
newState.editMode = nextProps.editMode;
|
||||
// If we're switching to edit mode, fetch the metadata of the current tab.
|
||||
// But _only_ if there's no context currently attached to the room; the
|
||||
// checkbox will be disabled in that case.
|
||||
if (nextProps.editMode) {
|
||||
this.props.mozLoop.getSelectedTabMetadata(function(metadata) {
|
||||
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
|
||||
var description = metadata.description || metadata.title;
|
||||
var url = metadata.url;
|
||||
this.setState({
|
||||
availableContext: {
|
||||
previewImage: previewImage,
|
||||
description: description,
|
||||
url: url
|
||||
}
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
// When we receive an update for the `roomData` property, make sure that
|
||||
// the current form fields reflect reality. This is necessary, because the
|
||||
// form state is maintained in the components' state.
|
||||
if (nextProps.roomData) {
|
||||
// Right now it's only necessary to update the form input states when
|
||||
// they contain no text yet.
|
||||
if (!this.state.newRoomName && nextProps.roomData.roomName) {
|
||||
newState.newRoomName = nextProps.roomData.roomName;
|
||||
}
|
||||
var url = this._getURL(nextProps.roomData);
|
||||
if (url) {
|
||||
if (!this.state.newRoomURL && url.location) {
|
||||
newState.newRoomURL = url.location;
|
||||
}
|
||||
if (!this.state.newRoomDescription && url.description) {
|
||||
newState.newRoomDescription = url.description;
|
||||
}
|
||||
if (!this.state.newRoomThumbnail && url.thumbnail) {
|
||||
newState.newRoomThumbnail = url.thumbnail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.getOwnPropertyNames(newState).length) {
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return { editMode: false };
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var url = this._getURL();
|
||||
return {
|
||||
// `availableContext` prop only used in tests.
|
||||
availableContext: this.props.availableContext,
|
||||
editMode: this.props.editMode,
|
||||
show: this.props.show,
|
||||
newRoomName: this.props.roomData.roomName || "",
|
||||
newRoomURL: url && url.location || "",
|
||||
newRoomDescription: url && url.description || "",
|
||||
newRoomThumbnail: url && url.thumbnail || ""
|
||||
};
|
||||
},
|
||||
|
||||
handleCloseClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.state.editMode) {
|
||||
this.setState({ editMode: false });
|
||||
if (this.props.onEditModeChange) {
|
||||
this.props.onEditModeChange(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.setState({ show: false });
|
||||
},
|
||||
|
||||
handleEditClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({ editMode: true });
|
||||
if (this.props.onEditModeChange) {
|
||||
this.props.onEditModeChange(true);
|
||||
}
|
||||
},
|
||||
|
||||
handleCheckboxChange: function(state) {
|
||||
if (state.checked) {
|
||||
// The checkbox was checked, prefill the fields with the values available
|
||||
// in `availableContext`.
|
||||
var context = this.state.availableContext;
|
||||
this.setState({
|
||||
newRoomURL: context.url,
|
||||
newRoomDescription: context.description,
|
||||
newRoomThumbnail: context.previewImage
|
||||
}, this.handleFormSubmit);
|
||||
} else {
|
||||
this.setState({
|
||||
newRoomURL: "",
|
||||
newRoomDescription: "",
|
||||
newRoomThumbnail: ""
|
||||
}, this.handleFormSubmit);
|
||||
}
|
||||
},
|
||||
|
||||
handleFormSubmit: function(event) {
|
||||
event && event.preventDefault();
|
||||
|
||||
this.props.dispatcher.dispatch(new sharedActions.UpdateRoomContext({
|
||||
roomToken: this.props.roomData.roomToken,
|
||||
newRoomName: this.state.newRoomName,
|
||||
newRoomURL: this.state.newRoomURL,
|
||||
newRoomDescription: this.state.newRoomDescription,
|
||||
newRoomThumbnail: this.state.newRoomThumbnail
|
||||
}));
|
||||
},
|
||||
|
||||
handleTextareaKeyDown: function(event) {
|
||||
// Submit the form as soon as the user press Enter in that field
|
||||
// Note: We're using a textarea instead of a simple text input to display
|
||||
// placeholder and entered text on two lines, to circumvent l10n
|
||||
// rendering/UX issues for some locales.
|
||||
if (event.which === 13) {
|
||||
this.handleFormSubmit(event);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Utility function to extract URL context data from the `roomData` property
|
||||
* that can also be supplied as an argument.
|
||||
*
|
||||
* @param {Object} roomData Optional room data object to use, equivalent to
|
||||
* the activeRoomStore state.
|
||||
* @return {Object} The first context URL found on the `roomData` object.
|
||||
*/
|
||||
_getURL: function(roomData) {
|
||||
roomData = roomData || this.props.roomData;
|
||||
return this.props.roomData.roomContextUrls &&
|
||||
this.props.roomData.roomContextUrls[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Truncate a string if it exceeds the length as defined in `maxLen`, which
|
||||
* is defined as '72' characters by default. If the string needs trimming,
|
||||
* it'll be suffixed with the unicode ellipsis char, \u2026.
|
||||
*
|
||||
* @param {String} str The string to truncate, if needed.
|
||||
* @param {Number} maxLen Maximum number of characters that the string is
|
||||
* allowed to contain. Optional, defaults to 72.
|
||||
* @return {String} Truncated version of `str`.
|
||||
*/
|
||||
_truncate: function(str, maxLen) {
|
||||
if (!maxLen) {
|
||||
maxLen = 72;
|
||||
}
|
||||
return (str.length > maxLen) ? str.substr(0, maxLen) + "…" : str;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.state.show && !this.state.editMode)
|
||||
return null;
|
||||
|
||||
var url = this._getURL();
|
||||
var thumbnail = url && url.thumbnail || "";
|
||||
var urlDescription = url && url.description || "";
|
||||
var location = url && url.location || "";
|
||||
var checkboxLabel = null;
|
||||
var locationData = null;
|
||||
if (location) {
|
||||
locationData = checkboxLabel = sharedUtils.formatURL(location);
|
||||
}
|
||||
if (!checkboxLabel) {
|
||||
checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
|
||||
this.state.availableContext.url : ""));
|
||||
}
|
||||
|
||||
var cx = React.addons.classSet;
|
||||
if (this.state.editMode) {
|
||||
return (
|
||||
React.createElement("div", {className: "room-context"},
|
||||
React.createElement("div", {className: "room-context-content"},
|
||||
React.createElement("p", {className: cx({"error": !!this.props.error,
|
||||
"error-display-area": true})},
|
||||
mozL10n.get("rooms_change_failed_label")
|
||||
),
|
||||
React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")),
|
||||
React.createElement(sharedViews.Checkbox, {
|
||||
checked: !!url,
|
||||
disabled: !!url || !checkboxLabel,
|
||||
label: mozL10n.get("context_edit_activate_label", {
|
||||
title: checkboxLabel ? checkboxLabel.hostname : ""
|
||||
}),
|
||||
onChange: this.handleCheckboxChange,
|
||||
value: location}),
|
||||
React.createElement("form", {onSubmit: this.handleFormSubmit},
|
||||
React.createElement("textarea", {rows: "2", type: "text", className: "room-context-name",
|
||||
onBlur: this.handleFormSubmit,
|
||||
onKeyDown: this.handleTextareaKeyDown,
|
||||
placeholder: mozL10n.get("context_edit_name_placeholder"),
|
||||
valueLink: this.linkState("newRoomName")}),
|
||||
React.createElement("input", {type: "text", className: "room-context-url",
|
||||
onBlur: this.handleFormSubmit,
|
||||
onKeyDown: this.handleTextareaKeyDown,
|
||||
placeholder: "https://",
|
||||
valueLink: this.linkState("newRoomURL")}),
|
||||
React.createElement("textarea", {rows: "4", type: "text", className: "room-context-comments",
|
||||
onBlur: this.handleFormSubmit,
|
||||
onKeyDown: this.handleTextareaKeyDown,
|
||||
placeholder: mozL10n.get("context_edit_comments_placeholder"),
|
||||
valueLink: this.linkState("newRoomDescription")})
|
||||
),
|
||||
React.createElement("button", {className: "room-context-btn-close",
|
||||
onClick: this.handleCloseClick})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!locationData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "room-context"},
|
||||
React.createElement("img", {className: "room-context-thumbnail", src: thumbnail}),
|
||||
React.createElement("div", {className: "room-context-content"},
|
||||
React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")),
|
||||
React.createElement("div", {className: "room-context-description",
|
||||
title: urlDescription}, this._truncate(urlDescription)),
|
||||
React.createElement("a", {className: "room-context-url",
|
||||
href: location,
|
||||
target: "_blank",
|
||||
title: locationData.location}, locationData.hostname),
|
||||
this.props.roomData.roomDescription ?
|
||||
React.createElement("div", {className: "room-context-comment"}, this.props.roomData.roomDescription) :
|
||||
null,
|
||||
React.createElement("button", {className: "room-context-btn-close",
|
||||
onClick: this.handleCloseClick}),
|
||||
React.createElement("button", {className: "room-context-btn-edit",
|
||||
onClick: this.handleEditClick})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Desktop room conversation view.
|
||||
*/
|
||||
var DesktopRoomConversationView = React.createClass({displayName: "DesktopRoomConversationView",
|
||||
mixins: [
|
||||
ActiveRoomStoreMixin,
|
||||
sharedMixins.DocumentTitleMixin,
|
||||
sharedMixins.MediaSetupMixin,
|
||||
sharedMixins.RoomsAudioMixin,
|
||||
sharedMixins.WindowCloseMixin
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
componentWillUpdate: function(nextProps, nextState) {
|
||||
// The SDK needs to know about the configuration and the elements to use
|
||||
// for display. So the best way seems to pass the information here - ideally
|
||||
// the sdk wouldn't need to know this, but we can't change that.
|
||||
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
|
||||
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
|
||||
publisherConfig: this.getDefaultPublisherConfig({
|
||||
publishVideo: !this.state.videoMuted
|
||||
}),
|
||||
getLocalElementFunc: this._getElement.bind(this, ".local"),
|
||||
getScreenShareElementFunc: this._getElement.bind(this, ".screen"),
|
||||
getRemoteElementFunc: this._getElement.bind(this, ".remote")
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* User clicked on the "Leave" button.
|
||||
*/
|
||||
leaveRoom: function() {
|
||||
if (this.state.used) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
|
||||
} else {
|
||||
this.closeWindow();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to control publishing a stream - i.e. to mute a stream
|
||||
*
|
||||
* @param {String} type The type of stream, e.g. "audio" or "video".
|
||||
* @param {Boolean} enabled True to enable the stream, false otherwise.
|
||||
*/
|
||||
publishStream: function(type, enabled) {
|
||||
this.props.dispatcher.dispatch(
|
||||
new sharedActions.SetMute({
|
||||
type: type,
|
||||
enabled: enabled
|
||||
}));
|
||||
},
|
||||
|
||||
_shouldRenderInvitationOverlay: function() {
|
||||
return (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS);
|
||||
},
|
||||
|
||||
_shouldRenderContextView: function() {
|
||||
return !!(
|
||||
this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
|
||||
(this.state.roomContextUrls || this.state.roomDescription)
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.roomName) {
|
||||
this.setTitle(this.state.roomName);
|
||||
}
|
||||
|
||||
var localStreamClasses = React.addons.classSet({
|
||||
local: true,
|
||||
"local-stream": true,
|
||||
"local-stream-audio": this.state.videoMuted,
|
||||
"room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
|
||||
});
|
||||
|
||||
var screenShareData = {
|
||||
state: this.state.screenSharingState,
|
||||
visible: true
|
||||
};
|
||||
|
||||
var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
|
||||
var shouldRenderContextView = this._shouldRenderContextView();
|
||||
var roomData = this.props.roomStore.getStoreState("activeRoom");
|
||||
|
||||
switch(this.state.roomState) {
|
||||
case ROOM_STATES.FAILED:
|
||||
case ROOM_STATES.FULL: {
|
||||
// Note: While rooms are set to hold a maximum of 2 participants, the
|
||||
// FULL case should never happen on desktop.
|
||||
return (
|
||||
React.createElement(loop.conversationViews.GenericFailureView, {
|
||||
cancelCall: this.closeWindow,
|
||||
failureReason: this.state.failureReason})
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.ENDED: {
|
||||
return (
|
||||
React.createElement(sharedViews.FeedbackView, {
|
||||
onAfterFeedbackReceived: this.closeWindow})
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
React.createElement("div", {className: "room-conversation-wrapper"},
|
||||
React.createElement(DesktopRoomInvitationView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
error: this.state.error,
|
||||
mozLoop: this.props.mozLoop,
|
||||
roomData: roomData,
|
||||
show: shouldRenderInvitationOverlay,
|
||||
showContext: shouldRenderContextView,
|
||||
socialShareButtonAvailable: this.state.socialShareButtonAvailable,
|
||||
socialShareProviders: this.state.socialShareProviders}),
|
||||
React.createElement("div", {className: "video-layout-wrapper"},
|
||||
React.createElement("div", {className: "conversation room-conversation"},
|
||||
React.createElement("div", {className: "media nested"},
|
||||
React.createElement("div", {className: "video_wrapper remote_wrapper"},
|
||||
React.createElement("div", {className: "video_inner remote focus-stream"})
|
||||
),
|
||||
React.createElement("div", {className: localStreamClasses}),
|
||||
React.createElement("div", {className: "screen hide"})
|
||||
),
|
||||
React.createElement(sharedViews.ConversationToolbar, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
video: {enabled: !this.state.videoMuted, visible: true},
|
||||
audio: {enabled: !this.state.audioMuted, visible: true},
|
||||
publishStream: this.publishStream,
|
||||
hangup: this.leaveRoom,
|
||||
screenShare: screenShareData})
|
||||
)
|
||||
),
|
||||
React.createElement(DesktopRoomContextView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
error: this.state.error,
|
||||
mozLoop: this.props.mozLoop,
|
||||
roomData: roomData,
|
||||
show: !shouldRenderInvitationOverlay && shouldRenderContextView})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
|
||||
SocialShareDropdown: SocialShareDropdown,
|
||||
DesktopRoomContextView: DesktopRoomContextView,
|
||||
DesktopRoomConversationView: DesktopRoomConversationView,
|
||||
DesktopRoomInvitationView: DesktopRoomInvitationView
|
||||
};
|
||||
|
||||
})(document.mozL10n || navigator.mozL10n);
|
||||
@@ -1,708 +0,0 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
/* 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/. */
|
||||
|
||||
/* jshint newcap:false */
|
||||
/* global loop:true, React */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.roomViews = (function(mozL10n) {
|
||||
"use strict";
|
||||
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var sharedUtils = loop.shared.utils;
|
||||
var sharedViews = loop.shared.views;
|
||||
|
||||
/**
|
||||
* ActiveRoomStore mixin.
|
||||
* @type {Object}
|
||||
*/
|
||||
var ActiveRoomStoreMixin = {
|
||||
mixins: [Backbone.Events],
|
||||
|
||||
propTypes: {
|
||||
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.listenTo(this.props.roomStore, "change:activeRoom",
|
||||
this._onActiveRoomStateChanged);
|
||||
this.listenTo(this.props.roomStore, "change:error",
|
||||
this._onRoomError);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.stopListening(this.props.roomStore);
|
||||
},
|
||||
|
||||
_onActiveRoomStateChanged: function() {
|
||||
// Only update the state if we're mounted, to avoid the problem where
|
||||
// stopListening doesn't nuke the active listeners during a event
|
||||
// processing.
|
||||
if (this.isMounted()) {
|
||||
this.setState(this.props.roomStore.getStoreState("activeRoom"));
|
||||
}
|
||||
},
|
||||
|
||||
_onRoomError: function() {
|
||||
// Only update the state if we're mounted, to avoid the problem where
|
||||
// stopListening doesn't nuke the active listeners during a event
|
||||
// processing.
|
||||
if (this.isMounted()) {
|
||||
this.setState({error: this.props.roomStore.getStoreState("error")});
|
||||
}
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var storeState = this.props.roomStore.getStoreState("activeRoom");
|
||||
return _.extend({
|
||||
// Used by the UI showcase.
|
||||
roomState: this.props.roomState || storeState.roomState
|
||||
}, storeState);
|
||||
}
|
||||
};
|
||||
|
||||
var SocialShareDropdown = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
roomUrl: React.PropTypes.string,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
socialShareButtonAvailable: React.PropTypes.bool,
|
||||
socialShareProviders: React.PropTypes.array
|
||||
},
|
||||
|
||||
handleToolbarAddButtonClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatcher.dispatch(new sharedActions.AddSocialShareButton());
|
||||
},
|
||||
|
||||
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(provider) {
|
||||
return provider.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 = React.addons.classSet;
|
||||
var shareDropdown = cx({
|
||||
"share-service-dropdown": true,
|
||||
"dropdown-menu": true,
|
||||
"share-button-unavailable": !this.props.socialShareButtonAvailable,
|
||||
"hide": !this.props.show
|
||||
});
|
||||
|
||||
// When the button is not yet available, we offer to put it in the navbar
|
||||
// for the user.
|
||||
if (!this.props.socialShareButtonAvailable) {
|
||||
return (
|
||||
<div className={shareDropdown}>
|
||||
<div className="share-panel-header">
|
||||
{mozL10n.get("share_panel_header")}
|
||||
</div>
|
||||
<div className="share-panel-body">
|
||||
{
|
||||
mozL10n.get("share_panel_body", {
|
||||
brandShortname: mozL10n.get("brandShortname"),
|
||||
clientSuperShortname: mozL10n.get("clientSuperShortname")
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<button className="btn btn-info btn-toolbar-add"
|
||||
onClick={this.handleToolbarAddButtonClick}>
|
||||
{mozL10n.get("add_to_toolbar_button")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={shareDropdown}>
|
||||
<li className="dropdown-menu-item" onClick={this.handleAddServiceClick}>
|
||||
<i className="icon icon-add-share-service"></i>
|
||||
<span>{mozL10n.get("share_add_service_button")}</span>
|
||||
</li>
|
||||
{this.props.socialShareProviders.length ? <li className="dropdown-menu-separator"/> : null}
|
||||
{
|
||||
this.props.socialShareProviders.map(function(provider, idx) {
|
||||
return (
|
||||
<li className="dropdown-menu-item"
|
||||
key={"provider-" + idx}
|
||||
data-provider={provider.origin}
|
||||
onClick={this.handleProviderClick}>
|
||||
<img className="icon" src={provider.iconURL}/>
|
||||
<span>{provider.name}</span>
|
||||
</li>
|
||||
);
|
||||
}.bind(this))
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Desktop room invitation view (overlay).
|
||||
*/
|
||||
var DesktopRoomInvitationView = React.createClass({
|
||||
mixins: [sharedMixins.DropdownMenuMixin],
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
error: React.PropTypes.object,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
// This data is supplied by the activeRoomStore.
|
||||
roomData: React.PropTypes.object.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
showContext: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
copiedUrl: false,
|
||||
editMode: false
|
||||
};
|
||||
},
|
||||
|
||||
handleEmailButtonClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatcher.dispatch(
|
||||
new sharedActions.EmailRoomUrl({roomUrl: this.props.roomData.roomUrl}));
|
||||
},
|
||||
|
||||
handleCopyButtonClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatcher.dispatch(
|
||||
new sharedActions.CopyRoomUrl({roomUrl: this.props.roomData.roomUrl}));
|
||||
|
||||
this.setState({copiedUrl: true});
|
||||
},
|
||||
|
||||
handleShareButtonClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.toggleDropdownMenu();
|
||||
},
|
||||
|
||||
handleAddContextClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.handleEditModeChange(true);
|
||||
},
|
||||
|
||||
handleEditModeChange: function(newEditMode) {
|
||||
this.setState({ editMode: newEditMode });
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.props.show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var canAddContext = this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
|
||||
!this.props.showContext && !this.state.editMode;
|
||||
|
||||
var cx = React.addons.classSet;
|
||||
return (
|
||||
<div className="room-invitation-overlay">
|
||||
<div className="room-invitation-content">
|
||||
<p className={cx({hide: this.state.editMode})}>
|
||||
{mozL10n.get("invite_header_text")}
|
||||
</p>
|
||||
<a className={cx({hide: !canAddContext, "room-invitation-addcontext": true})}
|
||||
onClick={this.handleAddContextClick}>
|
||||
{mozL10n.get("context_add_some_label")}
|
||||
</a>
|
||||
<div className="btn-group call-action-group">
|
||||
<button className="btn btn-info btn-email"
|
||||
onClick={this.handleEmailButtonClick}>
|
||||
{mozL10n.get("email_link_button")}
|
||||
</button>
|
||||
<button className="btn btn-info btn-copy"
|
||||
onClick={this.handleCopyButtonClick}>
|
||||
{this.state.copiedUrl ? mozL10n.get("copied_url_button") :
|
||||
mozL10n.get("copy_url_button2")}
|
||||
</button>
|
||||
<button className="btn btn-info btn-share"
|
||||
ref="anchor"
|
||||
onClick={this.handleShareButtonClick}>
|
||||
{mozL10n.get("share_button3")}
|
||||
</button>
|
||||
</div>
|
||||
<SocialShareDropdown
|
||||
dispatcher={this.props.dispatcher}
|
||||
roomUrl={this.props.roomData.roomUrl}
|
||||
show={this.state.showMenu}
|
||||
socialShareButtonAvailable={this.props.socialShareButtonAvailable}
|
||||
socialShareProviders={this.props.socialShareProviders}
|
||||
ref="menu" />
|
||||
</div>
|
||||
<DesktopRoomContextView
|
||||
dispatcher={this.props.dispatcher}
|
||||
editMode={this.state.editMode}
|
||||
error={this.props.error}
|
||||
mozLoop={this.props.mozLoop}
|
||||
onEditModeChange={this.handleEditModeChange}
|
||||
roomData={this.props.roomData}
|
||||
show={this.props.showContext || this.state.editMode} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var DesktopRoomContextView = React.createClass({
|
||||
mixins: [React.addons.LinkedStateMixin],
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
editMode: React.PropTypes.bool,
|
||||
error: React.PropTypes.object,
|
||||
mozLoop: React.PropTypes.object.isRequired,
|
||||
onEditModeChange: React.PropTypes.func,
|
||||
// This data is supplied by the activeRoomStore.
|
||||
roomData: React.PropTypes.object.isRequired,
|
||||
show: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
var newState = {};
|
||||
// When the 'show' prop is changed from outside this component, we do need
|
||||
// to update the state.
|
||||
if (("show" in nextProps) && nextProps.show !== this.props.show) {
|
||||
newState.show = nextProps.show;
|
||||
}
|
||||
if (("editMode" in nextProps && nextProps.editMode !== this.props.editMode)) {
|
||||
newState.editMode = nextProps.editMode;
|
||||
// If we're switching to edit mode, fetch the metadata of the current tab.
|
||||
// But _only_ if there's no context currently attached to the room; the
|
||||
// checkbox will be disabled in that case.
|
||||
if (nextProps.editMode) {
|
||||
this.props.mozLoop.getSelectedTabMetadata(function(metadata) {
|
||||
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
|
||||
var description = metadata.description || metadata.title;
|
||||
var url = metadata.url;
|
||||
this.setState({
|
||||
availableContext: {
|
||||
previewImage: previewImage,
|
||||
description: description,
|
||||
url: url
|
||||
}
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
// When we receive an update for the `roomData` property, make sure that
|
||||
// the current form fields reflect reality. This is necessary, because the
|
||||
// form state is maintained in the components' state.
|
||||
if (nextProps.roomData) {
|
||||
// Right now it's only necessary to update the form input states when
|
||||
// they contain no text yet.
|
||||
if (!this.state.newRoomName && nextProps.roomData.roomName) {
|
||||
newState.newRoomName = nextProps.roomData.roomName;
|
||||
}
|
||||
var url = this._getURL(nextProps.roomData);
|
||||
if (url) {
|
||||
if (!this.state.newRoomURL && url.location) {
|
||||
newState.newRoomURL = url.location;
|
||||
}
|
||||
if (!this.state.newRoomDescription && url.description) {
|
||||
newState.newRoomDescription = url.description;
|
||||
}
|
||||
if (!this.state.newRoomThumbnail && url.thumbnail) {
|
||||
newState.newRoomThumbnail = url.thumbnail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.getOwnPropertyNames(newState).length) {
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return { editMode: false };
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var url = this._getURL();
|
||||
return {
|
||||
// `availableContext` prop only used in tests.
|
||||
availableContext: this.props.availableContext,
|
||||
editMode: this.props.editMode,
|
||||
show: this.props.show,
|
||||
newRoomName: this.props.roomData.roomName || "",
|
||||
newRoomURL: url && url.location || "",
|
||||
newRoomDescription: url && url.description || "",
|
||||
newRoomThumbnail: url && url.thumbnail || ""
|
||||
};
|
||||
},
|
||||
|
||||
handleCloseClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.state.editMode) {
|
||||
this.setState({ editMode: false });
|
||||
if (this.props.onEditModeChange) {
|
||||
this.props.onEditModeChange(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.setState({ show: false });
|
||||
},
|
||||
|
||||
handleEditClick: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({ editMode: true });
|
||||
if (this.props.onEditModeChange) {
|
||||
this.props.onEditModeChange(true);
|
||||
}
|
||||
},
|
||||
|
||||
handleCheckboxChange: function(state) {
|
||||
if (state.checked) {
|
||||
// The checkbox was checked, prefill the fields with the values available
|
||||
// in `availableContext`.
|
||||
var context = this.state.availableContext;
|
||||
this.setState({
|
||||
newRoomURL: context.url,
|
||||
newRoomDescription: context.description,
|
||||
newRoomThumbnail: context.previewImage
|
||||
}, this.handleFormSubmit);
|
||||
} else {
|
||||
this.setState({
|
||||
newRoomURL: "",
|
||||
newRoomDescription: "",
|
||||
newRoomThumbnail: ""
|
||||
}, this.handleFormSubmit);
|
||||
}
|
||||
},
|
||||
|
||||
handleFormSubmit: function(event) {
|
||||
event && event.preventDefault();
|
||||
|
||||
this.props.dispatcher.dispatch(new sharedActions.UpdateRoomContext({
|
||||
roomToken: this.props.roomData.roomToken,
|
||||
newRoomName: this.state.newRoomName,
|
||||
newRoomURL: this.state.newRoomURL,
|
||||
newRoomDescription: this.state.newRoomDescription,
|
||||
newRoomThumbnail: this.state.newRoomThumbnail
|
||||
}));
|
||||
},
|
||||
|
||||
handleTextareaKeyDown: function(event) {
|
||||
// Submit the form as soon as the user press Enter in that field
|
||||
// Note: We're using a textarea instead of a simple text input to display
|
||||
// placeholder and entered text on two lines, to circumvent l10n
|
||||
// rendering/UX issues for some locales.
|
||||
if (event.which === 13) {
|
||||
this.handleFormSubmit(event);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Utility function to extract URL context data from the `roomData` property
|
||||
* that can also be supplied as an argument.
|
||||
*
|
||||
* @param {Object} roomData Optional room data object to use, equivalent to
|
||||
* the activeRoomStore state.
|
||||
* @return {Object} The first context URL found on the `roomData` object.
|
||||
*/
|
||||
_getURL: function(roomData) {
|
||||
roomData = roomData || this.props.roomData;
|
||||
return this.props.roomData.roomContextUrls &&
|
||||
this.props.roomData.roomContextUrls[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Truncate a string if it exceeds the length as defined in `maxLen`, which
|
||||
* is defined as '72' characters by default. If the string needs trimming,
|
||||
* it'll be suffixed with the unicode ellipsis char, \u2026.
|
||||
*
|
||||
* @param {String} str The string to truncate, if needed.
|
||||
* @param {Number} maxLen Maximum number of characters that the string is
|
||||
* allowed to contain. Optional, defaults to 72.
|
||||
* @return {String} Truncated version of `str`.
|
||||
*/
|
||||
_truncate: function(str, maxLen) {
|
||||
if (!maxLen) {
|
||||
maxLen = 72;
|
||||
}
|
||||
return (str.length > maxLen) ? str.substr(0, maxLen) + "…" : str;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.state.show && !this.state.editMode)
|
||||
return null;
|
||||
|
||||
var url = this._getURL();
|
||||
var thumbnail = url && url.thumbnail || "";
|
||||
var urlDescription = url && url.description || "";
|
||||
var location = url && url.location || "";
|
||||
var checkboxLabel = null;
|
||||
var locationData = null;
|
||||
if (location) {
|
||||
locationData = checkboxLabel = sharedUtils.formatURL(location);
|
||||
}
|
||||
if (!checkboxLabel) {
|
||||
checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
|
||||
this.state.availableContext.url : ""));
|
||||
}
|
||||
|
||||
var cx = React.addons.classSet;
|
||||
if (this.state.editMode) {
|
||||
return (
|
||||
<div className="room-context">
|
||||
<div className="room-context-content">
|
||||
<p className={cx({"error": !!this.props.error,
|
||||
"error-display-area": true})}>
|
||||
{mozL10n.get("rooms_change_failed_label")}
|
||||
</p>
|
||||
<div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
|
||||
<sharedViews.Checkbox
|
||||
checked={!!url}
|
||||
disabled={!!url || !checkboxLabel}
|
||||
label={mozL10n.get("context_edit_activate_label", {
|
||||
title: checkboxLabel ? checkboxLabel.hostname : ""
|
||||
})}
|
||||
onChange={this.handleCheckboxChange}
|
||||
value={location} />
|
||||
<form onSubmit={this.handleFormSubmit}>
|
||||
<textarea rows="2" type="text" className="room-context-name"
|
||||
onBlur={this.handleFormSubmit}
|
||||
onKeyDown={this.handleTextareaKeyDown}
|
||||
placeholder={mozL10n.get("context_edit_name_placeholder")}
|
||||
valueLink={this.linkState("newRoomName")} />
|
||||
<input type="text" className="room-context-url"
|
||||
onBlur={this.handleFormSubmit}
|
||||
onKeyDown={this.handleTextareaKeyDown}
|
||||
placeholder="https://"
|
||||
valueLink={this.linkState("newRoomURL")} />
|
||||
<textarea rows="4" type="text" className="room-context-comments"
|
||||
onBlur={this.handleFormSubmit}
|
||||
onKeyDown={this.handleTextareaKeyDown}
|
||||
placeholder={mozL10n.get("context_edit_comments_placeholder")}
|
||||
valueLink={this.linkState("newRoomDescription")} />
|
||||
</form>
|
||||
<button className="room-context-btn-close"
|
||||
onClick={this.handleCloseClick}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!locationData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="room-context">
|
||||
<img className="room-context-thumbnail" src={thumbnail}/>
|
||||
<div className="room-context-content">
|
||||
<div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
|
||||
<div className="room-context-description"
|
||||
title={urlDescription}>{this._truncate(urlDescription)}</div>
|
||||
<a className="room-context-url"
|
||||
href={location}
|
||||
target="_blank"
|
||||
title={locationData.location}>{locationData.hostname}</a>
|
||||
{this.props.roomData.roomDescription ?
|
||||
<div className="room-context-comment">{this.props.roomData.roomDescription}</div> :
|
||||
null}
|
||||
<button className="room-context-btn-close"
|
||||
onClick={this.handleCloseClick}/>
|
||||
<button className="room-context-btn-edit"
|
||||
onClick={this.handleEditClick}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Desktop room conversation view.
|
||||
*/
|
||||
var DesktopRoomConversationView = React.createClass({
|
||||
mixins: [
|
||||
ActiveRoomStoreMixin,
|
||||
sharedMixins.DocumentTitleMixin,
|
||||
sharedMixins.MediaSetupMixin,
|
||||
sharedMixins.RoomsAudioMixin,
|
||||
sharedMixins.WindowCloseMixin
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
mozLoop: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
componentWillUpdate: function(nextProps, nextState) {
|
||||
// The SDK needs to know about the configuration and the elements to use
|
||||
// for display. So the best way seems to pass the information here - ideally
|
||||
// the sdk wouldn't need to know this, but we can't change that.
|
||||
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
|
||||
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
|
||||
publisherConfig: this.getDefaultPublisherConfig({
|
||||
publishVideo: !this.state.videoMuted
|
||||
}),
|
||||
getLocalElementFunc: this._getElement.bind(this, ".local"),
|
||||
getScreenShareElementFunc: this._getElement.bind(this, ".screen"),
|
||||
getRemoteElementFunc: this._getElement.bind(this, ".remote")
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* User clicked on the "Leave" button.
|
||||
*/
|
||||
leaveRoom: function() {
|
||||
if (this.state.used) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
|
||||
} else {
|
||||
this.closeWindow();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to control publishing a stream - i.e. to mute a stream
|
||||
*
|
||||
* @param {String} type The type of stream, e.g. "audio" or "video".
|
||||
* @param {Boolean} enabled True to enable the stream, false otherwise.
|
||||
*/
|
||||
publishStream: function(type, enabled) {
|
||||
this.props.dispatcher.dispatch(
|
||||
new sharedActions.SetMute({
|
||||
type: type,
|
||||
enabled: enabled
|
||||
}));
|
||||
},
|
||||
|
||||
_shouldRenderInvitationOverlay: function() {
|
||||
return (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS);
|
||||
},
|
||||
|
||||
_shouldRenderContextView: function() {
|
||||
return !!(
|
||||
this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
|
||||
(this.state.roomContextUrls || this.state.roomDescription)
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.roomName) {
|
||||
this.setTitle(this.state.roomName);
|
||||
}
|
||||
|
||||
var localStreamClasses = React.addons.classSet({
|
||||
local: true,
|
||||
"local-stream": true,
|
||||
"local-stream-audio": this.state.videoMuted,
|
||||
"room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
|
||||
});
|
||||
|
||||
var screenShareData = {
|
||||
state: this.state.screenSharingState,
|
||||
visible: true
|
||||
};
|
||||
|
||||
var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
|
||||
var shouldRenderContextView = this._shouldRenderContextView();
|
||||
var roomData = this.props.roomStore.getStoreState("activeRoom");
|
||||
|
||||
switch(this.state.roomState) {
|
||||
case ROOM_STATES.FAILED:
|
||||
case ROOM_STATES.FULL: {
|
||||
// Note: While rooms are set to hold a maximum of 2 participants, the
|
||||
// FULL case should never happen on desktop.
|
||||
return (
|
||||
<loop.conversationViews.GenericFailureView
|
||||
cancelCall={this.closeWindow}
|
||||
failureReason={this.state.failureReason} />
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.ENDED: {
|
||||
return (
|
||||
<sharedViews.FeedbackView
|
||||
onAfterFeedbackReceived={this.closeWindow} />
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<div className="room-conversation-wrapper">
|
||||
<DesktopRoomInvitationView
|
||||
dispatcher={this.props.dispatcher}
|
||||
error={this.state.error}
|
||||
mozLoop={this.props.mozLoop}
|
||||
roomData={roomData}
|
||||
show={shouldRenderInvitationOverlay}
|
||||
showContext={shouldRenderContextView}
|
||||
socialShareButtonAvailable={this.state.socialShareButtonAvailable}
|
||||
socialShareProviders={this.state.socialShareProviders} />
|
||||
<div className="video-layout-wrapper">
|
||||
<div className="conversation room-conversation">
|
||||
<div className="media nested">
|
||||
<div className="video_wrapper remote_wrapper">
|
||||
<div className="video_inner remote focus-stream"></div>
|
||||
</div>
|
||||
<div className={localStreamClasses}></div>
|
||||
<div className="screen hide"></div>
|
||||
</div>
|
||||
<sharedViews.ConversationToolbar
|
||||
dispatcher={this.props.dispatcher}
|
||||
video={{enabled: !this.state.videoMuted, visible: true}}
|
||||
audio={{enabled: !this.state.audioMuted, visible: true}}
|
||||
publishStream={this.publishStream}
|
||||
hangup={this.leaveRoom}
|
||||
screenShare={screenShareData} />
|
||||
</div>
|
||||
</div>
|
||||
<DesktopRoomContextView
|
||||
dispatcher={this.props.dispatcher}
|
||||
error={this.state.error}
|
||||
mozLoop={this.props.mozLoop}
|
||||
roomData={roomData}
|
||||
show={!shouldRenderInvitationOverlay && shouldRenderContextView} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
|
||||
SocialShareDropdown: SocialShareDropdown,
|
||||
DesktopRoomContextView: DesktopRoomContextView,
|
||||
DesktopRoomConversationView: DesktopRoomConversationView,
|
||||
DesktopRoomInvitationView: DesktopRoomInvitationView
|
||||
};
|
||||
|
||||
})(document.mozL10n || navigator.mozL10n);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px"
|
||||
viewBox="0 0 10 10"
|
||||
enable-background="new 0 0 10 10"
|
||||
xml:space="preserve">
|
||||
<style>
|
||||
use:not(:target) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
use {
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
use[id$="-hover"] {
|
||||
fill: #444;
|
||||
}
|
||||
|
||||
use[id$="-active"] {
|
||||
fill: #0095dd;
|
||||
}
|
||||
|
||||
use[id$="-white"] {
|
||||
fill: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
use[id$="-disabled"] {
|
||||
fill: rgba(255,255,255,0.4);
|
||||
}
|
||||
</style>
|
||||
<defs style="display:none">
|
||||
<polygon id="close-shape" fill-rule="evenodd" clip-rule="evenodd" points="10,1.717 8.336,0.049 5.024,3.369 1.663,0 0,1.668
|
||||
3.36,5.037 0.098,8.307 1.762,9.975 5.025,6.705 8.311,10 9.975,8.332 6.688,5.037"/>
|
||||
<path id="dropdown-shape" fill-rule="evenodd" clip-rule="evenodd" d="M9,3L4.984,7L1,3H9z"/>
|
||||
<polygon id="expand-shape" fill-rule="evenodd" clip-rule="evenodd" points="10,0 4.838,0 6.506,1.669 0,8.175 1.825,10 8.331,3.494
|
||||
10,5.162"/>
|
||||
<path id="edit-shape" d="M5.493,1.762l2.745,2.745L2.745,10H0V7.255L5.493,1.762z M2.397,9.155l0.601-0.601L1.446,7.002L0.845,7.603
|
||||
V8.31H1.69v0.845H2.397z M5.849,3.028c0-0.096-0.048-0.144-0.146-0.144c-0.044,0-0.081,0.015-0.112,0.046L2.014,6.508
|
||||
C1.983,6.538,1.968,6.577,1.968,6.619c0,0.098,0.048,0.146,0.144,0.146c0.044,0,0.081-0.015,0.112-0.046l3.579-3.577
|
||||
C5.834,3.111,5.849,3.073,5.849,3.028z M10,2.395c0,0.233-0.081,0.431-0.245,0.595L8.66,4.085L5.915,1.34L7.01,0.25
|
||||
C7.168,0.083,7.366,0,7.605,0c0.233,0,0.433,0.083,0.601,0.25l1.55,1.544C9.919,1.966,10,2.166,10,2.395z"/>
|
||||
<rect id="minimize-shape" y="3.6" fill-rule="evenodd" clip-rule="evenodd" width="10" height="2.8"/>
|
||||
</defs>
|
||||
<use id="close" xlink:href="#close-shape"/>
|
||||
<use id="close-active" xlink:href="#close-shape"/>
|
||||
<use id="close-disabled" xlink:href="#close-shape"/>
|
||||
<use id="dropdown" xlink:href="#dropdown-shape"/>
|
||||
<use id="dropdown-white" xlink:href="#dropdown-shape"/>
|
||||
<use id="dropdown-active" xlink:href="#dropdown-shape"/>
|
||||
<use id="dropdown-disabled" xlink:href="#dropdown-shape"/>
|
||||
<use id="edit" xlink:href="#edit-shape"/>
|
||||
<use id="edit-active" xlink:href="#edit-shape"/>
|
||||
<use id="edit-disabled" xlink:href="#edit-shape"/>
|
||||
<use id="expand" xlink:href="#expand-shape"/>
|
||||
<use id="expand-active" xlink:href="#expand-shape"/>
|
||||
<use id="expand-disabled" xlink:href="#expand-shape"/>
|
||||
<use id="minimize" xlink:href="#minimize-shape"/>
|
||||
<use id="minimize-active" xlink:href="#minimize-shape"/>
|
||||
<use id="minimize-disabled" xlink:href="#minimize-shape"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1,301 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.CallConnectionWebSocket = (function() {
|
||||
"use strict";
|
||||
|
||||
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
|
||||
|
||||
// Response timeout is 5 seconds as per API.
|
||||
var kResponseTimeout = 5000;
|
||||
|
||||
/**
|
||||
* Handles a websocket specifically for a call connection.
|
||||
*
|
||||
* There should be one of these created for each call connection.
|
||||
*
|
||||
* options items:
|
||||
* - url The url of the websocket to connect to.
|
||||
* - callId The call id for the call
|
||||
* - websocketToken The authentication token for the websocket
|
||||
*
|
||||
* @param {Object} options The options for this websocket.
|
||||
*/
|
||||
function CallConnectionWebSocket(options) {
|
||||
this.options = options || {};
|
||||
|
||||
if (!this.options.url) {
|
||||
throw new Error("No url in options");
|
||||
}
|
||||
if (!this.options.callId) {
|
||||
throw new Error("No callId in options");
|
||||
}
|
||||
if (!this.options.websocketToken) {
|
||||
throw new Error("No websocketToken in options");
|
||||
}
|
||||
|
||||
this._lastServerState = "init";
|
||||
|
||||
// Set loop.debug.sdk to true in the browser, or standalone:
|
||||
// localStorage.setItem("debug.websocket", true);
|
||||
this._debugWebSocket =
|
||||
loop.shared.utils.getBoolPreference("debug.websocket");
|
||||
|
||||
_.extend(this, Backbone.Events);
|
||||
}
|
||||
|
||||
CallConnectionWebSocket.prototype = {
|
||||
/**
|
||||
* Start the connection to the websocket.
|
||||
*
|
||||
* @return {Promise} A promise that resolves when the websocket
|
||||
* server connection is open and "hello"s have been
|
||||
* exchanged. It is rejected if there is a failure in
|
||||
* connection or the initial exchange of "hello"s.
|
||||
*/
|
||||
promiseConnect: function() {
|
||||
var promise = new Promise(
|
||||
function(resolve, reject) {
|
||||
this.socket = new WebSocket(this.options.url);
|
||||
this.socket.onopen = this._onopen.bind(this);
|
||||
this.socket.onmessage = this._onmessage.bind(this);
|
||||
this.socket.onerror = this._onerror.bind(this);
|
||||
this.socket.onclose = this._onclose.bind(this);
|
||||
|
||||
var timeout = setTimeout(function() {
|
||||
if (this.connectDetails && this.connectDetails.reject) {
|
||||
this.connectDetails.reject(WEBSOCKET_REASONS.TIMEOUT);
|
||||
this._clearConnectionFlags();
|
||||
}
|
||||
}.bind(this), kResponseTimeout);
|
||||
this.connectDetails = {
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
timeout: timeout
|
||||
};
|
||||
}.bind(this));
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes the websocket. This shouldn't be the normal action as the server
|
||||
* will normally close the socket. Only in bad error cases, or where we need
|
||||
* to close the socket just before closing the window (to avoid an error)
|
||||
* should we call this.
|
||||
*/
|
||||
close: function() {
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
}
|
||||
},
|
||||
|
||||
_clearConnectionFlags: function() {
|
||||
clearTimeout(this.connectDetails.timeout);
|
||||
delete this.connectDetails;
|
||||
},
|
||||
|
||||
/**
|
||||
* Internal function called to resolve the connection promise.
|
||||
*
|
||||
* It will log an error if no promise is found.
|
||||
*
|
||||
* @param {String} progressState The current state of progress of the
|
||||
* websocket.
|
||||
*/
|
||||
_completeConnection: function(progressState) {
|
||||
if (this.connectDetails && this.connectDetails.resolve) {
|
||||
this.connectDetails.resolve(progressState);
|
||||
this._clearConnectionFlags();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Failed to complete connection promise - no promise available");
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the websocket is connecting, and rejects the connection
|
||||
* promise if appropriate.
|
||||
*
|
||||
* @param {Object} event The event to reject the promise with if
|
||||
* appropriate.
|
||||
*/
|
||||
_checkConnectionFailed: function(event) {
|
||||
if (this.connectDetails && this.connectDetails.reject) {
|
||||
this.connectDetails.reject(event);
|
||||
this._clearConnectionFlags();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies the server that the user has declined the call.
|
||||
*/
|
||||
decline: function() {
|
||||
this._send({
|
||||
messageType: "action",
|
||||
event: "terminate",
|
||||
reason: WEBSOCKET_REASONS.REJECT
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies the server that the user has accepted the call.
|
||||
*/
|
||||
accept: function() {
|
||||
this._send({
|
||||
messageType: "action",
|
||||
event: "accept"
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies the server that the outgoing media is up, and the
|
||||
* incoming media is being received.
|
||||
*/
|
||||
mediaUp: function() {
|
||||
this._send({
|
||||
messageType: "action",
|
||||
event: "media-up"
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies the server that the outgoing call is cancelled by the
|
||||
* user.
|
||||
*/
|
||||
cancel: function() {
|
||||
this._send({
|
||||
messageType: "action",
|
||||
event: "terminate",
|
||||
reason: WEBSOCKET_REASONS.CANCEL
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies the server that something failed during setup.
|
||||
*/
|
||||
mediaFail: function() {
|
||||
this._send({
|
||||
messageType: "action",
|
||||
event: "terminate",
|
||||
reason: WEBSOCKET_REASONS.MEDIA_FAIL
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends data on the websocket.
|
||||
*
|
||||
* @param {Object} data The data to send.
|
||||
*/
|
||||
_send: function(data) {
|
||||
this._log("WS Sending", data);
|
||||
|
||||
this.socket.send(JSON.stringify(data));
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to determine if the server state is in a completed state, i.e.
|
||||
* the server has determined the connection is terminated or connected.
|
||||
*
|
||||
* @return True if the last received state is terminated or connected.
|
||||
*/
|
||||
get _stateIsCompleted() {
|
||||
return this._lastServerState === "terminated" ||
|
||||
this._lastServerState === "connected";
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the socket is open. Automatically sends a "hello"
|
||||
* message to the server.
|
||||
*/
|
||||
_onopen: function() {
|
||||
// Auto-register with the server.
|
||||
this._send({
|
||||
messageType: "hello",
|
||||
callId: this.options.callId,
|
||||
auth: this.options.websocketToken
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a message is received from the server.
|
||||
*
|
||||
* @param {Object} event The websocket onmessage event.
|
||||
*/
|
||||
_onmessage: function(event) {
|
||||
var msgData;
|
||||
try {
|
||||
msgData = JSON.parse(event.data);
|
||||
} catch (x) {
|
||||
console.error("Error parsing received message:", x);
|
||||
return;
|
||||
}
|
||||
|
||||
this._log("WS Receiving", event.data);
|
||||
|
||||
var previousState = this._lastServerState;
|
||||
this._lastServerState = msgData.state;
|
||||
|
||||
switch(msgData.messageType) {
|
||||
case "hello":
|
||||
this._completeConnection(msgData.state);
|
||||
break;
|
||||
case "progress":
|
||||
this.trigger("progress:" + msgData.state);
|
||||
this.trigger("progress", msgData, previousState);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when there is an error on the websocket.
|
||||
*
|
||||
* @param {Object} event A simple error event.
|
||||
*/
|
||||
_onerror: function(event) {
|
||||
this._log("WS Error", event);
|
||||
|
||||
if (!this._stateIsCompleted &&
|
||||
!this._checkConnectionFailed(event)) {
|
||||
this.trigger("error", event);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the websocket is closed.
|
||||
*
|
||||
* @param {CloseEvent} event The details of the websocket closing.
|
||||
*/
|
||||
_onclose: function(event) {
|
||||
this._log("WS Close", event);
|
||||
|
||||
// If the websocket goes away when we're not in a completed state
|
||||
// then its an error. So we either pass it back via the connection
|
||||
// promise, or trigger the closed event.
|
||||
if (!this._stateIsCompleted &&
|
||||
!this._checkConnectionFailed(event)) {
|
||||
this.trigger("closed", event);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Logs debug to the console.
|
||||
*
|
||||
* Parameters: same as console.log
|
||||
*/
|
||||
_log: function() {
|
||||
if (this._debugWebSocket) {
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return CallConnectionWebSocket;
|
||||
})();
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"ecmaFeatures": {
|
||||
"arrowFunctions": true,
|
||||
"blockBindings": true,
|
||||
"destructuring": true,
|
||||
"generators": true,
|
||||
"restParams": true,
|
||||
"spread": true,
|
||||
"objectLiteralShorthandMethods": true,
|
||||
},
|
||||
"rules": {
|
||||
"generator-star-spacing": [2, "after"],
|
||||
// We should fix the errors and enable this (set to 2)
|
||||
"no-var": 0,
|
||||
"strict": [2, "global"]
|
||||
}
|
||||
}
|
||||
@@ -1,463 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["CardDavImporter"];
|
||||
|
||||
let log = Log.repository.getLogger("Loop.Importer.CardDAV");
|
||||
log.level = Log.Level.Debug;
|
||||
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
|
||||
|
||||
const DEPTH_RESOURCE_ONLY = "0";
|
||||
const DEPTH_RESOURCE_AND_CHILDREN = "1";
|
||||
const DEPTH_RESOURCE_AND_ALL_DESCENDENTS = "infinity";
|
||||
|
||||
this.CardDavImporter = function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* CardDAV Address Book importer for Loop.
|
||||
*
|
||||
* The model for address book importers is to have a single public method,
|
||||
* "startImport." When the import is done (or upon a fatal error), the
|
||||
* caller's callback method is called.
|
||||
*
|
||||
* The current model for this importer is based on the subset of CardDAV
|
||||
* implemented by Google. In theory, it should work with other CardDAV
|
||||
* sources, but it has only been tested against Google at the moment.
|
||||
*
|
||||
* At the moment, this importer assumes that no local changes will be made
|
||||
* to data retreived from a remote source: when performing a re-import,
|
||||
* any records that have been previously imported will be completely
|
||||
* removed and replaced with the data received from the CardDAV server.
|
||||
* Witout this behavior, it would be impossible for users to take any
|
||||
* actions to remove fields that are no longer valid.
|
||||
*/
|
||||
|
||||
this.CardDavImporter.prototype = {
|
||||
/**
|
||||
* Begin import of an address book from a CardDAV server.
|
||||
*
|
||||
* @param {Object} options Information needed to perform the address
|
||||
* book import. The following fields are currently
|
||||
* defined:
|
||||
* - "host": CardDAV server base address
|
||||
* (e.g., "google.com")
|
||||
* - "auth": Authentication mechanism to use.
|
||||
* Currently, only "basic" is implemented.
|
||||
* - "user": Username to use for basic auth
|
||||
* - "password": Password to use for basic auth
|
||||
* @param {Function} callback Callback function that will be invoked once the
|
||||
* import operation is complete. The first argument
|
||||
* passed to the callback will be an 'Error' object
|
||||
* or 'null'. If the import operation was
|
||||
* successful, then the second parameter will be a
|
||||
* count of the number of contacts that were
|
||||
* successfully imported.
|
||||
* @param {Object} db Database to add imported contacts into.
|
||||
* Nominally, this is the LoopContacts API. In
|
||||
* practice, anything with the same interface
|
||||
* should work here.
|
||||
*/
|
||||
|
||||
startImport: function(options, callback, db) {
|
||||
let auth;
|
||||
if (!("auth" in options)) {
|
||||
callback(new Error("No authentication specified"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.auth === "basic") {
|
||||
if (!("user" in options) || !("password" in options)) {
|
||||
callback(new Error("Missing user or password for basic authentication"));
|
||||
return;
|
||||
}
|
||||
auth = { method: "basic",
|
||||
user: options.user,
|
||||
password: options.password };
|
||||
} else {
|
||||
callback(new Error("Unknown authentication method"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!("host" in options)){
|
||||
callback(new Error("Missing host for CardDav import"));
|
||||
return;
|
||||
}
|
||||
let host = options.host;
|
||||
|
||||
Task.spawn(function* () {
|
||||
log.info("Starting CardDAV import from " + host);
|
||||
let baseURL = "https://" + host;
|
||||
let startURL = baseURL + "/.well-known/carddav";
|
||||
let abookURL;
|
||||
|
||||
// Get list of contact URLs
|
||||
let body = "<d:propfind xmlns:d='DAV:'><d:prop><d:getetag />" +
|
||||
"</d:prop></d:propfind>";
|
||||
let abook = yield this._davPromise("PROPFIND", startURL, auth,
|
||||
DEPTH_RESOURCE_AND_CHILDREN, body);
|
||||
|
||||
// Build multiget REPORT body from URLs in PROPFIND result
|
||||
let contactElements = abook.responseXML.
|
||||
getElementsByTagNameNS("DAV:", "href");
|
||||
|
||||
body = "<c:addressbook-multiget xmlns:d='DAV:' " +
|
||||
"xmlns:c='urn:ietf:params:xml:ns:carddav'>" +
|
||||
"<d:prop><d:getetag /> <c:address-data /></d:prop>\n";
|
||||
|
||||
for (let element of contactElements) {
|
||||
let href = element.textContent;
|
||||
if (href.substr(-1) == "/") {
|
||||
abookURL = baseURL + href;
|
||||
} else {
|
||||
body += "<d:href>" + href + "</d:href>\n";
|
||||
}
|
||||
}
|
||||
body += "</c:addressbook-multiget>";
|
||||
|
||||
// Retreive contact URL contents
|
||||
let allEntries = yield this._davPromise("REPORT", abookURL, auth,
|
||||
DEPTH_RESOURCE_AND_CHILDREN,
|
||||
body);
|
||||
|
||||
// Parse multiget entites and add to DB
|
||||
let addressData = allEntries.responseXML.getElementsByTagNameNS(
|
||||
"urn:ietf:params:xml:ns:carddav", "address-data");
|
||||
|
||||
log.info("Retreived " + addressData.length + " contacts from " +
|
||||
host + "; importing into database");
|
||||
|
||||
let importCount = 0;
|
||||
for (let i = 0; i < addressData.length; i++) {
|
||||
let vcard = addressData.item(i).textContent;
|
||||
let contact = this._convertVcard(vcard);
|
||||
contact.id += "@" + host;
|
||||
contact.category = ["carddav@" + host];
|
||||
|
||||
let existing = yield this._dbPromise(db, "getByServiceId", contact.id);
|
||||
if (existing) {
|
||||
yield this._dbPromise(db, "remove", existing._guid);
|
||||
}
|
||||
|
||||
// If the contact contains neither email nor phone number, then it
|
||||
// is not useful in the Loop address book: do not add.
|
||||
if (!("tel" in contact) && !("email" in contact)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield this._dbPromise(db, "add", contact);
|
||||
importCount++;
|
||||
}
|
||||
|
||||
return importCount;
|
||||
}.bind(this)).then(
|
||||
(result) => {
|
||||
log.info("Import complete: " + result + " contacts imported.");
|
||||
callback(null, result);
|
||||
},
|
||||
(error) => {
|
||||
log.error("Aborting import: " + error.fileName + ":" +
|
||||
error.lineNumber + ": " + error.message);
|
||||
callback(error);
|
||||
}).then(null,
|
||||
(error) => {
|
||||
log.error("Error in callback: " + error.fileName +
|
||||
":" + error.lineNumber + ": " + error.message);
|
||||
callback(error);
|
||||
}).then(null,
|
||||
(error) => {
|
||||
log.error("Error calling failure callback, giving up: " +
|
||||
error.fileName + ":" + error.lineNumber + ": " +
|
||||
error.message);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrap a LoopContacts-style operation in a promise. The operation is run
|
||||
* immediately, and a corresponding Promise is returned. Error callbacks
|
||||
* cause the promise to be rejected, and success cause it to be resolved.
|
||||
*
|
||||
* @param {Object} db Object the operation is to be performed on
|
||||
* @param {String} method Name of operation being wrapped
|
||||
* @param {Object} param Parameter to be passed to the operation
|
||||
*
|
||||
* @return {Object} Promise corresponding to the result of the operation.
|
||||
*/
|
||||
|
||||
_dbPromise: function(db, method, param) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db[method](param, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a contact in VCard format (see RFC 6350) to the format used
|
||||
* by the LoopContacts class.
|
||||
*
|
||||
* @param {String} vcard The contact to convert, in vcard format
|
||||
* @return {Object} a LoopContacts-style contact object containing
|
||||
* the relevant fields from the vcard.
|
||||
*/
|
||||
|
||||
_convertVcard: function(vcard) {
|
||||
let contact = {};
|
||||
let nickname;
|
||||
vcard.split(/[\r\n]+(?! )/).forEach(
|
||||
function (contentline) {
|
||||
contentline = contentline.replace(/[\r\n]+ /g, "");
|
||||
let match = /^(.*?[^\\]):(.*)$/.exec(contentline);
|
||||
if (match) {
|
||||
let nameparam = match[1];
|
||||
let value = match[2];
|
||||
|
||||
// Poor-man's unescaping
|
||||
value = value.replace(/\\:/g, ":");
|
||||
value = value.replace(/\\,/g, ",");
|
||||
value = value.replace(/\\n/gi, "\n");
|
||||
value = value.replace(/\\\\/g, "\\");
|
||||
|
||||
let param = nameparam.split(/;/);
|
||||
let name = param[0];
|
||||
let pref = false;
|
||||
let type = [];
|
||||
|
||||
for (let i = 1; i < param.length; i++) {
|
||||
if (/^PREF/.exec(param[i]) || /^TYPE=PREF/.exec(param[i])) {
|
||||
pref = true;
|
||||
}
|
||||
let typeMatch = /^TYPE=(.*)/.exec(param[i]);
|
||||
if (typeMatch) {
|
||||
type.push(typeMatch[1].toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
if (!type.length) {
|
||||
type.push("other");
|
||||
}
|
||||
|
||||
if (name === "FN") {
|
||||
value = value.replace(/\\;/g, ";");
|
||||
contact.name = [value];
|
||||
}
|
||||
|
||||
if (name === "N") {
|
||||
// Because we don't have lookbehinds, matching unescaped
|
||||
// semicolons is a pain. Luckily, we know that \r and \n
|
||||
// cannot appear in the strings, so we use them to swap
|
||||
// unescaped semicolons for \n.
|
||||
value = value.replace(/\\;/g, "\r");
|
||||
value = value.replace(/;/g, "\n");
|
||||
value = value.replace(/\r/g, ";");
|
||||
|
||||
let family, given, additional, prefix, suffix;
|
||||
let values = value.split(/\n/);
|
||||
if (values.length >= 5) {
|
||||
[family, given, additional, prefix, suffix] = values;
|
||||
if (prefix.length) {
|
||||
contact.honorificPrefix = [prefix];
|
||||
}
|
||||
if (given.length) {
|
||||
contact.givenName = [given];
|
||||
}
|
||||
if (additional.length) {
|
||||
contact.additionalName = [additional];
|
||||
}
|
||||
if (family.length) {
|
||||
contact.familyName = [family];
|
||||
}
|
||||
if (suffix.length) {
|
||||
contact.honorificSuffix = [suffix];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (name === "EMAIL") {
|
||||
value = value.replace(/\\;/g, ";");
|
||||
if (!("email" in contact)) {
|
||||
contact.email = [];
|
||||
}
|
||||
contact.email.push({
|
||||
pref: pref,
|
||||
type: type,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
if (name === "NICKNAME") {
|
||||
value = value.replace(/\\;/g, ";");
|
||||
// We don't store nickname in contact because it's not
|
||||
// a supported field. We're saving it off here in case we
|
||||
// need to use it if the fullname is blank.
|
||||
nickname = value;
|
||||
}
|
||||
|
||||
if (name === "ADR") {
|
||||
value = value.replace(/\\;/g, "\r");
|
||||
value = value.replace(/;/g, "\n");
|
||||
value = value.replace(/\r/g, ";");
|
||||
let pobox, extra, street, locality, region, code, country;
|
||||
let values = value.split(/\n/);
|
||||
if (values.length >= 7) {
|
||||
[pobox, extra, street, locality, region, code, country] = values;
|
||||
if (!("adr" in contact)) {
|
||||
contact.adr = [];
|
||||
}
|
||||
contact.adr.push({
|
||||
pref: pref,
|
||||
type: type,
|
||||
streetAddress: (street || pobox) + (extra ? (" " + extra) : ""),
|
||||
locality: locality,
|
||||
region: region,
|
||||
postalCode: code,
|
||||
countryName: country
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (name === "TEL") {
|
||||
value = value.replace(/\\;/g, ";");
|
||||
if (!("tel" in contact)) {
|
||||
contact.tel = [];
|
||||
}
|
||||
contact.tel.push({
|
||||
pref: pref,
|
||||
type: type,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
if (name === "ORG") {
|
||||
value = value.replace(/\\;/g, "\r");
|
||||
value = value.replace(/;/g, "\n");
|
||||
value = value.replace(/\r/g, ";");
|
||||
if (!("org" in contact)) {
|
||||
contact.org = [];
|
||||
}
|
||||
contact.org.push(value.replace(/\n.*/, ""));
|
||||
}
|
||||
|
||||
if (name === "TITLE") {
|
||||
value = value.replace(/\\;/g, ";");
|
||||
if (!("jobTitle" in contact)) {
|
||||
contact.jobTitle = [];
|
||||
}
|
||||
contact.jobTitle.push(value);
|
||||
}
|
||||
|
||||
if (name === "BDAY") {
|
||||
value = value.replace(/\\;/g, ";");
|
||||
contact.bday = Date.parse(value);
|
||||
}
|
||||
|
||||
if (name === "UID") {
|
||||
contact.id = value;
|
||||
}
|
||||
|
||||
if (name === "NOTE") {
|
||||
value = value.replace(/\\;/g, ";");
|
||||
if (!("note" in contact)) {
|
||||
contact.note = [];
|
||||
}
|
||||
contact.note.push(value);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Basic sanity checking: make sure the name field isn't empty
|
||||
if (!("name" in contact) || contact.name[0].length == 0) {
|
||||
if (("familyName" in contact) && ("givenName" in contact)) {
|
||||
// First, try to synthesize a full name from the name fields.
|
||||
// Ordering is culturally sensitive, but we don't have
|
||||
// cultural origin information available here. The best we
|
||||
// can really do is "family, given additional"
|
||||
contact.name = [contact.familyName[0] + ", " + contact.givenName[0]];
|
||||
if (("additionalName" in contact)) {
|
||||
contact.name[0] += " " + contact.additionalName[0];
|
||||
}
|
||||
} else {
|
||||
if (nickname) {
|
||||
contact.name = [nickname];
|
||||
} else if ("familyName" in contact) {
|
||||
contact.name = [contact.familyName[0]];
|
||||
} else if ("givenName" in contact) {
|
||||
contact.name = [contact.givenName[0]];
|
||||
} else if ("org" in contact) {
|
||||
contact.name = [contact.org[0]];
|
||||
} else if ("email" in contact) {
|
||||
contact.name = [contact.email[0].value];
|
||||
} else if ("tel" in contact) {
|
||||
contact.name = [contact.tel[0].value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return contact;
|
||||
},
|
||||
|
||||
/**
|
||||
* Issues a CardDAV request (see RFC 6352) and returns a Promise to represent
|
||||
* the success or failure state of the request.
|
||||
*
|
||||
* @param {String} method WebDAV method to use (e.g., "PROPFIND")
|
||||
* @param {String} url HTTP URL to use for the request
|
||||
* @param {Object} auth Object with authentication-related configuration.
|
||||
* See documentation for startImport for details.
|
||||
* @param {Number} depth Value to use for the WebDAV (HTTP) "Depth" header
|
||||
* @param {String} body Body to include in the WebDAV (HTTP) request
|
||||
*
|
||||
* @return {Object} Promise representing the request operation outcome.
|
||||
* If resolved, the resolution value is the XMLHttpRequest
|
||||
* that was used to perform the request.
|
||||
*/
|
||||
_davPromise: function(method, url, auth, depth, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
||||
createInstance(Ci.nsIXMLHttpRequest);
|
||||
let user = "";
|
||||
let password = "";
|
||||
|
||||
if (auth.method == "basic") {
|
||||
user = auth.user;
|
||||
password = auth.password;
|
||||
}
|
||||
|
||||
req.open(method, url, true, user, password);
|
||||
|
||||
req.setRequestHeader("Depth", depth);
|
||||
req.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
|
||||
|
||||
req.onload = function() {
|
||||
if (req.status < 400) {
|
||||
resolve(req);
|
||||
} else {
|
||||
reject(new Error(req.status + " " + req.statusText));
|
||||
}
|
||||
};
|
||||
|
||||
req.onerror = function(error) {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
req.send(body);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,598 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Log",
|
||||
"resource://gre/modules/Log.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["GoogleImporter"];
|
||||
|
||||
let log = Log.repository.getLogger("Loop.Importer.Google");
|
||||
log.level = Log.Level.Debug;
|
||||
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
|
||||
|
||||
/**
|
||||
* Helper function that reads and maps the respective node value from specific
|
||||
* XML DOMNodes to fields on a `target` object.
|
||||
* Example: the value for field 'fullName' can be read from the XML DOMNode
|
||||
* 'name', so that's the mapping we need to make; get the nodeValue of
|
||||
* the node called 'name' and tack it to the target objects' 'fullName'
|
||||
* property.
|
||||
*
|
||||
* @param {Map} fieldMap Map object containing the field name -> node
|
||||
* name mapping
|
||||
* @param {XMLDOMNode} node DOM node to fetch the values from for each field
|
||||
* @param {String} ns XML namespace for the DOM nodes to retrieve. Optional.
|
||||
* @param {Object} target Object to store the values found. Optional.
|
||||
* Defaults to a new object.
|
||||
* @param {Boolean} wrapInArray Indicates whether to map the field values in
|
||||
* an Array. Optional. Defaults to `false`.
|
||||
* @returns The `target` object with the node values mapped to the appropriate fields.
|
||||
*/
|
||||
const extractFieldsFromNode = function(fieldMap, node, ns = null, target = {}, wrapInArray = false) {
|
||||
for (let [field, nodeName] of fieldMap) {
|
||||
let nodeList = ns ? node.getElementsByTagNameNS(ns, nodeName) :
|
||||
node.getElementsByTagName(nodeName);
|
||||
if (nodeList.length) {
|
||||
if (!nodeList[0].firstChild) {
|
||||
continue;
|
||||
}
|
||||
let value = nodeList[0].textContent;
|
||||
target[field] = wrapInArray ? [value] : value;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that reads the type of (email-)address or phone number from an
|
||||
* XMLDOMNode.
|
||||
*
|
||||
* @param {XMLDOMNode} node
|
||||
* @returns String that depicts the type of field value.
|
||||
*/
|
||||
const getFieldType = function(node) {
|
||||
if (node.hasAttribute("rel")) {
|
||||
let rel = node.getAttribute("rel");
|
||||
// The 'rel' attribute is formatted like: http://schemas.google.com/g/2005#work.
|
||||
return rel.substr(rel.lastIndexOf("#") + 1);
|
||||
}
|
||||
if (node.hasAttribute("label")) {
|
||||
return node.getAttribute("label");
|
||||
}
|
||||
return "other";
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the preferred entry of a contact. Returns the first entry when no
|
||||
* preferred flag is set.
|
||||
*
|
||||
* @param {Object} contact The contact object to check for preferred entries
|
||||
* @param {String} which Type of entry to check. Optional, defaults to 'email'
|
||||
* @throws An Error when no (preferred) entries are listed for this contact.
|
||||
*/
|
||||
const getPreferred = function(contact, which = "email") {
|
||||
if (!(which in contact) || !contact[which].length) {
|
||||
throw new Error("No " + which + " entry available.");
|
||||
}
|
||||
let preferred = contact[which][0];
|
||||
contact[which].some(function(entry) {
|
||||
if (entry.pref) {
|
||||
preferred = entry;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return preferred;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch an auth token (clientID or client secret), which may be overridden by
|
||||
* a pref if it's set.
|
||||
*
|
||||
* @param {String} paramValue Initial, default, value of the parameter
|
||||
* @param {String} prefName Fully qualified name of the pref to check for
|
||||
* @param {Boolean} encode Whether to URLEncode the param string
|
||||
*/
|
||||
const getUrlParam = function(paramValue, prefName, encode = true) {
|
||||
if (Services.prefs.getPrefType(prefName))
|
||||
paramValue = Services.prefs.getCharPref(prefName);
|
||||
paramValue = Services.urlFormatter.formatURL(paramValue);
|
||||
|
||||
return encode ? encodeURIComponent(paramValue) : paramValue;
|
||||
};
|
||||
|
||||
let gAuthWindow, gProfileId;
|
||||
const kAuthWindowSize = {
|
||||
width: 420,
|
||||
height: 460
|
||||
};
|
||||
const kContactsMaxResults = 10000000;
|
||||
const kContactsChunkSize = 100;
|
||||
const kTitlebarPollTimeout = 200;
|
||||
const kNS_GD = "http://schemas.google.com/g/2005";
|
||||
|
||||
/**
|
||||
* GoogleImporter class.
|
||||
*
|
||||
* Main entrypoint is the `startImport` method which calls several tasks necessary
|
||||
* to import contacts from Google.
|
||||
* Authentication is performed using an OAuth strategy which is loaded in a popup
|
||||
* window.
|
||||
*/
|
||||
this.GoogleImporter = function() {};
|
||||
|
||||
this.GoogleImporter.prototype = {
|
||||
/**
|
||||
* Start the import process of contacts from the Google service, using its Contacts
|
||||
* API - https://developers.google.com/google-apps/contacts/v3/.
|
||||
* The import consists of four tasks:
|
||||
* 1. Get the authentication code which can be used to retrieve an OAuth token
|
||||
* pair. This is the bulk of the authentication flow that will be handled in
|
||||
* a popup window by Google. The user will need to login to the Google service
|
||||
* with his or her account and grant permission to our app to manage their
|
||||
* contacts.
|
||||
* 2. Get the tokenset from the Google service, using the authentication code
|
||||
* that was retrieved in task 1.
|
||||
* 3. Fetch all the contacts from the Google service, using the OAuth tokenset
|
||||
* that was retrieved in task 2.
|
||||
* 4. Process the contacts, map them to the MozContact format and store each
|
||||
* contact in the database, if it doesn't exist yet.
|
||||
*
|
||||
* @param {Object} options Options to control the behavior of the import.
|
||||
* Not used by this importer class.
|
||||
* @param {Function} callback Function to invoke when the import process
|
||||
* is done or when an error occurs that halts
|
||||
* the import process. The first argument passed
|
||||
* in an Error object or `null` and the second
|
||||
* argument is an object with import statistics.
|
||||
* @param {LoopContacts} db Instance of the LoopContacts database object,
|
||||
* which will store the newly found contacts
|
||||
* @param {nsIDomWindow} windowRef Reference to the ChromeWindow the import is
|
||||
* invoked from. It will be used to be able to
|
||||
* open a window for the OAuth process with chrome
|
||||
* privileges.
|
||||
*/
|
||||
startImport: function(options, callback, db, windowRef) {
|
||||
Task.spawn(function* () {
|
||||
let code = yield this._promiseAuthCode(windowRef);
|
||||
let tokenSet = yield this._promiseTokenSet(code);
|
||||
let contactEntries = yield this._getContactEntries(tokenSet);
|
||||
let {total, success, ids} = yield this._processContacts(contactEntries, db, tokenSet);
|
||||
yield this._purgeContacts(ids, db);
|
||||
|
||||
return {
|
||||
total: total,
|
||||
success: success
|
||||
};
|
||||
}.bind(this)).then(stats => callback(null, stats),
|
||||
error => callback(error))
|
||||
.then(null, ex => log.error(ex.fileName + ":" + ex.lineNumber + ": " + ex.message));
|
||||
},
|
||||
|
||||
/**
|
||||
* Task that yields an authentication code that is returned after the user signs
|
||||
* in to the Google service. This code can be used by this class to retrieve an
|
||||
* OAuth tokenset.
|
||||
*
|
||||
* @param {nsIDOMWindow} windowRef Reference to the ChromeWindow the import is
|
||||
* invoked from. It will be used to be able to
|
||||
* open a window for the OAuth process with chrome
|
||||
* privileges.
|
||||
* @throws An `Error` object when authentication fails, or the authentication
|
||||
* code as a String.
|
||||
*/
|
||||
_promiseAuthCode: Task.async(function* (windowRef) {
|
||||
// Close a window that got lost in a previous login attempt.
|
||||
if (gAuthWindow && !gAuthWindow.closed) {
|
||||
gAuthWindow.close();
|
||||
gAuthWindow = null;
|
||||
}
|
||||
|
||||
let url = getUrlParam("https://accounts.google.com/o/oauth2/",
|
||||
"loop.oauth.google.URL", false) +
|
||||
"auth?response_type=code&client_id=" +
|
||||
getUrlParam("%GOOGLE_OAUTH_API_CLIENTID%", "loop.oauth.google.clientIdOverride");
|
||||
for (let param of ["redirect_uri", "scope"]) {
|
||||
url += "&" + param + "=" + encodeURIComponent(
|
||||
Services.prefs.getCharPref("loop.oauth.google." + param));
|
||||
}
|
||||
const features = "centerscreen,resizable=yes,toolbar=no,menubar=no,status=no,directories=no," +
|
||||
"width=" + kAuthWindowSize.width + ",height=" + kAuthWindowSize.height;
|
||||
gAuthWindow = windowRef.openDialog(windowRef.getBrowserURL(), "_blank", features, url);
|
||||
gAuthWindow.focus();
|
||||
|
||||
let code;
|
||||
|
||||
function promiseTimeOut() {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, kTitlebarPollTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
// The following loops runs as long as the OAuth windows' titlebar doesn't
|
||||
// yield a response from the Google service. If an error occurs, the loop
|
||||
// will terminate early.
|
||||
while (!code) {
|
||||
if (!gAuthWindow || gAuthWindow.closed) {
|
||||
throw new Error("Popup window was closed before authentication succeeded");
|
||||
}
|
||||
|
||||
let matches = gAuthWindow.document.title.match(/(error|code)=([^\s]+)/);
|
||||
if (matches && matches.length) {
|
||||
let [, type, message] = matches;
|
||||
gAuthWindow.close();
|
||||
gAuthWindow = null;
|
||||
if (type == "error") {
|
||||
throw new Error("Google authentication failed with error: " + message.trim());
|
||||
} else if (type == "code") {
|
||||
code = message.trim();
|
||||
} else {
|
||||
throw new Error("Unknown response from Google");
|
||||
}
|
||||
} else {
|
||||
yield promiseTimeOut();
|
||||
}
|
||||
}
|
||||
|
||||
return code;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Fetch an OAuth tokenset, that will be used to authenticate Google API calls,
|
||||
* using the authentication token retrieved in `_promiseAuthCode`.
|
||||
*
|
||||
* @param {String} code The authentication code.
|
||||
* @returns an `Error` object upon failure or an object containing OAuth tokens.
|
||||
*/
|
||||
_promiseTokenSet: function(code) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
||||
.createInstance(Ci.nsIXMLHttpRequest);
|
||||
|
||||
request.open("POST", getUrlParam("https://accounts.google.com/o/oauth2/",
|
||||
"loop.oauth.google.URL",
|
||||
false) + "token");
|
||||
|
||||
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
request.onload = function() {
|
||||
if (request.status < 400) {
|
||||
let tokenSet = JSON.parse(request.responseText);
|
||||
tokenSet.date = Date.now();
|
||||
resolve(tokenSet);
|
||||
} else {
|
||||
reject(new Error(request.status + " " + request.statusText));
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = function(error) {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
let body = "grant_type=authorization_code&code=" + encodeURIComponent(code) +
|
||||
"&client_id=" + getUrlParam("%GOOGLE_OAUTH_API_CLIENTID%",
|
||||
"loop.oauth.google.clientIdOverride") +
|
||||
"&client_secret=" + getUrlParam("%GOOGLE_OAUTH_API_KEY%",
|
||||
"loop.oauth.google.clientSecretOverride") +
|
||||
"&redirect_uri=" + encodeURIComponent(Services.prefs.getCharPref(
|
||||
"loop.oauth.google.redirect_uri"));
|
||||
|
||||
request.send(body);
|
||||
});
|
||||
},
|
||||
|
||||
_promiseRequestXML: function(URL, tokenSet) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
||||
.createInstance(Ci.nsIXMLHttpRequest);
|
||||
|
||||
request.open("GET", URL);
|
||||
|
||||
request.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
|
||||
request.setRequestHeader("GData-Version", "3.0");
|
||||
request.setRequestHeader("Authorization", "Bearer " + tokenSet.access_token);
|
||||
|
||||
request.onload = function() {
|
||||
if (request.status < 400) {
|
||||
let doc = request.responseXML;
|
||||
// First get the profile id, which is present in each XML request.
|
||||
let currNode = doc.documentElement.firstChild;
|
||||
while (currNode) {
|
||||
if (currNode.nodeType == 1 && currNode.localName == "id") {
|
||||
gProfileId = currNode.textContent;
|
||||
break;
|
||||
}
|
||||
currNode = currNode.nextSibling;
|
||||
}
|
||||
|
||||
resolve(doc);
|
||||
} else {
|
||||
reject(new Error(request.status + " " + request.statusText));
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = function(error) {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
request.send();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches all the contacts in a users' address book.
|
||||
*
|
||||
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contacts
|
||||
*
|
||||
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
|
||||
* @returns An `Error` object upon failure or an Array of contact XML nodes.
|
||||
*/
|
||||
_getContactEntries: Task.async(function* (tokenSet) {
|
||||
let URL = getUrlParam("https://www.google.com/m8/feeds/contacts/default/full",
|
||||
"loop.oauth.google.getContactsURL",
|
||||
false) + "?max-results=" + kContactsMaxResults;
|
||||
let xmlDoc = yield this._promiseRequestXML(URL, tokenSet);
|
||||
// Then kick of the importing of contact entries.
|
||||
return Array.prototype.slice.call(xmlDoc.querySelectorAll("entry"));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Fetches the default group from a users' address book, called 'Contacts'.
|
||||
*
|
||||
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contact_groups
|
||||
*
|
||||
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
|
||||
* @returns An `Error` object upon failure or the String group ID.
|
||||
*/
|
||||
_getContactsGroupId: Task.async(function* (tokenSet) {
|
||||
let URL = getUrlParam("https://www.google.com/m8/feeds/groups/default/full",
|
||||
"loop.oauth.google.getGroupsURL",
|
||||
false) + "?max-results=" + kContactsMaxResults;
|
||||
let xmlDoc = yield this._promiseRequestXML(URL, tokenSet);
|
||||
let contactsEntry = xmlDoc.querySelector("systemGroup[id=\"Contacts\"]");
|
||||
if (!contactsEntry) {
|
||||
throw new Error("Contacts group not present");
|
||||
}
|
||||
// Select the actual <entry> node, which is the parent of the <systemGroup>
|
||||
// node we just selected.
|
||||
contactsEntry = contactsEntry.parentNode;
|
||||
return contactsEntry.getElementsByTagName("id")[0].textContent;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Process the contact XML nodes that Google provides, convert them to the MozContact
|
||||
* format, check if the contact already exists in the database and when it doesn't,
|
||||
* store it permanently.
|
||||
* During this process statistics are collected about the amount of successful
|
||||
* imports. The consumer of this class may use these statistics to inform the
|
||||
* user.
|
||||
* Note: only contacts that are part of the 'Contacts' system group will be
|
||||
* imported.
|
||||
*
|
||||
* @param {Array} contactEntries List of XML DOMNodes contact entries.
|
||||
* @param {LoopContacts} db Instance of the LoopContacts database
|
||||
* object, which will store the newly found
|
||||
* contacts.
|
||||
* @param {Object} tokenSet OAuth tokenset used to authenticate a
|
||||
* request
|
||||
* @returns An `Error` object upon failure or an Object with statistics in the
|
||||
* following format: `{ total: 25, success: 13, ids: {} }`.
|
||||
*/
|
||||
_processContacts: Task.async(function* (contactEntries, db, tokenSet) {
|
||||
let stats = {
|
||||
total: contactEntries.length,
|
||||
success: 0,
|
||||
ids: {}
|
||||
};
|
||||
|
||||
// Contacts that are _not_ part of the 'Contacts' group will be ignored.
|
||||
let contactsGroupId = yield this._getContactsGroupId(tokenSet);
|
||||
|
||||
for (let entry of contactEntries) {
|
||||
let contact = this._processContactFields(entry);
|
||||
|
||||
stats.ids[contact.id] = 1;
|
||||
let existing = yield db.promise("getByServiceId", contact.id);
|
||||
if (existing) {
|
||||
yield db.promise("remove", existing._guid);
|
||||
}
|
||||
|
||||
// After contact removal, check if the entry is part of the correct group.
|
||||
if (!entry.querySelector("groupMembershipInfo[deleted=\"false\"][href=\"" +
|
||||
contactsGroupId + "\"]")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the contact contains neither email nor phone number, then it is not
|
||||
// useful in the Loop address book: do not add.
|
||||
if (!("email" in contact) && !("tel" in contact)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield db.promise("add", contact);
|
||||
stats.success++;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Parse an XML node to map the appropriate data to MozContact field equivalents.
|
||||
*
|
||||
* @param {XMLDOMNode} entry The contact XML node in Google format to process.
|
||||
* @returns `null` if the contact entry appears to be invalid or an Object containing
|
||||
* all the contact data found in the XML.
|
||||
*/
|
||||
_processContactFields: function(entry) {
|
||||
// Basic fields in the main 'atom' namespace.
|
||||
let contact = extractFieldsFromNode(new Map([
|
||||
["id", "id"],
|
||||
// published: n/a
|
||||
["updated", "updated"]
|
||||
// bday: n/a
|
||||
]), entry);
|
||||
|
||||
// Fields that need to wrapped in an Array.
|
||||
extractFieldsFromNode(new Map([
|
||||
["name", "fullName"],
|
||||
["givenName", "givenName"],
|
||||
["familyName", "familyName"],
|
||||
["additionalName", "additionalName"]
|
||||
]), entry, kNS_GD, contact, true);
|
||||
|
||||
// The 'note' field needs to wrapped in an array, but its source node is not
|
||||
// namespaced.
|
||||
extractFieldsFromNode(new Map([
|
||||
["note", "content"]
|
||||
]), entry, null, contact, true);
|
||||
|
||||
// Process physical, earthly addresses.
|
||||
let addressNodes = entry.getElementsByTagNameNS(kNS_GD, "structuredPostalAddress");
|
||||
if (addressNodes.length) {
|
||||
contact.adr = [];
|
||||
for (let [,addressNode] of Iterator(addressNodes)) {
|
||||
let adr = extractFieldsFromNode(new Map([
|
||||
["countryName", "country"],
|
||||
["locality", "city"],
|
||||
["postalCode", "postcode"],
|
||||
["region", "region"],
|
||||
["streetAddress", "street"]
|
||||
]), addressNode, kNS_GD);
|
||||
if (Object.keys(adr).length) {
|
||||
adr.pref = (addressNode.getAttribute("primary") == "true");
|
||||
adr.type = [getFieldType(addressNode)];
|
||||
contact.adr.push(adr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process email addresses.
|
||||
let emailNodes = entry.getElementsByTagNameNS(kNS_GD, "email");
|
||||
if (emailNodes.length) {
|
||||
contact.email = [];
|
||||
for (let [,emailNode] of Iterator(emailNodes)) {
|
||||
contact.email.push({
|
||||
pref: (emailNode.getAttribute("primary") == "true"),
|
||||
type: [getFieldType(emailNode)],
|
||||
value: emailNode.getAttribute("address")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process telephone numbers.
|
||||
let phoneNodes = entry.getElementsByTagNameNS(kNS_GD, "phoneNumber");
|
||||
if (phoneNodes.length) {
|
||||
contact.tel = [];
|
||||
for (let [,phoneNode] of Iterator(phoneNodes)) {
|
||||
let phoneNumber = phoneNode.hasAttribute("uri") ?
|
||||
phoneNode.getAttribute("uri").replace("tel:", "") :
|
||||
phoneNode.textContent;
|
||||
contact.tel.push({
|
||||
pref: (phoneNode.getAttribute("primary") == "true"),
|
||||
type: [getFieldType(phoneNode)],
|
||||
value: phoneNumber
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let orgNodes = entry.getElementsByTagNameNS(kNS_GD, "organization");
|
||||
if (orgNodes.length) {
|
||||
contact.org = [];
|
||||
contact.jobTitle = [];
|
||||
for (let [,orgNode] of Iterator(orgNodes)) {
|
||||
let orgElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0];
|
||||
let titleElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0];
|
||||
contact.org.push(orgElement ? orgElement.textContent : "");
|
||||
contact.jobTitle.push(titleElement ? titleElement.textContent : "");
|
||||
}
|
||||
}
|
||||
|
||||
contact.category = ["google"];
|
||||
|
||||
// Basic sanity checking: make sure the name field isn't empty
|
||||
if (!("name" in contact) || contact.name[0].length == 0) {
|
||||
if (("familyName" in contact) && ("givenName" in contact)) {
|
||||
// First, try to synthesize a full name from the name fields.
|
||||
// Ordering is culturally sensitive, but we don't have
|
||||
// cultural origin information available here. The best we
|
||||
// can really do is "family, given additional"
|
||||
contact.name = [contact.familyName[0] + ", " + contact.givenName[0]];
|
||||
if (("additionalName" in contact)) {
|
||||
contact.name[0] += " " + contact.additionalName[0];
|
||||
}
|
||||
} else {
|
||||
let profileTitle = extractFieldsFromNode(new Map([["title", "title"]]), entry);
|
||||
if (("title" in profileTitle)) {
|
||||
contact.name = [profileTitle.title];
|
||||
} else if ("familyName" in contact) {
|
||||
contact.name = [contact.familyName[0]];
|
||||
} else if ("givenName" in contact) {
|
||||
contact.name = [contact.givenName[0]];
|
||||
} else if ("org" in contact) {
|
||||
contact.name = [contact.org[0]];
|
||||
} else {
|
||||
let email;
|
||||
try {
|
||||
email = getPreferred(contact);
|
||||
} catch (ex) {}
|
||||
if (email) {
|
||||
contact.name = [email.value];
|
||||
} else {
|
||||
let tel;
|
||||
try {
|
||||
tel = getPreferred(contact, "tel");
|
||||
} catch (ex) {}
|
||||
if (tel) {
|
||||
contact.name = [tel.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return contact;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all contacts from the database that are not present anymore in the
|
||||
* remote data-source.
|
||||
*
|
||||
* @param {Object} ids Map of IDs collected earlier of all the contacts
|
||||
* that are available on the remote data-source
|
||||
* @param {LoopContacts} db Instance of the LoopContacts database object, which
|
||||
* will store the newly found contacts
|
||||
*/
|
||||
_purgeContacts: Task.async(function* (ids, db) {
|
||||
let contacts = yield db.promise("getAll");
|
||||
let profileId = "https://www.google.com/m8/feeds/contacts/" + encodeURIComponent(gProfileId);
|
||||
let processed = 0;
|
||||
|
||||
function promiseSkipABeat() {
|
||||
return new Promise(resolve => Services.tm.currentThread.dispatch(resolve,
|
||||
Ci.nsIThread.DISPATCH_NORMAL));
|
||||
}
|
||||
|
||||
for (let [guid, contact] of Iterator(contacts)) {
|
||||
if (++processed % kContactsChunkSize === 0) {
|
||||
// Skip a beat every time we processed a chunk.
|
||||
yield promiseSkipABeat;
|
||||
}
|
||||
|
||||
if (contact.id.indexOf(profileId) >= 0 && !ids[contact.id]) {
|
||||
yield db.promise("remove", guid);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -1,467 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["LoopCalls"];
|
||||
|
||||
const EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService",
|
||||
"resource:///modules/loop/MozLoopService.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LOOP_SESSION_TYPE",
|
||||
"resource:///modules/loop/MozLoopService.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
|
||||
"resource:///modules/loop/LoopContacts.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
/**
|
||||
* Attempts to open a websocket.
|
||||
*
|
||||
* A new websocket interface is used each time. If an onStop callback
|
||||
* was received, calling asyncOpen() on the same interface will
|
||||
* trigger a "alreay open socket" exception even though the channel
|
||||
* is logically closed.
|
||||
*/
|
||||
function CallProgressSocket(progressUrl, callId, token) {
|
||||
if (!progressUrl || !callId || !token) {
|
||||
throw new Error("missing required arguments");
|
||||
}
|
||||
|
||||
this._progressUrl = progressUrl;
|
||||
this._callId = callId;
|
||||
this._token = token;
|
||||
}
|
||||
|
||||
CallProgressSocket.prototype = {
|
||||
/**
|
||||
* Open websocket and run hello exchange.
|
||||
* Sends a hello message to the server.
|
||||
*
|
||||
* @param {function} Callback used after a successful handshake
|
||||
* over the progressUrl.
|
||||
* @param {function} Callback used if an error is encountered
|
||||
*/
|
||||
connect: function(onSuccess, onError) {
|
||||
this._onSuccess = onSuccess;
|
||||
this._onError = onError || (reason => {
|
||||
MozLoopService.log.warn("LoopCalls::callProgessSocket - ", reason);
|
||||
});
|
||||
|
||||
if (!onSuccess) {
|
||||
this._onError("missing onSuccess argument");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Services.io.offline) {
|
||||
this._onError("IO offline");
|
||||
return;
|
||||
}
|
||||
|
||||
let uri = Services.io.newURI(this._progressUrl, null, null);
|
||||
|
||||
// Allow _websocket to be set for testing.
|
||||
if (!this._websocket) {
|
||||
this._websocket = Cc["@mozilla.org/network/protocol;1?name=" + uri.scheme]
|
||||
.createInstance(Ci.nsIWebSocketChannel);
|
||||
|
||||
this._websocket.initLoadInfo(null, // aLoadingNode
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
null, // aTriggeringPrincipal
|
||||
Ci.nsILoadInfo.SEC_NORMAL,
|
||||
Ci.nsIContentPolicy.TYPE_WEBSOCKET);
|
||||
}
|
||||
|
||||
this._websocket.asyncOpen(uri, this._progressUrl, this, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener method, handles the start of the websocket stream.
|
||||
* Sends a hello message to the server.
|
||||
*
|
||||
* @param {nsISupports} aContext Not used
|
||||
*/
|
||||
onStart: function() {
|
||||
let helloMsg = {
|
||||
messageType: "hello",
|
||||
callId: this._callId,
|
||||
auth: this._token,
|
||||
};
|
||||
try { // in case websocket has closed before this handler is run
|
||||
this._websocket.sendMsg(JSON.stringify(helloMsg));
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener method, called when the websocket is closed.
|
||||
*
|
||||
* @param {nsISupports} aContext Not used
|
||||
* @param {nsresult} aStatusCode Reason for stopping (NS_OK = successful)
|
||||
*/
|
||||
onStop: function(aContext, aStatusCode) {
|
||||
if (!this._handshakeComplete) {
|
||||
this._onError("[" + aStatusCode + "]");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener method, called when the websocket is closed by the server.
|
||||
* If there are errors, onStop may be called without ever calling this
|
||||
* method.
|
||||
*
|
||||
* @param {nsISupports} aContext Not used
|
||||
* @param {integer} aCode the websocket closing handshake close code
|
||||
* @param {String} aReason the websocket closing handshake close reason
|
||||
*/
|
||||
onServerClose: function(aContext, aCode, aReason) {
|
||||
if (!this._handshakeComplete) {
|
||||
this._onError("[" + aCode + "]" + aReason);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener method, called when the websocket receives a message.
|
||||
*
|
||||
* @param {nsISupports} aContext Not used
|
||||
* @param {String} aMsg The message data
|
||||
*/
|
||||
onMessageAvailable: function(aContext, aMsg) {
|
||||
let msg = {};
|
||||
try {
|
||||
msg = JSON.parse(aMsg);
|
||||
} catch (error) {
|
||||
MozLoopService.log.error("LoopCalls: error parsing progress message - ", error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.messageType && msg.messageType === 'hello') {
|
||||
this._handshakeComplete = true;
|
||||
this._onSuccess();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Create a JSON message payload and send on websocket.
|
||||
*
|
||||
* @param {Object} aMsg Message to send.
|
||||
*/
|
||||
_send: function(aMsg) {
|
||||
if (!this._handshakeComplete) {
|
||||
MozLoopService.log.warn("LoopCalls::_send error - handshake not complete");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._websocket.sendMsg(JSON.stringify(aMsg));
|
||||
}
|
||||
catch (error) {
|
||||
this._onError(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies the server that the user has declined the call
|
||||
* with a reason of busy.
|
||||
*/
|
||||
sendBusy: function() {
|
||||
this._send({
|
||||
messageType: "action",
|
||||
event: "terminate",
|
||||
reason: "busy"
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal helper methods and state
|
||||
*
|
||||
* The registration is a two-part process. First we need to connect to
|
||||
* and register with the push server. Then we need to take the result of that
|
||||
* and register with the Loop server.
|
||||
*/
|
||||
let LoopCallsInternal = {
|
||||
mocks: {
|
||||
webSocket: undefined,
|
||||
},
|
||||
|
||||
conversationInProgress: {},
|
||||
|
||||
/**
|
||||
* Callback from MozLoopPushHandler - A push notification has been received from
|
||||
* the server.
|
||||
*
|
||||
* @param {String} version The version information from the server.
|
||||
*/
|
||||
onNotification: function(version, channelID) {
|
||||
if (MozLoopService.doNotDisturb) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We set this here as it is assumed that once the user receives an incoming
|
||||
// call, they'll have had enough time to see the terms of service. See
|
||||
// bug 1046039 for background.
|
||||
Services.prefs.setCharPref("loop.seenToS", "seen");
|
||||
|
||||
// Request the information on the new call(s) associated with this version.
|
||||
// The registered FxA session is checked first, then the anonymous session.
|
||||
// Make the call to get the GUEST session regardless of whether the FXA
|
||||
// request fails.
|
||||
|
||||
if (channelID == MozLoopService.channelIDs.callsFxA && MozLoopService.userProfile) {
|
||||
this._getCalls(LOOP_SESSION_TYPE.FXA, version);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a hawkRequest to GET/calls?=version for this session type.
|
||||
*
|
||||
* @param {LOOP_SESSION_TYPE} sessionType - type of hawk token used
|
||||
* for the GET operation.
|
||||
* @param {Object} version - LoopPushService notification version
|
||||
*
|
||||
* @returns {Promise}
|
||||
*
|
||||
*/
|
||||
|
||||
_getCalls: function(sessionType, version) {
|
||||
return MozLoopService.hawkRequest(sessionType, "/calls?version=" + version, "GET").then(
|
||||
response => { this._processCalls(response, sessionType); }
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Process the calls array returned from a GET/calls?version request.
|
||||
* Only one active call is permitted at this time.
|
||||
*
|
||||
* @param {Object} response - response payload from GET
|
||||
*
|
||||
* @param {LOOP_SESSION_TYPE} sessionType - type of hawk token used
|
||||
* for the GET operation.
|
||||
*
|
||||
*/
|
||||
|
||||
_processCalls: function(response, sessionType) {
|
||||
try {
|
||||
let respData = JSON.parse(response.body);
|
||||
if (respData.calls && Array.isArray(respData.calls)) {
|
||||
respData.calls.forEach((callData) => {
|
||||
if ("id" in this.conversationInProgress) {
|
||||
this._returnBusy(callData);
|
||||
} else {
|
||||
callData.sessionType = sessionType;
|
||||
callData.type = "incoming";
|
||||
this._startCall(callData);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
MozLoopService.log.warn("Error: missing calls[] in response");
|
||||
}
|
||||
} catch (err) {
|
||||
MozLoopService.log.warn("Error parsing calls info", err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts a call, saves the call data, and opens a chat window.
|
||||
*
|
||||
* @param {Object} callData The data associated with the call including an id.
|
||||
* The data should include the type - "incoming" or
|
||||
* "outgoing".
|
||||
*/
|
||||
_startCall: function(callData) {
|
||||
const openChat = () => {
|
||||
let windowId = MozLoopService.openChatWindow(callData);
|
||||
if (windowId) {
|
||||
this.conversationInProgress.id = windowId;
|
||||
}
|
||||
};
|
||||
|
||||
if (callData.type == "incoming" && ("callerId" in callData) &&
|
||||
EMAIL_OR_PHONE_RE.test(callData.callerId)) {
|
||||
LoopContacts.search({
|
||||
q: callData.callerId,
|
||||
field: callData.callerId.includes("@") ? "email" : "tel"
|
||||
}, (err, contacts) => {
|
||||
if (err) {
|
||||
// Database error, helas!
|
||||
openChat();
|
||||
return;
|
||||
}
|
||||
|
||||
for (let contact of contacts) {
|
||||
if (contact.blocked) {
|
||||
// Blocked! Send a busy signal back to the caller.
|
||||
this._returnBusy(callData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
openChat();
|
||||
});
|
||||
} else {
|
||||
openChat();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts a direct call to the contact addresses.
|
||||
*
|
||||
* @param {Object} contact The contact to call
|
||||
* @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
|
||||
* @return true if the call is opened, false if it is not opened (i.e. busy)
|
||||
*/
|
||||
startDirectCall: function(contact, callType) {
|
||||
if ("id" in this.conversationInProgress)
|
||||
return false;
|
||||
|
||||
var callData = {
|
||||
contact: contact,
|
||||
callType: callType,
|
||||
type: "outgoing"
|
||||
};
|
||||
|
||||
this._startCall(callData);
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Block a caller so it will show up in the contacts list as a blocked contact.
|
||||
* If the contact is not yet part of the users' contacts list, it will be added
|
||||
* as a blocked contact directly.
|
||||
*
|
||||
* @param {String} callerId Email address or phone number that may identify
|
||||
* the caller as an existing contact
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* has completed. When an error occurs, it will be
|
||||
* passed as its first argument
|
||||
*/
|
||||
blockDirectCaller: function(callerId, callback) {
|
||||
let field = callerId.contains("@") ? "email" : "tel";
|
||||
Task.spawn(function* () {
|
||||
// See if we can find the caller in our database.
|
||||
let contacts = yield LoopContacts.promise("search", {
|
||||
q: callerId,
|
||||
field: field
|
||||
});
|
||||
|
||||
let contact;
|
||||
if (contacts.length) {
|
||||
for (contact of contacts) {
|
||||
yield LoopContacts.promise("block", contact._guid);
|
||||
}
|
||||
} else {
|
||||
// If the contact doesn't exist yet, add it as a blocked contact.
|
||||
contact = {
|
||||
id: MozLoopService.generateUUID(),
|
||||
name: [callerId],
|
||||
category: ["local"],
|
||||
blocked: true
|
||||
};
|
||||
// Add the phone OR email field to the contact.
|
||||
contact[field] = [{
|
||||
pref: true,
|
||||
value: callerId
|
||||
}];
|
||||
|
||||
yield LoopContacts.promise("add", contact);
|
||||
}
|
||||
}).then(callback, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Open call progress websocket and terminate with a reason of busy
|
||||
* the server.
|
||||
*
|
||||
* @param {callData} Must contain the progressURL, callId and websocketToken
|
||||
* returned by the LoopService.
|
||||
*/
|
||||
_returnBusy: function(callData) {
|
||||
let callProgress = new CallProgressSocket(
|
||||
callData.progressURL,
|
||||
callData.callId,
|
||||
callData.websocketToken);
|
||||
if (this.mocks.webSocket) {
|
||||
callProgress._websocket = this.mocks.webSocket;
|
||||
}
|
||||
// This instance of CallProgressSocket should stay alive until the underlying
|
||||
// websocket is closed since it is passed to the websocket as the nsIWebSocketListener.
|
||||
callProgress.connect(() => { callProgress.sendBusy(); });
|
||||
}
|
||||
};
|
||||
Object.freeze(LoopCallsInternal);
|
||||
|
||||
/**
|
||||
* Public API
|
||||
*/
|
||||
this.LoopCalls = {
|
||||
/**
|
||||
* Callback from MozLoopPushHandler - A push notification has been received from
|
||||
* the server.
|
||||
*
|
||||
* @param {String} version The version information from the server.
|
||||
*/
|
||||
onNotification: function(version, channelID) {
|
||||
LoopCallsInternal.onNotification(version, channelID);
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to signify that a call is in progress.
|
||||
*
|
||||
* @param {String} The window id for the call in progress.
|
||||
*/
|
||||
setCallInProgress: function(conversationWindowId) {
|
||||
if ("id" in LoopCallsInternal.conversationInProgress &&
|
||||
LoopCallsInternal.conversationInProgress.id != conversationWindowId) {
|
||||
MozLoopService.log.error("Starting a new conversation when one is already in progress?");
|
||||
return;
|
||||
}
|
||||
|
||||
LoopCallsInternal.conversationInProgress.id = conversationWindowId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Releases the callData for a specific conversation window id.
|
||||
*
|
||||
* The result of this call will be a free call session slot.
|
||||
*
|
||||
* @param {Number} conversationWindowId
|
||||
*/
|
||||
clearCallInProgress: function(conversationWindowId) {
|
||||
if ("id" in LoopCallsInternal.conversationInProgress &&
|
||||
LoopCallsInternal.conversationInProgress.id == conversationWindowId) {
|
||||
delete LoopCallsInternal.conversationInProgress.id;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts a direct call to the contact addresses.
|
||||
*
|
||||
* @param {Object} contact The contact to call
|
||||
* @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
|
||||
* @return true if the call is opened, false if it is not opened (i.e. busy)
|
||||
*/
|
||||
startDirectCall: function(contact, callType) {
|
||||
LoopCallsInternal.startDirectCall(contact, callType);
|
||||
},
|
||||
|
||||
/**
|
||||
* @see LoopCallsInternal#blockDirectCaller
|
||||
*/
|
||||
blockDirectCaller: function(callerId, callback) {
|
||||
return LoopCallsInternal.blockDirectCaller(callerId, callback);
|
||||
}
|
||||
};
|
||||
Object.freeze(LoopCalls);
|
||||
@@ -1,961 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "console",
|
||||
"resource://gre/modules/devtools/Console.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
|
||||
"resource:///modules/loop/LoopStorage.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CardDavImporter",
|
||||
"resource:///modules/loop/CardDavImporter.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "GoogleImporter",
|
||||
"resource:///modules/loop/GoogleImporter.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
||||
return new EventEmitter();
|
||||
});
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["LoopContacts"];
|
||||
|
||||
const kObjectStoreName = "contacts";
|
||||
|
||||
/*
|
||||
* The table used to store contacts information contains two identifiers,
|
||||
* both of which can be used to look up entries in the table. The table
|
||||
* key path (primary index, which must be unique) is "_guid", and is
|
||||
* automatically generated by IndexedDB when an entry is first inserted.
|
||||
* The other identifier, "id", is the supposedly unique key assigned to this
|
||||
* entry by whatever service generated it (e.g., Google Contacts). While
|
||||
* this key should, in theory, be completely unique, we don't use it
|
||||
* as the key path to avoid generating errors when an external database
|
||||
* violates this constraint. This second ID is referred to as the "serviceId".
|
||||
*/
|
||||
const kKeyPath = "_guid";
|
||||
const kServiceIdIndex = "id";
|
||||
|
||||
/**
|
||||
* Contacts validation.
|
||||
*
|
||||
* To allow for future integration with the Contacts API and/ or potential
|
||||
* integration with contact synchronization across devices (including Firefox OS
|
||||
* devices), we are using objects with properties having the same names and
|
||||
* structure as those used by mozContact.
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/API/mozContact for more
|
||||
* information.
|
||||
*/
|
||||
const kFieldTypeString = "string";
|
||||
const kFieldTypeNumber = "number";
|
||||
const kFieldTypeNumberOrString = "number|string";
|
||||
const kFieldTypeArray = "array";
|
||||
const kFieldTypeBool = "boolean";
|
||||
const kContactFields = {
|
||||
"id": {
|
||||
// Because "id" is externally generated, it might be numeric
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"published": {
|
||||
// mozContact, from which we are derived, defines dates as
|
||||
// "a Date object, which will eventually be converted to a
|
||||
// long long" -- to be forwards compatible, we allow both
|
||||
// formats for now.
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"updated": {
|
||||
// mozContact, from which we are derived, defines dates as
|
||||
// "a Date object, which will eventually be converted to a
|
||||
// long long" -- to be forwards compatible, we allow both
|
||||
// formats for now.
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"bday": {
|
||||
// mozContact, from which we are derived, defines dates as
|
||||
// "a Date object, which will eventually be converted to a
|
||||
// long long" -- to be forwards compatible, we allow both
|
||||
// formats for now.
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"blocked": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"adr": {
|
||||
type: kFieldTypeArray,
|
||||
contains: {
|
||||
"countryName": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"locality": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"postalCode": {
|
||||
// In some (but not all) locations, postal codes can be strictly numeric
|
||||
type: kFieldTypeNumberOrString
|
||||
},
|
||||
"pref": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"region": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"streetAddress": {
|
||||
type: kFieldTypeString
|
||||
},
|
||||
"type": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
}
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
type: kFieldTypeArray,
|
||||
contains: {
|
||||
"pref": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"type": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"value": {
|
||||
type: kFieldTypeString
|
||||
}
|
||||
}
|
||||
},
|
||||
"tel": {
|
||||
type: kFieldTypeArray,
|
||||
contains: {
|
||||
"pref": {
|
||||
type: kFieldTypeBool
|
||||
},
|
||||
"type": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"value": {
|
||||
type: kFieldTypeString
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"honorificPrefix": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"givenName": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"additionalName": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"familyName": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"honorificSuffix": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"category": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"org": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"jobTitle": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
},
|
||||
"note": {
|
||||
type: kFieldTypeArray,
|
||||
contains: kFieldTypeString
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares the properties contained in an object to the definition as defined in
|
||||
* `kContactFields`.
|
||||
* If a property is encountered that is not found in the spec, an Error is thrown.
|
||||
* If a property is encountered with an invalid value, an Error is thrown.
|
||||
*
|
||||
* Please read the spec at https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
||||
* for more information.
|
||||
*
|
||||
* @param {Object} obj The contact object, or part of it when called recursively
|
||||
* @param {Object} def The definition of properties to validate against. Defaults
|
||||
* to `kContactFields`
|
||||
*/
|
||||
const validateContact = function(obj, def = kContactFields) {
|
||||
for (let propName of Object.getOwnPropertyNames(obj)) {
|
||||
// Ignore internal properties.
|
||||
if (propName.startsWith("_")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let propDef = def[propName];
|
||||
if (!propDef) {
|
||||
throw new Error("Field '" + propName + "' is not supported for contacts");
|
||||
}
|
||||
|
||||
let val = obj[propName];
|
||||
|
||||
switch (propDef.type) {
|
||||
case kFieldTypeString:
|
||||
if (typeof val != kFieldTypeString) {
|
||||
throw new Error("Field '" + propName + "' must be of type String");
|
||||
}
|
||||
break;
|
||||
case kFieldTypeNumberOrString:
|
||||
let type = typeof val;
|
||||
if (type != kFieldTypeNumber && type != kFieldTypeString) {
|
||||
throw new Error("Field '" + propName + "' must be of type Number or String");
|
||||
}
|
||||
break;
|
||||
case kFieldTypeBool:
|
||||
if (typeof val != kFieldTypeBool) {
|
||||
throw new Error("Field '" + propName + "' must be of type Boolean");
|
||||
}
|
||||
break;
|
||||
case kFieldTypeArray:
|
||||
if (!Array.isArray(val)) {
|
||||
throw new Error("Field '" + propName + "' must be an Array");
|
||||
}
|
||||
|
||||
let contains = propDef.contains;
|
||||
// If the type of `contains` is a scalar value, it means that the array
|
||||
// consists of items of only that type.
|
||||
let isScalarCheck = (typeof contains == kFieldTypeString);
|
||||
for (let arrayValue of val) {
|
||||
if (isScalarCheck) {
|
||||
if (typeof arrayValue != contains) {
|
||||
throw new Error("Field '" + propName + "' must be of type " + contains);
|
||||
}
|
||||
} else {
|
||||
validateContact(arrayValue, contains);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides a method to perform multiple operations in a single transaction on the
|
||||
* contacts store.
|
||||
*
|
||||
* @param {String} operation Name of an operation supported by `IDBObjectStore`
|
||||
* @param {Array} data List of objects that will be passed to the object
|
||||
* store operation
|
||||
* @param {Function} callback Function that will be invoked once the operations
|
||||
* have finished. The first argument passed will be
|
||||
* an `Error` object or `null`. The second argument
|
||||
* will be the `data` Array, if all operations finished
|
||||
* successfully.
|
||||
*/
|
||||
const batch = function(operation, data, callback) {
|
||||
let processed = [];
|
||||
if (!LoopContactsInternal.hasOwnProperty(operation) ||
|
||||
typeof LoopContactsInternal[operation] != 'function') {
|
||||
callback(new Error ("LoopContactsInternal does not contain a '" +
|
||||
operation + "' method"));
|
||||
return;
|
||||
}
|
||||
LoopStorage.asyncForEach(data, (item, next) => {
|
||||
LoopContactsInternal[operation](item, (err, result) => {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
processed.push(result);
|
||||
next();
|
||||
});
|
||||
}, err => {
|
||||
if (err) {
|
||||
callback(err, processed);
|
||||
return;
|
||||
}
|
||||
callback(null, processed);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend a `target` object with the properties defined in `source`.
|
||||
*
|
||||
* @param {Object} target The target object to receive properties defined in `source`
|
||||
* @param {Object} source The source object to copy properties from
|
||||
*/
|
||||
const extend = function(target, source) {
|
||||
for (let key of Object.getOwnPropertyNames(source)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
LoopStorage.on("upgrade", function(e, db) {
|
||||
if (db.objectStoreNames.contains(kObjectStoreName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the 'contacts' store as it doesn't exist yet.
|
||||
let store = db.createObjectStore(kObjectStoreName, {
|
||||
keyPath: kKeyPath,
|
||||
autoIncrement: true
|
||||
});
|
||||
store.createIndex(kServiceIdIndex, kServiceIdIndex, {unique: false});
|
||||
});
|
||||
|
||||
/**
|
||||
* The Contacts class.
|
||||
*
|
||||
* Each method that is a member of this class requires the last argument to be a
|
||||
* callback Function. MozLoopAPI will cause things to break if this invariant is
|
||||
* violated. You'll notice this as well in the documentation for each method.
|
||||
*/
|
||||
let LoopContactsInternal = Object.freeze({
|
||||
/**
|
||||
* Map of contact importer names to instances
|
||||
*/
|
||||
_importServices: {
|
||||
"carddav": new CardDavImporter(),
|
||||
"google": new GoogleImporter()
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a contact to the data store.
|
||||
*
|
||||
* @param {Object} details An object that will be added to the data store
|
||||
* as-is. Please read https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
||||
* for more information of this objects' structure
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if it was stored successfully.
|
||||
*/
|
||||
add: function(details, callback) {
|
||||
if (!(kServiceIdIndex in details)) {
|
||||
callback(new Error("No '" + kServiceIdIndex + "' field present"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateContact(details);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let contact = extend({}, details);
|
||||
let now = Date.now();
|
||||
// The data source should have included "published" and "updated" values
|
||||
// for any imported records, and we need to keep track of those dated for
|
||||
// sync purposes (i.e., when we add functionality to push local changes to
|
||||
// a remote server from which we originally got a contact). We also need
|
||||
// to track the time at which *we* added and most recently changed the
|
||||
// contact, so as to determine whether the local or the remote store has
|
||||
// fresher data.
|
||||
//
|
||||
// For clarity: the fields "published" and "updated" indicate when the
|
||||
// *remote* data source published and updated the contact. The fields
|
||||
// "_date_add" and "_date_lch" track when the *local* data source
|
||||
// created and updated the contact.
|
||||
contact.published = contact.published ? new Date(contact.published).getTime() : now;
|
||||
contact.updated = contact.updated ? new Date(contact.updated).getTime() : now;
|
||||
contact._date_add = contact._date_lch = now;
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.add(contact);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
contact[kKeyPath] = event.target.result;
|
||||
eventEmitter.emit("add", contact);
|
||||
callback(null, contact);
|
||||
};
|
||||
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a batch of contacts to the data store.
|
||||
*
|
||||
* @param {Array} contacts A list of contact objects to be added
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the list of added contacts.
|
||||
*/
|
||||
addMany: function(contacts, callback) {
|
||||
batch("add", contacts, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a contact from the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to remove
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the result of the operation.
|
||||
*/
|
||||
remove: function(guid, callback) {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.delete(guid);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
if (contact) {
|
||||
eventEmitter.emit("remove", contact);
|
||||
}
|
||||
callback(null, event.target.result);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a batch of contacts from the data store.
|
||||
*
|
||||
* @param {Array} guids A list of IDs of the contacts to remove
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the list of IDs, if successfull.
|
||||
*/
|
||||
removeMany: function(guids, callback) {
|
||||
batch("remove", guids, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove _all_ contacts from the data store.
|
||||
* CAUTION: this method will clear the whole data store - you won't have any
|
||||
* contacts left!
|
||||
*
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the result of the operation, if successfull.
|
||||
*/
|
||||
removeAll: function(callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.clear();
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
eventEmitter.emit("removeAll", event.target.result);
|
||||
callback(null, event.target.result);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a specific contact from the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to retrieve
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successful.
|
||||
* If no object matching guid could be found,
|
||||
* then the callback is called with both arguments
|
||||
* set to `null`.
|
||||
*/
|
||||
get: function(guid, callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let request;
|
||||
try {
|
||||
request = store.get(guid);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
if (!event.target.result) {
|
||||
callback(null, null);
|
||||
return;
|
||||
}
|
||||
let contact = extend({}, event.target.result);
|
||||
contact[kKeyPath] = guid;
|
||||
callback(null, contact);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a specific contact from the data store using the kServiceIdIndex
|
||||
* property.
|
||||
*
|
||||
* @param {String} serviceId String identifier of the contact to retrieve
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
* If no object matching serviceId could be found,
|
||||
* then the callback is called with both arguments
|
||||
* set to `null`.
|
||||
*/
|
||||
getByServiceId: function(serviceId, callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let index = store.index(kServiceIdIndex);
|
||||
let request;
|
||||
try {
|
||||
request = index.get(serviceId);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
if (!event.target.result) {
|
||||
callback(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
let contact = extend({}, event.target.result);
|
||||
callback(null, contact);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve _all_ contacts from the data store.
|
||||
* CAUTION: If the amount of contacts is very large (say > 100000), this method
|
||||
* may slow down your application!
|
||||
*
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be an `Array` of contact objects, if successfull.
|
||||
*/
|
||||
getAll: function(callback) {
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let cursorRequest = store.openCursor();
|
||||
let contactsList = [];
|
||||
|
||||
cursorRequest.onsuccess = event => {
|
||||
let cursor = event.target.result;
|
||||
// No more results, return the list.
|
||||
if (!cursor) {
|
||||
callback(null, contactsList);
|
||||
return;
|
||||
}
|
||||
|
||||
let contact = extend({}, cursor.value);
|
||||
contact[kKeyPath] = cursor.key;
|
||||
contactsList.push(contact);
|
||||
|
||||
cursor.continue();
|
||||
};
|
||||
|
||||
cursorRequest.onerror = event => callback(event.target.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve an arbitrary amount of contacts from the data store.
|
||||
* CAUTION: If the amount of contacts is very large (say > 1000), this method
|
||||
* may slow down your application!
|
||||
*
|
||||
* @param {Array} guids List of contact IDs to retrieve contact objects of
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be an `Array` of contact objects, if successfull.
|
||||
*/
|
||||
getMany: function(guids, callback) {
|
||||
let contacts = [];
|
||||
LoopStorage.asyncParallel(guids, (guid, next) => {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
contacts.push(contact);
|
||||
next();
|
||||
});
|
||||
}, err => {
|
||||
callback(err, !err ? contacts : null);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a specific contact in the data store.
|
||||
* The contact object is modified by replacing the fields passed in the `details`
|
||||
* param and any fields not passed in are left unchanged.
|
||||
*
|
||||
* @param {Object} details An object that will be updated in the data store
|
||||
* as-is. Please read https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
||||
* for more information of this objects' structure
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
update: function(details, callback) {
|
||||
if (!(kKeyPath in details)) {
|
||||
callback(new Error("No '" + kKeyPath + "' field present"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateContact(details);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
let guid = details[kKeyPath];
|
||||
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contact) {
|
||||
callback(new Error("Contact with " + kKeyPath + " '" +
|
||||
guid + "' could not be found"));
|
||||
return;
|
||||
}
|
||||
|
||||
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let previous = extend({}, contact);
|
||||
// Update the contact with properties provided by `details`.
|
||||
extend(contact, details);
|
||||
|
||||
details._date_lch = Date.now();
|
||||
let request;
|
||||
try {
|
||||
request = store.put(contact);
|
||||
} catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
request.onsuccess = event => {
|
||||
eventEmitter.emit("update", contact, previous);
|
||||
callback(null, event.target.result);
|
||||
};
|
||||
request.onerror = event => callback(event.target.error);
|
||||
}, "readwrite");
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Block a specific contact in the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to block
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
block: function(guid, callback) {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contact) {
|
||||
callback(new Error("Contact with " + kKeyPath + " '" +
|
||||
guid + "' could not be found"));
|
||||
return;
|
||||
}
|
||||
|
||||
contact.blocked = true;
|
||||
this.update(contact, callback);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Un-block a specific contact in the data store.
|
||||
*
|
||||
* @param {String} guid String identifier of the contact to unblock
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the contact object, if successfull.
|
||||
*/
|
||||
unblock: function(guid, callback) {
|
||||
this.get(guid, (err, contact) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contact) {
|
||||
callback(new Error("Contact with " + kKeyPath + " '" +
|
||||
guid + "' could not be found"));
|
||||
return;
|
||||
}
|
||||
|
||||
contact.blocked = false;
|
||||
this.update(contact, callback);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Import a list of (new) contacts from an external data source.
|
||||
*
|
||||
* @param {Object} options Property bag of options for the importer
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the result of the operation, if successfull.
|
||||
*/
|
||||
startImport: function(options, windowRef, callback) {
|
||||
if (!("service" in options)) {
|
||||
callback(new Error("No import service specified in options"));
|
||||
return;
|
||||
}
|
||||
if (!(options.service in this._importServices)) {
|
||||
callback(new Error("Unknown import service specified: " + options.service));
|
||||
return;
|
||||
}
|
||||
this._importServices[options.service].startImport(options, callback,
|
||||
LoopContacts, windowRef);
|
||||
},
|
||||
|
||||
/**
|
||||
* Search through the data store for contacts that match a certain (sub-)string.
|
||||
* NB: The current implementation is very simple, naive if you will; we fetch
|
||||
* _all_ the contacts via `getAll()` and iterate over all of them to find
|
||||
* the contacts matching the supplied query (brute-force search in
|
||||
* exponential time).
|
||||
*
|
||||
* @param {Object} query Needle to search for in our haystack of contacts
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be an `Array` of contact objects, if successfull.
|
||||
*
|
||||
* Example:
|
||||
* LoopContacts.search({
|
||||
* q: "foo@bar.com",
|
||||
* field: "email" // 'email' is the default.
|
||||
* }, function(err, contacts) {
|
||||
* if (err) {
|
||||
* throw err;
|
||||
* }
|
||||
* console.dir(contacts);
|
||||
* });
|
||||
*/
|
||||
search: function(query, callback) {
|
||||
if (!("q" in query) || !query.q) {
|
||||
callback(new Error("Nothing to search for. 'q' is required."));
|
||||
return;
|
||||
}
|
||||
if (!("field" in query)) {
|
||||
query.field = "email";
|
||||
}
|
||||
let queryValue = query.q;
|
||||
if (query.field == "tel") {
|
||||
queryValue = queryValue.replace(/[\D]+/g, "");
|
||||
}
|
||||
|
||||
const checkForMatch = function(fieldValue) {
|
||||
if (typeof fieldValue == "string") {
|
||||
if (query.field == "tel") {
|
||||
return fieldValue.replace(/[\D]+/g, "").endsWith(queryValue);
|
||||
}
|
||||
return fieldValue == queryValue;
|
||||
}
|
||||
if (typeof fieldValue == "number" || typeof fieldValue == "boolean") {
|
||||
return fieldValue == queryValue;
|
||||
}
|
||||
if ("value" in fieldValue) {
|
||||
return checkForMatch(fieldValue.value);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let foundContacts = [];
|
||||
this.getAll((err, contacts) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let contact of contacts) {
|
||||
let matchWith = contact[query.field];
|
||||
if (!matchWith) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Many fields are defined as Arrays.
|
||||
if (Array.isArray(matchWith)) {
|
||||
for (let fieldValue of matchWith) {
|
||||
if (checkForMatch(fieldValue)) {
|
||||
foundContacts.push(contact);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (checkForMatch(matchWith)) {
|
||||
foundContacts.push(contact);
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, foundContacts);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Public Loop Contacts API.
|
||||
*
|
||||
* LoopContacts implements the EventEmitter interface by exposing three methods -
|
||||
* `on`, `once` and `off` - to subscribe to events.
|
||||
* At this point the following events may be subscribed to:
|
||||
* - 'add': A new contact object was successfully added to the data store.
|
||||
* - 'remove': A contact was successfully removed from the data store.
|
||||
* - 'removeAll': All contacts were successfully removed from the data store.
|
||||
* - 'update': A contact object was successfully updated with changed
|
||||
* properties in the data store.
|
||||
*/
|
||||
this.LoopContacts = Object.freeze({
|
||||
add: function(details, callback) {
|
||||
return LoopContactsInternal.add(details, callback);
|
||||
},
|
||||
|
||||
addMany: function(contacts, callback) {
|
||||
return LoopContactsInternal.addMany(contacts, callback);
|
||||
},
|
||||
|
||||
remove: function(guid, callback) {
|
||||
return LoopContactsInternal.remove(guid, callback);
|
||||
},
|
||||
|
||||
removeMany: function(guids, callback) {
|
||||
return LoopContactsInternal.removeMany(guids, callback);
|
||||
},
|
||||
|
||||
removeAll: function(callback) {
|
||||
return LoopContactsInternal.removeAll(callback);
|
||||
},
|
||||
|
||||
get: function(guid, callback) {
|
||||
return LoopContactsInternal.get(guid, callback);
|
||||
},
|
||||
|
||||
getByServiceId: function(serviceId, callback) {
|
||||
return LoopContactsInternal.getByServiceId(serviceId, callback);
|
||||
},
|
||||
|
||||
getAll: function(callback) {
|
||||
return LoopContactsInternal.getAll(callback);
|
||||
},
|
||||
|
||||
getMany: function(guids, callback) {
|
||||
return LoopContactsInternal.getMany(guids, callback);
|
||||
},
|
||||
|
||||
update: function(details, callback) {
|
||||
return LoopContactsInternal.update(details, callback);
|
||||
},
|
||||
|
||||
block: function(guid, callback) {
|
||||
return LoopContactsInternal.block(guid, callback);
|
||||
},
|
||||
|
||||
unblock: function(guid, callback) {
|
||||
return LoopContactsInternal.unblock(guid, callback);
|
||||
},
|
||||
|
||||
startImport: function(options, windowRef, callback) {
|
||||
return LoopContactsInternal.startImport(options, windowRef, callback);
|
||||
},
|
||||
|
||||
search: function(query, callback) {
|
||||
return LoopContactsInternal.search(query, callback);
|
||||
},
|
||||
|
||||
promise: function(method, ...params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this[method](...params, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
on: (...params) => eventEmitter.on(...params),
|
||||
|
||||
once: (...params) => eventEmitter.once(...params),
|
||||
|
||||
off: (...params) => eventEmitter.off(...params)
|
||||
});
|
||||
@@ -1,377 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
// Make it possible to load LoopStorage.jsm in xpcshell tests
|
||||
try {
|
||||
Cu.importGlobalProperties(["indexedDB"]);
|
||||
} catch (ex) {
|
||||
// don't write this is out in xpcshell, since it's expected there
|
||||
if (typeof window !== 'undefined' && "console" in window) {
|
||||
console.log("Failed to import indexedDB; if this isn't a unit test," +
|
||||
" something is wrong", ex);
|
||||
}
|
||||
}
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
||||
return new EventEmitter();
|
||||
});
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["LoopStorage"];
|
||||
|
||||
const kDatabasePrefix = "loop-";
|
||||
const kDefaultDatabaseName = "default";
|
||||
let gDatabaseName = kDatabasePrefix + kDefaultDatabaseName;
|
||||
const kDatabaseVersion = 1;
|
||||
|
||||
let gWaitForOpenCallbacks = new Set();
|
||||
let gDatabase = null;
|
||||
let gClosed = false;
|
||||
|
||||
/**
|
||||
* Properly shut the database instance down. This is done on application shutdown.
|
||||
*/
|
||||
const closeDatabase = function() {
|
||||
Services.obs.removeObserver(closeDatabase, "quit-application");
|
||||
if (!gDatabase) {
|
||||
return;
|
||||
}
|
||||
gDatabase.close();
|
||||
gDatabase = null;
|
||||
gClosed = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a connection to the IndexedDB database.
|
||||
* This function is different than IndexedDBHelper.jsm provides, as it ensures
|
||||
* only one connection is open during the lifetime of this API. Callbacks are
|
||||
* queued when a connection attempt is in progress and are invoked once the
|
||||
* connection is established.
|
||||
*
|
||||
* @param {Function} onOpen Callback to be invoked once a database connection is
|
||||
* established. It takes an Error object as first argument
|
||||
* and the database connection object as second argument,
|
||||
* if successful.
|
||||
*/
|
||||
const ensureDatabaseOpen = function(onOpen) {
|
||||
if (gClosed) {
|
||||
onOpen(new Error("Database already closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (gDatabase) {
|
||||
onOpen(null, gDatabase);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gWaitForOpenCallbacks.has(onOpen)) {
|
||||
gWaitForOpenCallbacks.add(onOpen);
|
||||
|
||||
if (gWaitForOpenCallbacks.size !== 1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let invokeCallbacks = err => {
|
||||
for (let callback of gWaitForOpenCallbacks) {
|
||||
callback(err, gDatabase);
|
||||
}
|
||||
gWaitForOpenCallbacks.clear();
|
||||
};
|
||||
|
||||
let openRequest = indexedDB.open(gDatabaseName, kDatabaseVersion);
|
||||
|
||||
openRequest.onblocked = function(event) {
|
||||
invokeCallbacks(new Error("Database cannot be upgraded cause in use: " + event.target.error));
|
||||
};
|
||||
|
||||
openRequest.onerror = function(event) {
|
||||
// Try to delete the old database so that we can start this process over
|
||||
// next time.
|
||||
indexedDB.deleteDatabase(gDatabaseName);
|
||||
invokeCallbacks(new Error("Error while opening database: " + event.target.errorCode));
|
||||
};
|
||||
|
||||
openRequest.onupgradeneeded = function(event) {
|
||||
let db = event.target.result;
|
||||
eventEmitter.emit("upgrade", db, event.oldVersion, kDatabaseVersion);
|
||||
};
|
||||
|
||||
openRequest.onsuccess = function(event) {
|
||||
gDatabase = event.target.result;
|
||||
invokeCallbacks();
|
||||
// Close the database instance properly on application shutdown.
|
||||
Services.obs.addObserver(closeDatabase, "quit-application", false);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch to a database with a different name by closing the current connection
|
||||
* and making sure that the next connection attempt will be made using the updated
|
||||
* name.
|
||||
*
|
||||
* @param {String} name New name of the database to switch to.
|
||||
*/
|
||||
const switchDatabase = function(name) {
|
||||
if (!name) {
|
||||
name = kDefaultDatabaseName;
|
||||
}
|
||||
name = kDatabasePrefix + name;
|
||||
if (name == gDatabaseName) {
|
||||
// This is already the current database, so there's no need to switch.
|
||||
return;
|
||||
}
|
||||
|
||||
gDatabaseName = name;
|
||||
if (gDatabase) {
|
||||
try {
|
||||
gDatabase.close();
|
||||
} finally {
|
||||
gDatabase = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return it.
|
||||
*
|
||||
* @param {String} store Name of the object store to start a transaction on
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* transaction object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
const getTransaction = function(store, callback, mode) {
|
||||
ensureDatabaseOpen((err, db) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let trans;
|
||||
try {
|
||||
trans = db.transaction(store, mode);
|
||||
} catch(ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
callback(null, trans);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return the requested store.
|
||||
*
|
||||
* @param {String} store Name of the object store to retrieve
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* store object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
const getStore = function(store, callback, mode) {
|
||||
getTransaction(store, (err, trans) => {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, trans.objectStore(store));
|
||||
}, mode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Public Loop Storage API.
|
||||
*
|
||||
* Since IndexedDB transaction can not stand a spin of the event loop _before_
|
||||
* using a IDBTransaction object, we can't use Promise.jsm promises. Therefore
|
||||
* LoopStorage provides two async helper functions, `asyncForEach` and `asyncParallel`.
|
||||
*
|
||||
* LoopStorage implements the EventEmitter interface by exposing two methods, `on`
|
||||
* and `off`, to subscribe to events.
|
||||
* At this point only the `upgrade` event will be emitted. This happens when the
|
||||
* database is loaded in memory and consumers will be able to change its structure.
|
||||
*/
|
||||
this.LoopStorage = Object.freeze({
|
||||
/**
|
||||
* @var {String} databaseName The name of the database that is currently active,
|
||||
* WITHOUT the prefix
|
||||
*/
|
||||
get databaseName() {
|
||||
return gDatabaseName.substr(kDatabasePrefix.length);
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a connection to the IndexedDB database and return the database object.
|
||||
*
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established. It takes an Error object as first
|
||||
* argument and the database connection object as
|
||||
* second argument, if successful.
|
||||
*/
|
||||
getSingleton: function(callback) {
|
||||
ensureDatabaseOpen(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch to a database with a different name.
|
||||
*
|
||||
* @param {String} name New name of the database to switch to. Defaults to
|
||||
* `kDefaultDatabaseName`
|
||||
*/
|
||||
switchDatabase: function(name = kDefaultDatabaseName) {
|
||||
switchDatabase(name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return it.
|
||||
* If only two arguments are passed, the default mode will be assumed and the
|
||||
* second argument is assumed to be a callback.
|
||||
*
|
||||
* @param {String} store Name of the object store to start a transaction on
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* transaction object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
getTransaction: function(store, callback, mode = "readonly") {
|
||||
getTransaction(store, callback, mode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a transaction on the loop database and return the requested store.
|
||||
* If only two arguments are passed, the default mode will be assumed and the
|
||||
* second argument is assumed to be a callback.
|
||||
*
|
||||
* @param {String} store Name of the object store to retrieve
|
||||
* @param {Function} callback Callback to be invoked once a database connection
|
||||
* is established and a transaction can be started.
|
||||
* It takes an Error object as first argument and the
|
||||
* store object as second argument.
|
||||
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
|
||||
*
|
||||
* @note we can't use a Promise here, as they are resolved after a spin of the
|
||||
* event loop; the transaction will have finished by then and no operations
|
||||
* are possible anymore, yielding an error.
|
||||
*/
|
||||
getStore: function(store, callback, mode = "readonly") {
|
||||
getStore(store, callback, mode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform an async function in serial on each of the list items and call a
|
||||
* callback Function when all list items are done.
|
||||
* IMPORTANT: only use this iteration method if you are sure that the operations
|
||||
* performed in `onItem` are guaranteed to be async in the success case.
|
||||
*
|
||||
* @param {Array} list Non-empty list of items to iterate
|
||||
* @param {Function} onItem Callback to invoke for each item in the list. It
|
||||
* takes the item is first argument and a callback
|
||||
* function as second, which is to be invoked once
|
||||
* the consumer is done with its async operation. If
|
||||
* an error is passed as the first argument to this
|
||||
* callback function, the iteration will stop and
|
||||
* `onDone` callback will be invoked with that error.
|
||||
* @param {callback} onDone Callback to invoke when the list is completed or
|
||||
* on error. It takes an Error object as first
|
||||
* argument.
|
||||
*/
|
||||
asyncForEach: function(list, onItem, onDone) {
|
||||
let i = 0;
|
||||
let len = list.length;
|
||||
|
||||
if (!len) {
|
||||
onDone(new Error("Argument error: empty list"));
|
||||
return;
|
||||
}
|
||||
|
||||
onItem(list[i], function handler(err) {
|
||||
if (err) {
|
||||
onDone(err);
|
||||
return;
|
||||
}
|
||||
|
||||
i++;
|
||||
if (i < len) {
|
||||
onItem(list[i], handler, i);
|
||||
} else {
|
||||
onDone();
|
||||
}
|
||||
}, i);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform an async function in parallel on each of the list items and call a
|
||||
* callback Function when all list items are done.
|
||||
* IMPORTANT: only use this iteration method if you are sure that the operations
|
||||
* performed in `onItem` are guaranteed to be async in the success case.
|
||||
*
|
||||
* @param {Array} list Non-empty list of items to iterate
|
||||
* @param {Function} onItem Callback to invoke for each item in the list. It
|
||||
* takes the item is first argument and a callback
|
||||
* function as second, which is to be invoked once
|
||||
* the consumer is done with its async operation. If
|
||||
* an error is passed as the first argument to this
|
||||
* callback function, the iteration will stop and
|
||||
* `onDone` callback will be invoked with that error.
|
||||
* @param {callback} onDone Callback to invoke when the list is completed or
|
||||
* on error. It takes an Error object as first
|
||||
* argument.
|
||||
*/
|
||||
asyncParallel: function(list, onItem, onDone) {
|
||||
let i = 0;
|
||||
let done = 0;
|
||||
let callbackCalled = false;
|
||||
let len = list.length;
|
||||
|
||||
if (!len) {
|
||||
onDone(new Error("Argument error: empty list"));
|
||||
return;
|
||||
}
|
||||
|
||||
function handleCallback(err) {
|
||||
if (callbackCalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
onDone(err);
|
||||
callbackCalled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (++done === len) {
|
||||
onDone();
|
||||
callbackCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (; i < len; ++i) {
|
||||
onItem(list[i], handleCallback, i);
|
||||
}
|
||||
},
|
||||
|
||||
on: (...params) => eventEmitter.on(...params),
|
||||
|
||||
off: (...params) => eventEmitter.off(...params)
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += [
|
||||
'test/mochitest/browser.ini',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.loop += [
|
||||
'content/shared/js/crypto.js',
|
||||
'content/shared/js/utils.js',
|
||||
'modules/CardDavImporter.jsm',
|
||||
'modules/GoogleImporter.jsm',
|
||||
'modules/LoopCalls.jsm',
|
||||
'modules/LoopContacts.jsm',
|
||||
'modules/LoopRooms.jsm',
|
||||
'modules/LoopRoomsCache.jsm',
|
||||
'modules/LoopStorage.jsm',
|
||||
'modules/MozLoopAPI.jsm',
|
||||
'modules/MozLoopPushHandler.jsm',
|
||||
'modules/MozLoopService.jsm',
|
||||
'modules/MozLoopWorker.js',
|
||||
]
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Loop', 'Client')
|
||||
@@ -1,150 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
|
||||
var loop = loop || {};
|
||||
|
||||
/**
|
||||
* Monkeypatch getUserMedia in a way that prevents additional camera and
|
||||
* microphone prompts, at the cost of ignoring all constraints other than
|
||||
* the first set passed in.
|
||||
*
|
||||
* The first call to navigator.getUserMedia (also now aliased to
|
||||
* multiplexGum.getPermsAndCacheMedia to allow for explicit calling code)
|
||||
* will cause the underlying gUM implementation to be called.
|
||||
*
|
||||
* While permission is pending, subsequent calls will result in the callbacks
|
||||
* being queued. Once the call succeeds or fails, all queued success or
|
||||
* failure callbacks will be invoked. Subsequent calls to either function will
|
||||
* cause the success or failure callback to be invoked immediately.
|
||||
*/
|
||||
loop.standaloneMedia = (function() {
|
||||
"use strict";
|
||||
|
||||
function patchSymbolIfExtant(objectName, propertyName, replacement) {
|
||||
var object;
|
||||
if (window[objectName]) {
|
||||
object = window[objectName];
|
||||
}
|
||||
if (object && object[propertyName]) {
|
||||
object[propertyName] = replacement;
|
||||
}
|
||||
}
|
||||
|
||||
// originalGum _must_ be on navigator; otherwise things blow up.
|
||||
// For TBPlugin users, navigator.originalGum is set after the TB SDK is loaded.
|
||||
navigator.originalGum = navigator.getUserMedia ||
|
||||
navigator.mozGetUserMedia ||
|
||||
navigator.webkitGetUserMedia;
|
||||
|
||||
function _MultiplexGum() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
_MultiplexGum.prototype = {
|
||||
/**
|
||||
* @see The docs at the top of this file for overall semantics,
|
||||
* & http://developer.mozilla.org/en-US/docs/NavigatorUserMedia.getUserMedia
|
||||
* for params, since this is intended to be purely a passthrough to gUM.
|
||||
*/
|
||||
getPermsAndCacheMedia: function(constraints, onSuccess, onError) {
|
||||
function handleResult(callbacks, param) {
|
||||
// Operate on a copy of the array in case any of the callbacks
|
||||
// calls reset, which would cause an infinite-recursion.
|
||||
this.userMedia.successCallbacks = [];
|
||||
this.userMedia.errorCallbacks = [];
|
||||
callbacks.forEach(function(cb) {
|
||||
if (typeof cb == "function") {
|
||||
cb(param);
|
||||
}
|
||||
});
|
||||
}
|
||||
function handleSuccess(localStream) {
|
||||
this.userMedia.pending = false;
|
||||
this.userMedia.localStream = localStream;
|
||||
this.userMedia.error = null;
|
||||
handleResult.call(this, this.userMedia.successCallbacks.slice(0), localStream);
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
this.userMedia.pending = false;
|
||||
this.userMedia.error = error;
|
||||
handleResult.call(this, this.userMedia.errorCallbacks.slice(0), error);
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
if (this.userMedia.localStream &&
|
||||
this.userMedia.localStream.ended) {
|
||||
this.userMedia.localStream = null;
|
||||
}
|
||||
|
||||
this.userMedia.errorCallbacks.push(onError);
|
||||
this.userMedia.successCallbacks.push(onSuccess);
|
||||
|
||||
if (this.userMedia.localStream) {
|
||||
handleSuccess.call(this, this.userMedia.localStream);
|
||||
return;
|
||||
} else if (this.userMedia.error) {
|
||||
handleError.call(this, this.userMedia.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.userMedia.pending) {
|
||||
return;
|
||||
}
|
||||
this.userMedia.pending = true;
|
||||
|
||||
navigator.originalGum(constraints, handleSuccess.bind(this),
|
||||
handleError.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the cached permissions, callbacks, and media to their default
|
||||
* state and call any error callbacks to let any waiting callers know
|
||||
* not to ever expect any more callbacks. We use "PERMISSION_DENIED",
|
||||
* for lack of a better, more specific gUM code that callers are likely
|
||||
* to be prepared to handle.
|
||||
*/
|
||||
reset: function() {
|
||||
// When called from the ctor, userMedia is not created yet.
|
||||
if (this.userMedia) {
|
||||
this.userMedia.errorCallbacks.forEach(function(cb) {
|
||||
if (typeof cb == "function") {
|
||||
cb("PERMISSION_DENIED");
|
||||
}
|
||||
});
|
||||
if (this.userMedia.localStream &&
|
||||
typeof this.userMedia.localStream.stop == "function") {
|
||||
this.userMedia.localStream.stop();
|
||||
}
|
||||
}
|
||||
this.userMedia = {
|
||||
error: null,
|
||||
localStream: null,
|
||||
pending: false,
|
||||
errorCallbacks: [],
|
||||
successCallbacks: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var singletonMultiplexGum = new _MultiplexGum();
|
||||
function myGetUserMedia() {
|
||||
// This function is needed to pull in the instance
|
||||
// of the singleton for tests to overwrite the used instance.
|
||||
singletonMultiplexGum.getPermsAndCacheMedia.apply(singletonMultiplexGum, arguments);
|
||||
}
|
||||
patchSymbolIfExtant("navigator", "mozGetUserMedia", myGetUserMedia);
|
||||
patchSymbolIfExtant("navigator", "webkitGetUserMedia", myGetUserMedia);
|
||||
patchSymbolIfExtant("navigator", "getUserMedia", myGetUserMedia);
|
||||
patchSymbolIfExtant("TBPlugin", "getUserMedia", myGetUserMedia);
|
||||
|
||||
return {
|
||||
multiplexGum: singletonMultiplexGum,
|
||||
_MultiplexGum: _MultiplexGum,
|
||||
setSingleton: function(singleton) {
|
||||
singletonMultiplexGum = singleton;
|
||||
},
|
||||
};
|
||||
})();
|
||||
@@ -1,210 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.store = loop.store || {};
|
||||
|
||||
/**
|
||||
* The standalone metrics store is used to log activities to
|
||||
* analytics.
|
||||
*
|
||||
* Where possible we log events via receiving actions. However, some
|
||||
* combinations of actions and events require us to listen directly to
|
||||
* changes in the activeRoomStore so that we gain the benefit of the logic
|
||||
* in that store.
|
||||
*/
|
||||
loop.store.StandaloneMetricsStore = (function() {
|
||||
"use strict";
|
||||
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
|
||||
|
||||
loop.store.metrics = loop.store.metrics || {};
|
||||
|
||||
var METRICS_GA_CATEGORY = loop.store.METRICS_GA_CATEGORY = {
|
||||
general: "/conversation/ interactions",
|
||||
download: "Firefox Downloads"
|
||||
};
|
||||
|
||||
var METRICS_GA_ACTIONS = loop.store.METRICS_GA_ACTIONS = {
|
||||
audioMute: "audio mute",
|
||||
button: "button click",
|
||||
download: "download button click",
|
||||
faceMute: "face mute",
|
||||
link: "link click",
|
||||
pageLoad: "page load messages",
|
||||
success: "success",
|
||||
support: "support link click"
|
||||
};
|
||||
|
||||
var StandaloneMetricsStore = loop.store.createStore({
|
||||
actions: [
|
||||
"gotMediaPermission",
|
||||
"joinRoom",
|
||||
"leaveRoom",
|
||||
"mediaConnected",
|
||||
"recordClick"
|
||||
],
|
||||
|
||||
/**
|
||||
* Initializes the store and starts listening to the activeRoomStore.
|
||||
*
|
||||
* @param {Object} options Options for the store, should include a
|
||||
* reference to the activeRoomStore.
|
||||
*/
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
|
||||
if (!options.activeRoomStore) {
|
||||
throw new Error("Missing option activeRoomStore");
|
||||
}
|
||||
|
||||
// Don't bother listening if we're not storing metrics.
|
||||
// I'd love for ga to be an option, but that messes up the function
|
||||
// working properly.
|
||||
if (window.ga) {
|
||||
this.activeRoomStore = options.activeRoomStore;
|
||||
|
||||
this.listenTo(options.activeRoomStore, "change",
|
||||
this._onActiveRoomStoreChange.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns initial state data for this store. These are mainly reflections
|
||||
* of activeRoomStore so we can match initial states for change tracking
|
||||
* purposes.
|
||||
*
|
||||
* @return {Object} The initial store state.
|
||||
*/
|
||||
getInitialStoreState: function() {
|
||||
return {
|
||||
audioMuted: false,
|
||||
roomState: ROOM_STATES.INIT,
|
||||
videoMuted: false
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves an event to ga.
|
||||
*
|
||||
* @param {String} category The category for the event.
|
||||
* @param {String} action The type of action.
|
||||
* @param {String} label The label detailing the action.
|
||||
*/
|
||||
_storeEvent: function(category, action, label) {
|
||||
// ga might not be defined if donottrack is enabled, see index.html.
|
||||
if (!window.ga) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For now all we need to do is forward onto ga.
|
||||
window.ga("send", "event", category, action, label);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles media permssion being obtained.
|
||||
*/
|
||||
gotMediaPermission: function() {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
|
||||
"Media granted");
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the user clicking the join room button.
|
||||
*/
|
||||
joinRoom: function() {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
|
||||
"Join the conversation");
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the user clicking the leave room button.
|
||||
*/
|
||||
leaveRoom: function() {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
|
||||
"Leave conversation");
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles notification that two-way media has been achieved.
|
||||
*/
|
||||
mediaConnected: function() {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
|
||||
"Media connected");
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles recording link clicks.
|
||||
*
|
||||
* @param {sharedActions.RecordClick} actionData The data associated with
|
||||
* the link.
|
||||
*/
|
||||
recordClick: function(actionData) {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.linkClick,
|
||||
actionData.linkInfo);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles notifications that the activeRoomStore has changed, updating
|
||||
* the metrics for room state and mute state as necessary.
|
||||
*/
|
||||
_onActiveRoomStoreChange: function() {
|
||||
var roomStore = this.activeRoomStore.getStoreState();
|
||||
|
||||
this._checkRoomState(roomStore.roomState, roomStore.failureReason);
|
||||
|
||||
this._checkMuteState("audio", roomStore.audioMuted);
|
||||
this._checkMuteState("video", roomStore.videoMuted);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles checking of the room state to look for events we need to send.
|
||||
*
|
||||
* @param {String} roomState The new room state.
|
||||
* @param {String} failureReason Optional, if the room is in the failure
|
||||
* state, this should contain the reason for
|
||||
* the failure.
|
||||
*/
|
||||
_checkRoomState: function(roomState, failureReason) {
|
||||
if (this._storeState.roomState === roomState) {
|
||||
return;
|
||||
}
|
||||
this._storeState.roomState = roomState;
|
||||
|
||||
if (roomState === ROOM_STATES.FAILED &&
|
||||
failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID) {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
|
||||
"Link expired or invalid");
|
||||
}
|
||||
|
||||
if (roomState === ROOM_STATES.FULL) {
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
|
||||
"Room full");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles check of the mute state to look for events we need to send.
|
||||
*
|
||||
* @param {String} type The type of mute being adjusted, i.e. "audio" or
|
||||
* "video".
|
||||
* @param {Boolean} muted The new state of mute
|
||||
*/
|
||||
_checkMuteState: function(type, muted) {
|
||||
var muteItem = type + "Muted";
|
||||
if (this._storeState[muteItem] === muted) {
|
||||
return;
|
||||
}
|
||||
this._storeState[muteItem] = muted;
|
||||
|
||||
var muteType = type === "audio" ? METRICS_GA_ACTIONS.audioMute : METRICS_GA_ACTIONS.faceMute;
|
||||
var muteState = muted ? "mute" : "unmute";
|
||||
|
||||
this._storeEvent(METRICS_GA_CATEGORY.general, muteType, muteState);
|
||||
}
|
||||
});
|
||||
|
||||
return StandaloneMetricsStore;
|
||||
})();
|
||||
@@ -1,637 +0,0 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
/* 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/. */
|
||||
|
||||
/* global loop:true, React */
|
||||
/* jshint newcap:false, maxlen:false */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.standaloneRoomViews = (function(mozL10n) {
|
||||
"use strict";
|
||||
|
||||
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
|
||||
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var sharedUtils = loop.shared.utils;
|
||||
var sharedViews = loop.shared.views;
|
||||
|
||||
var StandaloneRoomInfoArea = React.createClass({displayName: "StandaloneRoomInfoArea",
|
||||
propTypes: {
|
||||
isFirefox: React.PropTypes.bool.isRequired,
|
||||
activeRoomStore: React.PropTypes.oneOfType([
|
||||
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
|
||||
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
|
||||
]).isRequired
|
||||
},
|
||||
|
||||
onFeedbackSent: function() {
|
||||
// We pass a tick to prevent React warnings regarding nested updates.
|
||||
setTimeout(function() {
|
||||
this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_renderCallToActionLink: function() {
|
||||
if (this.props.isFirefox) {
|
||||
return (
|
||||
React.createElement("a", {href: loop.config.learnMoreUrl, className: "btn btn-info"},
|
||||
mozL10n.get("rooms_room_full_call_to_action_label", {
|
||||
clientShortname: mozL10n.get("clientShortname2")
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
React.createElement("a", {href: loop.config.downloadFirefoxUrl, className: "btn btn-info"},
|
||||
mozL10n.get("rooms_room_full_call_to_action_nonFx_label", {
|
||||
brandShortname: mozL10n.get("brandShortname")
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return String An appropriate string according to the failureReason.
|
||||
*/
|
||||
_getFailureString: function() {
|
||||
switch(this.props.failureReason) {
|
||||
case FAILURE_DETAILS.MEDIA_DENIED:
|
||||
return mozL10n.get("rooms_media_denied_message");
|
||||
case FAILURE_DETAILS.EXPIRED_OR_INVALID:
|
||||
return mozL10n.get("rooms_unavailable_notification_message");
|
||||
default:
|
||||
return mozL10n.get("status_error");
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
switch(this.props.roomState) {
|
||||
case ROOM_STATES.INIT:
|
||||
case ROOM_STATES.READY: {
|
||||
// XXX: In ENDED state, we should rather display the feedback form.
|
||||
return (
|
||||
React.createElement("div", {className: "room-inner-info-area"},
|
||||
React.createElement("button", {className: "btn btn-join btn-info",
|
||||
onClick: this.props.joinRoom},
|
||||
mozL10n.get("rooms_room_join_label")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.MEDIA_WAIT: {
|
||||
var msg = mozL10n.get("call_progress_getting_media_description",
|
||||
{clientShortname: mozL10n.get("clientShortname2")});
|
||||
var utils = loop.shared.utils;
|
||||
var isChrome = utils.isChrome(navigator.userAgent);
|
||||
var isFirefox = utils.isFirefox(navigator.userAgent);
|
||||
var isOpera = utils.isOpera(navigator.userAgent);
|
||||
var promptMediaMessageClasses = React.addons.classSet({
|
||||
"prompt-media-message": true,
|
||||
"chrome": isChrome,
|
||||
"firefox": isFirefox,
|
||||
"opera": isOpera,
|
||||
"other": !isChrome && !isFirefox && !isOpera
|
||||
});
|
||||
return (
|
||||
React.createElement("div", {className: "room-inner-info-area"},
|
||||
React.createElement("p", {className: promptMediaMessageClasses},
|
||||
msg
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.JOINING:
|
||||
case ROOM_STATES.JOINED:
|
||||
case ROOM_STATES.SESSION_CONNECTED: {
|
||||
return (
|
||||
React.createElement("div", {className: "room-inner-info-area"},
|
||||
React.createElement("p", {className: "empty-room-message"},
|
||||
mozL10n.get("rooms_only_occupant_label")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.FULL: {
|
||||
return (
|
||||
React.createElement("div", {className: "room-inner-info-area"},
|
||||
React.createElement("p", {className: "full-room-message"},
|
||||
mozL10n.get("rooms_room_full_label")
|
||||
),
|
||||
React.createElement("p", null, this._renderCallToActionLink())
|
||||
)
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.ENDED: {
|
||||
if (this.props.roomUsed)
|
||||
return (
|
||||
React.createElement("div", {className: "ended-conversation"},
|
||||
React.createElement(sharedViews.FeedbackView, {
|
||||
onAfterFeedbackReceived: this.onFeedbackSent}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// In case the room was not used (no one was here), we
|
||||
// bypass the feedback form.
|
||||
this.onFeedbackSent();
|
||||
return null;
|
||||
}
|
||||
case ROOM_STATES.FAILED: {
|
||||
return (
|
||||
React.createElement("div", {className: "room-inner-info-area"},
|
||||
React.createElement("p", {className: "failed-room-message"},
|
||||
this._getFailureString()
|
||||
),
|
||||
React.createElement("button", {className: "btn btn-join btn-info",
|
||||
onClick: this.props.joinRoom},
|
||||
mozL10n.get("retry_call_button")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomHeader = React.createClass({displayName: "StandaloneRoomHeader",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
||||
},
|
||||
|
||||
recordClick: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: "Support link click"
|
||||
}));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("header", null,
|
||||
React.createElement("h1", null, mozL10n.get("clientShortname2")),
|
||||
React.createElement("a", {href: loop.config.generalSupportUrl,
|
||||
onClick: this.recordClick,
|
||||
target: "_blank"},
|
||||
React.createElement("i", {className: "icon icon-help"})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomFooter = React.createClass({displayName: "StandaloneRoomFooter",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
||||
},
|
||||
|
||||
_getContent: function() {
|
||||
// We use this technique of static markup as it means we get
|
||||
// just one overall string for L10n to define the structure of
|
||||
// the whole item.
|
||||
return mozL10n.get("legal_text_and_links", {
|
||||
"clientShortname": mozL10n.get("clientShortname2"),
|
||||
"terms_of_use_url": React.renderToStaticMarkup(
|
||||
React.createElement("a", {href: loop.config.legalWebsiteUrl, target: "_blank"},
|
||||
mozL10n.get("terms_of_use_link_text")
|
||||
)
|
||||
),
|
||||
"privacy_notice_url": React.renderToStaticMarkup(
|
||||
React.createElement("a", {href: loop.config.privacyWebsiteUrl, target: "_blank"},
|
||||
mozL10n.get("privacy_notice_link_text")
|
||||
)
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
recordClick: function(event) {
|
||||
// Check for valid href, as this is clicking on the paragraph -
|
||||
// so the user may be clicking on the text rather than the link.
|
||||
if (event.target && event.target.href) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: event.target.href
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("footer", null,
|
||||
React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()},
|
||||
onClick: this.recordClick}),
|
||||
React.createElement("div", {className: "footer-logo"})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomContextItem = React.createClass({displayName: "StandaloneRoomContextItem",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
receivingScreenShare: React.PropTypes.bool,
|
||||
roomContextUrl: React.PropTypes.object
|
||||
},
|
||||
|
||||
recordClick: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: "Shared URL"
|
||||
}));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.props.roomContextUrl ||
|
||||
!this.props.roomContextUrl.location) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var locationInfo = sharedUtils.formatURL(this.props.roomContextUrl.location);
|
||||
if (!locationInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var cx = React.addons.classSet;
|
||||
|
||||
var classes = cx({
|
||||
"standalone-context-url": true,
|
||||
"screen-share-active": this.props.receivingScreenShare
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: classes},
|
||||
React.createElement("img", {src: this.props.roomContextUrl.thumbnail}),
|
||||
React.createElement("div", {className: "standalone-context-url-description-wrapper"},
|
||||
this.props.roomContextUrl.description,
|
||||
React.createElement("br", null), React.createElement("a", {href: locationInfo.location,
|
||||
onClick: this.recordClick,
|
||||
target: "_blank",
|
||||
title: locationInfo.location}, locationInfo.hostname)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomContextView = React.createClass({displayName: "StandaloneRoomContextView",
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
receivingScreenShare: React.PropTypes.bool.isRequired,
|
||||
roomContextUrls: React.PropTypes.array,
|
||||
roomName: React.PropTypes.string,
|
||||
roomInfoFailure: React.PropTypes.string
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
failureLogged: false
|
||||
};
|
||||
},
|
||||
|
||||
_logFailure: function(message) {
|
||||
if (!this.state.failureLogged) {
|
||||
console.error(mozL10n.get(message));
|
||||
this.state.failureLogged = true;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// For failures, we currently just log the messages - UX doesn't want them
|
||||
// displayed on primary UI at the moment.
|
||||
if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
|
||||
this._logFailure("room_information_failure_unsupported_browser");
|
||||
return null;
|
||||
} else if (this.props.roomInfoFailure) {
|
||||
this._logFailure("room_information_failure_not_available");
|
||||
return null;
|
||||
}
|
||||
|
||||
// We only support one item in the context Urls array for now.
|
||||
var roomContextUrl = (this.props.roomContextUrls &&
|
||||
this.props.roomContextUrls.length > 0) ?
|
||||
this.props.roomContextUrls[0] : null;
|
||||
return (
|
||||
React.createElement("div", {className: "standalone-room-info"},
|
||||
React.createElement("h2", {className: "room-name"}, this.props.roomName),
|
||||
React.createElement(StandaloneRoomContextItem, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
receivingScreenShare: this.props.receivingScreenShare,
|
||||
roomContextUrl: roomContextUrl})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView",
|
||||
mixins: [
|
||||
Backbone.Events,
|
||||
sharedMixins.MediaSetupMixin,
|
||||
sharedMixins.RoomsAudioMixin
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
activeRoomStore: React.PropTypes.oneOfType([
|
||||
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
|
||||
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
|
||||
]).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
isFirefox: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var storeState = this.props.activeRoomStore.getStoreState();
|
||||
return _.extend({}, storeState, {
|
||||
// Used by the UI showcase.
|
||||
roomState: this.props.roomState || storeState.roomState
|
||||
});
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.listenTo(this.props.activeRoomStore, "change",
|
||||
this._onActiveRoomStateChanged);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles a "change" event on the roomStore, and updates this.state
|
||||
* to match the store.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onActiveRoomStateChanged: function() {
|
||||
var state = this.props.activeRoomStore.getStoreState();
|
||||
this.updateVideoDimensions(state.localVideoDimensions, state.remoteVideoDimensions);
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// Adding a class to the document body element from here to ease styling it.
|
||||
document.body.classList.add("is-standalone-room");
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.stopListening(this.props.activeRoomStore);
|
||||
},
|
||||
|
||||
/**
|
||||
* Watches for when we transition to MEDIA_WAIT room state, so we can request
|
||||
* user media access.
|
||||
*
|
||||
* @param {Object} nextProps (Unused)
|
||||
* @param {Object} nextState Next state object.
|
||||
*/
|
||||
componentWillUpdate: function(nextProps, nextState) {
|
||||
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
|
||||
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
|
||||
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
|
||||
getLocalElementFunc: this._getElement.bind(this, ".local"),
|
||||
getRemoteElementFunc: this._getElement.bind(this, ".remote"),
|
||||
getScreenShareElementFunc: this._getElement.bind(this, ".screen")
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.state.roomState !== ROOM_STATES.JOINED &&
|
||||
nextState.roomState === ROOM_STATES.JOINED) {
|
||||
// This forces the video size to update - creating the publisher
|
||||
// first, and then connecting to the session doesn't seem to set the
|
||||
// initial size correctly.
|
||||
this.updateVideoContainer();
|
||||
}
|
||||
|
||||
if (nextState.roomState === ROOM_STATES.INIT ||
|
||||
nextState.roomState === ROOM_STATES.GATHER ||
|
||||
nextState.roomState === ROOM_STATES.READY) {
|
||||
this.resetDimensionsCache();
|
||||
}
|
||||
|
||||
// When screen sharing stops.
|
||||
if (this.state.receivingScreenShare && !nextState.receivingScreenShare) {
|
||||
// Remove the custom screenshare styles on the remote camera.
|
||||
var node = this._getElement(".remote");
|
||||
node.removeAttribute("style");
|
||||
|
||||
// Force the video sizes to update.
|
||||
this.updateVideoContainer();
|
||||
}
|
||||
},
|
||||
|
||||
joinRoom: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
|
||||
},
|
||||
|
||||
leaveRoom: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles streaming status for a given stream type.
|
||||
*
|
||||
* @param {String} type Stream type ("audio" or "video").
|
||||
* @param {Boolean} enabled Enabled stream flag.
|
||||
*/
|
||||
publishStream: function(type, enabled) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.SetMute({
|
||||
type: type,
|
||||
enabled: enabled
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifically updates the local camera stream size and position, depending
|
||||
* on the size and position of the remote video stream.
|
||||
* This method gets called from `updateVideoContainer`, which is defined in
|
||||
* the `MediaSetupMixin`.
|
||||
*
|
||||
* @param {Object} ratio Aspect ratio of the local camera stream
|
||||
*/
|
||||
updateLocalCameraPosition: function(ratio) {
|
||||
// The local stream is a quarter of the remote stream.
|
||||
var LOCAL_STREAM_SIZE = 0.25;
|
||||
// The local stream overlaps the remote stream by a quarter of the local stream.
|
||||
var LOCAL_STREAM_OVERLAP = 0.25;
|
||||
// The minimum size of video height/width allowed by the sdk css.
|
||||
var SDK_MIN_SIZE = 48;
|
||||
|
||||
var node = this._getElement(".local");
|
||||
var targetWidth;
|
||||
|
||||
node.style.right = "auto";
|
||||
if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
|
||||
// For reduced screen widths, we just go for a fixed size and no overlap.
|
||||
targetWidth = 180;
|
||||
node.style.width = (targetWidth * ratio.width) + "px";
|
||||
node.style.height = (targetWidth * ratio.height) + "px";
|
||||
node.style.left = "auto";
|
||||
} else {
|
||||
// The local camera view should be a quarter of the size of the remote stream
|
||||
// and positioned to overlap with the remote stream at a quarter of its width.
|
||||
|
||||
// Now position the local camera view correctly with respect to the remote
|
||||
// video stream or the screen share stream.
|
||||
var remoteVideoDimensions = this.getRemoteVideoDimensions(
|
||||
this.state.receivingScreenShare ? "screen" : "camera");
|
||||
|
||||
targetWidth = remoteVideoDimensions.streamWidth * LOCAL_STREAM_SIZE;
|
||||
|
||||
var realWidth = targetWidth * ratio.width;
|
||||
var realHeight = targetWidth * ratio.height;
|
||||
|
||||
// If we've hit the min size limits, then limit at the minimum.
|
||||
if (realWidth < SDK_MIN_SIZE) {
|
||||
realWidth = SDK_MIN_SIZE;
|
||||
realHeight = realWidth / ratio.width * ratio.height;
|
||||
}
|
||||
if (realHeight < SDK_MIN_SIZE) {
|
||||
realHeight = SDK_MIN_SIZE;
|
||||
realWidth = realHeight / ratio.height * ratio.width;
|
||||
}
|
||||
|
||||
var offsetX = (remoteVideoDimensions.streamWidth + remoteVideoDimensions.offsetX);
|
||||
// The horizontal offset of the stream, and the width of the resulting
|
||||
// pillarbox, is determined by the height exponent of the aspect ratio.
|
||||
// Therefore we multiply the width of the local camera view by the height
|
||||
// ratio.
|
||||
node.style.left = (offsetX - (realWidth * LOCAL_STREAM_OVERLAP)) + "px";
|
||||
node.style.width = realWidth + "px";
|
||||
node.style.height = realHeight + "px";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifically updates the remote camera stream size and position, if
|
||||
* a screen share is being received. It is slaved from the position of the
|
||||
* local stream.
|
||||
* This method gets called from `updateVideoContainer`, which is defined in
|
||||
* the `MediaSetupMixin`.
|
||||
*
|
||||
* @param {Object} ratio Aspect ratio of the remote camera stream
|
||||
*/
|
||||
updateRemoteCameraPosition: function(ratio) {
|
||||
// Nothing to do for screenshare
|
||||
if (!this.state.receivingScreenShare) {
|
||||
return;
|
||||
}
|
||||
// XXX For the time being, if we're a narrow screen, aka mobile, we don't display
|
||||
// the remote media (bug 1133534).
|
||||
if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 10px separation between the two streams.
|
||||
var LOCAL_REMOTE_SEPARATION = 10;
|
||||
|
||||
var node = this._getElement(".remote");
|
||||
var localNode = this._getElement(".local");
|
||||
|
||||
// Match the width to the local video.
|
||||
node.style.width = localNode.offsetWidth + "px";
|
||||
|
||||
// The height is then determined from the aspect ratio
|
||||
var height = ((localNode.offsetWidth / ratio.width) * ratio.height);
|
||||
node.style.height = height + "px";
|
||||
|
||||
node.style.right = "auto";
|
||||
node.style.bottom = "auto";
|
||||
|
||||
// Now position the local camera view correctly with respect to the remote
|
||||
// video stream.
|
||||
|
||||
// The top is measured from the top of the element down the screen,
|
||||
// so subtract the height of the video and the separation distance.
|
||||
node.style.top = (localNode.offsetTop - height - LOCAL_REMOTE_SEPARATION) + "px";
|
||||
|
||||
// Match the left-hand sides.
|
||||
node.style.left = localNode.offsetLeft + "px";
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if current room is active.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
_roomIsActive: function() {
|
||||
return this.state.roomState === ROOM_STATES.JOINED ||
|
||||
this.state.roomState === ROOM_STATES.SESSION_CONNECTED ||
|
||||
this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var localStreamClasses = React.addons.classSet({
|
||||
hide: !this._roomIsActive(),
|
||||
local: true,
|
||||
"local-stream": true,
|
||||
"local-stream-audio": this.state.videoMuted
|
||||
});
|
||||
|
||||
var remoteStreamClasses = React.addons.classSet({
|
||||
"video_inner": true,
|
||||
"remote": true,
|
||||
"focus-stream": !this.state.receivingScreenShare,
|
||||
"remote-inset-stream": this.state.receivingScreenShare
|
||||
});
|
||||
|
||||
var screenShareStreamClasses = React.addons.classSet({
|
||||
"screen": true,
|
||||
"focus-stream": this.state.receivingScreenShare,
|
||||
hide: !this.state.receivingScreenShare,
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "room-conversation-wrapper"},
|
||||
React.createElement("div", {className: "beta-logo"}),
|
||||
React.createElement(StandaloneRoomHeader, {dispatcher: this.props.dispatcher}),
|
||||
React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState,
|
||||
failureReason: this.state.failureReason,
|
||||
joinRoom: this.joinRoom,
|
||||
isFirefox: this.props.isFirefox,
|
||||
activeRoomStore: this.props.activeRoomStore,
|
||||
roomUsed: this.state.used}),
|
||||
React.createElement("div", {className: "video-layout-wrapper"},
|
||||
React.createElement("div", {className: "conversation room-conversation"},
|
||||
React.createElement(StandaloneRoomContextView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
receivingScreenShare: this.state.receivingScreenShare,
|
||||
roomContextUrls: this.state.roomContextUrls,
|
||||
roomName: this.state.roomName,
|
||||
roomInfoFailure: this.state.roomInfoFailure}),
|
||||
React.createElement("div", {className: "media nested"},
|
||||
React.createElement("span", {className: "self-view-hidden-message"},
|
||||
mozL10n.get("self_view_hidden_message")
|
||||
),
|
||||
React.createElement("div", {className: "video_wrapper remote_wrapper"},
|
||||
React.createElement("div", {className: remoteStreamClasses}),
|
||||
React.createElement("div", {className: screenShareStreamClasses})
|
||||
),
|
||||
React.createElement("div", {className: localStreamClasses})
|
||||
),
|
||||
React.createElement(sharedViews.ConversationToolbar, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
video: {enabled: !this.state.videoMuted,
|
||||
visible: this._roomIsActive()},
|
||||
audio: {enabled: !this.state.audioMuted,
|
||||
visible: this._roomIsActive()},
|
||||
publishStream: this.publishStream,
|
||||
hangup: this.leaveRoom,
|
||||
hangupButtonLabel: mozL10n.get("rooms_leave_button_label"),
|
||||
enableHangup: this._roomIsActive()})
|
||||
)
|
||||
),
|
||||
React.createElement(loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView, {
|
||||
marketplaceSrc: this.state.marketplaceSrc,
|
||||
onMarketplaceMessage: this.state.onMarketplaceMessage}),
|
||||
React.createElement(StandaloneRoomFooter, {dispatcher: this.props.dispatcher})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
StandaloneRoomContextView: StandaloneRoomContextView,
|
||||
StandaloneRoomFooter: StandaloneRoomFooter,
|
||||
StandaloneRoomHeader: StandaloneRoomHeader,
|
||||
StandaloneRoomView: StandaloneRoomView
|
||||
};
|
||||
})(navigator.mozL10n);
|
||||
@@ -1,637 +0,0 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
/* 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/. */
|
||||
|
||||
/* global loop:true, React */
|
||||
/* jshint newcap:false, maxlen:false */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.standaloneRoomViews = (function(mozL10n) {
|
||||
"use strict";
|
||||
|
||||
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
|
||||
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var sharedUtils = loop.shared.utils;
|
||||
var sharedViews = loop.shared.views;
|
||||
|
||||
var StandaloneRoomInfoArea = React.createClass({
|
||||
propTypes: {
|
||||
isFirefox: React.PropTypes.bool.isRequired,
|
||||
activeRoomStore: React.PropTypes.oneOfType([
|
||||
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
|
||||
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
|
||||
]).isRequired
|
||||
},
|
||||
|
||||
onFeedbackSent: function() {
|
||||
// We pass a tick to prevent React warnings regarding nested updates.
|
||||
setTimeout(function() {
|
||||
this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_renderCallToActionLink: function() {
|
||||
if (this.props.isFirefox) {
|
||||
return (
|
||||
<a href={loop.config.learnMoreUrl} className="btn btn-info">
|
||||
{mozL10n.get("rooms_room_full_call_to_action_label", {
|
||||
clientShortname: mozL10n.get("clientShortname2")
|
||||
})}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a href={loop.config.downloadFirefoxUrl} className="btn btn-info">
|
||||
{mozL10n.get("rooms_room_full_call_to_action_nonFx_label", {
|
||||
brandShortname: mozL10n.get("brandShortname")
|
||||
})}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return String An appropriate string according to the failureReason.
|
||||
*/
|
||||
_getFailureString: function() {
|
||||
switch(this.props.failureReason) {
|
||||
case FAILURE_DETAILS.MEDIA_DENIED:
|
||||
return mozL10n.get("rooms_media_denied_message");
|
||||
case FAILURE_DETAILS.EXPIRED_OR_INVALID:
|
||||
return mozL10n.get("rooms_unavailable_notification_message");
|
||||
default:
|
||||
return mozL10n.get("status_error");
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
switch(this.props.roomState) {
|
||||
case ROOM_STATES.INIT:
|
||||
case ROOM_STATES.READY: {
|
||||
// XXX: In ENDED state, we should rather display the feedback form.
|
||||
return (
|
||||
<div className="room-inner-info-area">
|
||||
<button className="btn btn-join btn-info"
|
||||
onClick={this.props.joinRoom}>
|
||||
{mozL10n.get("rooms_room_join_label")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.MEDIA_WAIT: {
|
||||
var msg = mozL10n.get("call_progress_getting_media_description",
|
||||
{clientShortname: mozL10n.get("clientShortname2")});
|
||||
var utils = loop.shared.utils;
|
||||
var isChrome = utils.isChrome(navigator.userAgent);
|
||||
var isFirefox = utils.isFirefox(navigator.userAgent);
|
||||
var isOpera = utils.isOpera(navigator.userAgent);
|
||||
var promptMediaMessageClasses = React.addons.classSet({
|
||||
"prompt-media-message": true,
|
||||
"chrome": isChrome,
|
||||
"firefox": isFirefox,
|
||||
"opera": isOpera,
|
||||
"other": !isChrome && !isFirefox && !isOpera
|
||||
});
|
||||
return (
|
||||
<div className="room-inner-info-area">
|
||||
<p className={promptMediaMessageClasses}>
|
||||
{msg}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.JOINING:
|
||||
case ROOM_STATES.JOINED:
|
||||
case ROOM_STATES.SESSION_CONNECTED: {
|
||||
return (
|
||||
<div className="room-inner-info-area">
|
||||
<p className="empty-room-message">
|
||||
{mozL10n.get("rooms_only_occupant_label")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.FULL: {
|
||||
return (
|
||||
<div className="room-inner-info-area">
|
||||
<p className="full-room-message">
|
||||
{mozL10n.get("rooms_room_full_label")}
|
||||
</p>
|
||||
<p>{this._renderCallToActionLink()}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.ENDED: {
|
||||
if (this.props.roomUsed)
|
||||
return (
|
||||
<div className="ended-conversation">
|
||||
<sharedViews.FeedbackView
|
||||
onAfterFeedbackReceived={this.onFeedbackSent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
// In case the room was not used (no one was here), we
|
||||
// bypass the feedback form.
|
||||
this.onFeedbackSent();
|
||||
return null;
|
||||
}
|
||||
case ROOM_STATES.FAILED: {
|
||||
return (
|
||||
<div className="room-inner-info-area">
|
||||
<p className="failed-room-message">
|
||||
{this._getFailureString()}
|
||||
</p>
|
||||
<button className="btn btn-join btn-info"
|
||||
onClick={this.props.joinRoom}>
|
||||
{mozL10n.get("retry_call_button")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomHeader = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
||||
},
|
||||
|
||||
recordClick: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: "Support link click"
|
||||
}));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<header>
|
||||
<h1>{mozL10n.get("clientShortname2")}</h1>
|
||||
<a href={loop.config.generalSupportUrl}
|
||||
onClick={this.recordClick}
|
||||
target="_blank">
|
||||
<i className="icon icon-help"></i>
|
||||
</a>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomFooter = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
||||
},
|
||||
|
||||
_getContent: function() {
|
||||
// We use this technique of static markup as it means we get
|
||||
// just one overall string for L10n to define the structure of
|
||||
// the whole item.
|
||||
return mozL10n.get("legal_text_and_links", {
|
||||
"clientShortname": mozL10n.get("clientShortname2"),
|
||||
"terms_of_use_url": React.renderToStaticMarkup(
|
||||
<a href={loop.config.legalWebsiteUrl} target="_blank">
|
||||
{mozL10n.get("terms_of_use_link_text")}
|
||||
</a>
|
||||
),
|
||||
"privacy_notice_url": React.renderToStaticMarkup(
|
||||
<a href={loop.config.privacyWebsiteUrl} target="_blank">
|
||||
{mozL10n.get("privacy_notice_link_text")}
|
||||
</a>
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
recordClick: function(event) {
|
||||
// Check for valid href, as this is clicking on the paragraph -
|
||||
// so the user may be clicking on the text rather than the link.
|
||||
if (event.target && event.target.href) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: event.target.href
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<footer>
|
||||
<p dangerouslySetInnerHTML={{__html: this._getContent()}}
|
||||
onClick={this.recordClick}></p>
|
||||
<div className="footer-logo" />
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomContextItem = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
receivingScreenShare: React.PropTypes.bool,
|
||||
roomContextUrl: React.PropTypes.object
|
||||
},
|
||||
|
||||
recordClick: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
|
||||
linkInfo: "Shared URL"
|
||||
}));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (!this.props.roomContextUrl ||
|
||||
!this.props.roomContextUrl.location) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var locationInfo = sharedUtils.formatURL(this.props.roomContextUrl.location);
|
||||
if (!locationInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var cx = React.addons.classSet;
|
||||
|
||||
var classes = cx({
|
||||
"standalone-context-url": true,
|
||||
"screen-share-active": this.props.receivingScreenShare
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<img src={this.props.roomContextUrl.thumbnail} />
|
||||
<div className="standalone-context-url-description-wrapper">
|
||||
{this.props.roomContextUrl.description}
|
||||
<br /><a href={locationInfo.location}
|
||||
onClick={this.recordClick}
|
||||
target="_blank"
|
||||
title={locationInfo.location}>{locationInfo.hostname}</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomContextView = React.createClass({
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
receivingScreenShare: React.PropTypes.bool.isRequired,
|
||||
roomContextUrls: React.PropTypes.array,
|
||||
roomName: React.PropTypes.string,
|
||||
roomInfoFailure: React.PropTypes.string
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
failureLogged: false
|
||||
};
|
||||
},
|
||||
|
||||
_logFailure: function(message) {
|
||||
if (!this.state.failureLogged) {
|
||||
console.error(mozL10n.get(message));
|
||||
this.state.failureLogged = true;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// For failures, we currently just log the messages - UX doesn't want them
|
||||
// displayed on primary UI at the moment.
|
||||
if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
|
||||
this._logFailure("room_information_failure_unsupported_browser");
|
||||
return null;
|
||||
} else if (this.props.roomInfoFailure) {
|
||||
this._logFailure("room_information_failure_not_available");
|
||||
return null;
|
||||
}
|
||||
|
||||
// We only support one item in the context Urls array for now.
|
||||
var roomContextUrl = (this.props.roomContextUrls &&
|
||||
this.props.roomContextUrls.length > 0) ?
|
||||
this.props.roomContextUrls[0] : null;
|
||||
return (
|
||||
<div className="standalone-room-info">
|
||||
<h2 className="room-name">{this.props.roomName}</h2>
|
||||
<StandaloneRoomContextItem
|
||||
dispatcher={this.props.dispatcher}
|
||||
receivingScreenShare={this.props.receivingScreenShare}
|
||||
roomContextUrl={roomContextUrl} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomView = React.createClass({
|
||||
mixins: [
|
||||
Backbone.Events,
|
||||
sharedMixins.MediaSetupMixin,
|
||||
sharedMixins.RoomsAudioMixin
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
activeRoomStore: React.PropTypes.oneOfType([
|
||||
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
|
||||
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
|
||||
]).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
isFirefox: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var storeState = this.props.activeRoomStore.getStoreState();
|
||||
return _.extend({}, storeState, {
|
||||
// Used by the UI showcase.
|
||||
roomState: this.props.roomState || storeState.roomState
|
||||
});
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.listenTo(this.props.activeRoomStore, "change",
|
||||
this._onActiveRoomStateChanged);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles a "change" event on the roomStore, and updates this.state
|
||||
* to match the store.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onActiveRoomStateChanged: function() {
|
||||
var state = this.props.activeRoomStore.getStoreState();
|
||||
this.updateVideoDimensions(state.localVideoDimensions, state.remoteVideoDimensions);
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// Adding a class to the document body element from here to ease styling it.
|
||||
document.body.classList.add("is-standalone-room");
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.stopListening(this.props.activeRoomStore);
|
||||
},
|
||||
|
||||
/**
|
||||
* Watches for when we transition to MEDIA_WAIT room state, so we can request
|
||||
* user media access.
|
||||
*
|
||||
* @param {Object} nextProps (Unused)
|
||||
* @param {Object} nextState Next state object.
|
||||
*/
|
||||
componentWillUpdate: function(nextProps, nextState) {
|
||||
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
|
||||
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
|
||||
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
|
||||
getLocalElementFunc: this._getElement.bind(this, ".local"),
|
||||
getRemoteElementFunc: this._getElement.bind(this, ".remote"),
|
||||
getScreenShareElementFunc: this._getElement.bind(this, ".screen")
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.state.roomState !== ROOM_STATES.JOINED &&
|
||||
nextState.roomState === ROOM_STATES.JOINED) {
|
||||
// This forces the video size to update - creating the publisher
|
||||
// first, and then connecting to the session doesn't seem to set the
|
||||
// initial size correctly.
|
||||
this.updateVideoContainer();
|
||||
}
|
||||
|
||||
if (nextState.roomState === ROOM_STATES.INIT ||
|
||||
nextState.roomState === ROOM_STATES.GATHER ||
|
||||
nextState.roomState === ROOM_STATES.READY) {
|
||||
this.resetDimensionsCache();
|
||||
}
|
||||
|
||||
// When screen sharing stops.
|
||||
if (this.state.receivingScreenShare && !nextState.receivingScreenShare) {
|
||||
// Remove the custom screenshare styles on the remote camera.
|
||||
var node = this._getElement(".remote");
|
||||
node.removeAttribute("style");
|
||||
|
||||
// Force the video sizes to update.
|
||||
this.updateVideoContainer();
|
||||
}
|
||||
},
|
||||
|
||||
joinRoom: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
|
||||
},
|
||||
|
||||
leaveRoom: function() {
|
||||
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles streaming status for a given stream type.
|
||||
*
|
||||
* @param {String} type Stream type ("audio" or "video").
|
||||
* @param {Boolean} enabled Enabled stream flag.
|
||||
*/
|
||||
publishStream: function(type, enabled) {
|
||||
this.props.dispatcher.dispatch(new sharedActions.SetMute({
|
||||
type: type,
|
||||
enabled: enabled
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifically updates the local camera stream size and position, depending
|
||||
* on the size and position of the remote video stream.
|
||||
* This method gets called from `updateVideoContainer`, which is defined in
|
||||
* the `MediaSetupMixin`.
|
||||
*
|
||||
* @param {Object} ratio Aspect ratio of the local camera stream
|
||||
*/
|
||||
updateLocalCameraPosition: function(ratio) {
|
||||
// The local stream is a quarter of the remote stream.
|
||||
var LOCAL_STREAM_SIZE = 0.25;
|
||||
// The local stream overlaps the remote stream by a quarter of the local stream.
|
||||
var LOCAL_STREAM_OVERLAP = 0.25;
|
||||
// The minimum size of video height/width allowed by the sdk css.
|
||||
var SDK_MIN_SIZE = 48;
|
||||
|
||||
var node = this._getElement(".local");
|
||||
var targetWidth;
|
||||
|
||||
node.style.right = "auto";
|
||||
if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
|
||||
// For reduced screen widths, we just go for a fixed size and no overlap.
|
||||
targetWidth = 180;
|
||||
node.style.width = (targetWidth * ratio.width) + "px";
|
||||
node.style.height = (targetWidth * ratio.height) + "px";
|
||||
node.style.left = "auto";
|
||||
} else {
|
||||
// The local camera view should be a quarter of the size of the remote stream
|
||||
// and positioned to overlap with the remote stream at a quarter of its width.
|
||||
|
||||
// Now position the local camera view correctly with respect to the remote
|
||||
// video stream or the screen share stream.
|
||||
var remoteVideoDimensions = this.getRemoteVideoDimensions(
|
||||
this.state.receivingScreenShare ? "screen" : "camera");
|
||||
|
||||
targetWidth = remoteVideoDimensions.streamWidth * LOCAL_STREAM_SIZE;
|
||||
|
||||
var realWidth = targetWidth * ratio.width;
|
||||
var realHeight = targetWidth * ratio.height;
|
||||
|
||||
// If we've hit the min size limits, then limit at the minimum.
|
||||
if (realWidth < SDK_MIN_SIZE) {
|
||||
realWidth = SDK_MIN_SIZE;
|
||||
realHeight = realWidth / ratio.width * ratio.height;
|
||||
}
|
||||
if (realHeight < SDK_MIN_SIZE) {
|
||||
realHeight = SDK_MIN_SIZE;
|
||||
realWidth = realHeight / ratio.height * ratio.width;
|
||||
}
|
||||
|
||||
var offsetX = (remoteVideoDimensions.streamWidth + remoteVideoDimensions.offsetX);
|
||||
// The horizontal offset of the stream, and the width of the resulting
|
||||
// pillarbox, is determined by the height exponent of the aspect ratio.
|
||||
// Therefore we multiply the width of the local camera view by the height
|
||||
// ratio.
|
||||
node.style.left = (offsetX - (realWidth * LOCAL_STREAM_OVERLAP)) + "px";
|
||||
node.style.width = realWidth + "px";
|
||||
node.style.height = realHeight + "px";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifically updates the remote camera stream size and position, if
|
||||
* a screen share is being received. It is slaved from the position of the
|
||||
* local stream.
|
||||
* This method gets called from `updateVideoContainer`, which is defined in
|
||||
* the `MediaSetupMixin`.
|
||||
*
|
||||
* @param {Object} ratio Aspect ratio of the remote camera stream
|
||||
*/
|
||||
updateRemoteCameraPosition: function(ratio) {
|
||||
// Nothing to do for screenshare
|
||||
if (!this.state.receivingScreenShare) {
|
||||
return;
|
||||
}
|
||||
// XXX For the time being, if we're a narrow screen, aka mobile, we don't display
|
||||
// the remote media (bug 1133534).
|
||||
if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 10px separation between the two streams.
|
||||
var LOCAL_REMOTE_SEPARATION = 10;
|
||||
|
||||
var node = this._getElement(".remote");
|
||||
var localNode = this._getElement(".local");
|
||||
|
||||
// Match the width to the local video.
|
||||
node.style.width = localNode.offsetWidth + "px";
|
||||
|
||||
// The height is then determined from the aspect ratio
|
||||
var height = ((localNode.offsetWidth / ratio.width) * ratio.height);
|
||||
node.style.height = height + "px";
|
||||
|
||||
node.style.right = "auto";
|
||||
node.style.bottom = "auto";
|
||||
|
||||
// Now position the local camera view correctly with respect to the remote
|
||||
// video stream.
|
||||
|
||||
// The top is measured from the top of the element down the screen,
|
||||
// so subtract the height of the video and the separation distance.
|
||||
node.style.top = (localNode.offsetTop - height - LOCAL_REMOTE_SEPARATION) + "px";
|
||||
|
||||
// Match the left-hand sides.
|
||||
node.style.left = localNode.offsetLeft + "px";
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if current room is active.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
_roomIsActive: function() {
|
||||
return this.state.roomState === ROOM_STATES.JOINED ||
|
||||
this.state.roomState === ROOM_STATES.SESSION_CONNECTED ||
|
||||
this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var localStreamClasses = React.addons.classSet({
|
||||
hide: !this._roomIsActive(),
|
||||
local: true,
|
||||
"local-stream": true,
|
||||
"local-stream-audio": this.state.videoMuted
|
||||
});
|
||||
|
||||
var remoteStreamClasses = React.addons.classSet({
|
||||
"video_inner": true,
|
||||
"remote": true,
|
||||
"focus-stream": !this.state.receivingScreenShare,
|
||||
"remote-inset-stream": this.state.receivingScreenShare
|
||||
});
|
||||
|
||||
var screenShareStreamClasses = React.addons.classSet({
|
||||
"screen": true,
|
||||
"focus-stream": this.state.receivingScreenShare,
|
||||
hide: !this.state.receivingScreenShare,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="room-conversation-wrapper">
|
||||
<div className="beta-logo" />
|
||||
<StandaloneRoomHeader dispatcher={this.props.dispatcher} />
|
||||
<StandaloneRoomInfoArea roomState={this.state.roomState}
|
||||
failureReason={this.state.failureReason}
|
||||
joinRoom={this.joinRoom}
|
||||
isFirefox={this.props.isFirefox}
|
||||
activeRoomStore={this.props.activeRoomStore}
|
||||
roomUsed={this.state.used} />
|
||||
<div className="video-layout-wrapper">
|
||||
<div className="conversation room-conversation">
|
||||
<StandaloneRoomContextView
|
||||
dispatcher={this.props.dispatcher}
|
||||
receivingScreenShare={this.state.receivingScreenShare}
|
||||
roomContextUrls={this.state.roomContextUrls}
|
||||
roomName={this.state.roomName}
|
||||
roomInfoFailure={this.state.roomInfoFailure} />
|
||||
<div className="media nested">
|
||||
<span className="self-view-hidden-message">
|
||||
{mozL10n.get("self_view_hidden_message")}
|
||||
</span>
|
||||
<div className="video_wrapper remote_wrapper">
|
||||
<div className={remoteStreamClasses}></div>
|
||||
<div className={screenShareStreamClasses}></div>
|
||||
</div>
|
||||
<div className={localStreamClasses}></div>
|
||||
</div>
|
||||
<sharedViews.ConversationToolbar
|
||||
dispatcher={this.props.dispatcher}
|
||||
video={{enabled: !this.state.videoMuted,
|
||||
visible: this._roomIsActive()}}
|
||||
audio={{enabled: !this.state.audioMuted,
|
||||
visible: this._roomIsActive()}}
|
||||
publishStream={this.publishStream}
|
||||
hangup={this.leaveRoom}
|
||||
hangupButtonLabel={mozL10n.get("rooms_leave_button_label")}
|
||||
enableHangup={this._roomIsActive()} />
|
||||
</div>
|
||||
</div>
|
||||
<loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView
|
||||
marketplaceSrc={this.state.marketplaceSrc}
|
||||
onMarketplaceMessage={this.state.onMarketplaceMessage} />
|
||||
<StandaloneRoomFooter dispatcher={this.props.dispatcher} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
StandaloneRoomContextView: StandaloneRoomContextView,
|
||||
StandaloneRoomFooter: StandaloneRoomFooter,
|
||||
StandaloneRoomHeader: StandaloneRoomHeader,
|
||||
StandaloneRoomView: StandaloneRoomView
|
||||
};
|
||||
})(navigator.mozL10n);
|
||||
@@ -1,139 +0,0 @@
|
||||
## LOCALIZATION NOTE: In this file, don't translate the part between {{..}}
|
||||
restart_call=Rejoin
|
||||
conversation_has_ended=Your conversation has ended.
|
||||
call_timeout_notification_text=Your call did not go through.
|
||||
missing_conversation_info=Missing conversation information.
|
||||
network_disconnected=The network connection terminated abruptly.
|
||||
peer_ended_conversation2=The person you were calling has ended the conversation.
|
||||
call_failed_title=Call failed.
|
||||
generic_failure_title=Something went wrong.
|
||||
generic_failure_with_reason2=You can try again or email a link to be reached at later.
|
||||
generic_failure_no_reason2=Would you like to try again?
|
||||
retry_call_button=Retry
|
||||
unable_retrieve_call_info=Unable to retrieve conversation information.
|
||||
hangup_button_title=Hang up
|
||||
hangup_button_caption2=Exit
|
||||
mute_local_audio_button_title=Mute your audio
|
||||
unmute_local_audio_button_title=Unmute your audio
|
||||
mute_local_video_button_title=Mute your video
|
||||
unmute_local_video_button_title=Unmute your video
|
||||
active_screenshare_button_title=Stop sharing
|
||||
inactive_screenshare_button_title=Share your screen
|
||||
|
||||
outgoing_call_title=Start conversation?
|
||||
call_with_contact_title=Conversation with {{incomingCallIdentity}}
|
||||
welcome=Welcome to the {{clientShortname}} web client.
|
||||
incompatible_browser_heading=Oops!
|
||||
incompatible_browser_message=Firefox Hello only works in browsers that support WebRTC
|
||||
powered_by_webrtc=The audio and video components of {{clientShortname}} are powered by WebRTC.
|
||||
use_latest_firefox=Please try this link in a WebRTC-enabled browser, such as {{firefoxBrandNameLink}}.
|
||||
unsupported_platform_heading=Sorry!
|
||||
unsupported_platform_message={{platform}} does not currently support {{clientShortname}}
|
||||
unsupported_platform_ios=iOS
|
||||
unsupported_platform_windows_phone=Windows Phone
|
||||
unsupported_platform_blackberry=Blackberry
|
||||
unsupported_platform_learn_more_link=Learn more about why your platform doesn't support {{clientShortname}}
|
||||
connection_error_see_console_notification=Call failed; see console for details.
|
||||
call_url_unavailable_notification_heading=Oops!
|
||||
call_url_unavailable_notification_message2=Sorry, this URL is not available. It may be expired or entered incorrectly.
|
||||
promote_firefox_hello_heading=Download {{brandShortname}} to make free audio and video calls!
|
||||
get_firefox_button=Get {{brandShortname}}
|
||||
initiate_call_button_label2=Ready to start your conversation?
|
||||
initiate_audio_video_call_button2=Start
|
||||
initiate_audio_video_call_tooltip2=Start a video conversation
|
||||
initiate_audio_call_button2=Voice conversation
|
||||
initiate_call_cancel_button=Cancel
|
||||
legal_text_and_links=By using {{clientShortname}} you agree to the {{terms_of_use_url}} and {{privacy_notice_url}}
|
||||
terms_of_use_link_text=Terms of use
|
||||
privacy_notice_link_text=Privacy notice
|
||||
invite_header_text=Invite someone to join you.
|
||||
self_view_hidden_message=Self-view hidden but still being sent; resize window \
|
||||
to show
|
||||
|
||||
## LOCALIZATION NOTE(brandShortname): This should not be localized and
|
||||
## should remain "Firefox" for all locales.
|
||||
brandShortname=Firefox
|
||||
## LOCALIZATION NOTE(clientShortname2): This should not be localized and
|
||||
## should remain "Firefox Hello" for all locales.
|
||||
clientShortname2=Firefox Hello
|
||||
## LOCALIZATION NOTE(vendorShortname): This should not be localized and
|
||||
## should remain "Mozilla" for all locales.
|
||||
vendorShortname=Mozilla
|
||||
|
||||
## LOCALIZATION NOTE(client_alttext): {{clientShortname}} will be replaced with the
|
||||
## value of the clientShortname2 string above.
|
||||
client_alttext={{clientShortname}} logo
|
||||
vendor_alttext={{vendorShortname}} logo
|
||||
|
||||
## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
|
||||
call_url_creation_date_label=(from {{call_url_creation_date}})
|
||||
call_progress_getting_media_description={{clientShortname}} requires access to your camera and microphone.
|
||||
call_progress_getting_media_title=Waiting for media…
|
||||
call_progress_connecting_description=Connecting…
|
||||
call_progress_ringing_description=Ringing…
|
||||
fxos_app_needed=Please install the {{fxosAppName}} app from the Firefox Marketplace.
|
||||
|
||||
feedback_call_experience_heading2=How was your conversation?
|
||||
feedback_thank_you_heading=Thank you for your feedback!
|
||||
feedback_category_list_heading=What made you sad?
|
||||
feedback_category_audio_quality=Audio quality
|
||||
feedback_category_video_quality=Video quality
|
||||
feedback_category_was_disconnected=Was disconnected
|
||||
feedback_category_confusing2=Confusing controls
|
||||
feedback_category_other2=Other
|
||||
feedback_custom_category_text_placeholder=What went wrong?
|
||||
feedback_submit_button=Submit
|
||||
feedback_back_button=Back
|
||||
## LOCALIZATION NOTE (feedback_window_will_close_in2):
|
||||
## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387
|
||||
## In this item, don't translate the part between {{..}}
|
||||
feedback_window_will_close_in2={[ plural(countdown) ]}
|
||||
feedback_window_will_close_in2[one] = This window will close in {{countdown}} second
|
||||
feedback_window_will_close_in2[two] = This window will close in {{countdown}} seconds
|
||||
feedback_window_will_close_in2[few] = This window will close in {{countdown}} seconds
|
||||
feedback_window_will_close_in2[many] = This window will close in {{countdown}} seconds
|
||||
feedback_window_will_close_in2[other] = This window will close in {{countdown}} seconds
|
||||
|
||||
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
|
||||
## a signed-in to signed-in user call.
|
||||
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
|
||||
feedback_rejoin_button=Rejoin
|
||||
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
|
||||
## an abusive user.
|
||||
feedback_report_user_button=Report User
|
||||
|
||||
## LOCALIZATION_NOTE(first_time_experience.title): clientShortname will be
|
||||
## replaced by the brand name
|
||||
first_time_experience_title={{clientShortname}} — Join the conversation
|
||||
first_time_experience_button_label=Get Started
|
||||
|
||||
help_label=Help
|
||||
tour_label=Tour
|
||||
|
||||
rooms_default_room_name_template=Conversation {{conversationLabel}}
|
||||
rooms_leave_button_label=Leave
|
||||
rooms_list_copy_url_tooltip=Copy Link
|
||||
rooms_list_delete_tooltip=Delete conversation
|
||||
rooms_list_deleteConfirmation_label=Are you sure?
|
||||
rooms_new_room_button_label=Start a conversation
|
||||
rooms_only_occupant_label=You're the first one here.
|
||||
rooms_panel_title=Choose a conversation or start a new one
|
||||
rooms_room_full_label=There are already two people in this conversation.
|
||||
rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
|
||||
rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
|
||||
rooms_room_joined_label=Someone has joined the conversation!
|
||||
rooms_room_join_label=Join the conversation
|
||||
rooms_display_name_guest=Guest
|
||||
rooms_unavailable_notification_message=Sorry, you cannot join this conversation. The link may be expired or invalid.
|
||||
rooms_media_denied_message=We could not get access to your microphone or camera. Please reload the page to try again.
|
||||
room_information_failure_not_available=No information about this conversation is available. Please request a new link from the person who sent it to you.
|
||||
room_information_failure_unsupported_browser=Your browser cannot access any information about this conversation. Please make sure you're using the latest version.
|
||||
|
||||
## LOCALIZATION_NOTE(standalone_title_with_status): {{clientShortname}} will be
|
||||
## replaced by the brand name and {{currentStatus}} will be replaced
|
||||
## by the current call status (Connecting, Ringing, etc.)
|
||||
standalone_title_with_status={{clientShortname}} — {{currentStatus}}
|
||||
status_in_conversation=In conversation
|
||||
status_conversation_ended=Conversation ended
|
||||
status_error=Something went wrong
|
||||
support_link=Get Help
|
||||
@@ -1,685 +0,0 @@
|
||||
var expect = chai.expect;
|
||||
|
||||
/* jshint newcap:false */
|
||||
|
||||
describe("loop.roomViews", function () {
|
||||
"use strict";
|
||||
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
|
||||
|
||||
var sandbox, dispatcher, roomStore, activeRoomStore, fakeWindow,
|
||||
fakeMozLoop, fakeContextURL;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
dispatcher = new loop.Dispatcher();
|
||||
|
||||
fakeMozLoop = {
|
||||
getAudioBlob: sinon.stub(),
|
||||
getLoopPref: sinon.stub(),
|
||||
isSocialShareButtonAvailable: sinon.stub()
|
||||
};
|
||||
|
||||
fakeWindow = {
|
||||
close: sinon.stub(),
|
||||
document: {},
|
||||
navigator: {
|
||||
mozLoop: fakeMozLoop
|
||||
},
|
||||
addEventListener: function() {},
|
||||
removeEventListener: function() {},
|
||||
setTimeout: function(callback) { callback(); }
|
||||
};
|
||||
loop.shared.mixins.setRootObject(fakeWindow);
|
||||
|
||||
// XXX These stubs should be hoisted in a common file
|
||||
// Bug 1040968
|
||||
sandbox.stub(document.mozL10n, "get", function(x) {
|
||||
return x;
|
||||
});
|
||||
|
||||
activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
|
||||
mozLoop: {},
|
||||
sdkDriver: {}
|
||||
});
|
||||
roomStore = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: {},
|
||||
activeRoomStore: activeRoomStore
|
||||
});
|
||||
|
||||
fakeContextURL = {
|
||||
description: "An invalid page",
|
||||
location: "http://invalid.com",
|
||||
thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
loop.shared.mixins.setRootObject(window);
|
||||
});
|
||||
|
||||
describe("ActiveRoomStoreMixin", function() {
|
||||
it("should merge initial state", function() {
|
||||
var TestView = React.createClass({
|
||||
mixins: [loop.roomViews.ActiveRoomStoreMixin],
|
||||
getInitialState: function() {
|
||||
return {foo: "bar"};
|
||||
},
|
||||
render: function() { return React.DOM.div(); }
|
||||
});
|
||||
|
||||
var testView = TestUtils.renderIntoDocument(
|
||||
React.createElement(TestView, {
|
||||
roomStore: roomStore
|
||||
}));
|
||||
|
||||
var expectedState = _.extend({foo: "bar"},
|
||||
activeRoomStore.getInitialStoreState());
|
||||
|
||||
expect(testView.state).eql(expectedState);
|
||||
});
|
||||
|
||||
it("should listen to store changes", function() {
|
||||
var TestView = React.createClass({
|
||||
mixins: [loop.roomViews.ActiveRoomStoreMixin],
|
||||
render: function() { return React.DOM.div(); }
|
||||
});
|
||||
var testView = TestUtils.renderIntoDocument(
|
||||
React.createElement(TestView, {
|
||||
roomStore: roomStore
|
||||
}));
|
||||
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
|
||||
|
||||
expect(testView.state.roomState).eql(ROOM_STATES.READY);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DesktopRoomInvitationView", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
view = null;
|
||||
});
|
||||
|
||||
function mountTestComponent(props) {
|
||||
props = _.extend({
|
||||
dispatcher: dispatcher,
|
||||
roomData: {},
|
||||
show: true,
|
||||
showContext: false
|
||||
}, props);
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(loop.roomViews.DesktopRoomInvitationView, props));
|
||||
}
|
||||
|
||||
it("should dispatch an EmailRoomUrl action when the email button is pressed",
|
||||
function() {
|
||||
view = mountTestComponent({
|
||||
roomData: { roomUrl: "http://invalid" }
|
||||
});
|
||||
|
||||
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"}));
|
||||
});
|
||||
|
||||
describe("Rename Room", function() {
|
||||
var roomNameBox;
|
||||
|
||||
beforeEach(function() {
|
||||
view = mountTestComponent({
|
||||
roomData: {
|
||||
roomToken: "fakeToken",
|
||||
roomName: "fakeName"
|
||||
}
|
||||
});
|
||||
|
||||
roomNameBox = view.getDOMNode().querySelector(".input-room-name");
|
||||
});
|
||||
|
||||
it("should dispatch a RenameRoom action when the focus is lost",
|
||||
function() {
|
||||
React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
|
||||
value: "reallyFake"
|
||||
}});
|
||||
|
||||
React.addons.TestUtils.Simulate.blur(roomNameBox);
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.RenameRoom({
|
||||
roomToken: "fakeToken",
|
||||
newRoomName: "reallyFake"
|
||||
}));
|
||||
});
|
||||
|
||||
it("should dispatch a RenameRoom action when Enter key is pressed",
|
||||
function() {
|
||||
React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
|
||||
value: "reallyFake"
|
||||
}});
|
||||
|
||||
TestUtils.Simulate.keyDown(roomNameBox, {key: "Enter", which: 13});
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.RenameRoom({
|
||||
roomToken: "fakeToken",
|
||||
newRoomName: "reallyFake"
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Copy Button", function() {
|
||||
beforeEach(function() {
|
||||
view = mountTestComponent({
|
||||
roomData: { roomUrl: "http://invalid" }
|
||||
});
|
||||
});
|
||||
|
||||
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"}));
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// copied_url_button is the l10n string.
|
||||
expect(copyBtn.textContent).eql("copied_url_button");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Share button", function() {
|
||||
beforeEach(function() {
|
||||
view = mountTestComponent();
|
||||
});
|
||||
|
||||
it("should toggle the share dropdown when the share button is clicked", function() {
|
||||
var shareBtn = view.getDOMNode().querySelector(".btn-share");
|
||||
|
||||
React.addons.TestUtils.Simulate.click(shareBtn);
|
||||
|
||||
expect(view.state.showMenu).to.eql(true);
|
||||
expect(view.refs.menu.props.show).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Context", function() {
|
||||
it("should not render the context data when told not to", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
expect(view.getDOMNode().querySelector(".room-context")).to.eql(null);
|
||||
});
|
||||
|
||||
it("should render context when data is available", function() {
|
||||
view = mountTestComponent({
|
||||
showContext: true,
|
||||
roomData: {
|
||||
roomContextUrls: [fakeContextURL]
|
||||
}
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
|
||||
});
|
||||
|
||||
it("should format the context url for display", function() {
|
||||
sandbox.stub(sharedUtils, "formatURL").returns({
|
||||
location: "location",
|
||||
hostname: "hostname"
|
||||
});
|
||||
|
||||
view = mountTestComponent({
|
||||
showContext: true,
|
||||
roomData: {
|
||||
roomContextUrls: [fakeContextURL]
|
||||
}
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".room-context-url").textContent)
|
||||
.eql("hostname");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("DesktopRoomConversationView", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
loop.store.StoreMixin.register({
|
||||
feedbackStore: new loop.store.FeedbackStore(dispatcher, {
|
||||
feedbackClient: {}
|
||||
})
|
||||
});
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
});
|
||||
|
||||
function mountTestComponent() {
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(loop.roomViews.DesktopRoomConversationView, {
|
||||
dispatcher: dispatcher,
|
||||
roomStore: roomStore,
|
||||
mozLoop: fakeMozLoop
|
||||
}));
|
||||
}
|
||||
|
||||
it("should dispatch a setMute action when the audio mute button is pressed",
|
||||
function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
view.setState({audioMuted: true});
|
||||
|
||||
var muteBtn = view.getDOMNode().querySelector('.btn-mute-audio');
|
||||
|
||||
React.addons.TestUtils.Simulate.click(muteBtn);
|
||||
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "setMute"));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("enabled", true));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("type", "audio"));
|
||||
});
|
||||
|
||||
it("should dispatch a setMute action when the video mute button is pressed",
|
||||
function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
view.setState({videoMuted: false});
|
||||
|
||||
var muteBtn = view.getDOMNode().querySelector('.btn-mute-video');
|
||||
|
||||
React.addons.TestUtils.Simulate.click(muteBtn);
|
||||
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "setMute"));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("enabled", false));
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("type", "video"));
|
||||
});
|
||||
|
||||
it("should set the mute button as mute off", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
view.setState({videoMuted: false});
|
||||
|
||||
var muteBtn = view.getDOMNode().querySelector('.btn-mute-video');
|
||||
|
||||
expect(muteBtn.classList.contains("muted")).eql(false);
|
||||
});
|
||||
|
||||
it("should set the mute button as mute on", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
view.setState({audioMuted: true});
|
||||
|
||||
var muteBtn = view.getDOMNode().querySelector('.btn-mute-audio');
|
||||
|
||||
expect(muteBtn.classList.contains("muted")).eql(true);
|
||||
});
|
||||
|
||||
it("should dispatch a `StartScreenShare` action when sharing is not active " +
|
||||
"and the screen share button is pressed", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
view.setState({screenSharingState: SCREEN_SHARE_STATES.INACTIVE});
|
||||
|
||||
var muteBtn = view.getDOMNode().querySelector('.btn-mute-video');
|
||||
|
||||
React.addons.TestUtils.Simulate.click(muteBtn);
|
||||
|
||||
sinon.assert.calledWithMatch(dispatcher.dispatch,
|
||||
sinon.match.hasOwn("name", "setMute"));
|
||||
});
|
||||
|
||||
it("should dispatch a `LeaveRoom` action when the hangup button is pressed and the room has been used", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
view.setState({used: true});
|
||||
|
||||
var hangupBtn = view.getDOMNode().querySelector(".btn-hangup");
|
||||
|
||||
React.addons.TestUtils.Simulate.click(hangupBtn);
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.LeaveRoom());
|
||||
});
|
||||
|
||||
it("should close the window when the hangup button is pressed and the room has not been used", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
view.setState({used: false});
|
||||
|
||||
var hangupBtn = view.getDOMNode().querySelector(".btn-hangup");
|
||||
|
||||
React.addons.TestUtils.Simulate.click(hangupBtn);
|
||||
|
||||
sinon.assert.calledOnce(fakeWindow.close);
|
||||
});
|
||||
|
||||
describe("#componentWillUpdate", function() {
|
||||
function expectActionDispatched(view) {
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
sinon.match.instanceOf(sharedActions.SetupStreamElements));
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
sinon.match(function(value) {
|
||||
return value.getLocalElementFunc() ===
|
||||
view.getDOMNode().querySelector(".local");
|
||||
}));
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
sinon.match(function(value) {
|
||||
return value.getRemoteElementFunc() ===
|
||||
view.getDOMNode().querySelector(".remote");
|
||||
}));
|
||||
}
|
||||
|
||||
it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
|
||||
"is entered", function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
|
||||
var view = mountTestComponent();
|
||||
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
|
||||
|
||||
expectActionDispatched(view);
|
||||
});
|
||||
|
||||
it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is " +
|
||||
"re-entered", function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
|
||||
var view = mountTestComponent();
|
||||
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
|
||||
|
||||
expectActionDispatched(view);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#render", function() {
|
||||
it("should set document.title to store.serverData.roomName", function() {
|
||||
mountTestComponent();
|
||||
|
||||
activeRoomStore.setStoreState({roomName: "fakeName"});
|
||||
|
||||
expect(fakeWindow.document.title).to.equal("fakeName");
|
||||
});
|
||||
|
||||
it("should render the GenericFailureView if the roomState is `FAILED`",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.conversationViews.GenericFailureView);
|
||||
});
|
||||
|
||||
it("should render the GenericFailureView if the roomState is `FULL`",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.conversationViews.GenericFailureView);
|
||||
});
|
||||
|
||||
it("should render the DesktopRoomInvitationView if roomState is `JOINED`",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.roomViews.DesktopRoomInvitationView);
|
||||
});
|
||||
|
||||
it("should render the DesktopRoomConversationView if roomState is `HAS_PARTICIPANTS`",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.roomViews.DesktopRoomConversationView);
|
||||
});
|
||||
|
||||
it("should render the FeedbackView if roomState is `ENDED`",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({
|
||||
roomState: ROOM_STATES.ENDED,
|
||||
used: true
|
||||
});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.shared.views.FeedbackView);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Mute", function() {
|
||||
it("should render local media as audio-only if video is muted",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({
|
||||
roomState: ROOM_STATES.SESSION_CONNECTED,
|
||||
videoMuted: true
|
||||
});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
expect(view.getDOMNode().querySelector(".local-stream-audio"))
|
||||
.not.eql(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("SocialShareDropdown", function() {
|
||||
var view, fakeProvider;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
|
||||
fakeProvider = {
|
||||
name: "foo",
|
||||
origin: "https://foo",
|
||||
iconURL: "http://example.com/foo.png"
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
view = 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 different contents when the Share XUL button is not available", function() {
|
||||
view = mountTestComponent({
|
||||
socialShareProviders: []
|
||||
});
|
||||
|
||||
var node = view.getDOMNode();
|
||||
expect(node.querySelector(".share-panel-header")).to.not.eql(null);
|
||||
});
|
||||
|
||||
it("should show an empty list when no Social Providers are available", function() {
|
||||
view = mountTestComponent({
|
||||
socialShareButtonAvailable: true,
|
||||
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({
|
||||
socialShareButtonAvailable: true,
|
||||
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("#handleToolbarAddButtonClick", function() {
|
||||
it("should dispatch an action when the 'add to toolbar' button is clicked", function() {
|
||||
view = mountTestComponent({
|
||||
socialShareProviders: []
|
||||
});
|
||||
|
||||
var addButton = view.getDOMNode().querySelector(".btn-toolbar-add");
|
||||
React.addons.TestUtils.Simulate.click(addButton);
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.AddSocialShareButton());
|
||||
});
|
||||
});
|
||||
|
||||
describe("#handleAddServiceClick", function() {
|
||||
it("should dispatch an action when the 'add provider' item is clicked", function() {
|
||||
view = mountTestComponent({
|
||||
socialShareProviders: [],
|
||||
socialShareButtonAvailable: true
|
||||
});
|
||||
|
||||
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",
|
||||
socialShareButtonAvailable: true,
|
||||
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: []
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("DesktopRoomContextView", function() {
|
||||
var view;
|
||||
|
||||
afterEach(function() {
|
||||
view = null;
|
||||
});
|
||||
|
||||
function mountTestComponent(props) {
|
||||
props = _.extend({
|
||||
show: true
|
||||
}, props);
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(loop.roomViews.DesktopRoomContextView, props));
|
||||
}
|
||||
|
||||
describe("#render", function() {
|
||||
it("should show the context information properly when available", function() {
|
||||
view = mountTestComponent({
|
||||
roomData: {
|
||||
roomDescription: "Hello, is it me you're looking for?",
|
||||
roomContextUrls: [fakeContextURL]
|
||||
}
|
||||
});
|
||||
|
||||
var node = view.getDOMNode();
|
||||
expect(node).to.not.eql(null);
|
||||
expect(node.querySelector(".room-context-thumbnail").src).to.
|
||||
eql(fakeContextURL.thumbnail);
|
||||
expect(node.querySelector(".room-context-description").textContent).to.
|
||||
eql(fakeContextURL.description);
|
||||
expect(node.querySelector(".room-context-comment").textContent).to.
|
||||
eql(view.props.roomData.roomDescription);
|
||||
});
|
||||
|
||||
it("should not render optional data", function() {
|
||||
view = mountTestComponent({
|
||||
roomData: { roomContextUrls: [fakeContextURL] }
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".room-context-comment")).to.
|
||||
eql(null);
|
||||
});
|
||||
|
||||
it("should not render the component when 'show' is false", function() {
|
||||
view = mountTestComponent({
|
||||
show: false
|
||||
});
|
||||
|
||||
expect(view.getDOMNode()).to.eql(null);
|
||||
});
|
||||
|
||||
it("should close the view when the close button is clicked", function() {
|
||||
view = mountTestComponent({
|
||||
roomData: { roomContextUrls: [fakeContextURL] }
|
||||
});
|
||||
|
||||
var closeBtn = view.getDOMNode().querySelector(".room-context-btn-close");
|
||||
React.addons.TestUtils.Simulate.click(closeBtn);
|
||||
expect(view.getDOMNode()).to.eql(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,326 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {CardDavImporter} = Cu.import("resource:///modules/loop/CardDavImporter.jsm", {});
|
||||
|
||||
const kAuth = {
|
||||
"method": "basic",
|
||||
"user": "username",
|
||||
"password": "p455w0rd"
|
||||
};
|
||||
|
||||
|
||||
// "pid" for "provider ID"
|
||||
let vcards = [
|
||||
"VERSION:3.0\n" +
|
||||
"N:Smith;John;;;\n" +
|
||||
"FN:John Smith\n" +
|
||||
"EMAIL;TYPE=work:john.smith@example.com\n" +
|
||||
"REV:2011-07-12T14:43:20Z\n" +
|
||||
"UID:pid1\n" +
|
||||
"END:VCARD\n",
|
||||
|
||||
"VERSION:3.0\n" +
|
||||
"N:Smith;Jane;;;\n" +
|
||||
"FN:Jane Smith\n" +
|
||||
"EMAIL:jane.smith@example.com\n" +
|
||||
"REV:2011-07-12T14:43:20Z\n" +
|
||||
"UID:pid2\n" +
|
||||
"END:VCARD\n",
|
||||
|
||||
"VERSION:3.0\n" +
|
||||
"N:García Fernández;Miguel Angel;José Antonio;Mr.;Jr.\n" +
|
||||
"FN:Mr. Miguel Angel José Antonio\n García Fernández, Jr.\n" +
|
||||
"EMAIL:mike@example.org\n" +
|
||||
"EMAIL;PREF=1;TYPE=work:miguel.angel@example.net\n" +
|
||||
"EMAIL;TYPE=home;UNKNOWNPARAMETER=frotz:majacf@example.com\n" +
|
||||
"TEL:+3455555555\n" +
|
||||
"TEL;PREF=1;TYPE=work:+3455556666\n" +
|
||||
"TEL;TYPE=home;UNKNOWNPARAMETER=frotz:+3455557777\n" +
|
||||
"ADR:;Suite 123;Calle Aduana\\, 29;MADRID;;28070;SPAIN\n" +
|
||||
"ADR;TYPE=work:P.O. BOX 555;;;Washington;DC;20024-00555;USA\n" +
|
||||
"ORG:Acme España SL\n" +
|
||||
"TITLE:President\n" +
|
||||
"BDAY:1965-05-05\n" +
|
||||
"NOTE:Likes tulips\n" +
|
||||
"REV:2011-07-12T14:43:20Z\n" +
|
||||
"UID:pid3\n" +
|
||||
"END:VCARD\n",
|
||||
|
||||
"VERSION:3.0\n" +
|
||||
"N:Jones;Bob;;;\n" +
|
||||
"EMAIL:bob.jones@example.com\n" +
|
||||
"REV:2011-07-12T14:43:20Z\n" +
|
||||
"UID:pid4\n" +
|
||||
"END:VCARD\n",
|
||||
|
||||
"VERSION:3.0\n" +
|
||||
"N:Jones;Davy;Randall;;\n" +
|
||||
"EMAIL:davy.jones@example.com\n" +
|
||||
"REV:2011-07-12T14:43:20Z\n" +
|
||||
"UID:pid5\n" +
|
||||
"END:VCARD\n",
|
||||
|
||||
"VERSION:3.0\n" +
|
||||
"EMAIL:trip@example.com\n" +
|
||||
"NICKNAME:Trip\n" +
|
||||
"REV:2011-07-12T14:43:20Z\n" +
|
||||
"UID:pid6\n" +
|
||||
"END:VCARD\n",
|
||||
|
||||
"VERSION:3.0\n" +
|
||||
"EMAIL:acme@example.com\n" +
|
||||
"ORG:Acme, Inc.\n" +
|
||||
"REV:2011-07-12T14:43:20Z\n" +
|
||||
"UID:pid7\n" +
|
||||
"END:VCARD\n",
|
||||
|
||||
"VERSION:3.0\n" +
|
||||
"EMAIL:anyone@example.com\n" +
|
||||
"REV:2011-07-12T14:43:20Z\n" +
|
||||
"UID:pid8\n" +
|
||||
"END:VCARD\n",
|
||||
];
|
||||
|
||||
|
||||
const monkeyPatchImporter = function(importer) {
|
||||
// Set up the response bodies
|
||||
let listPropfind =
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n' +
|
||||
'<d:multistatus xmlns:card="urn:ietf:params:xml:ns:carddav"\n' +
|
||||
' xmlns:d="DAV:">\n' +
|
||||
' <d:response>\n' +
|
||||
' <d:href>/carddav/abook/</d:href>\n' +
|
||||
' <d:propstat>\n' +
|
||||
' <d:status>HTTP/1.1 200 OK</d:status>\n' +
|
||||
' </d:propstat>\n' +
|
||||
' <d:propstat>\n' +
|
||||
' <d:status>HTTP/1.1 404 Not Found</d:status>\n' +
|
||||
' <d:prop>\n' +
|
||||
' <d:getetag/>\n' +
|
||||
' </d:prop>\n' +
|
||||
' </d:propstat>\n' +
|
||||
' </d:response>\n';
|
||||
|
||||
let listReportMultiget =
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n' +
|
||||
'<d:multistatus xmlns:card="urn:ietf:params:xml:ns:carddav"\n' +
|
||||
' xmlns:d="DAV:">\n';
|
||||
|
||||
vcards.forEach(vcard => {
|
||||
let uid = /\nUID:(.*?)\n/.exec(vcard);
|
||||
listPropfind +=
|
||||
' <d:response>\n' +
|
||||
' <d:href>/carddav/abook/' + uid + '</d:href>\n' +
|
||||
' <d:propstat>\n' +
|
||||
' <d:status>HTTP/1.1 200 OK</d:status>\n' +
|
||||
' <d:prop>\n' +
|
||||
' <d:getetag>"2011-07-12T07:43:20.855-07:00"</d:getetag>\n' +
|
||||
' </d:prop>\n' +
|
||||
' </d:propstat>\n' +
|
||||
' </d:response>\n';
|
||||
|
||||
listReportMultiget +=
|
||||
' <d:response>\n' +
|
||||
' <d:href>/carddav/abook/' + uid + '</d:href>\n' +
|
||||
' <d:propstat>\n' +
|
||||
' <d:status>HTTP/1.1 200 OK</d:status>\n' +
|
||||
' <d:prop>\n' +
|
||||
' <d:getetag>"2011-07-12T07:43:20.855-07:00"</d:getetag>\n' +
|
||||
' <card:address-data>' + vcard + '</card:address-data>\n' +
|
||||
' </d:prop>\n' +
|
||||
' </d:propstat>\n' +
|
||||
' </d:response>\n';
|
||||
});
|
||||
|
||||
listPropfind += "</d:multistatus>\n";
|
||||
listReportMultiget += "</d:multistatus>\n";
|
||||
|
||||
importer._davPromise = function(method, url, auth, depth, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (auth.method != "basic" ||
|
||||
auth.user != kAuth.user ||
|
||||
auth.password != kAuth.password) {
|
||||
reject(new Error("401 Auth Failure"));
|
||||
return;
|
||||
}
|
||||
|
||||
let request = method + " " + url + " " + depth;
|
||||
let xmlParser = new DOMParser();
|
||||
let responseXML;
|
||||
switch (request) {
|
||||
case "PROPFIND https://example.com/.well-known/carddav 1":
|
||||
responseXML = xmlParser.parseFromString(listPropfind, "text/xml");
|
||||
break;
|
||||
case "REPORT https://example.com/carddav/abook/ 1":
|
||||
responseXML = xmlParser.parseFromString(listReportMultiget, "text/xml");
|
||||
break;
|
||||
default:
|
||||
reject(new Error("404 Not Found"));
|
||||
return;
|
||||
}
|
||||
resolve({"responseXML": responseXML});
|
||||
});
|
||||
}.bind(importer);
|
||||
return importer;
|
||||
};
|
||||
|
||||
add_task(function* test_CardDavImport() {
|
||||
let importer = monkeyPatchImporter(new CardDavImporter());
|
||||
yield new Promise ((resolve, reject) => {
|
||||
info("Initiating import");
|
||||
importer.startImport({
|
||||
"host": "example.com",
|
||||
"auth": kAuth.method,
|
||||
"user": kAuth.user,
|
||||
"password": kAuth.password
|
||||
}, (err, result) => { err ? reject(err) : resolve(result); }, mockDb);
|
||||
});
|
||||
info("Import succeeded");
|
||||
|
||||
Assert.equal(vcards.length, Object.keys(mockDb._store).length,
|
||||
"Should import all VCards into database");
|
||||
|
||||
// Basic checks
|
||||
let c = mockDb._store[1];
|
||||
Assert.equal(c.name[0], "John Smith", "Full name should match");
|
||||
Assert.equal(c.givenName[0], "John", "Given name should match");
|
||||
Assert.equal(c.familyName[0], "Smith", "Family name should match");
|
||||
Assert.equal(c.email[0].type, "work", "Email type should match");
|
||||
Assert.equal(c.email[0].value, "john.smith@example.com", "Email should match");
|
||||
Assert.equal(c.email[0].pref, false, "Pref should match");
|
||||
Assert.equal(c.id, "pid1@example.com", "UID should match and be scoped to provider");
|
||||
|
||||
c = mockDb._store[2];
|
||||
Assert.equal(c.name[0], "Jane Smith", "Full name should match");
|
||||
Assert.equal(c.givenName[0], "Jane", "Given name should match");
|
||||
Assert.equal(c.familyName[0], "Smith", "Family name should match");
|
||||
Assert.equal(c.email[0].type, "other", "Email type should match");
|
||||
Assert.equal(c.email[0].value, "jane.smith@example.com", "Email should match");
|
||||
Assert.equal(c.email[0].pref, false, "Pref should match");
|
||||
Assert.equal(c.id, "pid2@example.com", "UID should match and be scoped to provider");
|
||||
|
||||
// Check every field
|
||||
c = mockDb._store[3];
|
||||
Assert.equal(c.name[0], "Mr. Miguel Angel José Antonio García Fernández, Jr.", "Full name should match");
|
||||
Assert.equal(c.givenName[0], "Miguel Angel", "Given name should match");
|
||||
Assert.equal(c.additionalName[0], "José Antonio", "Other name should match");
|
||||
Assert.equal(c.familyName[0], "García Fernández", "Family name should match");
|
||||
Assert.equal(c.email.length, 3, "Email count should match");
|
||||
Assert.equal(c.email[0].type, "other", "Email type should match");
|
||||
Assert.equal(c.email[0].value, "mike@example.org", "Email should match");
|
||||
Assert.equal(c.email[0].pref, false, "Pref should match");
|
||||
Assert.equal(c.email[1].type, "work", "Email type should match");
|
||||
Assert.equal(c.email[1].value, "miguel.angel@example.net", "Email should match");
|
||||
Assert.equal(c.email[1].pref, true, "Pref should match");
|
||||
Assert.equal(c.email[2].type, "home", "Email type should match");
|
||||
Assert.equal(c.email[2].value, "majacf@example.com", "Email should match");
|
||||
Assert.equal(c.email[2].pref, false, "Pref should match");
|
||||
Assert.equal(c.tel.length, 3, "Phone number count should match");
|
||||
Assert.equal(c.tel[0].type, "other", "Phone type should match");
|
||||
Assert.equal(c.tel[0].value, "+3455555555", "Phone number should match");
|
||||
Assert.equal(c.tel[0].pref, false, "Pref should match");
|
||||
Assert.equal(c.tel[1].type, "work", "Phone type should match");
|
||||
Assert.equal(c.tel[1].value, "+3455556666", "Phone number should match");
|
||||
Assert.equal(c.tel[1].pref, true, "Pref should match");
|
||||
Assert.equal(c.tel[2].type, "home", "Phone type should match");
|
||||
Assert.equal(c.tel[2].value, "+3455557777", "Phone number should match");
|
||||
Assert.equal(c.tel[2].pref, false, "Pref should match");
|
||||
Assert.equal(c.adr.length, 2, "Address count should match");
|
||||
Assert.equal(c.adr[0].pref, false, "Pref should match");
|
||||
Assert.equal(c.adr[0].type, "other", "Type should match");
|
||||
Assert.equal(c.adr[0].streetAddress, "Calle Aduana, 29 Suite 123", "Street address should match");
|
||||
Assert.equal(c.adr[0].locality, "MADRID", "Locality should match");
|
||||
Assert.equal(c.adr[0].postalCode, "28070", "Post code should match");
|
||||
Assert.equal(c.adr[0].countryName, "SPAIN", "Country should match");
|
||||
Assert.equal(c.adr[1].pref, false, "Pref should match");
|
||||
Assert.equal(c.adr[1].type, "work", "Type should match");
|
||||
Assert.equal(c.adr[1].streetAddress, "P.O. BOX 555", "Street address should match");
|
||||
Assert.equal(c.adr[1].locality, "Washington", "Locality should match");
|
||||
Assert.equal(c.adr[1].region, "DC", "Region should match");
|
||||
Assert.equal(c.adr[1].postalCode, "20024-00555", "Post code should match");
|
||||
Assert.equal(c.adr[1].countryName, "USA", "Country should match");
|
||||
Assert.equal(c.org[0], "Acme España SL", "Org should match");
|
||||
Assert.equal(c.jobTitle[0], "President", "Title should match");
|
||||
Assert.equal(c.note[0], "Likes tulips", "Note should match");
|
||||
let bday = new Date(c.bday);
|
||||
Assert.equal(bday.getUTCFullYear(), 1965, "Birthday year should match");
|
||||
Assert.equal(bday.getUTCMonth(), 4, "Birthday month should match");
|
||||
Assert.equal(bday.getUTCDate(), 5, "Birthday day should match");
|
||||
Assert.equal(c.id, "pid3@example.com", "UID should match and be scoped to provider");
|
||||
|
||||
// Check name synthesis
|
||||
c = mockDb._store[4];
|
||||
Assert.equal(c.name[0], "Jones, Bob", "Full name should be synthesized correctly");
|
||||
c = mockDb._store[5];
|
||||
Assert.equal(c.name[0], "Jones, Davy Randall", "Full name should be synthesized correctly");
|
||||
c = mockDb._store[6];
|
||||
Assert.equal(c.name[0], "Trip", "Full name should be synthesized correctly");
|
||||
c = mockDb._store[7];
|
||||
Assert.equal(c.name[0], "Acme, Inc.", "Full name should be synthesized correctly");
|
||||
c = mockDb._store[8];
|
||||
Assert.equal(c.name[0], "anyone@example.com", "Full name should be synthesized correctly");
|
||||
|
||||
// Check that a re-import doesn't cause contact duplication.
|
||||
yield new Promise ((resolve, reject) => {
|
||||
info("Initiating import");
|
||||
importer.startImport({
|
||||
"host": "example.com",
|
||||
"auth": kAuth.method,
|
||||
"user": kAuth.user,
|
||||
"password": kAuth.password
|
||||
}, (err, result) => { err ? reject(err) : resolve(result); }, mockDb);
|
||||
});
|
||||
Assert.equal(vcards.length, Object.keys(mockDb._store).length,
|
||||
"Second import shouldn't increase DB size");
|
||||
|
||||
// Check that errors are propagated back to caller
|
||||
let error = yield new Promise ((resolve, reject) => {
|
||||
info("Initiating import");
|
||||
importer.startImport({
|
||||
"host": "example.com",
|
||||
"auth": kAuth.method,
|
||||
"user": kAuth.user,
|
||||
"password": "invalidpassword"
|
||||
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
|
||||
});
|
||||
Assert.equal(error.message, "401 Auth Failure", "Auth error should propagate");
|
||||
|
||||
error = yield new Promise ((resolve, reject) => {
|
||||
info("Initiating import");
|
||||
importer.startImport({
|
||||
"host": "example.invalid",
|
||||
"auth": kAuth.method,
|
||||
"user": kAuth.user,
|
||||
"password": kAuth.password
|
||||
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
|
||||
});
|
||||
Assert.equal(error.message, "404 Not Found", "Not found error should propagate");
|
||||
|
||||
let tmp = mockDb.getByServiceId;
|
||||
mockDb.getByServiceId = function(serviceId, callback) {
|
||||
callback(new Error("getByServiceId failed"));
|
||||
};
|
||||
error = yield new Promise ((resolve, reject) => {
|
||||
info("Initiating import");
|
||||
importer.startImport({
|
||||
"host": "example.com",
|
||||
"auth": kAuth.method,
|
||||
"user": kAuth.user,
|
||||
"password": kAuth.password
|
||||
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
|
||||
});
|
||||
Assert.equal(error.message, "getByServiceId failed", "Database error should propagate");
|
||||
mockDb.getByServiceId = tmp;
|
||||
|
||||
error = yield new Promise ((resolve, reject) => {
|
||||
info("Initiating import");
|
||||
importer.startImport({
|
||||
"host": "example.com",
|
||||
}, (err, result) => { err ? resolve(err) : reject(new Error("Should have failed")); }, mockDb);
|
||||
});
|
||||
Assert.equal(error.message, "No authentication specified", "Missing parameters should generate error");
|
||||
});
|
||||
@@ -1,489 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {LoopContacts} = Cu.import("resource:///modules/loop/LoopContacts.jsm", {});
|
||||
const {LoopStorage} = Cu.import("resource:///modules/loop/LoopStorage.jsm", {});
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
|
||||
const kContacts = [{
|
||||
id: 1,
|
||||
name: ["Ally Avocado"],
|
||||
email: [{
|
||||
"pref": true,
|
||||
"type": ["work"],
|
||||
"value": "ally@mail.com"
|
||||
}],
|
||||
tel: [{
|
||||
"pref": true,
|
||||
"type": ["mobile"],
|
||||
"value": "+31-6-12345678"
|
||||
}],
|
||||
category: ["google"],
|
||||
published: 1406798311748,
|
||||
updated: 1406798311748
|
||||
},{
|
||||
id: 2,
|
||||
name: ["Bob Banana"],
|
||||
email: [{
|
||||
"pref": true,
|
||||
"type": ["work"],
|
||||
"value": "bob@gmail.com"
|
||||
}],
|
||||
tel: [{
|
||||
"pref": true,
|
||||
"type": ["mobile"],
|
||||
"value": "+1-214-5551234"
|
||||
}],
|
||||
category: ["local"],
|
||||
published: 1406798311748,
|
||||
updated: 1406798311748
|
||||
}, {
|
||||
id: 3,
|
||||
name: ["Caitlin Cantaloupe"],
|
||||
email: [{
|
||||
"pref": true,
|
||||
"type": ["work"],
|
||||
"value": "caitlin.cant@hotmail.com"
|
||||
}],
|
||||
category: ["local"],
|
||||
published: 1406798311748,
|
||||
updated: 1406798311748
|
||||
}, {
|
||||
id: 4,
|
||||
name: ["Dave Dragonfruit"],
|
||||
email: [{
|
||||
"pref": true,
|
||||
"type": ["work"],
|
||||
"value": "dd@dragons.net"
|
||||
}],
|
||||
category: ["google"],
|
||||
published: 1406798311748,
|
||||
updated: 1406798311748
|
||||
}];
|
||||
|
||||
const kDanglingContact = {
|
||||
id: 5,
|
||||
name: ["Ellie Eggplant"],
|
||||
email: [{
|
||||
"pref": true,
|
||||
"type": ["work"],
|
||||
"value": "ellie@yahoo.com"
|
||||
}],
|
||||
category: ["google"],
|
||||
blocked: true,
|
||||
published: 1406798311748,
|
||||
updated: 1406798311748
|
||||
};
|
||||
|
||||
const promiseLoadContacts = function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
LoopContacts.removeAll(err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
gExpectedAdds.push(...kContacts);
|
||||
LoopContacts.addMany(kContacts, (err, contacts) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(contacts);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Get a copy of a contact without private properties.
|
||||
const normalizeContact = function(contact) {
|
||||
let result = {};
|
||||
// Get a copy of contact without private properties.
|
||||
for (let prop of Object.getOwnPropertyNames(contact)) {
|
||||
if (!prop.startsWith("_")) {
|
||||
result[prop] = contact[prop];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const compareContacts = function(contact1, contact2) {
|
||||
Assert.ok("_guid" in contact1, "First contact should have an ID.");
|
||||
Assert.deepEqual(normalizeContact(contact1), normalizeContact(contact2));
|
||||
};
|
||||
|
||||
// LoopContacts emits various events. Test if they work as expected here.
|
||||
let gExpectedAdds = [];
|
||||
let gExpectedRemovals = [];
|
||||
let gExpectedUpdates = [];
|
||||
|
||||
const onContactAdded = function(e, contact) {
|
||||
let expectedIds = gExpectedAdds.map(contact => contact.id);
|
||||
let idx = expectedIds.indexOf(contact.id);
|
||||
Assert.ok(idx > -1, "Added contact should be expected");
|
||||
let expected = gExpectedAdds[idx];
|
||||
compareContacts(contact, expected);
|
||||
gExpectedAdds.splice(idx, 1);
|
||||
};
|
||||
|
||||
const onContactRemoved = function(e, contact) {
|
||||
let idx = gExpectedRemovals.indexOf(contact._guid);
|
||||
Assert.ok(idx > -1, "Removed contact should be expected");
|
||||
gExpectedRemovals.splice(idx, 1);
|
||||
};
|
||||
|
||||
const onContactUpdated = function(e, contact) {
|
||||
let idx = gExpectedUpdates.indexOf(contact._guid);
|
||||
Assert.ok(idx > -1, "Updated contact should be expected");
|
||||
gExpectedUpdates.splice(idx, 1);
|
||||
};
|
||||
|
||||
LoopContacts.on("add", onContactAdded);
|
||||
LoopContacts.on("remove", onContactRemoved);
|
||||
LoopContacts.on("update", onContactUpdated);
|
||||
|
||||
registerCleanupFunction(function () {
|
||||
LoopContacts.removeAll(() => {});
|
||||
LoopContacts.off("add", onContactAdded);
|
||||
LoopContacts.off("remove", onContactRemoved);
|
||||
LoopContacts.off("update", onContactUpdated);
|
||||
});
|
||||
|
||||
// Test adding a contact.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
for (let i = 0, l = contacts.length; i < l; ++i) {
|
||||
compareContacts(contacts[i], kContacts[i]);
|
||||
}
|
||||
|
||||
info("Add a contact.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
gExpectedAdds.push(kDanglingContact);
|
||||
LoopContacts.add(kDanglingContact, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
compareContacts(contact, kDanglingContact);
|
||||
|
||||
info("Check if it's persisted.");
|
||||
LoopContacts.get(contact._guid, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
compareContacts(contact, kDanglingContact);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function* () {
|
||||
info("Test removing all contacts.");
|
||||
let contacts = yield promiseLoadContacts();
|
||||
|
||||
yield new Promise((resolve, reject) => {
|
||||
LoopContacts.removeAll(function(err) {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
LoopContacts.getAll(function(err, found) {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(found.length, 0, "There shouldn't be any contacts left");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test retrieving a contact.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
|
||||
info("Get a single contact.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
LoopContacts.get(contacts[1]._guid, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
compareContacts(contact, kContacts[1]);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
info("Get a single contact by id.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
LoopContacts.getByServiceId(2, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
compareContacts(contact, kContacts[1]);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
info("Get a couple of contacts.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
let toRetrieve = [contacts[0], contacts[2], contacts[3]];
|
||||
LoopContacts.getMany(toRetrieve.map(contact => contact._guid), (err, result) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(result.length, toRetrieve.length, "Result list should be the same " +
|
||||
"size as the list of items to retrieve");
|
||||
|
||||
function resultFilter(c) {
|
||||
return c._guid == this._guid;
|
||||
}
|
||||
|
||||
for (let contact of toRetrieve) {
|
||||
let found = result.filter(resultFilter.bind(contact));
|
||||
Assert.ok(found.length, "Contact " + contact._guid + " should be in the list");
|
||||
compareContacts(found[0], contact);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
info("Get all contacts.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
LoopContacts.getAll((err, contacts) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
for (let i = 0, l = contacts.length; i < l; ++i) {
|
||||
compareContacts(contacts[i], kContacts[i]);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
info("Get a non-existent contact.");
|
||||
return new Promise((resolve, reject) => {
|
||||
LoopContacts.get(1000, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.ok(!contact, "There shouldn't be a contact");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test removing a contact.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
|
||||
info("Remove a single contact.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
let toRemove = contacts[2]._guid;
|
||||
gExpectedRemovals.push(toRemove);
|
||||
LoopContacts.remove(toRemove, err => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
|
||||
LoopContacts.get(toRemove, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.ok(!contact, "There shouldn't be a contact");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
info("Remove a non-existing contact.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
LoopContacts.remove(1000, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.ok(!contact, "There shouldn't be a contact");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
info("Remove multiple contacts.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
let toRemove = [contacts[0]._guid, contacts[1]._guid];
|
||||
gExpectedRemovals.push(...toRemove);
|
||||
LoopContacts.removeMany(toRemove, err => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
|
||||
LoopContacts.getAll((err, contacts) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
let ids = contacts.map(contact => contact._guid);
|
||||
Assert.equal(ids.indexOf(toRemove[0]), -1, "Contact '" + toRemove[0] +
|
||||
"' shouldn't be there");
|
||||
Assert.equal(ids.indexOf(toRemove[1]), -1, "Contact '" + toRemove[1] +
|
||||
"' shouldn't be there");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test updating a contact.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
|
||||
const newBday = (new Date(403920000000)).toISOString();
|
||||
|
||||
info("Update a single contact.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
let toUpdate = {
|
||||
_guid: contacts[2]._guid,
|
||||
bday: newBday
|
||||
};
|
||||
gExpectedUpdates.push(contacts[2]._guid);
|
||||
LoopContacts.update(toUpdate, (err, result) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(result, toUpdate._guid, "Result should be the same as the contact ID");
|
||||
|
||||
LoopContacts.get(toUpdate._guid, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(contact.bday, newBday, "Birthday should be the same");
|
||||
info("Check that all other properties were left intact.");
|
||||
contacts[2].bday = newBday;
|
||||
compareContacts(contact, contacts[2]);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
info("Update a non-existing contact.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
let toUpdate = {
|
||||
_guid: 1000,
|
||||
bday: newBday
|
||||
};
|
||||
LoopContacts.update(toUpdate, (err, contact) => {
|
||||
Assert.ok(err, "There should be an error");
|
||||
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
|
||||
"Error message should be correct");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test blocking and unblocking a contact.
|
||||
add_task(function* () {
|
||||
let contacts = yield promiseLoadContacts();
|
||||
|
||||
info("Block contact.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
let toBlock = contacts[1]._guid;
|
||||
gExpectedUpdates.push(toBlock);
|
||||
LoopContacts.block(toBlock, (err, result) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(result, toBlock, "Result should be the same as the contact ID");
|
||||
|
||||
LoopContacts.get(toBlock, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.strictEqual(contact.blocked, true, "Blocked status should be set");
|
||||
info("Check that all other properties were left intact.");
|
||||
delete contact.blocked;
|
||||
compareContacts(contact, contacts[1]);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
info("Block a non-existing contact.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
LoopContacts.block(1000, err => {
|
||||
Assert.ok(err, "There should be an error");
|
||||
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
|
||||
"Error message should be correct");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
info("Unblock a contact.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
let toUnblock = contacts[1]._guid;
|
||||
gExpectedUpdates.push(toUnblock);
|
||||
LoopContacts.unblock(toUnblock, (err, result) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.equal(result, toUnblock, "Result should be the same as the contact ID");
|
||||
|
||||
LoopContacts.get(toUnblock, (err, contact) => {
|
||||
Assert.ok(!err, "There shouldn't be an error");
|
||||
Assert.strictEqual(contact.blocked, false, "Blocked status should be set");
|
||||
info("Check that all other properties were left intact.");
|
||||
delete contact.blocked;
|
||||
compareContacts(contact, contacts[1]);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
info("Unblock a non-existing contact.");
|
||||
yield new Promise((resolve, reject) => {
|
||||
LoopContacts.unblock(1000, err => {
|
||||
Assert.ok(err, "There should be an error");
|
||||
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
|
||||
"Error message should be correct");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test if the event emitter implementation doesn't leak and is working as expected.
|
||||
add_task(function* () {
|
||||
yield promiseLoadContacts();
|
||||
|
||||
Assert.strictEqual(gExpectedAdds.length, 0, "No contact additions should be expected anymore");
|
||||
Assert.strictEqual(gExpectedRemovals.length, 0, "No contact removals should be expected anymore");
|
||||
Assert.strictEqual(gExpectedUpdates.length, 0, "No contact updates should be expected anymore");
|
||||
});
|
||||
|
||||
// Test switching between different databases.
|
||||
add_task(function* () {
|
||||
Assert.equal(LoopStorage.databaseName, "default", "First active partition should be the default");
|
||||
yield promiseLoadContacts();
|
||||
|
||||
let uuid = uuidgen.generateUUID().toString().replace(/[{}]+/g, "");
|
||||
LoopStorage.switchDatabase(uuid);
|
||||
Assert.equal(LoopStorage.databaseName, uuid, "The active partition should have changed");
|
||||
|
||||
yield promiseLoadContacts();
|
||||
|
||||
let contacts = yield promiseLoadContacts();
|
||||
for (let i = 0, l = contacts.length; i < l; ++i) {
|
||||
compareContacts(contacts[i], kContacts[i]);
|
||||
}
|
||||
|
||||
LoopStorage.switchDatabase();
|
||||
Assert.equal(LoopStorage.databaseName, "default", "The active partition should have changed");
|
||||
|
||||
contacts = yield LoopContacts.promise("getAll");
|
||||
for (let i = 0, l = contacts.length; i < l; ++i) {
|
||||
compareContacts(contacts[i], kContacts[i]);
|
||||
}
|
||||
});
|
||||
|
||||
// Test searching for contacts.
|
||||
add_task(function* () {
|
||||
yield promiseLoadContacts();
|
||||
|
||||
let contacts = yield LoopContacts.promise("search", {
|
||||
q: "bob@gmail.com"
|
||||
});
|
||||
Assert.equal(contacts.length, 1, "There should be one contact found");
|
||||
compareContacts(contacts[0], kContacts[1]);
|
||||
|
||||
// Test searching by name.
|
||||
contacts = yield LoopContacts.promise("search", {
|
||||
q: "Ally Avocado",
|
||||
field: "name"
|
||||
});
|
||||
Assert.equal(contacts.length, 1, "There should be one contact found");
|
||||
compareContacts(contacts[0], kContacts[0]);
|
||||
|
||||
// Test searching for multiple contacts.
|
||||
contacts = yield LoopContacts.promise("search", {
|
||||
q: "google",
|
||||
field: "category"
|
||||
});
|
||||
Assert.equal(contacts.length, 2, "There should be two contacts found");
|
||||
|
||||
// Test searching for telephone numbers.
|
||||
contacts = yield LoopContacts.promise("search", {
|
||||
q: "+31612345678",
|
||||
field: "tel"
|
||||
});
|
||||
Assert.equal(contacts.length, 1, "There should be one contact found");
|
||||
compareContacts(contacts[0], kContacts[0]);
|
||||
|
||||
// Test searching for telephone numbers without prefixes.
|
||||
contacts = yield LoopContacts.promise("search", {
|
||||
q: "5551234",
|
||||
field: "tel"
|
||||
});
|
||||
Assert.equal(contacts.length, 1, "There should be one contact found");
|
||||
compareContacts(contacts[0], kContacts[1]);
|
||||
});
|
||||
@@ -1,201 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/*
|
||||
* This file contains tests for the window.LoopUI active tab trackers.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const {injectLoopAPI} = Cu.import("resource:///modules/loop/MozLoopAPI.jsm", {});
|
||||
gMozLoopAPI = injectLoopAPI({});
|
||||
|
||||
let handlers = [
|
||||
{
|
||||
resolve: null,
|
||||
windowId: null,
|
||||
listener: function(err, windowId) {
|
||||
handlers[0].windowId = windowId;
|
||||
handlers[0].resolve();
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: null,
|
||||
windowId: null,
|
||||
listener: function(err, windowId) {
|
||||
handlers[1].windowId = windowId;
|
||||
handlers[1].resolve();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function promiseWindowIdReceivedOnAdd(handler) {
|
||||
return new Promise(resolve => {
|
||||
handler.resolve = resolve;
|
||||
gMozLoopAPI.addBrowserSharingListener(handler.listener);
|
||||
});
|
||||
}
|
||||
|
||||
let createdTabs = [];
|
||||
|
||||
function promiseWindowIdReceivedNewTab(handlers = []) {
|
||||
let promiseHandlers = [];
|
||||
|
||||
handlers.forEach(handler => {
|
||||
promiseHandlers.push(new Promise(resolve => {
|
||||
handler.resolve = resolve;
|
||||
}));
|
||||
});
|
||||
|
||||
let createdTab = gBrowser.selectedTab = gBrowser.addTab();
|
||||
createdTabs.push(createdTab);
|
||||
|
||||
promiseHandlers.push(promiseTabLoadEvent(createdTab, "about:mozilla"));
|
||||
|
||||
return Promise.all(promiseHandlers);
|
||||
}
|
||||
|
||||
function promiseRemoveTab(tab) {
|
||||
return new Promise(resolve => {
|
||||
gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
|
||||
gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
|
||||
resolve();
|
||||
});
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
}
|
||||
|
||||
function* removeTabs() {
|
||||
for (let createdTab of createdTabs) {
|
||||
yield promiseRemoveTab(createdTab);
|
||||
}
|
||||
|
||||
createdTabs = [];
|
||||
}
|
||||
|
||||
add_task(function* test_singleListener() {
|
||||
yield promiseWindowIdReceivedOnAdd(handlers[0]);
|
||||
|
||||
let initialWindowId = handlers[0].windowId;
|
||||
|
||||
Assert.notEqual(initialWindowId, null, "window id should be valid");
|
||||
|
||||
// Check that a new tab updates the window id.
|
||||
yield promiseWindowIdReceivedNewTab([handlers[0]]);
|
||||
|
||||
let newWindowId = handlers[0].windowId;
|
||||
|
||||
Assert.notEqual(initialWindowId, newWindowId, "Tab contentWindow IDs shouldn't be the same");
|
||||
|
||||
// Now remove the listener.
|
||||
gMozLoopAPI.removeBrowserSharingListener(handlers[0].listener);
|
||||
|
||||
yield removeTabs();
|
||||
});
|
||||
|
||||
add_task(function* test_multipleListener() {
|
||||
yield promiseWindowIdReceivedOnAdd(handlers[0]);
|
||||
|
||||
let initialWindowId0 = handlers[0].windowId;
|
||||
|
||||
Assert.notEqual(initialWindowId0, null, "window id should be valid");
|
||||
|
||||
yield promiseWindowIdReceivedOnAdd(handlers[1]);
|
||||
|
||||
let initialWindowId1 = handlers[1].windowId;
|
||||
|
||||
Assert.notEqual(initialWindowId1, null, "window id should be valid");
|
||||
Assert.equal(initialWindowId0, initialWindowId1, "window ids should be the same");
|
||||
|
||||
// Check that a new tab updates the window id.
|
||||
yield promiseWindowIdReceivedNewTab(handlers);
|
||||
|
||||
let newWindowId0 = handlers[0].windowId;
|
||||
let newWindowId1 = handlers[1].windowId;
|
||||
|
||||
Assert.ok(newWindowId0, "windowId should not be null anymore");
|
||||
Assert.equal(newWindowId0, newWindowId1, "Listeners should have the same windowId");
|
||||
Assert.notEqual(initialWindowId0, newWindowId0, "Tab contentWindow IDs shouldn't be the same");
|
||||
|
||||
// Now remove the first listener.
|
||||
gMozLoopAPI.removeBrowserSharingListener(handlers[0].listener);
|
||||
|
||||
// Check that a new tab updates the window id.
|
||||
yield promiseWindowIdReceivedNewTab([handlers[1]]);
|
||||
|
||||
let nextWindowId0 = handlers[0].windowId;
|
||||
let nextWindowId1 = handlers[1].windowId;
|
||||
|
||||
Assert.ok(nextWindowId0, "windowId should not be null anymore");
|
||||
Assert.equal(newWindowId0, nextWindowId0, "First listener shouldn't have updated");
|
||||
Assert.notEqual(newWindowId1, nextWindowId1, "Second listener should have updated");
|
||||
|
||||
// Cleanup.
|
||||
gMozLoopAPI.removeBrowserSharingListener(handlers[1].listener);
|
||||
|
||||
yield removeTabs();
|
||||
});
|
||||
|
||||
add_task(function* test_infoBar() {
|
||||
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const kBrowserSharingNotificationId = "loop-sharing-notification";
|
||||
const kPrefBrowserSharingInfoBar = "loop.browserSharing.showInfoBar";
|
||||
|
||||
Services.prefs.setBoolPref(kPrefBrowserSharingInfoBar, true);
|
||||
|
||||
// First we add two tabs.
|
||||
yield promiseWindowIdReceivedNewTab();
|
||||
yield promiseWindowIdReceivedNewTab();
|
||||
Assert.strictEqual(gBrowser.selectedTab, createdTabs[1],
|
||||
"The second tab created should be selected now");
|
||||
|
||||
// Add one sharing listener, which should show the infobar.
|
||||
yield promiseWindowIdReceivedOnAdd(handlers[0]);
|
||||
|
||||
let getInfoBar = function() {
|
||||
let box = gBrowser.getNotificationBox(gBrowser.selectedBrowser);
|
||||
return box.getNotificationWithValue(kBrowserSharingNotificationId);
|
||||
};
|
||||
|
||||
let testBarProps = function() {
|
||||
let bar = getInfoBar();
|
||||
|
||||
// Start with some basic assertions for the bar.
|
||||
Assert.ok(bar, "The notification bar should be visible");
|
||||
Assert.strictEqual(bar.hidden, false, "Again, the notification bar should be visible");
|
||||
|
||||
let button = bar.querySelector(".notification-button");
|
||||
Assert.ok(button, "There should be a button present");
|
||||
Assert.strictEqual(button.type, "menu-button", "We're expecting a menu-button");
|
||||
Assert.strictEqual(button.getAttribute("anchor"), "dropmarker",
|
||||
"The popup should be opening anchored to the dropmarker");
|
||||
Assert.strictEqual(button.getElementsByTagNameNS(kNSXUL, "menupopup").length, 1,
|
||||
"There should be a popup attached to the button");
|
||||
};
|
||||
|
||||
testBarProps();
|
||||
|
||||
// When we switch tabs, the infobar should move along with it. We use `selectedIndex`
|
||||
// here, because that's the setter that triggers the 'select' event. This event
|
||||
// is what LoopUI listens to and moves the infobar.
|
||||
gBrowser.selectedIndex = Array.indexOf(gBrowser.tabs, createdTabs[0]);
|
||||
|
||||
// We now know that the second tab is selected and should be displaying the
|
||||
// infobar.
|
||||
testBarProps();
|
||||
|
||||
// Test hiding the infoBar.
|
||||
getInfoBar().querySelector(".notification-button")
|
||||
.getElementsByTagNameNS(kNSXUL, "menuitem")[0].click();
|
||||
Assert.equal(getInfoBar(), null, "The notification should be hidden now");
|
||||
Assert.strictEqual(Services.prefs.getBoolPref(kPrefBrowserSharingInfoBar), false,
|
||||
"The pref should be set to false when the menu item is clicked");
|
||||
|
||||
gBrowser.selectedIndex = Array.indexOf(gBrowser.tabs, createdTabs[1]);
|
||||
|
||||
Assert.equal(getInfoBar(), null, "The notification should still be hidden");
|
||||
|
||||
// Cleanup.
|
||||
gMozLoopAPI.removeBrowserSharingListener(handlers[0].listener);
|
||||
yield removeTabs();
|
||||
Services.prefs.clearUserPref(kPrefBrowserSharingInfoBar);
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* This is an integration test to make sure that passing window IDs is working as
|
||||
* expected, with or without e10s enabled - rather than just testing MozLoopAPI
|
||||
* alone.
|
||||
*/
|
||||
|
||||
const {injectLoopAPI} = Cu.import("resource:///modules/loop/MozLoopAPI.jsm");
|
||||
gMozLoopAPI = injectLoopAPI({});
|
||||
|
||||
let promiseTabWindowId = function() {
|
||||
return new Promise(resolve => {
|
||||
gMozLoopAPI.getActiveTabWindowId((err, windowId) => {
|
||||
Assert.equal(null, err, "No error should've occurred.");
|
||||
Assert.equal(typeof windowId, "number", "We should have a window ID");
|
||||
resolve(windowId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
add_task(function* test_windowIdFetch_simple() {
|
||||
Assert.ok(gMozLoopAPI, "mozLoop should exist");
|
||||
|
||||
yield promiseTabWindowId();
|
||||
});
|
||||
|
||||
add_task(function* test_windowIdFetch_multipleTabs() {
|
||||
let previousTab = gBrowser.selectedTab;
|
||||
let previousTabId = yield promiseTabWindowId();
|
||||
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab();
|
||||
yield promiseTabLoadEvent(tab, "about:mozilla");
|
||||
let tabId = yield promiseTabWindowId();
|
||||
Assert.ok(tabId !== previousTabId, "Tab contentWindow IDs shouldn't be the same");
|
||||
gBrowser.removeTab(tab);
|
||||
|
||||
tabId = yield promiseTabWindowId();
|
||||
Assert.equal(previousTabId, tabId, "Window IDs should be back to what they were");
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,142 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.store.StandaloneMetricsStore", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox, dispatcher, store, fakeActiveRoomStore;
|
||||
|
||||
var sharedActions = loop.shared.actions;
|
||||
var METRICS_GA_CATEGORY = loop.store.METRICS_GA_CATEGORY;
|
||||
var METRICS_GA_ACTIONS = loop.store.METRICS_GA_ACTIONS;
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
dispatcher = new loop.Dispatcher();
|
||||
|
||||
window.ga = sinon.stub();
|
||||
|
||||
var fakeStore = loop.store.createStore({
|
||||
getInitialStoreState: function() {
|
||||
return {
|
||||
audioMuted: false,
|
||||
roomState: ROOM_STATES.INIT,
|
||||
videoMuted: false
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
fakeActiveRoomStore = new fakeStore(dispatcher);
|
||||
|
||||
store = new loop.store.StandaloneMetricsStore(dispatcher, {
|
||||
activeRoomStore: fakeActiveRoomStore
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
delete window.ga;
|
||||
});
|
||||
|
||||
describe("Action Handlers", function() {
|
||||
beforeEach(function() {
|
||||
});
|
||||
|
||||
it("should log an event on GotMediaPermission", function() {
|
||||
store.gotMediaPermission();
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
|
||||
"Media granted");
|
||||
});
|
||||
|
||||
it("should log an event on JoinRoom", function() {
|
||||
store.joinRoom();
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
|
||||
"Join the conversation");
|
||||
});
|
||||
|
||||
it("should log an event on LeaveRoom", function() {
|
||||
store.leaveRoom();
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
|
||||
"Leave conversation");
|
||||
});
|
||||
|
||||
it("should log an event on MediaConnected", function() {
|
||||
store.mediaConnected();
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.success,
|
||||
"Media connected");
|
||||
});
|
||||
|
||||
it("should log an event on RecordClick", function() {
|
||||
store.recordClick(new sharedActions.RecordClick({
|
||||
linkInfo: "fake"
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.linkClick,
|
||||
"fake");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Store Change Handlers", function() {
|
||||
it("should log an event on room full", function() {
|
||||
fakeActiveRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
|
||||
"Room full");
|
||||
});
|
||||
|
||||
it("should log an event when the room is expired or invalid", function() {
|
||||
fakeActiveRoomStore.setStoreState({
|
||||
roomState: ROOM_STATES.FAILED,
|
||||
failureReason: FAILURE_DETAILS.EXPIRED_OR_INVALID
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
|
||||
"Link expired or invalid");
|
||||
});
|
||||
|
||||
it("should log an event when video mute is changed", function() {
|
||||
fakeActiveRoomStore.setStoreState({
|
||||
videoMuted: true
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.faceMute,
|
||||
"mute");
|
||||
});
|
||||
|
||||
it("should log an event when audio mute is changed", function() {
|
||||
fakeActiveRoomStore.setStoreState({
|
||||
audioMuted: true
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(window.ga);
|
||||
sinon.assert.calledWithExactly(window.ga,
|
||||
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.audioMute,
|
||||
"mute");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,697 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop, sinon */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.standaloneRoomViews", function() {
|
||||
"use strict";
|
||||
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
|
||||
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedUtils = loop.shared.utils;
|
||||
|
||||
var sandbox, dispatcher, activeRoomStore, feedbackStore, dispatch;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
dispatcher = new loop.Dispatcher();
|
||||
dispatch = sandbox.stub(dispatcher, "dispatch");
|
||||
activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
|
||||
mozLoop: {},
|
||||
sdkDriver: {}
|
||||
});
|
||||
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||
feedbackClient: {}
|
||||
});
|
||||
loop.store.StoreMixin.register({feedbackStore: feedbackStore});
|
||||
|
||||
sandbox.useFakeTimers();
|
||||
|
||||
// Prevents audio request errors in the test console.
|
||||
sandbox.useFakeXMLHttpRequest();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("StandaloneRoomContextView", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(navigator.mozL10n, "get").returnsArg(0);
|
||||
});
|
||||
|
||||
function mountTestComponent(extraProps) {
|
||||
var props = _.extend({
|
||||
dispatcher: dispatcher,
|
||||
receivingScreenShare: false
|
||||
}, extraProps);
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(
|
||||
loop.standaloneRoomViews.StandaloneRoomContextView, props));
|
||||
}
|
||||
|
||||
it("should display the room name if no failures are known", function() {
|
||||
var view = mountTestComponent({
|
||||
roomName: "Mike's room",
|
||||
receivingScreenShare: false
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().textContent).eql("Mike's room");
|
||||
});
|
||||
|
||||
it("should log an unsupported browser message if crypto is unsupported", function() {
|
||||
var view = mountTestComponent({
|
||||
roomName: "Mark's room",
|
||||
roomInfoFailure: ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED
|
||||
});
|
||||
|
||||
sinon.assert.called(console.error);
|
||||
sinon.assert.calledWithMatch(console.error, sinon.match("unsupported"));
|
||||
});
|
||||
|
||||
it("should display a general error message for any other failure", function() {
|
||||
var view = mountTestComponent({
|
||||
roomName: "Mark's room",
|
||||
roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA
|
||||
});
|
||||
|
||||
sinon.assert.called(console.error);
|
||||
sinon.assert.calledWithMatch(console.error, sinon.match("not_available"));
|
||||
});
|
||||
|
||||
it("should display context information if a url is supplied", function() {
|
||||
var view = mountTestComponent({
|
||||
roomName: "Mike's room",
|
||||
roomContextUrls: [{
|
||||
description: "Mark's super page",
|
||||
location: "http://invalid.com",
|
||||
thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
|
||||
}]
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".standalone-context-url")).not.eql(null);
|
||||
});
|
||||
|
||||
it("should format the url for display", function() {
|
||||
sandbox.stub(sharedUtils, "formatURL").returns({
|
||||
location: "location",
|
||||
hostname: "hostname"
|
||||
});
|
||||
|
||||
var view = mountTestComponent({
|
||||
roomName: "Mike's room",
|
||||
roomContextUrls: [{
|
||||
description: "Mark's super page",
|
||||
location: "http://invalid.com",
|
||||
thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
|
||||
}]
|
||||
});
|
||||
|
||||
expect(view.getDOMNode()
|
||||
.querySelector(".standalone-context-url-description-wrapper > a").textContent)
|
||||
.eql("hostname");
|
||||
});
|
||||
|
||||
it("should not display context information if no urls are supplied", function() {
|
||||
var view = mountTestComponent({
|
||||
roomName: "Mike's room"
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".standalone-context-url")).eql(null);
|
||||
});
|
||||
|
||||
it("should dispatch a RecordClick action when the link is clicked", function() {
|
||||
var view = mountTestComponent({
|
||||
roomName: "Mark's room",
|
||||
roomContextUrls: [{
|
||||
description: "Mark's super page",
|
||||
location: "http://invalid.com",
|
||||
thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
|
||||
}]
|
||||
});
|
||||
|
||||
TestUtils.Simulate.click(view.getDOMNode()
|
||||
.querySelector(".standalone-context-url-description-wrapper > a"));
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.RecordClick({
|
||||
linkInfo: "Shared URL"
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("StandaloneRoomHeader", function() {
|
||||
function mountTestComponent() {
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(
|
||||
loop.standaloneRoomViews.StandaloneRoomHeader, {
|
||||
dispatcher: dispatcher
|
||||
}));
|
||||
}
|
||||
|
||||
it("should dispatch a RecordClick action when the support link is clicked", function() {
|
||||
var view = mountTestComponent();
|
||||
|
||||
TestUtils.Simulate.click(view.getDOMNode().querySelector("a"));
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.RecordClick({
|
||||
linkInfo: "Support link click"
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("StandaloneRoomView", function() {
|
||||
function mountTestComponent() {
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(
|
||||
loop.standaloneRoomViews.StandaloneRoomView, {
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
isFirefox: true
|
||||
}));
|
||||
}
|
||||
|
||||
function expectActionDispatched(view) {
|
||||
sinon.assert.calledOnce(dispatch);
|
||||
sinon.assert.calledWithExactly(dispatch,
|
||||
sinon.match.instanceOf(sharedActions.SetupStreamElements));
|
||||
sinon.assert.calledWithExactly(dispatch, sinon.match(function(value) {
|
||||
return value.getLocalElementFunc() ===
|
||||
view.getDOMNode().querySelector(".local");
|
||||
}));
|
||||
sinon.assert.calledWithExactly(dispatch, sinon.match(function(value) {
|
||||
return value.getRemoteElementFunc() ===
|
||||
view.getDOMNode().querySelector(".remote");
|
||||
}));
|
||||
}
|
||||
|
||||
describe("#componentWillUpdate", function() {
|
||||
it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
|
||||
"is entered", function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
|
||||
var view = mountTestComponent();
|
||||
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
|
||||
|
||||
expectActionDispatched(view);
|
||||
});
|
||||
|
||||
it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is " +
|
||||
"re-entered", function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
|
||||
var view = mountTestComponent();
|
||||
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
|
||||
|
||||
expectActionDispatched(view);
|
||||
});
|
||||
|
||||
it("should updateVideoContainer when the JOINED state is entered", function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
|
||||
|
||||
var view = mountTestComponent();
|
||||
|
||||
sandbox.stub(view, "updateVideoContainer");
|
||||
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
|
||||
|
||||
sinon.assert.calledOnce(view.updateVideoContainer);
|
||||
});
|
||||
|
||||
it("should updateVideoContainer when the JOINED state is re-entered", function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
|
||||
|
||||
var view = mountTestComponent();
|
||||
|
||||
sandbox.stub(view, "updateVideoContainer");
|
||||
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
|
||||
|
||||
sinon.assert.calledOnce(view.updateVideoContainer);
|
||||
});
|
||||
|
||||
it("should reset the video dimensions cache when the gather state is entered", function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
|
||||
|
||||
var view = mountTestComponent();
|
||||
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.GATHER});
|
||||
|
||||
expect(view._videoDimensionsCache).eql({
|
||||
local: {},
|
||||
remote: {}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#publishStream", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
view = mountTestComponent();
|
||||
view.setState({
|
||||
audioMuted: true,
|
||||
videoMuted: true
|
||||
});
|
||||
});
|
||||
|
||||
it("should mute local audio stream", function() {
|
||||
TestUtils.Simulate.click(
|
||||
view.getDOMNode().querySelector(".btn-mute-audio"));
|
||||
|
||||
sinon.assert.calledOnce(dispatch);
|
||||
sinon.assert.calledWithExactly(dispatch, new sharedActions.SetMute({
|
||||
type: "audio",
|
||||
enabled: true
|
||||
}));
|
||||
});
|
||||
|
||||
it("should mute local video stream", function() {
|
||||
TestUtils.Simulate.click(
|
||||
view.getDOMNode().querySelector(".btn-mute-video"));
|
||||
|
||||
sinon.assert.calledOnce(dispatch);
|
||||
sinon.assert.calledWithExactly(dispatch, new sharedActions.SetMute({
|
||||
type: "video",
|
||||
enabled: true
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Local Stream Size Position", function() {
|
||||
var view, localElement;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(window, "matchMedia").returns({
|
||||
matches: false
|
||||
});
|
||||
view = mountTestComponent();
|
||||
localElement = view._getElement(".local");
|
||||
});
|
||||
|
||||
it("should be a quarter of the width of the main stream", function() {
|
||||
sandbox.stub(view, "getRemoteVideoDimensions").returns({
|
||||
streamWidth: 640,
|
||||
offsetX: 0
|
||||
});
|
||||
|
||||
view.updateLocalCameraPosition({
|
||||
width: 1,
|
||||
height: 0.75
|
||||
});
|
||||
|
||||
expect(localElement.style.width).eql("160px");
|
||||
expect(localElement.style.height).eql("120px");
|
||||
});
|
||||
|
||||
it("should be a quarter of the width reduced for aspect ratio", function() {
|
||||
sandbox.stub(view, "getRemoteVideoDimensions").returns({
|
||||
streamWidth: 640,
|
||||
offsetX: 0
|
||||
});
|
||||
|
||||
view.updateLocalCameraPosition({
|
||||
width: 0.75,
|
||||
height: 1
|
||||
});
|
||||
|
||||
expect(localElement.style.width).eql("120px");
|
||||
expect(localElement.style.height).eql("160px");
|
||||
});
|
||||
|
||||
it("should ensure the height is a minimum of 48px", function() {
|
||||
sandbox.stub(view, "getRemoteVideoDimensions").returns({
|
||||
streamWidth: 180,
|
||||
offsetX: 0
|
||||
});
|
||||
|
||||
view.updateLocalCameraPosition({
|
||||
width: 1,
|
||||
height: 0.75
|
||||
});
|
||||
|
||||
expect(localElement.style.width).eql("64px");
|
||||
expect(localElement.style.height).eql("48px");
|
||||
});
|
||||
|
||||
it("should ensure the width is a minimum of 48px", function() {
|
||||
sandbox.stub(view, "getRemoteVideoDimensions").returns({
|
||||
streamWidth: 180,
|
||||
offsetX: 0
|
||||
});
|
||||
|
||||
view.updateLocalCameraPosition({
|
||||
width: 0.75,
|
||||
height: 1
|
||||
});
|
||||
|
||||
expect(localElement.style.width).eql("48px");
|
||||
expect(localElement.style.height).eql("64px");
|
||||
});
|
||||
|
||||
it("should position the stream to overlap the main stream by a quarter", function() {
|
||||
sandbox.stub(view, "getRemoteVideoDimensions").returns({
|
||||
streamWidth: 640,
|
||||
offsetX: 0
|
||||
});
|
||||
|
||||
view.updateLocalCameraPosition({
|
||||
width: 1,
|
||||
height: 0.75
|
||||
});
|
||||
|
||||
expect(localElement.style.width).eql("160px");
|
||||
expect(localElement.style.left).eql("600px");
|
||||
});
|
||||
|
||||
it("should position the stream to overlap the main stream by a quarter when the aspect ratio is vertical", function() {
|
||||
sandbox.stub(view, "getRemoteVideoDimensions").returns({
|
||||
streamWidth: 640,
|
||||
offsetX: 0
|
||||
});
|
||||
|
||||
view.updateLocalCameraPosition({
|
||||
width: 0.75,
|
||||
height: 1
|
||||
});
|
||||
|
||||
expect(localElement.style.width).eql("120px");
|
||||
expect(localElement.style.left).eql("610px");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Remote Stream Size Position", function() {
|
||||
var view, localElement, remoteElement;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(window, "matchMedia").returns({
|
||||
matches: false
|
||||
});
|
||||
view = mountTestComponent();
|
||||
|
||||
localElement = {
|
||||
style: {}
|
||||
};
|
||||
remoteElement = {
|
||||
style: {},
|
||||
removeAttribute: sinon.spy()
|
||||
};
|
||||
|
||||
sandbox.stub(view, "_getElement", function(className) {
|
||||
return className === ".local" ? localElement : remoteElement;
|
||||
});
|
||||
|
||||
view.setState({"receivingScreenShare": true});
|
||||
});
|
||||
|
||||
it("should do nothing if not receiving screenshare", function() {
|
||||
view.setState({"receivingScreenShare": false});
|
||||
remoteElement.style.width = "10px";
|
||||
|
||||
view.updateRemoteCameraPosition({
|
||||
width: 1,
|
||||
height: 0.75
|
||||
});
|
||||
|
||||
expect(remoteElement.style.width).eql("10px");
|
||||
});
|
||||
|
||||
it("should be the same width as the local video", function() {
|
||||
localElement.offsetWidth = 100;
|
||||
|
||||
view.updateRemoteCameraPosition({
|
||||
width: 1,
|
||||
height: 0.75
|
||||
});
|
||||
|
||||
expect(remoteElement.style.width).eql("100px");
|
||||
});
|
||||
|
||||
it("should be the same left edge as the local video", function() {
|
||||
localElement.offsetLeft = 50;
|
||||
|
||||
view.updateRemoteCameraPosition({
|
||||
width: 1,
|
||||
height: 0.75
|
||||
});
|
||||
|
||||
expect(remoteElement.style.left).eql("50px");
|
||||
});
|
||||
|
||||
it("should have a height determined by the aspect ratio", function() {
|
||||
localElement.offsetWidth = 100;
|
||||
|
||||
view.updateRemoteCameraPosition({
|
||||
width: 1,
|
||||
height: 0.75
|
||||
});
|
||||
|
||||
expect(remoteElement.style.height).eql("75px");
|
||||
});
|
||||
|
||||
it("should have the top be set such that the bottom is 10px above the local video", function() {
|
||||
localElement.offsetWidth = 100;
|
||||
localElement.offsetTop = 200;
|
||||
|
||||
view.updateRemoteCameraPosition({
|
||||
width: 1,
|
||||
height: 0.75
|
||||
});
|
||||
|
||||
// 200 (top) - 75 (height) - 10 (spacing) = 115
|
||||
expect(remoteElement.style.top).eql("115px");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("#render", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
view = mountTestComponent();
|
||||
});
|
||||
|
||||
describe("Empty room message", function() {
|
||||
it("should display an empty room message on JOINED",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".empty-room-message"))
|
||||
.not.eql(null);
|
||||
});
|
||||
|
||||
it("should display an empty room message on SESSION_CONNECTED",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".empty-room-message"))
|
||||
.not.eql(null);
|
||||
});
|
||||
|
||||
it("shouldn't display an empty room message on HAS_PARTICIPANTS",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".empty-room-message"))
|
||||
.eql(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Prompt media message", function() {
|
||||
it("should display a prompt for user media on MEDIA_WAIT",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".prompt-media-message"))
|
||||
.not.eql(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Full room message", function() {
|
||||
it("should display a full room message on FULL",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".full-room-message"))
|
||||
.not.eql(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Failed room message", function() {
|
||||
it("should display a failed room message on FAILED",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".failed-room-message"))
|
||||
.not.eql(null);
|
||||
});
|
||||
|
||||
it("should display a retry button",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".btn-info"))
|
||||
.not.eql(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Join button", function() {
|
||||
function getJoinButton(view) {
|
||||
return view.getDOMNode().querySelector(".btn-join");
|
||||
}
|
||||
|
||||
it("should render the Join button when room isn't active", function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
|
||||
|
||||
expect(getJoinButton(view)).not.eql(null);
|
||||
});
|
||||
|
||||
it("should not render the Join button when room is active",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
|
||||
|
||||
expect(getJoinButton(view)).eql(null);
|
||||
});
|
||||
|
||||
it("should join the room when clicking the Join button", function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
|
||||
|
||||
TestUtils.Simulate.click(getJoinButton(view));
|
||||
|
||||
sinon.assert.calledOnce(dispatch);
|
||||
sinon.assert.calledWithExactly(dispatch, new sharedActions.JoinRoom());
|
||||
});
|
||||
});
|
||||
|
||||
describe("Leave button", function() {
|
||||
function getLeaveButton(view) {
|
||||
return view.getDOMNode().querySelector(".btn-hangup");
|
||||
}
|
||||
|
||||
it("should disable the Leave button when the room state is READY",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
|
||||
|
||||
expect(getLeaveButton(view).disabled).eql(true);
|
||||
});
|
||||
|
||||
it("should disable the Leave button when the room state is FAILED",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
|
||||
|
||||
expect(getLeaveButton(view).disabled).eql(true);
|
||||
});
|
||||
|
||||
it("should disable the Leave button when the room state is FULL",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
|
||||
|
||||
expect(getLeaveButton(view).disabled).eql(true);
|
||||
});
|
||||
|
||||
it("should enable the Leave button when the room state is SESSION_CONNECTED",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
|
||||
|
||||
expect(getLeaveButton(view).disabled).eql(false);
|
||||
});
|
||||
|
||||
it("should enable the Leave button when the room state is JOINED",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
|
||||
|
||||
expect(getLeaveButton(view).disabled).eql(false);
|
||||
});
|
||||
|
||||
it("should enable the Leave button when the room state is HAS_PARTICIPANTS",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
|
||||
|
||||
expect(getLeaveButton(view).disabled).eql(false);
|
||||
});
|
||||
|
||||
it("should leave the room when clicking the Leave button", function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
|
||||
|
||||
TestUtils.Simulate.click(getLeaveButton(view));
|
||||
|
||||
sinon.assert.calledOnce(dispatch);
|
||||
sinon.assert.calledWithExactly(dispatch, new sharedActions.LeaveRoom());
|
||||
});
|
||||
});
|
||||
|
||||
describe("Feedback", function() {
|
||||
beforeEach(function() {
|
||||
activeRoomStore.setStoreState({
|
||||
roomState: ROOM_STATES.ENDED,
|
||||
used: true
|
||||
});
|
||||
});
|
||||
|
||||
it("should display a feedback form when the user leaves the room",
|
||||
function() {
|
||||
expect(view.getDOMNode().querySelector(".faces")).not.eql(null);
|
||||
});
|
||||
|
||||
it("should dispatch a `FeedbackComplete` action after feedback is sent",
|
||||
function() {
|
||||
feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
|
||||
|
||||
sandbox.clock.tick(
|
||||
loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS * 1000 + 1000);
|
||||
|
||||
sinon.assert.calledOnce(dispatch);
|
||||
sinon.assert.calledWithExactly(dispatch, new sharedActions.FeedbackComplete());
|
||||
});
|
||||
|
||||
it("should NOT display a feedback form if the room has not been used",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({used: false});
|
||||
expect(view.getDOMNode().querySelector(".faces")).eql(null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Mute", function() {
|
||||
it("should render local media as audio-only if video is muted",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({
|
||||
roomState: ROOM_STATES.SESSION_CONNECTED,
|
||||
videoMuted: true
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".local-stream-audio"))
|
||||
.not.eql(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Marketplace hidden iframe", function() {
|
||||
|
||||
it("should set src when the store state change",
|
||||
function(done) {
|
||||
|
||||
var marketplace = view.getDOMNode().querySelector("#marketplace");
|
||||
expect(marketplace.src).to.be.equal("");
|
||||
|
||||
activeRoomStore.setStoreState({
|
||||
marketplaceSrc: "http://market/",
|
||||
onMarketplaceMessage: function () {}
|
||||
});
|
||||
|
||||
view.forceUpdate(function() {
|
||||
expect(marketplace.src).to.be.equal("http://market/");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,182 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/*global loop, sinon, it, beforeEach, afterEach, describe, hawk */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.StandaloneClient", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox,
|
||||
fakeXHR,
|
||||
requests = [],
|
||||
callback,
|
||||
fakeToken;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
fakeXHR = sandbox.useFakeXMLHttpRequest();
|
||||
requests = [];
|
||||
// https://github.com/cjohansen/Sinon.JS/issues/393
|
||||
fakeXHR.xhr.onCreate = function (xhr) {
|
||||
requests.push(xhr);
|
||||
};
|
||||
callback = sinon.spy();
|
||||
fakeToken = "fakeTokenText";
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("loop.StandaloneClient", function() {
|
||||
describe("#constructor", function() {
|
||||
it("should require a baseServerUrl setting", function() {
|
||||
expect(function() {
|
||||
new loop.StandaloneClient();
|
||||
}).to.Throw(Error, /required/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#requestCallUrlInfo", function() {
|
||||
var client, fakeServerErrorDescription;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.StandaloneClient(
|
||||
{baseServerUrl: "http://fake.api"}
|
||||
);
|
||||
});
|
||||
|
||||
describe("should make the requests to the server", function() {
|
||||
|
||||
it("should throw if loopToken is missing", function() {
|
||||
expect(client.requestCallUrlInfo).to
|
||||
.throw(/Missing required parameter loopToken/);
|
||||
});
|
||||
|
||||
it("should make a GET request for the call url creation date", function() {
|
||||
client.requestCallUrlInfo("fakeCallUrlToken", function() {});
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url)
|
||||
.to.eql("http://fake.api/calls/fakeCallUrlToken");
|
||||
expect(requests[0].method).to.eql("GET");
|
||||
});
|
||||
|
||||
it("should call the callback with (null, serverResponse)", function() {
|
||||
var successCallback = sandbox.spy(function() {});
|
||||
var serverResponse = {
|
||||
calleeFriendlyName: "Andrei",
|
||||
urlCreationDate: 0
|
||||
};
|
||||
|
||||
client.requestCallUrlInfo("fakeCallUrlToken", successCallback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(serverResponse));
|
||||
|
||||
sinon.assert.calledWithExactly(successCallback,
|
||||
null,
|
||||
serverResponse);
|
||||
});
|
||||
|
||||
it("should log the error if the requests fails", function() {
|
||||
sinon.stub(console, "error");
|
||||
var serverResponse = {error: true};
|
||||
var error = JSON.stringify(serverResponse);
|
||||
|
||||
client.requestCallUrlInfo("fakeCallUrlToken", sandbox.stub());
|
||||
requests[0].respond(404, {"Content-Type": "application/json"},
|
||||
error);
|
||||
|
||||
sinon.assert.calledOnce(console.error);
|
||||
sinon.assert.calledWithExactly(console.error, "Server error",
|
||||
"HTTP 404 Not Found", serverResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe("requestCallInfo", function() {
|
||||
var client, fakeServerErrorDescription;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.StandaloneClient(
|
||||
{baseServerUrl: "http://fake.api"}
|
||||
);
|
||||
fakeServerErrorDescription = {
|
||||
code: 401,
|
||||
errno: 101,
|
||||
error: "error",
|
||||
message: "invalid token",
|
||||
info: "error info"
|
||||
};
|
||||
});
|
||||
|
||||
it("should prevent launching a conversation when token is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallInfo();
|
||||
}).to.Throw(Error, /missing.*[Tt]oken/);
|
||||
});
|
||||
|
||||
it("should post data for the given call", function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/calls/fake");
|
||||
expect(requests[0].method).to.be.equal("POST");
|
||||
expect(requests[0].requestBody).to.be.equal('{"callType":"audio","channel":"standalone"}');
|
||||
});
|
||||
|
||||
it("should receive call data for the given call", function() {
|
||||
client.requestCallInfo("fake", "audio-video", callback);
|
||||
|
||||
var sessionData = {
|
||||
sessionId: "one",
|
||||
sessionToken: "two",
|
||||
apiKey: "three"
|
||||
};
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(sessionData));
|
||||
sinon.assert.calledWithExactly(callback, null, sessionData);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
requests[0].respond(401, {"Content-Type": "application/json"},
|
||||
JSON.stringify(fakeServerErrorDescription));
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /HTTP 401 Unauthorized/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should attach the server error description object to the error " +
|
||||
"passed to the callback",
|
||||
function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
requests[0].respond(401, {"Content-Type": "application/json"},
|
||||
JSON.stringify(fakeServerErrorDescription));
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return err.errno === fakeServerErrorDescription.errno;
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": "one"}');
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Unit tests for the hawkRequest API
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource:///modules/loop/MozLoopAPI.jsm");
|
||||
|
||||
let sandbox;
|
||||
function assertInSandbox(expr, msg_opt) {
|
||||
Assert.ok(Cu.evalInSandbox(expr, sandbox), msg_opt);
|
||||
}
|
||||
|
||||
sandbox = Cu.Sandbox("about:looppanel", { wantXrays: false } );
|
||||
injectLoopAPI(sandbox, true);
|
||||
|
||||
add_task(function* hawk_session_scope_constants() {
|
||||
assertInSandbox("typeof mozLoop.LOOP_SESSION_TYPE !== 'undefined'");
|
||||
|
||||
assertInSandbox("mozLoop.LOOP_SESSION_TYPE.GUEST === 1");
|
||||
|
||||
assertInSandbox("mozLoop.LOOP_SESSION_TYPE.FXA === 2");
|
||||
});
|
||||
|
||||
function generateSessionTypeVerificationStub(desiredSessionType) {
|
||||
|
||||
function hawkRequestStub(sessionType, path, method, payloadObj, callback) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
Assert.equal(desiredSessionType, sessionType);
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
return hawkRequestStub;
|
||||
}
|
||||
|
||||
const origHawkRequest = MozLoopService.hawkRequest;
|
||||
do_register_cleanup(function() {
|
||||
MozLoopService.hawkRequest = origHawkRequest;
|
||||
});
|
||||
|
||||
add_task(function* hawk_request_scope_passthrough() {
|
||||
|
||||
// add a stub that verifies the parameter we want
|
||||
MozLoopService.hawkRequest =
|
||||
generateSessionTypeVerificationStub(sandbox.mozLoop.LOOP_SESSION_TYPE.FXA);
|
||||
|
||||
// call mozLoop.hawkRequest, which calls MozLoopAPI.hawkRequest, which calls
|
||||
// MozLoopService.hawkRequest
|
||||
Cu.evalInSandbox(
|
||||
"mozLoop.hawkRequest(mozLoop.LOOP_SESSION_TYPE.FXA," +
|
||||
" 'call-url/fakeToken', 'POST', {}, function() {})",
|
||||
sandbox);
|
||||
|
||||
MozLoopService.hawkRequest =
|
||||
generateSessionTypeVerificationStub(sandbox.mozLoop.LOOP_SESSION_TYPE.GUEST);
|
||||
|
||||
Cu.evalInSandbox(
|
||||
"mozLoop.hawkRequest(mozLoop.LOOP_SESSION_TYPE.GUEST," +
|
||||
" 'call-url/fakeToken', 'POST', {}, function() {})",
|
||||
sandbox);
|
||||
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
@@ -1,810 +0,0 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
/* 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/. */
|
||||
|
||||
/* jshint newcap:false */
|
||||
/* global loop:true, React */
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
// Stop the default init functions running to avoid conflicts.
|
||||
document.removeEventListener('DOMContentLoaded', loop.panel.init);
|
||||
document.removeEventListener('DOMContentLoaded', loop.conversation.init);
|
||||
|
||||
// 1. Desktop components
|
||||
// 1.1 Panel
|
||||
var PanelView = loop.panel.PanelView;
|
||||
// 1.2. Conversation Window
|
||||
var AcceptCallView = loop.conversationViews.AcceptCallView;
|
||||
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
|
||||
var CallFailedView = loop.conversationViews.CallFailedView;
|
||||
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
|
||||
|
||||
// 2. Standalone webapp
|
||||
var HomeView = loop.webapp.HomeView;
|
||||
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
|
||||
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
|
||||
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
|
||||
var GumPromptConversationView = loop.webapp.GumPromptConversationView;
|
||||
var WaitingConversationView = loop.webapp.WaitingConversationView;
|
||||
var StartConversationView = loop.webapp.StartConversationView;
|
||||
var FailedConversationView = loop.webapp.FailedConversationView;
|
||||
var EndedConversationView = loop.webapp.EndedConversationView;
|
||||
var StandaloneRoomView = loop.standaloneRoomViews.StandaloneRoomView;
|
||||
|
||||
// 3. Shared components
|
||||
var ConversationToolbar = loop.shared.views.ConversationToolbar;
|
||||
var ConversationView = loop.shared.views.ConversationView;
|
||||
var FeedbackView = loop.shared.views.FeedbackView;
|
||||
|
||||
// Store constants
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
|
||||
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
|
||||
|
||||
// Local helpers
|
||||
function returnTrue() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function returnFalse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function noop(){}
|
||||
|
||||
// We save the visibility change listeners so that we can fake an event
|
||||
// to the panel once we've loaded all the views.
|
||||
var visibilityListeners = [];
|
||||
var rootObject = window;
|
||||
|
||||
rootObject.document.addEventListener = function(eventName, func) {
|
||||
if (eventName === "visibilitychange") {
|
||||
visibilityListeners.push(func);
|
||||
}
|
||||
window.addEventListener(eventName, func);
|
||||
};
|
||||
|
||||
rootObject.document.removeEventListener = function(eventName, func) {
|
||||
if (eventName === "visibilitychange") {
|
||||
var index = visibilityListeners.indexOf(func);
|
||||
visibilityListeners.splice(index, 1);
|
||||
}
|
||||
window.removeEventListener(eventName, func);
|
||||
};
|
||||
|
||||
loop.shared.mixins.setRootObject(rootObject);
|
||||
|
||||
// Feedback API client configured to send data to the stage input server,
|
||||
// which is available at https://input.allizom.org
|
||||
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
|
||||
"https://input.allizom.org/api/v1/feedback", {
|
||||
product: "Loop"
|
||||
}
|
||||
);
|
||||
|
||||
var mockSDK = _.extend({}, Backbone.Events);
|
||||
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop,
|
||||
sdkDriver: mockSDK
|
||||
});
|
||||
var roomStore = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop
|
||||
});
|
||||
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||
feedbackClient: stageFeedbackApiClient
|
||||
});
|
||||
var conversationStore = new loop.store.ConversationStore(dispatcher, {
|
||||
client: {},
|
||||
mozLoop: navigator.mozLoop,
|
||||
sdkDriver: mockSDK
|
||||
});
|
||||
|
||||
loop.store.StoreMixin.register({
|
||||
conversationStore: conversationStore,
|
||||
feedbackStore: feedbackStore
|
||||
});
|
||||
|
||||
// Local mocks
|
||||
|
||||
var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
|
||||
|
||||
var mockContact = {
|
||||
name: ["Mr Smith"],
|
||||
email: [{
|
||||
value: "smith@invalid.com"
|
||||
}]
|
||||
};
|
||||
|
||||
var mockClient = {
|
||||
requestCallUrlInfo: noop
|
||||
};
|
||||
|
||||
var mockConversationModel = new loop.shared.models.ConversationModel({
|
||||
callerId: "Mrs Jones",
|
||||
urlCreationDate: (new Date() / 1000).toString()
|
||||
}, {
|
||||
sdk: mockSDK
|
||||
});
|
||||
mockConversationModel.startSession = noop;
|
||||
|
||||
var mockWebSocket = new loop.CallConnectionWebSocket({
|
||||
url: "fake",
|
||||
callId: "fakeId",
|
||||
websocketToken: "fakeToken"
|
||||
});
|
||||
|
||||
var notifications = new loop.shared.models.NotificationCollection();
|
||||
var errNotifications = new loop.shared.models.NotificationCollection();
|
||||
errNotifications.add({
|
||||
level: "error",
|
||||
message: "Could Not Authenticate",
|
||||
details: "Did you change your password?",
|
||||
detailsButtonLabel: "Retry"
|
||||
});
|
||||
|
||||
var SVGIcon = React.createClass({displayName: "SVGIcon",
|
||||
render: function() {
|
||||
var sizeUnit = this.props.size.split("x")[0] + "px";
|
||||
return (
|
||||
React.createElement("span", {className: "svg-icon", style: {
|
||||
"backgroundImage": "url(../content/shared/img/icons-" + this.props.size +
|
||||
".svg#" + this.props.shapeId + ")",
|
||||
"backgroundSize": sizeUnit + " " + sizeUnit
|
||||
}})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SVGIcons = React.createClass({displayName: "SVGIcons",
|
||||
shapes: {
|
||||
"10x10": ["close", "close-active", "close-disabled", "dropdown",
|
||||
"dropdown-white", "dropdown-active", "dropdown-disabled", "edit",
|
||||
"edit-active", "edit-disabled", "expand", "expand-active", "expand-disabled",
|
||||
"minimize", "minimize-active", "minimize-disabled"
|
||||
],
|
||||
"14x14": ["audio", "audio-active", "audio-disabled", "facemute",
|
||||
"facemute-active", "facemute-disabled", "hangup", "hangup-active",
|
||||
"hangup-disabled", "incoming", "incoming-active", "incoming-disabled",
|
||||
"link", "link-active", "link-disabled", "mute", "mute-active",
|
||||
"mute-disabled", "pause", "pause-active", "pause-disabled", "video",
|
||||
"video-white", "video-active", "video-disabled", "volume", "volume-active",
|
||||
"volume-disabled"
|
||||
],
|
||||
"16x16": ["add", "add-hover", "add-active", "audio", "audio-hover", "audio-active",
|
||||
"block", "block-red", "block-hover", "block-active", "contacts", "contacts-hover",
|
||||
"contacts-active", "copy", "checkmark", "google", "google-hover", "google-active",
|
||||
"history", "history-hover", "history-active", "leave", "precall", "precall-hover",
|
||||
"precall-active", "screen-white", "screenmute-white", "settings",
|
||||
"settings-hover", "settings-active", "share-darkgrey", "tag", "tag-hover",
|
||||
"tag-active", "trash", "unblock", "unblock-hover", "unblock-active", "video",
|
||||
"video-hover", "video-active", "tour"
|
||||
]
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var icons = this.shapes[this.props.size].map(function(shapeId, i) {
|
||||
return (
|
||||
React.createElement("li", {key: this.props.size + "-" + i, className: "svg-icon-entry"},
|
||||
React.createElement("p", null, React.createElement(SVGIcon, {shapeId: shapeId, size: this.props.size})),
|
||||
React.createElement("p", null, shapeId)
|
||||
)
|
||||
);
|
||||
}, this);
|
||||
return (
|
||||
React.createElement("ul", {className: "svg-icon-list"}, icons)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Example = React.createClass({displayName: "Example",
|
||||
makeId: function(prefix) {
|
||||
return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
return (
|
||||
React.createElement("div", {className: "example"},
|
||||
React.createElement("h3", {id: this.makeId()},
|
||||
this.props.summary,
|
||||
React.createElement("a", {href: this.makeId("#")}, " ¶")
|
||||
),
|
||||
React.createElement("div", {className: cx({comp: true, dashed: this.props.dashed}),
|
||||
style: this.props.style},
|
||||
this.props.children
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Section = React.createClass({displayName: "Section",
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("section", {id: this.props.name, className: this.props.className},
|
||||
React.createElement("h1", null, this.props.name),
|
||||
this.props.children
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ShowCase = React.createClass({displayName: "ShowCase",
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("div", {className: "showcase"},
|
||||
React.createElement("header", null,
|
||||
React.createElement("h1", null, "Loop UI Components Showcase"),
|
||||
React.createElement("nav", {className: "showcase-menu"},
|
||||
React.Children.map(this.props.children, function(section) {
|
||||
return (
|
||||
React.createElement("a", {className: "btn btn-info", href: "#" + section.props.name},
|
||||
section.props.name
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
),
|
||||
this.props.children
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var App = React.createClass({displayName: "App",
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement(ShowCase, null,
|
||||
React.createElement(Section, {name: "PanelView"},
|
||||
React.createElement("p", {className: "note"},
|
||||
React.createElement("strong", null, "Note:"), " 332px wide."
|
||||
),
|
||||
React.createElement(Example, {summary: "Room list tab", dashed: "true", style: {width: "332px"}},
|
||||
React.createElement(PanelView, {client: mockClient, notifications: notifications,
|
||||
userProfile: {email: "test@example.com"},
|
||||
mozLoop: mockMozLoopRooms,
|
||||
dispatcher: dispatcher,
|
||||
roomStore: roomStore,
|
||||
selectedTab: "rooms"})
|
||||
),
|
||||
React.createElement(Example, {summary: "Contact list tab", dashed: "true", style: {width: "332px"}},
|
||||
React.createElement(PanelView, {client: mockClient, notifications: notifications,
|
||||
userProfile: {email: "test@example.com"},
|
||||
mozLoop: mockMozLoopRooms,
|
||||
dispatcher: dispatcher,
|
||||
roomStore: roomStore,
|
||||
selectedTab: "contacts"})
|
||||
),
|
||||
React.createElement(Example, {summary: "Error Notification", dashed: "true", style: {width: "332px"}},
|
||||
React.createElement(PanelView, {client: mockClient, notifications: errNotifications,
|
||||
mozLoop: navigator.mozLoop,
|
||||
dispatcher: dispatcher,
|
||||
roomStore: roomStore})
|
||||
),
|
||||
React.createElement(Example, {summary: "Error Notification - authenticated", dashed: "true", style: {width: "332px"}},
|
||||
React.createElement(PanelView, {client: mockClient, notifications: errNotifications,
|
||||
userProfile: {email: "test@example.com"},
|
||||
mozLoop: navigator.mozLoop,
|
||||
dispatcher: dispatcher,
|
||||
roomStore: roomStore})
|
||||
),
|
||||
React.createElement(Example, {summary: "Contact import success", dashed: "true", style: {width: "332px"}},
|
||||
React.createElement(PanelView, {notifications: new loop.shared.models.NotificationCollection([{level: "success", message: "Import success"}]),
|
||||
userProfile: {email: "test@example.com"},
|
||||
mozLoop: mockMozLoopRooms,
|
||||
dispatcher: dispatcher,
|
||||
roomStore: roomStore,
|
||||
selectedTab: "contacts"})
|
||||
),
|
||||
React.createElement(Example, {summary: "Contact import error", dashed: "true", style: {width: "332px"}},
|
||||
React.createElement(PanelView, {notifications: new loop.shared.models.NotificationCollection([{level: "error", message: "Import error"}]),
|
||||
userProfile: {email: "test@example.com"},
|
||||
mozLoop: mockMozLoopRooms,
|
||||
dispatcher: dispatcher,
|
||||
roomStore: roomStore,
|
||||
selectedTab: "contacts"})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "AcceptCallView"},
|
||||
React.createElement(Example, {summary: "Default / incoming video call", dashed: "true", style: {width: "300px", height: "272px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_VIDEO,
|
||||
callerId: "Mr Smith",
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: mockMozLoopRooms})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Default / incoming audio only call", dashed: "true", style: {width: "300px", height: "272px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_ONLY,
|
||||
callerId: "Mr Smith",
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: mockMozLoopRooms})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "AcceptCallView-ActiveState"},
|
||||
React.createElement(Example, {summary: "Default", dashed: "true", style: {width: "300px", height: "272px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_VIDEO,
|
||||
callerId: "Mr Smith",
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: mockMozLoopRooms,
|
||||
showMenu: true})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "ConversationToolbar"},
|
||||
React.createElement("h2", null, "Desktop Conversation Window"),
|
||||
React.createElement("div", {className: "fx-embedded override-position"},
|
||||
React.createElement(Example, {summary: "Default", dashed: "true", style: {width: "300px", height: "272px"}},
|
||||
React.createElement(ConversationToolbar, {video: {enabled: true},
|
||||
audio: {enabled: true},
|
||||
hangup: noop,
|
||||
publishStream: noop})
|
||||
),
|
||||
React.createElement(Example, {summary: "Video muted", style: {width: "300px", height: "272px"}},
|
||||
React.createElement(ConversationToolbar, {video: {enabled: false},
|
||||
audio: {enabled: true},
|
||||
hangup: noop,
|
||||
publishStream: noop})
|
||||
),
|
||||
React.createElement(Example, {summary: "Audio muted", style: {width: "300px", height: "272px"}},
|
||||
React.createElement(ConversationToolbar, {video: {enabled: true},
|
||||
audio: {enabled: false},
|
||||
hangup: noop,
|
||||
publishStream: noop})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement("h2", null, "Standalone"),
|
||||
React.createElement("div", {className: "standalone override-position"},
|
||||
React.createElement(Example, {summary: "Default"},
|
||||
React.createElement(ConversationToolbar, {video: {enabled: true},
|
||||
audio: {enabled: true},
|
||||
hangup: noop,
|
||||
publishStream: noop})
|
||||
),
|
||||
React.createElement(Example, {summary: "Video muted"},
|
||||
React.createElement(ConversationToolbar, {video: {enabled: false},
|
||||
audio: {enabled: true},
|
||||
hangup: noop,
|
||||
publishStream: noop})
|
||||
),
|
||||
React.createElement(Example, {summary: "Audio muted"},
|
||||
React.createElement(ConversationToolbar, {video: {enabled: true},
|
||||
audio: {enabled: false},
|
||||
hangup: noop,
|
||||
publishStream: noop})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "GumPromptConversationView"},
|
||||
React.createElement(Example, {summary: "Gum Prompt conversation view", dashed: "true"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(GumPromptConversationView, null)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "WaitingConversationView"},
|
||||
React.createElement(Example, {summary: "Waiting conversation view (connecting)", dashed: "true"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(WaitingConversationView, {websocket: mockWebSocket,
|
||||
dispatcher: dispatcher})
|
||||
)
|
||||
),
|
||||
React.createElement(Example, {summary: "Waiting conversation view (ringing)", dashed: "true"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(WaitingConversationView, {websocket: mockWebSocket,
|
||||
dispatcher: dispatcher,
|
||||
callState: "ringing"})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "PendingConversationView (Desktop)"},
|
||||
React.createElement(Example, {summary: "Connecting", dashed: "true",
|
||||
style: {width: "300px", height: "272px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(DesktopPendingConversationView, {callState: "gather",
|
||||
contact: mockContact,
|
||||
dispatcher: dispatcher})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "CallFailedView"},
|
||||
React.createElement(Example, {summary: "Call Failed - Incoming", dashed: "true",
|
||||
style: {width: "300px", height: "272px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(CallFailedView, {dispatcher: dispatcher,
|
||||
outgoing: false,
|
||||
store: conversationStore})
|
||||
)
|
||||
),
|
||||
React.createElement(Example, {summary: "Call Failed - Outgoing", dashed: "true",
|
||||
style: {width: "300px", height: "272px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(CallFailedView, {dispatcher: dispatcher,
|
||||
outgoing: true,
|
||||
store: conversationStore})
|
||||
)
|
||||
),
|
||||
React.createElement(Example, {summary: "Call Failed — with call URL error", dashed: "true",
|
||||
style: {width: "300px", height: "272px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(CallFailedView, {dispatcher: dispatcher, emailLinkError: true,
|
||||
outgoing: true,
|
||||
store: conversationStore})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "StartConversationView"},
|
||||
React.createElement(Example, {summary: "Start conversation view", dashed: "true"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(StartConversationView, {conversation: mockConversationModel,
|
||||
client: mockClient,
|
||||
notifications: notifications})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "FailedConversationView"},
|
||||
React.createElement(Example, {summary: "Failed conversation view", dashed: "true"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(FailedConversationView, {conversation: mockConversationModel,
|
||||
client: mockClient,
|
||||
notifications: notifications})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "ConversationView"},
|
||||
React.createElement(Example, {summary: "Desktop conversation window", dashed: "true",
|
||||
style: {width: "300px", height: "272px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(ConversationView, {sdk: mockSDK,
|
||||
model: mockConversationModel,
|
||||
video: {enabled: true},
|
||||
audio: {enabled: true}})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Desktop conversation window large", dashed: "true"},
|
||||
React.createElement("div", {className: "breakpoint", "data-breakpoint-width": "800px",
|
||||
"data-breakpoint-height": "600px"},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(ConversationView, {sdk: mockSDK,
|
||||
video: {enabled: true},
|
||||
audio: {enabled: true},
|
||||
model: mockConversationModel})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Desktop conversation window local audio stream",
|
||||
dashed: "true", style: {width: "300px", height: "272px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(ConversationView, {sdk: mockSDK,
|
||||
video: {enabled: false},
|
||||
audio: {enabled: true},
|
||||
model: mockConversationModel})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Standalone version"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(ConversationView, {sdk: mockSDK,
|
||||
video: {enabled: true},
|
||||
audio: {enabled: true},
|
||||
model: mockConversationModel})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "ConversationView-640"},
|
||||
React.createElement(Example, {summary: "640px breakpoint for conversation view"},
|
||||
React.createElement("div", {className: "breakpoint",
|
||||
style: {"text-align":"center"},
|
||||
"data-breakpoint-width": "400px",
|
||||
"data-breakpoint-height": "780px"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(ConversationView, {sdk: mockSDK,
|
||||
video: {enabled: true},
|
||||
audio: {enabled: true},
|
||||
model: mockConversationModel})
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "ConversationView-LocalAudio"},
|
||||
React.createElement(Example, {summary: "Local stream is audio only"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(ConversationView, {sdk: mockSDK,
|
||||
video: {enabled: false},
|
||||
audio: {enabled: true},
|
||||
model: mockConversationModel})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "FeedbackView"},
|
||||
React.createElement("p", {className: "note"},
|
||||
React.createElement("strong", null, "Note:"), " For the useable demo, you can access submitted data at ",
|
||||
React.createElement("a", {href: "https://input.allizom.org/"}, "input.allizom.org"), "."
|
||||
),
|
||||
React.createElement(Example, {summary: "Default (useable demo)", dashed: "true", style: {width: "300px", height: "272px"}},
|
||||
React.createElement(FeedbackView, {feedbackStore: feedbackStore})
|
||||
),
|
||||
React.createElement(Example, {summary: "Detailed form", dashed: "true", style: {width: "300px", height: "272px"}},
|
||||
React.createElement(FeedbackView, {feedbackStore: feedbackStore, feedbackState: FEEDBACK_STATES.DETAILS})
|
||||
),
|
||||
React.createElement(Example, {summary: "Thank you!", dashed: "true", style: {width: "300px", height: "272px"}},
|
||||
React.createElement(FeedbackView, {feedbackStore: feedbackStore, feedbackState: FEEDBACK_STATES.SENT})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "CallUrlExpiredView"},
|
||||
React.createElement(Example, {summary: "Firefox User"},
|
||||
React.createElement(CallUrlExpiredView, {isFirefox: true})
|
||||
),
|
||||
React.createElement(Example, {summary: "Non-Firefox User"},
|
||||
React.createElement(CallUrlExpiredView, {isFirefox: false})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "EndedConversationView"},
|
||||
React.createElement(Example, {summary: "Displays the feedback form"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(EndedConversationView, {sdk: mockSDK,
|
||||
video: {enabled: true},
|
||||
audio: {enabled: true},
|
||||
conversation: mockConversationModel,
|
||||
feedbackStore: feedbackStore,
|
||||
onAfterFeedbackReceived: noop})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "AlertMessages"},
|
||||
React.createElement(Example, {summary: "Various alerts"},
|
||||
React.createElement("div", {className: "alert alert-warning"},
|
||||
React.createElement("button", {className: "close"}),
|
||||
React.createElement("p", {className: "message"},
|
||||
"The person you were calling has ended the conversation."
|
||||
)
|
||||
),
|
||||
React.createElement("br", null),
|
||||
React.createElement("div", {className: "alert alert-error"},
|
||||
React.createElement("button", {className: "close"}),
|
||||
React.createElement("p", {className: "message"},
|
||||
"The person you were calling has ended the conversation."
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "HomeView"},
|
||||
React.createElement(Example, {summary: "Standalone Home View"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(HomeView, null)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
|
||||
React.createElement(Section, {name: "UnsupportedBrowserView"},
|
||||
React.createElement(Example, {summary: "Standalone Unsupported Browser"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(UnsupportedBrowserView, {isFirefox: false})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "UnsupportedDeviceView"},
|
||||
React.createElement(Example, {summary: "Standalone Unsupported Device"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(UnsupportedDeviceView, {platform: "ios"})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "DesktopRoomConversationView"},
|
||||
React.createElement(Example, {summary: "Desktop room conversation (invitation)", dashed: "true",
|
||||
style: {width: "260px", height: "265px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(DesktopRoomConversationView, {
|
||||
roomStore: roomStore,
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: navigator.mozLoop,
|
||||
roomState: ROOM_STATES.INIT})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Desktop room conversation", dashed: "true",
|
||||
style: {width: "260px", height: "265px"}},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(DesktopRoomConversationView, {
|
||||
roomStore: roomStore,
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: navigator.mozLoop,
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "StandaloneRoomView"},
|
||||
React.createElement(Example, {summary: "Standalone room conversation (ready)"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(StandaloneRoomView, {
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.READY,
|
||||
isFirefox: true})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Standalone room conversation (joined)"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(StandaloneRoomView, {
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.JOINED,
|
||||
isFirefox: true})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Standalone room conversation (has-participants)"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(StandaloneRoomView, {
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
isFirefox: true})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Standalone room conversation (full - FFx user)"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(StandaloneRoomView, {
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.FULL,
|
||||
isFirefox: true})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Standalone room conversation (full - non FFx user)"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(StandaloneRoomView, {
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.FULL,
|
||||
isFirefox: false})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Standalone room conversation (feedback)"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(StandaloneRoomView, {
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
feedbackStore: feedbackStore,
|
||||
roomState: ROOM_STATES.ENDED,
|
||||
isFirefox: false})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {summary: "Standalone room conversation (failed)"},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(StandaloneRoomView, {
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.FAILED,
|
||||
isFirefox: false})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "SVG icons preview", className: "svg-icons"},
|
||||
React.createElement(Example, {summary: "10x10"},
|
||||
React.createElement(SVGIcons, {size: "10x10"})
|
||||
),
|
||||
React.createElement(Example, {summary: "14x14"},
|
||||
React.createElement(SVGIcons, {size: "14x14"})
|
||||
),
|
||||
React.createElement(Example, {summary: "16x16"},
|
||||
React.createElement(SVGIcons, {size: "16x16"})
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Render components that have different styles across
|
||||
* CSS media rules in their own iframe to mimic the viewport
|
||||
* */
|
||||
function _renderComponentsInIframes() {
|
||||
var parents = document.querySelectorAll('.breakpoint');
|
||||
[].forEach.call(parents, appendChildInIframe);
|
||||
|
||||
/**
|
||||
* Extracts the component from the DOM and appends in the an iframe
|
||||
*
|
||||
* @type {HTMLElement} parent - Parent DOM node of a component & iframe
|
||||
* */
|
||||
function appendChildInIframe(parent) {
|
||||
var styles = document.querySelector('head').children;
|
||||
var component = parent.children[0];
|
||||
var iframe = document.createElement('iframe');
|
||||
var width = parent.dataset.breakpointWidth;
|
||||
var height = parent.dataset.breakpointHeight;
|
||||
|
||||
iframe.style.width = width;
|
||||
iframe.style.height = height;
|
||||
|
||||
parent.appendChild(iframe);
|
||||
iframe.src = "about:blank";
|
||||
// Workaround for bug 297685
|
||||
iframe.onload = function () {
|
||||
var iframeHead = iframe.contentDocument.querySelector('head');
|
||||
iframe.contentDocument.documentElement.querySelector('body')
|
||||
.appendChild(component);
|
||||
|
||||
[].forEach.call(styles, function(style) {
|
||||
iframeHead.appendChild(style.cloneNode(true));
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function() {
|
||||
try {
|
||||
React.renderComponent(React.createElement(App, null), document.getElementById("main"));
|
||||
|
||||
for (var listener of visibilityListeners) {
|
||||
listener({target: {hidden: false}});
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
uncaughtError = err;
|
||||
}
|
||||
|
||||
_renderComponentsInIframes();
|
||||
|
||||
// Put the title back, in case views changed it.
|
||||
document.title = "Loop UI Components Showcase";
|
||||
|
||||
// This simulates the mocha layout for errors which means we can run
|
||||
// this alongside our other unit tests but use the same harness.
|
||||
if (uncaughtError) {
|
||||
$("#results").append("<div class='failures'><em>1</em></div>");
|
||||
$("#results").append("<li class='test fail'>" +
|
||||
"<h2>Errors rendering UI-Showcase</h2>" +
|
||||
"<pre class='error'>" + uncaughtError + "\n" + uncaughtError.stack + "</pre>" +
|
||||
"</li>");
|
||||
} else {
|
||||
$("#results").append("<div class='failures'><em>0</em></div>");
|
||||
}
|
||||
$("#results").append("<p id='complete'>Complete.</p>");
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -1,810 +0,0 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
/* 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/. */
|
||||
|
||||
/* jshint newcap:false */
|
||||
/* global loop:true, React */
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
// Stop the default init functions running to avoid conflicts.
|
||||
document.removeEventListener('DOMContentLoaded', loop.panel.init);
|
||||
document.removeEventListener('DOMContentLoaded', loop.conversation.init);
|
||||
|
||||
// 1. Desktop components
|
||||
// 1.1 Panel
|
||||
var PanelView = loop.panel.PanelView;
|
||||
// 1.2. Conversation Window
|
||||
var AcceptCallView = loop.conversationViews.AcceptCallView;
|
||||
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
|
||||
var CallFailedView = loop.conversationViews.CallFailedView;
|
||||
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
|
||||
|
||||
// 2. Standalone webapp
|
||||
var HomeView = loop.webapp.HomeView;
|
||||
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
|
||||
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
|
||||
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
|
||||
var GumPromptConversationView = loop.webapp.GumPromptConversationView;
|
||||
var WaitingConversationView = loop.webapp.WaitingConversationView;
|
||||
var StartConversationView = loop.webapp.StartConversationView;
|
||||
var FailedConversationView = loop.webapp.FailedConversationView;
|
||||
var EndedConversationView = loop.webapp.EndedConversationView;
|
||||
var StandaloneRoomView = loop.standaloneRoomViews.StandaloneRoomView;
|
||||
|
||||
// 3. Shared components
|
||||
var ConversationToolbar = loop.shared.views.ConversationToolbar;
|
||||
var ConversationView = loop.shared.views.ConversationView;
|
||||
var FeedbackView = loop.shared.views.FeedbackView;
|
||||
|
||||
// Store constants
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
|
||||
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
|
||||
|
||||
// Local helpers
|
||||
function returnTrue() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function returnFalse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function noop(){}
|
||||
|
||||
// We save the visibility change listeners so that we can fake an event
|
||||
// to the panel once we've loaded all the views.
|
||||
var visibilityListeners = [];
|
||||
var rootObject = window;
|
||||
|
||||
rootObject.document.addEventListener = function(eventName, func) {
|
||||
if (eventName === "visibilitychange") {
|
||||
visibilityListeners.push(func);
|
||||
}
|
||||
window.addEventListener(eventName, func);
|
||||
};
|
||||
|
||||
rootObject.document.removeEventListener = function(eventName, func) {
|
||||
if (eventName === "visibilitychange") {
|
||||
var index = visibilityListeners.indexOf(func);
|
||||
visibilityListeners.splice(index, 1);
|
||||
}
|
||||
window.removeEventListener(eventName, func);
|
||||
};
|
||||
|
||||
loop.shared.mixins.setRootObject(rootObject);
|
||||
|
||||
// Feedback API client configured to send data to the stage input server,
|
||||
// which is available at https://input.allizom.org
|
||||
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
|
||||
"https://input.allizom.org/api/v1/feedback", {
|
||||
product: "Loop"
|
||||
}
|
||||
);
|
||||
|
||||
var mockSDK = _.extend({}, Backbone.Events);
|
||||
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop,
|
||||
sdkDriver: mockSDK
|
||||
});
|
||||
var roomStore = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop
|
||||
});
|
||||
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||
feedbackClient: stageFeedbackApiClient
|
||||
});
|
||||
var conversationStore = new loop.store.ConversationStore(dispatcher, {
|
||||
client: {},
|
||||
mozLoop: navigator.mozLoop,
|
||||
sdkDriver: mockSDK
|
||||
});
|
||||
|
||||
loop.store.StoreMixin.register({
|
||||
conversationStore: conversationStore,
|
||||
feedbackStore: feedbackStore
|
||||
});
|
||||
|
||||
// Local mocks
|
||||
|
||||
var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
|
||||
|
||||
var mockContact = {
|
||||
name: ["Mr Smith"],
|
||||
email: [{
|
||||
value: "smith@invalid.com"
|
||||
}]
|
||||
};
|
||||
|
||||
var mockClient = {
|
||||
requestCallUrlInfo: noop
|
||||
};
|
||||
|
||||
var mockConversationModel = new loop.shared.models.ConversationModel({
|
||||
callerId: "Mrs Jones",
|
||||
urlCreationDate: (new Date() / 1000).toString()
|
||||
}, {
|
||||
sdk: mockSDK
|
||||
});
|
||||
mockConversationModel.startSession = noop;
|
||||
|
||||
var mockWebSocket = new loop.CallConnectionWebSocket({
|
||||
url: "fake",
|
||||
callId: "fakeId",
|
||||
websocketToken: "fakeToken"
|
||||
});
|
||||
|
||||
var notifications = new loop.shared.models.NotificationCollection();
|
||||
var errNotifications = new loop.shared.models.NotificationCollection();
|
||||
errNotifications.add({
|
||||
level: "error",
|
||||
message: "Could Not Authenticate",
|
||||
details: "Did you change your password?",
|
||||
detailsButtonLabel: "Retry"
|
||||
});
|
||||
|
||||
var SVGIcon = React.createClass({
|
||||
render: function() {
|
||||
var sizeUnit = this.props.size.split("x")[0] + "px";
|
||||
return (
|
||||
<span className="svg-icon" style={{
|
||||
"backgroundImage": "url(../content/shared/img/icons-" + this.props.size +
|
||||
".svg#" + this.props.shapeId + ")",
|
||||
"backgroundSize": sizeUnit + " " + sizeUnit
|
||||
}} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SVGIcons = React.createClass({
|
||||
shapes: {
|
||||
"10x10": ["close", "close-active", "close-disabled", "dropdown",
|
||||
"dropdown-white", "dropdown-active", "dropdown-disabled", "edit",
|
||||
"edit-active", "edit-disabled", "expand", "expand-active", "expand-disabled",
|
||||
"minimize", "minimize-active", "minimize-disabled"
|
||||
],
|
||||
"14x14": ["audio", "audio-active", "audio-disabled", "facemute",
|
||||
"facemute-active", "facemute-disabled", "hangup", "hangup-active",
|
||||
"hangup-disabled", "incoming", "incoming-active", "incoming-disabled",
|
||||
"link", "link-active", "link-disabled", "mute", "mute-active",
|
||||
"mute-disabled", "pause", "pause-active", "pause-disabled", "video",
|
||||
"video-white", "video-active", "video-disabled", "volume", "volume-active",
|
||||
"volume-disabled"
|
||||
],
|
||||
"16x16": ["add", "add-hover", "add-active", "audio", "audio-hover", "audio-active",
|
||||
"block", "block-red", "block-hover", "block-active", "contacts", "contacts-hover",
|
||||
"contacts-active", "copy", "checkmark", "google", "google-hover", "google-active",
|
||||
"history", "history-hover", "history-active", "leave", "precall", "precall-hover",
|
||||
"precall-active", "screen-white", "screenmute-white", "settings",
|
||||
"settings-hover", "settings-active", "share-darkgrey", "tag", "tag-hover",
|
||||
"tag-active", "trash", "unblock", "unblock-hover", "unblock-active", "video",
|
||||
"video-hover", "video-active", "tour"
|
||||
]
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var icons = this.shapes[this.props.size].map(function(shapeId, i) {
|
||||
return (
|
||||
<li key={this.props.size + "-" + i} className="svg-icon-entry">
|
||||
<p><SVGIcon shapeId={shapeId} size={this.props.size} /></p>
|
||||
<p>{shapeId}</p>
|
||||
</li>
|
||||
);
|
||||
}, this);
|
||||
return (
|
||||
<ul className="svg-icon-list">{icons}</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Example = React.createClass({
|
||||
makeId: function(prefix) {
|
||||
return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
return (
|
||||
<div className="example">
|
||||
<h3 id={this.makeId()}>
|
||||
{this.props.summary}
|
||||
<a href={this.makeId("#")}> ¶</a>
|
||||
</h3>
|
||||
<div className={cx({comp: true, dashed: this.props.dashed})}
|
||||
style={this.props.style}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Section = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<section id={this.props.name} className={this.props.className}>
|
||||
<h1>{this.props.name}</h1>
|
||||
{this.props.children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ShowCase = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div className="showcase">
|
||||
<header>
|
||||
<h1>Loop UI Components Showcase</h1>
|
||||
<nav className="showcase-menu">{
|
||||
React.Children.map(this.props.children, function(section) {
|
||||
return (
|
||||
<a className="btn btn-info" href={"#" + section.props.name}>
|
||||
{section.props.name}
|
||||
</a>
|
||||
);
|
||||
})
|
||||
}</nav>
|
||||
</header>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var App = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<ShowCase>
|
||||
<Section name="PanelView">
|
||||
<p className="note">
|
||||
<strong>Note:</strong> 332px wide.
|
||||
</p>
|
||||
<Example summary="Room list tab" dashed="true" style={{width: "332px"}}>
|
||||
<PanelView client={mockClient} notifications={notifications}
|
||||
userProfile={{email: "test@example.com"}}
|
||||
mozLoop={mockMozLoopRooms}
|
||||
dispatcher={dispatcher}
|
||||
roomStore={roomStore}
|
||||
selectedTab="rooms" />
|
||||
</Example>
|
||||
<Example summary="Contact list tab" dashed="true" style={{width: "332px"}}>
|
||||
<PanelView client={mockClient} notifications={notifications}
|
||||
userProfile={{email: "test@example.com"}}
|
||||
mozLoop={mockMozLoopRooms}
|
||||
dispatcher={dispatcher}
|
||||
roomStore={roomStore}
|
||||
selectedTab="contacts" />
|
||||
</Example>
|
||||
<Example summary="Error Notification" dashed="true" style={{width: "332px"}}>
|
||||
<PanelView client={mockClient} notifications={errNotifications}
|
||||
mozLoop={navigator.mozLoop}
|
||||
dispatcher={dispatcher}
|
||||
roomStore={roomStore} />
|
||||
</Example>
|
||||
<Example summary="Error Notification - authenticated" dashed="true" style={{width: "332px"}}>
|
||||
<PanelView client={mockClient} notifications={errNotifications}
|
||||
userProfile={{email: "test@example.com"}}
|
||||
mozLoop={navigator.mozLoop}
|
||||
dispatcher={dispatcher}
|
||||
roomStore={roomStore} />
|
||||
</Example>
|
||||
<Example summary="Contact import success" dashed="true" style={{width: "332px"}}>
|
||||
<PanelView notifications={new loop.shared.models.NotificationCollection([{level: "success", message: "Import success"}])}
|
||||
userProfile={{email: "test@example.com"}}
|
||||
mozLoop={mockMozLoopRooms}
|
||||
dispatcher={dispatcher}
|
||||
roomStore={roomStore}
|
||||
selectedTab="contacts" />
|
||||
</Example>
|
||||
<Example summary="Contact import error" dashed="true" style={{width: "332px"}}>
|
||||
<PanelView notifications={new loop.shared.models.NotificationCollection([{level: "error", message: "Import error"}])}
|
||||
userProfile={{email: "test@example.com"}}
|
||||
mozLoop={mockMozLoopRooms}
|
||||
dispatcher={dispatcher}
|
||||
roomStore={roomStore}
|
||||
selectedTab="contacts" />
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="AcceptCallView">
|
||||
<Example summary="Default / incoming video call" dashed="true" style={{width: "300px", height: "272px"}}>
|
||||
<div className="fx-embedded">
|
||||
<AcceptCallView callType={CALL_TYPES.AUDIO_VIDEO}
|
||||
callerId="Mr Smith"
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={mockMozLoopRooms} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Default / incoming audio only call" dashed="true" style={{width: "300px", height: "272px"}}>
|
||||
<div className="fx-embedded">
|
||||
<AcceptCallView callType={CALL_TYPES.AUDIO_ONLY}
|
||||
callerId="Mr Smith"
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={mockMozLoopRooms} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="AcceptCallView-ActiveState">
|
||||
<Example summary="Default" dashed="true" style={{width: "300px", height: "272px"}}>
|
||||
<div className="fx-embedded" >
|
||||
<AcceptCallView callType={CALL_TYPES.AUDIO_VIDEO}
|
||||
callerId="Mr Smith"
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={mockMozLoopRooms}
|
||||
showMenu={true} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="ConversationToolbar">
|
||||
<h2>Desktop Conversation Window</h2>
|
||||
<div className="fx-embedded override-position">
|
||||
<Example summary="Default" dashed="true" style={{width: "300px", height: "272px"}}>
|
||||
<ConversationToolbar video={{enabled: true}}
|
||||
audio={{enabled: true}}
|
||||
hangup={noop}
|
||||
publishStream={noop} />
|
||||
</Example>
|
||||
<Example summary="Video muted" style={{width: "300px", height: "272px"}}>
|
||||
<ConversationToolbar video={{enabled: false}}
|
||||
audio={{enabled: true}}
|
||||
hangup={noop}
|
||||
publishStream={noop} />
|
||||
</Example>
|
||||
<Example summary="Audio muted" style={{width: "300px", height: "272px"}}>
|
||||
<ConversationToolbar video={{enabled: true}}
|
||||
audio={{enabled: false}}
|
||||
hangup={noop}
|
||||
publishStream={noop} />
|
||||
</Example>
|
||||
</div>
|
||||
|
||||
<h2>Standalone</h2>
|
||||
<div className="standalone override-position">
|
||||
<Example summary="Default">
|
||||
<ConversationToolbar video={{enabled: true}}
|
||||
audio={{enabled: true}}
|
||||
hangup={noop}
|
||||
publishStream={noop} />
|
||||
</Example>
|
||||
<Example summary="Video muted">
|
||||
<ConversationToolbar video={{enabled: false}}
|
||||
audio={{enabled: true}}
|
||||
hangup={noop}
|
||||
publishStream={noop} />
|
||||
</Example>
|
||||
<Example summary="Audio muted">
|
||||
<ConversationToolbar video={{enabled: true}}
|
||||
audio={{enabled: false}}
|
||||
hangup={noop}
|
||||
publishStream={noop} />
|
||||
</Example>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section name="GumPromptConversationView">
|
||||
<Example summary="Gum Prompt conversation view" dashed="true">
|
||||
<div className="standalone">
|
||||
<GumPromptConversationView />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="WaitingConversationView">
|
||||
<Example summary="Waiting conversation view (connecting)" dashed="true">
|
||||
<div className="standalone">
|
||||
<WaitingConversationView websocket={mockWebSocket}
|
||||
dispatcher={dispatcher} />
|
||||
</div>
|
||||
</Example>
|
||||
<Example summary="Waiting conversation view (ringing)" dashed="true">
|
||||
<div className="standalone">
|
||||
<WaitingConversationView websocket={mockWebSocket}
|
||||
dispatcher={dispatcher}
|
||||
callState="ringing"/>
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="PendingConversationView (Desktop)">
|
||||
<Example summary="Connecting" dashed="true"
|
||||
style={{width: "300px", height: "272px"}}>
|
||||
<div className="fx-embedded">
|
||||
<DesktopPendingConversationView callState={"gather"}
|
||||
contact={mockContact}
|
||||
dispatcher={dispatcher} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="CallFailedView">
|
||||
<Example summary="Call Failed - Incoming" dashed="true"
|
||||
style={{width: "300px", height: "272px"}}>
|
||||
<div className="fx-embedded">
|
||||
<CallFailedView dispatcher={dispatcher}
|
||||
outgoing={false}
|
||||
store={conversationStore} />
|
||||
</div>
|
||||
</Example>
|
||||
<Example summary="Call Failed - Outgoing" dashed="true"
|
||||
style={{width: "300px", height: "272px"}}>
|
||||
<div className="fx-embedded">
|
||||
<CallFailedView dispatcher={dispatcher}
|
||||
outgoing={true}
|
||||
store={conversationStore} />
|
||||
</div>
|
||||
</Example>
|
||||
<Example summary="Call Failed — with call URL error" dashed="true"
|
||||
style={{width: "300px", height: "272px"}}>
|
||||
<div className="fx-embedded">
|
||||
<CallFailedView dispatcher={dispatcher} emailLinkError={true}
|
||||
outgoing={true}
|
||||
store={conversationStore} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="StartConversationView">
|
||||
<Example summary="Start conversation view" dashed="true">
|
||||
<div className="standalone">
|
||||
<StartConversationView conversation={mockConversationModel}
|
||||
client={mockClient}
|
||||
notifications={notifications} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="FailedConversationView">
|
||||
<Example summary="Failed conversation view" dashed="true">
|
||||
<div className="standalone">
|
||||
<FailedConversationView conversation={mockConversationModel}
|
||||
client={mockClient}
|
||||
notifications={notifications} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="ConversationView">
|
||||
<Example summary="Desktop conversation window" dashed="true"
|
||||
style={{width: "300px", height: "272px"}}>
|
||||
<div className="fx-embedded">
|
||||
<ConversationView sdk={mockSDK}
|
||||
model={mockConversationModel}
|
||||
video={{enabled: true}}
|
||||
audio={{enabled: true}} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Desktop conversation window large" dashed="true">
|
||||
<div className="breakpoint" data-breakpoint-width="800px"
|
||||
data-breakpoint-height="600px">
|
||||
<div className="fx-embedded">
|
||||
<ConversationView sdk={mockSDK}
|
||||
video={{enabled: true}}
|
||||
audio={{enabled: true}}
|
||||
model={mockConversationModel} />
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Desktop conversation window local audio stream"
|
||||
dashed="true" style={{width: "300px", height: "272px"}}>
|
||||
<div className="fx-embedded">
|
||||
<ConversationView sdk={mockSDK}
|
||||
video={{enabled: false}}
|
||||
audio={{enabled: true}}
|
||||
model={mockConversationModel} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Standalone version">
|
||||
<div className="standalone">
|
||||
<ConversationView sdk={mockSDK}
|
||||
video={{enabled: true}}
|
||||
audio={{enabled: true}}
|
||||
model={mockConversationModel} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="ConversationView-640">
|
||||
<Example summary="640px breakpoint for conversation view">
|
||||
<div className="breakpoint"
|
||||
style={{"text-align":"center"}}
|
||||
data-breakpoint-width="400px"
|
||||
data-breakpoint-height="780px">
|
||||
<div className="standalone">
|
||||
<ConversationView sdk={mockSDK}
|
||||
video={{enabled: true}}
|
||||
audio={{enabled: true}}
|
||||
model={mockConversationModel} />
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="ConversationView-LocalAudio">
|
||||
<Example summary="Local stream is audio only">
|
||||
<div className="standalone">
|
||||
<ConversationView sdk={mockSDK}
|
||||
video={{enabled: false}}
|
||||
audio={{enabled: true}}
|
||||
model={mockConversationModel} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="FeedbackView">
|
||||
<p className="note">
|
||||
<strong>Note:</strong> For the useable demo, you can access submitted data at
|
||||
<a href="https://input.allizom.org/">input.allizom.org</a>.
|
||||
</p>
|
||||
<Example summary="Default (useable demo)" dashed="true" style={{width: "300px", height: "272px"}}>
|
||||
<FeedbackView feedbackStore={feedbackStore} />
|
||||
</Example>
|
||||
<Example summary="Detailed form" dashed="true" style={{width: "300px", height: "272px"}}>
|
||||
<FeedbackView feedbackStore={feedbackStore} feedbackState={FEEDBACK_STATES.DETAILS} />
|
||||
</Example>
|
||||
<Example summary="Thank you!" dashed="true" style={{width: "300px", height: "272px"}}>
|
||||
<FeedbackView feedbackStore={feedbackStore} feedbackState={FEEDBACK_STATES.SENT} />
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="CallUrlExpiredView">
|
||||
<Example summary="Firefox User">
|
||||
<CallUrlExpiredView isFirefox={true} />
|
||||
</Example>
|
||||
<Example summary="Non-Firefox User">
|
||||
<CallUrlExpiredView isFirefox={false} />
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="EndedConversationView">
|
||||
<Example summary="Displays the feedback form">
|
||||
<div className="standalone">
|
||||
<EndedConversationView sdk={mockSDK}
|
||||
video={{enabled: true}}
|
||||
audio={{enabled: true}}
|
||||
conversation={mockConversationModel}
|
||||
feedbackStore={feedbackStore}
|
||||
onAfterFeedbackReceived={noop} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="AlertMessages">
|
||||
<Example summary="Various alerts">
|
||||
<div className="alert alert-warning">
|
||||
<button className="close"></button>
|
||||
<p className="message">
|
||||
The person you were calling has ended the conversation.
|
||||
</p>
|
||||
</div>
|
||||
<br />
|
||||
<div className="alert alert-error">
|
||||
<button className="close"></button>
|
||||
<p className="message">
|
||||
The person you were calling has ended the conversation.
|
||||
</p>
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="HomeView">
|
||||
<Example summary="Standalone Home View">
|
||||
<div className="standalone">
|
||||
<HomeView />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
|
||||
<Section name="UnsupportedBrowserView">
|
||||
<Example summary="Standalone Unsupported Browser">
|
||||
<div className="standalone">
|
||||
<UnsupportedBrowserView isFirefox={false}/>
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="UnsupportedDeviceView">
|
||||
<Example summary="Standalone Unsupported Device">
|
||||
<div className="standalone">
|
||||
<UnsupportedDeviceView platform="ios"/>
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="DesktopRoomConversationView">
|
||||
<Example summary="Desktop room conversation (invitation)" dashed="true"
|
||||
style={{width: "260px", height: "265px"}}>
|
||||
<div className="fx-embedded">
|
||||
<DesktopRoomConversationView
|
||||
roomStore={roomStore}
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={navigator.mozLoop}
|
||||
roomState={ROOM_STATES.INIT} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Desktop room conversation" dashed="true"
|
||||
style={{width: "260px", height: "265px"}}>
|
||||
<div className="fx-embedded">
|
||||
<DesktopRoomConversationView
|
||||
roomStore={roomStore}
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={navigator.mozLoop}
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="StandaloneRoomView">
|
||||
<Example summary="Standalone room conversation (ready)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.READY}
|
||||
isFirefox={true} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Standalone room conversation (joined)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.JOINED}
|
||||
isFirefox={true} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Standalone room conversation (has-participants)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS}
|
||||
isFirefox={true} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Standalone room conversation (full - FFx user)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.FULL}
|
||||
isFirefox={true} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Standalone room conversation (full - non FFx user)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.FULL}
|
||||
isFirefox={false} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Standalone room conversation (feedback)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
feedbackStore={feedbackStore}
|
||||
roomState={ROOM_STATES.ENDED}
|
||||
isFirefox={false} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Standalone room conversation (failed)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.FAILED}
|
||||
isFirefox={false} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="SVG icons preview" className="svg-icons">
|
||||
<Example summary="10x10">
|
||||
<SVGIcons size="10x10"/>
|
||||
</Example>
|
||||
<Example summary="14x14">
|
||||
<SVGIcons size="14x14" />
|
||||
</Example>
|
||||
<Example summary="16x16">
|
||||
<SVGIcons size="16x16"/>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
</ShowCase>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Render components that have different styles across
|
||||
* CSS media rules in their own iframe to mimic the viewport
|
||||
* */
|
||||
function _renderComponentsInIframes() {
|
||||
var parents = document.querySelectorAll('.breakpoint');
|
||||
[].forEach.call(parents, appendChildInIframe);
|
||||
|
||||
/**
|
||||
* Extracts the component from the DOM and appends in the an iframe
|
||||
*
|
||||
* @type {HTMLElement} parent - Parent DOM node of a component & iframe
|
||||
* */
|
||||
function appendChildInIframe(parent) {
|
||||
var styles = document.querySelector('head').children;
|
||||
var component = parent.children[0];
|
||||
var iframe = document.createElement('iframe');
|
||||
var width = parent.dataset.breakpointWidth;
|
||||
var height = parent.dataset.breakpointHeight;
|
||||
|
||||
iframe.style.width = width;
|
||||
iframe.style.height = height;
|
||||
|
||||
parent.appendChild(iframe);
|
||||
iframe.src = "about:blank";
|
||||
// Workaround for bug 297685
|
||||
iframe.onload = function () {
|
||||
var iframeHead = iframe.contentDocument.querySelector('head');
|
||||
iframe.contentDocument.documentElement.querySelector('body')
|
||||
.appendChild(component);
|
||||
|
||||
[].forEach.call(styles, function(style) {
|
||||
iframeHead.appendChild(style.cloneNode(true));
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function() {
|
||||
try {
|
||||
React.renderComponent(<App />, document.getElementById("main"));
|
||||
|
||||
for (var listener of visibilityListeners) {
|
||||
listener({target: {hidden: false}});
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
uncaughtError = err;
|
||||
}
|
||||
|
||||
_renderComponentsInIframes();
|
||||
|
||||
// Put the title back, in case views changed it.
|
||||
document.title = "Loop UI Components Showcase";
|
||||
|
||||
// This simulates the mocha layout for errors which means we can run
|
||||
// this alongside our other unit tests but use the same harness.
|
||||
if (uncaughtError) {
|
||||
$("#results").append("<div class='failures'><em>1</em></div>");
|
||||
$("#results").append("<li class='test fail'>" +
|
||||
"<h2>Errors rendering UI-Showcase</h2>" +
|
||||
"<pre class='error'>" + uncaughtError + "\n" + uncaughtError.stack + "</pre>" +
|
||||
"</li>");
|
||||
} else {
|
||||
$("#results").append("<div class='failures'><em>0</em></div>");
|
||||
}
|
||||
$("#results").append("<p id='complete'>Complete.</p>");
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -161,7 +161,7 @@ FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileD
|
||||
}
|
||||
}
|
||||
|
||||
// FHR related migrations.
|
||||
// Telemetry related migrations.
|
||||
let times = {
|
||||
name: "times", // name is used only by tests.
|
||||
type: types.OTHERDATA,
|
||||
@@ -178,69 +178,60 @@ FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileD
|
||||
);
|
||||
}
|
||||
};
|
||||
let healthReporter = {
|
||||
name: "healthreporter", // name is used only by tests...
|
||||
let telemetry = {
|
||||
name: "telemetry", // name is used only by tests...
|
||||
type: types.OTHERDATA,
|
||||
migrate: aCallback => {
|
||||
// the health-reporter can't have been initialized yet so it's safe to
|
||||
// copy the SQL file.
|
||||
let createSubDir = (name) => {
|
||||
let dir = currentProfileDir.clone();
|
||||
dir.append(name);
|
||||
dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
||||
return dir;
|
||||
};
|
||||
|
||||
// We only support the default database name - copied from healthreporter.jsm
|
||||
const DEFAULT_DATABASE_NAME = "healthreport.sqlite";
|
||||
let path = OS.Path.join(sourceProfileDir.path, DEFAULT_DATABASE_NAME);
|
||||
let sqliteFile = FileUtils.File(path);
|
||||
if (sqliteFile.exists()) {
|
||||
sqliteFile.copyTo(currentProfileDir, "");
|
||||
}
|
||||
// In unusual cases there may be 2 additional files - a "write ahead log"
|
||||
// (-wal) file and a "shared memory file" (-shm). The wal file contains
|
||||
// data that will be replayed when the DB is next opened, while the shm
|
||||
// file is ignored in that case - the replay happens using only the wal.
|
||||
// So we *do* copy a wal if it exists, but not a shm.
|
||||
// See https://www.sqlite.org/tempfiles.html for more.
|
||||
// (Note also we attempt these copies even if we can't find the DB, and
|
||||
// rely on FHR itself to do the right thing if it can)
|
||||
path = OS.Path.join(sourceProfileDir.path, DEFAULT_DATABASE_NAME + "-wal");
|
||||
let sqliteWal = FileUtils.File(path);
|
||||
if (sqliteWal.exists()) {
|
||||
sqliteWal.copyTo(currentProfileDir, "");
|
||||
}
|
||||
|
||||
// If the 'healthreport' directory exists we copy everything from it.
|
||||
let subdir = this._getFileObject(sourceProfileDir, "healthreport");
|
||||
// If the 'datareporting' directory exists we migrate files from it.
|
||||
let haveStateFile = false;
|
||||
let subdir = this._getFileObject(sourceProfileDir, "datareporting");
|
||||
if (subdir && subdir.isDirectory()) {
|
||||
// Copy all regular files.
|
||||
let dest = currentProfileDir.clone();
|
||||
dest.append("healthreport");
|
||||
dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE,
|
||||
FileUtils.PERMS_DIRECTORY);
|
||||
// Copy only specific files.
|
||||
let toCopy = ["state.json", "session-state.json"];
|
||||
|
||||
let dest = createSubDir("datareporting");
|
||||
let enumerator = subdir.directoryEntries;
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let file = enumerator.getNext().QueryInterface(Components.interfaces.nsIFile);
|
||||
if (file.isDirectory()) {
|
||||
let file = enumerator.getNext().QueryInterface(Ci.nsIFile);
|
||||
if (file.isDirectory() || toCopy.indexOf(file.leafName) == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.leafName == "state.json") {
|
||||
haveStateFile = true;
|
||||
}
|
||||
file.copyTo(dest, "");
|
||||
}
|
||||
}
|
||||
// If the 'datareporting' directory exists we copy just state.json
|
||||
subdir = this._getFileObject(sourceProfileDir, "datareporting");
|
||||
if (subdir && subdir.isDirectory()) {
|
||||
let stateFile = this._getFileObject(subdir, "state.json");
|
||||
if (stateFile) {
|
||||
let dest = currentProfileDir.clone();
|
||||
dest.append("datareporting");
|
||||
dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE,
|
||||
FileUtils.PERMS_DIRECTORY);
|
||||
stateFile.copyTo(dest, "");
|
||||
|
||||
if (!haveStateFile) {
|
||||
// Fall back to migrating the state file that contains the client id from healthreport/.
|
||||
// We first moved the client id management from the FHR implementation to the datareporting
|
||||
// service.
|
||||
// Consequently, we try to migrate an existing FHR state file here as a fallback.
|
||||
let subdir = this._getFileObject(sourceProfileDir, "healthreport");
|
||||
if (subdir && subdir.isDirectory()) {
|
||||
let stateFile = this._getFileObject(subdir, "state.json");
|
||||
if (stateFile) {
|
||||
let dest = createSubDir("healthreport");
|
||||
stateFile.copyTo(dest, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aCallback(true);
|
||||
}
|
||||
}
|
||||
|
||||
return [places, cookies, passwords, formData, dictionary, bookmarksBackups,
|
||||
session, times, healthReporter].filter(r => r);
|
||||
session, times, telemetry].filter(r => r);
|
||||
};
|
||||
|
||||
Object.defineProperty(FirefoxProfileMigrator.prototype, "startupOnlyMigrator", {
|
||||
|
||||
@@ -13,7 +13,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource:///modules/MigrationUtils.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource://gre/modules/LoginHelper.jsm");
|
||||
|
||||
Cu.importGlobalProperties(['FileReader']);
|
||||
@@ -275,9 +274,9 @@ CtypesVaultHelpers.prototype = {
|
||||
function hostIsIPAddress(aHost) {
|
||||
try {
|
||||
Services.eTLD.getBaseDomainFromHost(aHost);
|
||||
} catch (e if e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
|
||||
return true;
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -514,7 +513,7 @@ Cookies.prototype = {
|
||||
migrate(aCallback) {
|
||||
this.ctypesKernelHelpers = new CtypesKernelHelpers();
|
||||
|
||||
let cookiesGenerator = (function genCookie() {
|
||||
let cookiesGenerator = (function* genCookie() {
|
||||
let success = false;
|
||||
let folders = this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE ?
|
||||
this.__cookiesFolders : [this.__cookiesFolder];
|
||||
@@ -587,19 +586,35 @@ Cookies.prototype = {
|
||||
* - Creation time least significant integer
|
||||
* - Record delimiter "*"
|
||||
*
|
||||
* Unfortunately, "*" can also occur inside the value of the cookie, so we
|
||||
* can't rely exclusively on it as a record separator.
|
||||
*
|
||||
* @note All the times are in FILETIME format.
|
||||
*/
|
||||
_parseCookieBuffer(aTextBuffer) {
|
||||
// Note the last record is an empty string.
|
||||
let records = [r for each (r in aTextBuffer.split("*\n")) if (r)];
|
||||
// Note the last record is an empty string...
|
||||
let records = [];
|
||||
let lines = aTextBuffer.split("\n");
|
||||
while (lines.length > 0) {
|
||||
let record = lines.splice(0, 9);
|
||||
// ... which means this is going to be a 1-element array for that record
|
||||
if (record.length > 1) {
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
for (let record of records) {
|
||||
let [name, value, hostpath, flags,
|
||||
expireTimeLo, expireTimeHi] = record.split("\n");
|
||||
expireTimeLo, expireTimeHi] = record;
|
||||
|
||||
// IE stores deleted cookies with a zero-length value, skip them.
|
||||
if (value.length == 0)
|
||||
continue;
|
||||
|
||||
// IE sometimes has cookies created by apps that use "~~local~~/local/file/path"
|
||||
// as the hostpath, ignore those:
|
||||
if (hostpath.startsWith("~~local~~"))
|
||||
continue;
|
||||
|
||||
let hostLen = hostpath.indexOf("/");
|
||||
let host = hostpath.substr(0, hostLen);
|
||||
let path = hostpath.substr(hostLen);
|
||||
@@ -695,8 +710,12 @@ function getTypedURLs(registryKeyPath) {
|
||||
} catch (ex) {
|
||||
Cu.reportError("Error reading typed URL history: " + ex);
|
||||
} finally {
|
||||
typedURLKey.close();
|
||||
typedURLTimeKey.close();
|
||||
if (typedURLKey) {
|
||||
typedURLKey.close();
|
||||
}
|
||||
if (typedURLTimeKey) {
|
||||
typedURLTimeKey.close();
|
||||
}
|
||||
cTypes.finalize();
|
||||
}
|
||||
return typedURLs;
|
||||
@@ -776,12 +795,21 @@ WindowsVaultFormPasswords.prototype = {
|
||||
if (!_isIEOrEdgePassword(item.contents.schemaId.id)) {
|
||||
continue;
|
||||
}
|
||||
let url = item.contents.pResourceElement.contents.itemValue.readString();
|
||||
let realURL;
|
||||
try {
|
||||
realURL = Services.io.newURI(url, null, null);
|
||||
} catch (ex) { /* leave realURL as null */ }
|
||||
if (!realURL || ["http", "https", "ftp"].indexOf(realURL.scheme) == -1) {
|
||||
// Ignore items for non-URLs or URLs that aren't HTTP(S)/FTP
|
||||
continue;
|
||||
}
|
||||
|
||||
// if aOnlyCheckExists is set to true, the purpose of the call is to return true if there is at
|
||||
// least a password which is true in this case because a password was by now already found
|
||||
if (aOnlyCheckExists) {
|
||||
return true;
|
||||
}
|
||||
let url = item.contents.pResourceElement.contents.itemValue.readString();
|
||||
let username = item.contents.pIdentityElement.contents.itemValue.readString();
|
||||
// the current login credential object
|
||||
let credential = new ctypesVaultHelpers._structs.VAULT_ELEMENT.ptr;
|
||||
@@ -808,7 +836,7 @@ WindowsVaultFormPasswords.prototype = {
|
||||
// create a new login
|
||||
let login = {
|
||||
username, password,
|
||||
hostname: NetUtil.newURI(url).prePath,
|
||||
hostname: realURL.prePath,
|
||||
timeCreated: creation,
|
||||
};
|
||||
LoginHelper.maybeImportLogin(login);
|
||||
|
||||
@@ -12,7 +12,6 @@ DIRS += [
|
||||
'downloads',
|
||||
'extensions',
|
||||
'feeds',
|
||||
'fuel',
|
||||
'migration',
|
||||
'newtab',
|
||||
'places',
|
||||
|
||||
@@ -15,30 +15,22 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
|
||||
"@mozilla.org/browser/aboutnewtab-service;1",
|
||||
"nsIAboutNewTabService");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
|
||||
"resource://gre/modules/Deprecated.jsm");
|
||||
|
||||
const DepecationURL = "https://bugzilla.mozilla.org/show_bug.cgi?id=1204983#c89";
|
||||
|
||||
this.NewTabURL = {
|
||||
|
||||
get: function() {
|
||||
Deprecated.warning("NewTabURL.get is deprecated, please query aboutNewTabService.newTabURL", DepecationURL);
|
||||
return aboutNewTabService.newTabURL;
|
||||
},
|
||||
|
||||
get overridden() {
|
||||
Deprecated.warning("NewTabURL.overridden is deprecated, please query aboutNewTabService.overridden", DepecationURL);
|
||||
return aboutNewTabService.overridden;
|
||||
},
|
||||
|
||||
override: function(newURL) {
|
||||
Deprecated.warning("NewTabURL.override is deprecated, please set aboutNewTabService.newTabURL", DepecationURL);
|
||||
aboutNewTabService.newTabURL = newURL;
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
Deprecated.warning("NewTabURL.reset is deprecated, please use aboutNewTabService.resetNewTabURL()", DepecationURL);
|
||||
aboutNewTabService.resetNewTabURL();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,6 +27,9 @@ XPCOMUtils.defineLazyGetter(this, "gPrincipal", function() {
|
||||
return Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
// The maximum number of results PlacesProvider retrieves from history.
|
||||
const HISTORY_RESULTS_LIMIT = 100;
|
||||
|
||||
@@ -65,46 +68,6 @@ let LinkChecker = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Singleton that provides utility functions for links.
|
||||
* A link is a plain object that looks like this:
|
||||
*
|
||||
* {
|
||||
* url: "http://www.mozilla.org/",
|
||||
* title: "Mozilla",
|
||||
* frecency: 1337,
|
||||
* lastVisitDate: 1394678824766431,
|
||||
* }
|
||||
*/
|
||||
const LinkUtils = {
|
||||
_sortProperties: [
|
||||
"frecency",
|
||||
"lastVisitDate",
|
||||
"url",
|
||||
],
|
||||
|
||||
/**
|
||||
* Compares two links.
|
||||
*
|
||||
* @param {String} aLink1 The first link.
|
||||
* @param {String} aLink2 The second link.
|
||||
* @return {Number} A negative number if aLink1 is ordered before aLink2, zero if
|
||||
* aLink1 and aLink2 have the same ordering, or a positive number if
|
||||
* aLink1 is ordered after aLink2.
|
||||
* Order is ascending.
|
||||
*/
|
||||
compareLinks: function LinkUtils_compareLinks(aLink1, aLink2) {
|
||||
for (let prop of LinkUtils._sortProperties) {
|
||||
if (!aLink1.hasOwnProperty(prop) || !aLink2.hasOwnProperty(prop)) {
|
||||
throw new Error("Comparable link missing required property: " + prop);
|
||||
}
|
||||
}
|
||||
return aLink2.frecency - aLink1.frecency ||
|
||||
aLink2.lastVisitDate - aLink1.lastVisitDate ||
|
||||
aLink1.url.localeCompare(aLink2.url);
|
||||
},
|
||||
};
|
||||
|
||||
/* Queries history to retrieve the most visited sites. Emits events when the
|
||||
* history changes.
|
||||
* Implements the EventEmitter interface.
|
||||
@@ -197,71 +160,86 @@ Links.prototype = {
|
||||
*
|
||||
* @returns {Promise} Returns a promise with the array of links as payload.
|
||||
*/
|
||||
getLinks: function PlacesProvider_getLinks() {
|
||||
let getLinksPromise = new Promise((resolve, reject) => {
|
||||
let options = PlacesUtils.history.getNewQueryOptions();
|
||||
options.maxResults = this.maxNumLinks;
|
||||
getLinks: Task.async(function*() {
|
||||
// Select a single page per host with highest frecency, highest recency.
|
||||
// Choose N top such pages. Note +rev_host, to turn off optimizer per :mak
|
||||
// suggestion.
|
||||
let sqlQuery = `SELECT url, title, frecency,
|
||||
last_visit_date as lastVisitDate,
|
||||
"history" as type
|
||||
FROM moz_places
|
||||
WHERE frecency in (
|
||||
SELECT MAX(frecency) as frecency
|
||||
FROM moz_places
|
||||
WHERE hidden = 0 AND last_visit_date NOTNULL
|
||||
GROUP BY +rev_host
|
||||
ORDER BY frecency DESC
|
||||
LIMIT :limit
|
||||
)
|
||||
GROUP BY rev_host HAVING MAX(lastVisitDate)
|
||||
ORDER BY frecency DESC, lastVisitDate DESC, url`;
|
||||
|
||||
// Sort by frecency, descending.
|
||||
options.sortingMode = Ci.nsINavHistoryQueryOptions
|
||||
.SORT_BY_FRECENCY_DESCENDING;
|
||||
let links = yield this.executePlacesQuery(sqlQuery, {
|
||||
columns: ["url", "title", "lastVisitDate", "frecency", "type"],
|
||||
params: {limit: this.maxNumLinks}
|
||||
});
|
||||
|
||||
let links = [];
|
||||
return links.filter(link => LinkChecker.checkLoadURI(link.url));
|
||||
}),
|
||||
|
||||
let queryHandlers = {
|
||||
handleResult: function(aResultSet) {
|
||||
for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
|
||||
let url = row.getResultByIndex(1);
|
||||
if (LinkChecker.checkLoadURI(url)) {
|
||||
let link = {
|
||||
url: url,
|
||||
title: row.getResultByIndex(2),
|
||||
frecency: row.getResultByIndex(12),
|
||||
lastVisitDate: row.getResultByIndex(5),
|
||||
type: "history",
|
||||
};
|
||||
links.push(link);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleError: function(aError) {
|
||||
reject(aError);
|
||||
},
|
||||
|
||||
handleCompletion: function(aReason) { // jshint ignore:line
|
||||
// The Places query breaks ties in frecency by place ID descending, but
|
||||
// that's different from how Links.compareLinks breaks ties, because
|
||||
// compareLinks doesn't have access to place IDs. It's very important
|
||||
// that the initial list of links is sorted in the same order imposed by
|
||||
// compareLinks, because Links uses compareLinks to perform binary
|
||||
// searches on the list. So, ensure the list is so ordered.
|
||||
let i = 1;
|
||||
let outOfOrder = [];
|
||||
while (i < links.length) {
|
||||
if (LinkUtils.compareLinks(links[i - 1], links[i]) > 0) {
|
||||
outOfOrder.push(links.splice(i, 1)[0]);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
for (let link of outOfOrder) {
|
||||
i = BinarySearch.insertionIndexOf(LinkUtils.compareLinks, links, link);
|
||||
links.splice(i, 0, link);
|
||||
}
|
||||
|
||||
resolve(links);
|
||||
/**
|
||||
* Executes arbitrary query against places database
|
||||
*
|
||||
* @param {String} aSql
|
||||
* SQL query to execute
|
||||
* @param {Object} [optional] aOptions
|
||||
* aOptions.columns - an array of column names. if supplied the returned
|
||||
* items will consist of objects keyed on column names. Otherwise
|
||||
* an array of raw values is returned in the select order
|
||||
* aOptions.param - an object of SQL binding parameters
|
||||
* aOptions.callback - a callback to handle query rows
|
||||
*
|
||||
* @returns {Promise} Returns a promise with the array of retrieved items
|
||||
*/
|
||||
executePlacesQuery: Task.async(function*(aSql, aOptions={}) {
|
||||
let {columns, params, callback} = aOptions;
|
||||
let items = [];
|
||||
let queryError = null;
|
||||
let conn = yield PlacesUtils.promiseDBConnection();
|
||||
yield conn.executeCached(aSql, params, aRow => {
|
||||
try {
|
||||
// check if caller wants to handle query raws
|
||||
if (callback) {
|
||||
callback(aRow);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute the query.
|
||||
let query = PlacesUtils.history.getNewQuery();
|
||||
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase);
|
||||
db.asyncExecuteLegacyQueries([query], 1, options, queryHandlers);
|
||||
// otherwise fill in the item and add items array
|
||||
else {
|
||||
let item = null;
|
||||
// if columns array is given construct an object
|
||||
if (columns && Array.isArray(columns)) {
|
||||
item = {};
|
||||
columns.forEach(column => {
|
||||
item[column] = aRow.getResultByName(column);
|
||||
});
|
||||
} else {
|
||||
// if no columns - make an array of raw values
|
||||
item = [];
|
||||
for (let i = 0; i < aRow.numEntries; i++) {
|
||||
item.push(aRow.getResultByIndex(i));
|
||||
}
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
} catch (e) {
|
||||
queryError = e;
|
||||
throw StopIteration;
|
||||
}
|
||||
});
|
||||
|
||||
return getLinksPromise;
|
||||
}
|
||||
if (queryError) {
|
||||
throw new Error(queryError);
|
||||
}
|
||||
return items;
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -271,6 +249,5 @@ const gLinks = new Links(); // jshint ignore:line
|
||||
|
||||
let PlacesProvider = {
|
||||
LinkChecker: LinkChecker,
|
||||
LinkUtils: LinkUtils,
|
||||
links: gLinks,
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "NewTabRemoteResources",
|
||||
|
||||
const LOCAL_NEWTAB_URL = "chrome://browser/content/newtab/newTab.xhtml";
|
||||
|
||||
const REMOTE_NEWTAB_PATH = "/v%VERSION%/%CHANNEL%/%LOCALE%/index.html";
|
||||
const REMOTE_NEWTAB_PATH = "/newtab/v%VERSION%/%CHANNEL%/%LOCALE%/index.html";
|
||||
|
||||
const ABOUT_URL = "about:newtab";
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
// Set us up to use async prefs in the parent process.
|
||||
Cu.import("resource://gre/modules/AsyncPrefs.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
|
||||
"resource:///modules/AboutHome.jsm");
|
||||
@@ -85,6 +87,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "RemotePrompt",
|
||||
"resource:///modules/RemotePrompt.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ContentPrefServiceParent",
|
||||
"resource://gre/modules/ContentPrefServiceParent.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
|
||||
"resource:///modules/Feeds.jsm");
|
||||
|
||||
@@ -726,6 +731,7 @@ BrowserGlue.prototype = {
|
||||
RemotePrompt.init();
|
||||
}
|
||||
Feeds.init();
|
||||
ContentPrefServiceParent.init();
|
||||
|
||||
LoginManagerParent.init();
|
||||
ReaderParent.init();
|
||||
@@ -835,7 +841,7 @@ BrowserGlue.prototype = {
|
||||
label: win.gNavigatorBundle.getString("slowStartup.helpButton.label"),
|
||||
accessKey: win.gNavigatorBundle.getString("slowStartup.helpButton.accesskey"),
|
||||
callback: function () {
|
||||
win.openUILinkIn(Services.prefs.getCharPref("browser.slowstartup.help.url"), "tab");
|
||||
win.openUILinkIn("https://support.mozilla.org/kb/reset-firefox-easily-fix-most-problems", "tab");
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -954,13 +960,6 @@ BrowserGlue.prototype = {
|
||||
// passively.
|
||||
Services.ppmm.loadProcessScript("resource://pdf.js/pdfjschildbootstrap.js", true);
|
||||
|
||||
if (0 && AppConstants.NIGHTLY_BUILD) {
|
||||
// Registering Shumway bootstrap script the child processes.
|
||||
Services.ppmm.loadProcessScript("chrome://shumway/content/bootstrap-content.js", true);
|
||||
// Initializing Shumway (shall be run after child script registration).
|
||||
ShumwayUtils.init();
|
||||
}
|
||||
|
||||
if (AppConstants.platform == "win") {
|
||||
// For Windows 7, initialize the jump list module.
|
||||
const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
|
||||
@@ -1746,7 +1745,7 @@ BrowserGlue.prototype = {
|
||||
},
|
||||
|
||||
_migrateUI: function BG__migrateUI() {
|
||||
const UI_VERSION = 35;
|
||||
const UI_VERSION = 36;
|
||||
const BROWSER_DOCURL = "chrome://browser/content/browser.xul";
|
||||
|
||||
let currentUIVersion;
|
||||
@@ -2105,6 +2104,12 @@ BrowserGlue.prototype = {
|
||||
this._maybeMigrateTabGroups();
|
||||
}
|
||||
|
||||
if (currentUIVersion < 36) {
|
||||
xulStore.removeValue("chrome://passwordmgr/content/passwordManager.xul",
|
||||
"passwordCol",
|
||||
"hidden");
|
||||
}
|
||||
|
||||
// Update the migration version.
|
||||
Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
|
||||
},
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/* 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/. */
|
||||
|
||||
// This test makes sure that the window title changes correctly while switching
|
||||
// from and to private browsing mode.
|
||||
|
||||
add_task(function test() {
|
||||
const testPageURL = "http://mochi.test:8888/browser/" +
|
||||
"browser/components/privatebrowsing/test/browser/browser_privatebrowsing_windowtitle_page.html";
|
||||
requestLongerTimeout(2);
|
||||
|
||||
// initialization of expected titles
|
||||
let test_title = "Test title";
|
||||
let app_name = document.documentElement.getAttribute("title");
|
||||
const isOSX = ("nsILocalFileMac" in Ci);
|
||||
let page_with_title;
|
||||
let page_without_title;
|
||||
let about_pb_title;
|
||||
let pb_page_with_title;
|
||||
let pb_page_without_title;
|
||||
let pb_about_pb_title;
|
||||
if (isOSX) {
|
||||
page_with_title = test_title;
|
||||
page_without_title = app_name;
|
||||
about_pb_title = "Open a private window?";
|
||||
pb_page_with_title = test_title + " - (Private Browsing)";
|
||||
pb_page_without_title = app_name + " - (Private Browsing)";
|
||||
pb_about_pb_title = "Private Browsing - (Private Browsing)";
|
||||
}
|
||||
else {
|
||||
page_with_title = test_title + " - " + app_name;
|
||||
page_without_title = app_name;
|
||||
about_pb_title = "Open a private window?" + " - " + app_name;
|
||||
pb_page_with_title = test_title + " - " + app_name + " (Private Browsing)";
|
||||
pb_page_without_title = app_name + " (Private Browsing)";
|
||||
pb_about_pb_title = "Private Browsing - " + app_name + " (Private Browsing)";
|
||||
}
|
||||
|
||||
function* testTabTitle(aWindow, url, insidePB, expected_title) {
|
||||
let tab = (yield BrowserTestUtils.openNewForegroundTab(aWindow.gBrowser));
|
||||
yield BrowserTestUtils.loadURI(tab.linkedBrowser, url);
|
||||
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||
|
||||
yield BrowserTestUtils.waitForCondition(() => {
|
||||
return aWindow.document.title === expected_title;
|
||||
}, `Window title should be ${expected_title}, got ${aWindow.document.title}`);
|
||||
|
||||
is(aWindow.document.title, expected_title, "The window title for " + url +
|
||||
" is correct (" + (insidePB ? "inside" : "outside") +
|
||||
" private browsing mode)");
|
||||
|
||||
let win = aWindow.gBrowser.replaceTabWithWindow(tab);
|
||||
yield BrowserTestUtils.waitForEvent(win, "load", false);
|
||||
|
||||
yield BrowserTestUtils.waitForCondition(() => {
|
||||
return win.document.title === expected_title;
|
||||
}, `Window title should be ${expected_title}, got ${aWindow.document.title}`);
|
||||
|
||||
is(win.document.title, expected_title, "The window title for " + url +
|
||||
" detached tab is correct (" + (insidePB ? "inside" : "outside") +
|
||||
" private browsing mode)");
|
||||
|
||||
yield Promise.all([ BrowserTestUtils.closeWindow(win),
|
||||
BrowserTestUtils.closeWindow(aWindow) ]);
|
||||
}
|
||||
|
||||
function openWin(private) {
|
||||
return BrowserTestUtils.openNewBrowserWindow({ private });
|
||||
}
|
||||
yield Task.spawn(testTabTitle((yield openWin(false)), "about:blank", false, page_without_title));
|
||||
yield Task.spawn(testTabTitle((yield openWin(false)), testPageURL, false, page_with_title));
|
||||
yield Task.spawn(testTabTitle((yield openWin(false)), "about:privatebrowsing", false, about_pb_title));
|
||||
yield Task.spawn(testTabTitle((yield openWin(true)), "about:blank", true, pb_page_without_title));
|
||||
yield Task.spawn(testTabTitle((yield openWin(true)), testPageURL, true, pb_page_with_title));
|
||||
yield Task.spawn(testTabTitle((yield openWin(true)), "about:privatebrowsing", true, pb_about_pb_title));
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/* 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/. */
|
||||
|
||||
// This test makes sure that private browsing turns off doesn't cause zoom
|
||||
// settings to be reset on tab switch (bug 464962)
|
||||
|
||||
add_task(function* test() {
|
||||
let win = (yield BrowserTestUtils.openNewBrowserWindow({ private: true }));
|
||||
let tabAbout = (yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:"));
|
||||
let tabMozilla = (yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:"));
|
||||
|
||||
let mozillaZoom = win.ZoomManager.zoom;
|
||||
|
||||
// change the zoom on the mozilla page
|
||||
win.FullZoom.enlarge();
|
||||
// make sure the zoom level has been changed
|
||||
isnot(win.ZoomManager.zoom, mozillaZoom, "Zoom level can be changed");
|
||||
mozillaZoom = win.ZoomManager.zoom;
|
||||
|
||||
// switch to about: tab
|
||||
yield BrowserTestUtils.switchTab(win.gBrowser, tabAbout);
|
||||
|
||||
// switch back to mozilla tab
|
||||
yield BrowserTestUtils.switchTab(win.gBrowser, tabMozilla);
|
||||
|
||||
// make sure the zoom level has not changed
|
||||
is(win.ZoomManager.zoom, mozillaZoom,
|
||||
"Entering private browsing should not reset the zoom on a tab");
|
||||
|
||||
// cleanup
|
||||
win.FullZoom.reset();
|
||||
yield Promise.all([ BrowserTestUtils.removeTab(tabMozilla),
|
||||
BrowserTestUtils.removeTab(tabAbout) ]);
|
||||
|
||||
yield BrowserTestUtils.closeWindow(win);
|
||||
});
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/* 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/. */
|
||||
|
||||
// This test makes sure that about:privatebrowsing does not appear zoomed in
|
||||
// if there is already a zoom site pref for about:blank (bug 487656).
|
||||
|
||||
add_task(function* test() {
|
||||
// initialization
|
||||
let windowsToClose = [];
|
||||
let windowsToReset = [];
|
||||
|
||||
function promiseLocationChange() {
|
||||
return new Promise(resolve => {
|
||||
Services.obs.addObserver(function onLocationChange(subj, topic, data) {
|
||||
Services.obs.removeObserver(onLocationChange, topic);
|
||||
resolve();
|
||||
}, "browser-fullZoom:location-change", false);
|
||||
});
|
||||
}
|
||||
|
||||
function promiseTestReady(aIsZoomedWindow, aWindow) {
|
||||
// Need to wait on two things, the ordering of which is not guaranteed:
|
||||
// (1) the page load, and (2) FullZoom's update to the new page's zoom
|
||||
// level. FullZoom broadcasts "browser-fullZoom:location-change" when its
|
||||
// update is done. (See bug 856366 for details.)
|
||||
|
||||
|
||||
let browser = aWindow.gBrowser.selectedBrowser;
|
||||
return BrowserTestUtils.loadURI(browser, "about:blank").then(() => {
|
||||
return Promise.all([ BrowserTestUtils.browserLoaded(browser),
|
||||
promiseLocationChange() ]);
|
||||
}).then(() => doTest(aIsZoomedWindow, aWindow));
|
||||
}
|
||||
|
||||
function doTest(aIsZoomedWindow, aWindow) {
|
||||
if (aIsZoomedWindow) {
|
||||
is(aWindow.ZoomManager.zoom, 1,
|
||||
"Zoom level for freshly loaded about:blank should be 1");
|
||||
// change the zoom on the blank page
|
||||
aWindow.FullZoom.enlarge();
|
||||
isnot(aWindow.ZoomManager.zoom, 1, "Zoom level for about:blank should be changed");
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure the zoom level is set to 1
|
||||
is(aWindow.ZoomManager.zoom, 1, "Zoom level for about:privatebrowsing should be reset");
|
||||
}
|
||||
|
||||
function testOnWindow(options, callback) {
|
||||
return BrowserTestUtils.openNewBrowserWindow(options).then((win) => {
|
||||
windowsToClose.push(win);
|
||||
windowsToReset.push(win);
|
||||
return win;
|
||||
});
|
||||
}
|
||||
|
||||
yield testOnWindow({}).then(win => promiseTestReady(true, win));
|
||||
yield testOnWindow({private: true}).then(win => promiseTestReady(false, win));
|
||||
|
||||
// cleanup
|
||||
windowsToReset.forEach((win) => win.FullZoom.reset());
|
||||
yield Promise.all(windowsToClose.map(win => BrowserTestUtils.closeWindow(win)));
|
||||
});
|
||||
@@ -809,11 +809,14 @@ var SessionStoreInternal = {
|
||||
} else {
|
||||
// If the user was typing into the URL bar when we crashed, but hadn't hit
|
||||
// enter yet, then we just need to write that value to the URL bar without
|
||||
// loading anything. This must happen after the load, since it will clear
|
||||
// loading anything. This must happen after the load, as the load will clear
|
||||
// userTypedValue.
|
||||
let tabData = TabState.collect(tab);
|
||||
if (tabData.userTypedValue && !tabData.userTypedClear) {
|
||||
browser.userTypedValue = tabData.userTypedValue;
|
||||
if (data.didStartLoad) {
|
||||
browser.userTypedClear++;
|
||||
}
|
||||
win.URLBarSetURI();
|
||||
}
|
||||
|
||||
@@ -833,7 +836,7 @@ var SessionStoreInternal = {
|
||||
SessionStoreInternal._resetLocalTabRestoringState(tab);
|
||||
SessionStoreInternal.restoreNextTab();
|
||||
|
||||
this._sendTabRestoredNotification(tab);
|
||||
this._sendTabRestoredNotification(tab, data.isRemotenessUpdate);
|
||||
break;
|
||||
case "SessionStore:crashedTabRevived":
|
||||
// The browser was revived by navigating to a different page
|
||||
@@ -2506,7 +2509,6 @@ var SessionStoreInternal = {
|
||||
tabState.index = historyIndex + 1;
|
||||
tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
|
||||
} else {
|
||||
tabState.userTypedValue = null;
|
||||
options.loadArguments = recentLoadArguments;
|
||||
}
|
||||
|
||||
@@ -3264,6 +3266,9 @@ var SessionStoreInternal = {
|
||||
* the tab to restore
|
||||
* @param aLoadArguments
|
||||
* optional load arguments used for loadURI()
|
||||
* @param aRemotenessSwitch
|
||||
* true if we're restoring a tab's content because we flipped
|
||||
* its remoteness (out-of-process) state.
|
||||
*/
|
||||
restoreTabContent: function (aTab, aLoadArguments = null) {
|
||||
if (aTab.hasAttribute("customizemode")) {
|
||||
@@ -3287,7 +3292,8 @@ var SessionStoreInternal = {
|
||||
// flip the remoteness of any browser that is not being displayed.
|
||||
this.markTabAsRestoring(aTab);
|
||||
|
||||
if (tabbrowser.updateBrowserRemotenessByURL(browser, uri)) {
|
||||
let isRemotenessUpdate = tabbrowser.updateBrowserRemotenessByURL(browser, uri);
|
||||
if (isRemotenessUpdate) {
|
||||
// We updated the remoteness, so we need to send the history down again.
|
||||
//
|
||||
// Start a new epoch to discard all frame script messages relating to a
|
||||
@@ -3310,7 +3316,7 @@ var SessionStoreInternal = {
|
||||
}
|
||||
|
||||
browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent",
|
||||
{loadArguments: aLoadArguments});
|
||||
{loadArguments: aLoadArguments, isRemotenessUpdate});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -3955,11 +3961,17 @@ var SessionStoreInternal = {
|
||||
|
||||
/**
|
||||
* Dispatch the SSTabRestored event for the given tab.
|
||||
* @param aTab the which has been restored
|
||||
* @param aTab
|
||||
* The tab which has been restored
|
||||
* @param aIsRemotenessUpdate
|
||||
* True if this tab was restored due to flip from running from
|
||||
* out-of-main-process to in-main-process or vice-versa.
|
||||
*/
|
||||
_sendTabRestoredNotification: function ssi_sendTabRestoredNotification(aTab) {
|
||||
let event = aTab.ownerDocument.createEvent("Events");
|
||||
event.initEvent("SSTabRestored", true, false);
|
||||
_sendTabRestoredNotification(aTab, aIsRemotenessUpdate) {
|
||||
let event = aTab.ownerDocument.createEvent("CustomEvent");
|
||||
event.initCustomEvent("SSTabRestored", true, false, {
|
||||
isRemotenessUpdate: aIsRemotenessUpdate,
|
||||
});
|
||||
aTab.dispatchEvent(event);
|
||||
},
|
||||
|
||||
|
||||
@@ -199,9 +199,16 @@ this.StartupPerformance = {
|
||||
// to reach that point.
|
||||
let win = subject;
|
||||
|
||||
let observer = () => {
|
||||
this._latestRestoredTimeStamp = Date.now();
|
||||
this._totalNumberOfEagerTabs += 1;
|
||||
let observer = (event) => {
|
||||
// We don't care about tab restorations that are due to
|
||||
// a browser flipping from out-of-main-process to in-main-process
|
||||
// or vice-versa. We only care about restorations that are due
|
||||
// to the user switching to a lazily restored tab, or for tabs
|
||||
// that are restoring eagerly.
|
||||
if (!event.detail.isRemotenessUpdate) {
|
||||
this._latestRestoredTimeStamp = Date.now();
|
||||
this._totalNumberOfEagerTabs += 1;
|
||||
}
|
||||
};
|
||||
win.gBrowser.tabContainer.addEventListener("SSTabRestored", observer);
|
||||
this._totalNumberOfTabs += win.gBrowser.tabContainer.itemCount;
|
||||
|
||||
@@ -167,21 +167,21 @@ var MessageListener = {
|
||||
sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch});
|
||||
},
|
||||
|
||||
restoreTabContent({loadArguments}) {
|
||||
restoreTabContent({loadArguments, isRemotenessUpdate}) {
|
||||
let epoch = gCurrentEpoch;
|
||||
|
||||
// We need to pass the value of didStartLoad back to SessionStore.jsm.
|
||||
let didStartLoad = gContentRestore.restoreTabContent(loadArguments, () => {
|
||||
// Tell SessionStore.jsm that it may want to restore some more tabs,
|
||||
// since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
|
||||
sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch});
|
||||
sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate});
|
||||
});
|
||||
|
||||
sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch});
|
||||
sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch, didStartLoad});
|
||||
|
||||
if (!didStartLoad) {
|
||||
// Pretend that the load succeeded so that event handlers fire correctly.
|
||||
sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch});
|
||||
sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -100,49 +100,51 @@ add_task(function* () {
|
||||
let glue = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver)
|
||||
glue.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_DISTRIBUTION_CUSTOMIZATION);
|
||||
|
||||
Assert.equal(Services.prefs.getCharPref("distribution.id"), "disttest");
|
||||
Assert.equal(Services.prefs.getCharPref("distribution.version"), "1.0");
|
||||
Assert.equal(Services.prefs.getComplexValue("distribution.about", Ci.nsISupportsString).data, "Tèƨƭ δïƨƭřïβúƭïôñ ƒïℓè");
|
||||
var defaultBranch = Services.prefs.getDefaultBranch(null);
|
||||
|
||||
Assert.equal(Services.prefs.getCharPref("distribution.test.string"), "Test String");
|
||||
Assert.equal(Services.prefs.getCharPref("distribution.test.string.noquotes"), "Test String");
|
||||
Assert.equal(Services.prefs.getIntPref("distribution.test.int"), 777);
|
||||
Assert.equal(Services.prefs.getBoolPref("distribution.test.bool.true"), true);
|
||||
Assert.equal(Services.prefs.getBoolPref("distribution.test.bool.false"), false);
|
||||
Assert.equal(defaultBranch.getCharPref("distribution.id"), "disttest");
|
||||
Assert.equal(defaultBranch.getCharPref("distribution.version"), "1.0");
|
||||
Assert.equal(defaultBranch.getComplexValue("distribution.about", Ci.nsISupportsString).data, "Tèƨƭ δïƨƭřïβúƭïôñ ƒïℓè");
|
||||
|
||||
Assert.throws(() => Services.prefs.getCharPref("distribution.test.empty"));
|
||||
Assert.throws(() => Services.prefs.getIntPref("distribution.test.empty"));
|
||||
Assert.throws(() => Services.prefs.getBoolPref("distribution.test.empty"));
|
||||
Assert.equal(defaultBranch.getCharPref("distribution.test.string"), "Test String");
|
||||
Assert.equal(defaultBranch.getCharPref("distribution.test.string.noquotes"), "Test String");
|
||||
Assert.equal(defaultBranch.getIntPref("distribution.test.int"), 777);
|
||||
Assert.equal(defaultBranch.getBoolPref("distribution.test.bool.true"), true);
|
||||
Assert.equal(defaultBranch.getBoolPref("distribution.test.bool.false"), false);
|
||||
|
||||
Assert.equal(Services.prefs.getCharPref("distribution.test.pref.locale"), "en-US");
|
||||
Assert.equal(Services.prefs.getCharPref("distribution.test.pref.language.en"), "en");
|
||||
Assert.equal(Services.prefs.getCharPref("distribution.test.pref.locale.en-US"), "en-US");
|
||||
Assert.throws(() => Services.prefs.getCharPref("distribution.test.pref.language.de"));
|
||||
Assert.throws(() => defaultBranch.getCharPref("distribution.test.empty"));
|
||||
Assert.throws(() => defaultBranch.getIntPref("distribution.test.empty"));
|
||||
Assert.throws(() => defaultBranch.getBoolPref("distribution.test.empty"));
|
||||
|
||||
Assert.equal(defaultBranch.getCharPref("distribution.test.pref.locale"), "en-US");
|
||||
Assert.equal(defaultBranch.getCharPref("distribution.test.pref.language.en"), "en");
|
||||
Assert.equal(defaultBranch.getCharPref("distribution.test.pref.locale.en-US"), "en-US");
|
||||
Assert.throws(() => defaultBranch.getCharPref("distribution.test.pref.language.de"));
|
||||
// This value was never set because of the empty language specific pref
|
||||
Assert.throws(() => Services.prefs.getCharPref("distribution.test.pref.language.reset"));
|
||||
Assert.throws(() => defaultBranch.getCharPref("distribution.test.pref.language.reset"));
|
||||
// This value was never set because of the empty locale specific pref
|
||||
Assert.throws(() => Services.prefs.getCharPref("distribution.test.pref.locale.reset"));
|
||||
Assert.throws(() => defaultBranch.getCharPref("distribution.test.pref.locale.reset"));
|
||||
// This value was overridden by a locale specific setting
|
||||
Assert.equal(Services.prefs.getCharPref("distribution.test.pref.locale.set"), "Locale Set");
|
||||
Assert.equal(defaultBranch.getCharPref("distribution.test.pref.locale.set"), "Locale Set");
|
||||
// This value was overridden by a language specific setting
|
||||
Assert.equal(Services.prefs.getCharPref("distribution.test.pref.language.set"), "Language Set");
|
||||
Assert.equal(defaultBranch.getCharPref("distribution.test.pref.language.set"), "Language Set");
|
||||
// Language should not override locale
|
||||
Assert.notEqual(Services.prefs.getCharPref("distribution.test.pref.locale.set"), "Language Set");
|
||||
Assert.notEqual(defaultBranch.getCharPref("distribution.test.pref.locale.set"), "Language Set");
|
||||
|
||||
Assert.equal(Services.prefs.getComplexValue("distribution.test.locale", Ci.nsIPrefLocalizedString).data, "en-US");
|
||||
Assert.equal(Services.prefs.getComplexValue("distribution.test.language.en", Ci.nsIPrefLocalizedString).data, "en");
|
||||
Assert.equal(Services.prefs.getComplexValue("distribution.test.locale.en-US", Ci.nsIPrefLocalizedString).data, "en-US");
|
||||
Assert.throws(() => Services.prefs.getComplexValue("distribution.test.language.de", Ci.nsIPrefLocalizedString));
|
||||
Assert.equal(defaultBranch.getComplexValue("distribution.test.locale", Ci.nsIPrefLocalizedString).data, "en-US");
|
||||
Assert.equal(defaultBranch.getComplexValue("distribution.test.language.en", Ci.nsIPrefLocalizedString).data, "en");
|
||||
Assert.equal(defaultBranch.getComplexValue("distribution.test.locale.en-US", Ci.nsIPrefLocalizedString).data, "en-US");
|
||||
Assert.throws(() => defaultBranch.getComplexValue("distribution.test.language.de", Ci.nsIPrefLocalizedString));
|
||||
// This value was never set because of the empty language specific pref
|
||||
Assert.throws(() => Services.prefs.getComplexValue("distribution.test.language.reset", Ci.nsIPrefLocalizedString));
|
||||
Assert.throws(() => defaultBranch.getComplexValue("distribution.test.language.reset", Ci.nsIPrefLocalizedString));
|
||||
// This value was never set because of the empty locale specific pref
|
||||
Assert.throws(() => Services.prefs.getComplexValue("distribution.test.locale.reset", Ci.nsIPrefLocalizedString));
|
||||
Assert.throws(() => defaultBranch.getComplexValue("distribution.test.locale.reset", Ci.nsIPrefLocalizedString));
|
||||
// This value was overridden by a locale specific setting
|
||||
Assert.equal(Services.prefs.getComplexValue("distribution.test.locale.set", Ci.nsIPrefLocalizedString).data, "Locale Set");
|
||||
Assert.equal(defaultBranch.getComplexValue("distribution.test.locale.set", Ci.nsIPrefLocalizedString).data, "Locale Set");
|
||||
// This value was overridden by a language specific setting
|
||||
Assert.equal(Services.prefs.getComplexValue("distribution.test.language.set", Ci.nsIPrefLocalizedString).data, "Language Set");
|
||||
Assert.equal(defaultBranch.getComplexValue("distribution.test.language.set", Ci.nsIPrefLocalizedString).data, "Language Set");
|
||||
// Language should not override locale
|
||||
Assert.notEqual(Services.prefs.getComplexValue("distribution.test.locale.set", Ci.nsIPrefLocalizedString).data, "Language Set");
|
||||
Assert.notEqual(defaultBranch.getComplexValue("distribution.test.locale.set", Ci.nsIPrefLocalizedString).data, "Language Set");
|
||||
|
||||
do_test_pending();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user