mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-04-17 13:31:00 +00:00

MozReview-Commit-ID: GfuGF4v30pI --HG-- extra : rebase_source : e8b4399bd1e3fc5e5b220e80d6694c00a4b7e482
572 lines
20 KiB
JavaScript
572 lines
20 KiB
JavaScript
/* 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/. */
|
|
|
|
Cu.import("resource://services-sync/UIState.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
|
|
"resource://gre/modules/FxAccountsWebChannel.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Weave",
|
|
"resource://services-sync/main.js");
|
|
|
|
const MIN_STATUS_ANIMATION_DURATION = 1600;
|
|
|
|
var gSync = {
|
|
_initialized: false,
|
|
// The last sync start time. Used to calculate the leftover animation time
|
|
// once syncing completes (bug 1239042).
|
|
_syncStartTime: 0,
|
|
_syncAnimationTimer: 0,
|
|
|
|
_obs: [
|
|
"weave:engine:sync:finish",
|
|
"quit-application",
|
|
UIState.ON_UPDATE
|
|
],
|
|
|
|
get panelUIFooter() {
|
|
delete this.panelUIFooter;
|
|
return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa");
|
|
},
|
|
|
|
get panelUIStatus() {
|
|
delete this.panelUIStatus;
|
|
return this.panelUIStatus = document.getElementById("PanelUI-fxa-status");
|
|
},
|
|
|
|
get panelUIAvatar() {
|
|
delete this.panelUIAvatar;
|
|
return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar");
|
|
},
|
|
|
|
get panelUILabel() {
|
|
delete this.panelUILabel;
|
|
return this.panelUILabel = document.getElementById("PanelUI-fxa-label");
|
|
},
|
|
|
|
get panelUIIcon() {
|
|
delete this.panelUIIcon;
|
|
return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon");
|
|
},
|
|
|
|
get fxaStrings() {
|
|
delete this.fxaStrings;
|
|
return this.fxaStrings = Services.strings.createBundle(
|
|
"chrome://browser/locale/accounts.properties"
|
|
);
|
|
},
|
|
|
|
get syncStrings() {
|
|
delete this.syncStrings;
|
|
// XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
|
|
// but for now just make it work
|
|
return this.syncStrings = Services.strings.createBundle(
|
|
"chrome://weave/locale/sync.properties"
|
|
);
|
|
},
|
|
|
|
get sendTabToDeviceEnabled() {
|
|
return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled");
|
|
},
|
|
|
|
get remoteClients() {
|
|
return Weave.Service.clientsEngine.remoteClients
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
},
|
|
|
|
init() {
|
|
// Bail out if we're already initialized or for pop-up windows.
|
|
if (this._initialized || !window.toolbar.visible) {
|
|
return;
|
|
}
|
|
|
|
for (let topic of this._obs) {
|
|
Services.obs.addObserver(this, topic, true);
|
|
}
|
|
|
|
// initial label for the sync buttons.
|
|
let broadcaster = document.getElementById("sync-status");
|
|
broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
|
|
|
|
// Update the UI
|
|
if (UIState.isReady()) {
|
|
const state = UIState.get();
|
|
// If we are not configured, the UI is already in the right state when
|
|
// we open the window. We can avoid a repaint.
|
|
if (state.status != UIState.STATUS_NOT_CONFIGURED) {
|
|
this.updateAllUI(state);
|
|
}
|
|
}
|
|
|
|
this.maybeMoveSyncedTabsButton();
|
|
|
|
EnsureFxAccountsWebChannel();
|
|
|
|
this._initialized = true;
|
|
},
|
|
|
|
uninit() {
|
|
if (!this._initialized) {
|
|
return;
|
|
}
|
|
|
|
for (let topic of this._obs) {
|
|
Services.obs.removeObserver(this, topic);
|
|
}
|
|
|
|
this._initialized = false;
|
|
},
|
|
|
|
observe(subject, topic, data) {
|
|
if (!this._initialized) {
|
|
Cu.reportError("browser-sync observer called after unload: " + topic);
|
|
return;
|
|
}
|
|
switch (topic) {
|
|
case UIState.ON_UPDATE:
|
|
const state = UIState.get();
|
|
this.updateAllUI(state);
|
|
break;
|
|
case "quit-application":
|
|
// Stop the animation timer on shutdown, since we can't update the UI
|
|
// after this.
|
|
clearTimeout(this._syncAnimationTimer);
|
|
break;
|
|
case "weave:engine:sync:finish":
|
|
if (data != "clients") {
|
|
return;
|
|
}
|
|
this.onClientsSynced();
|
|
break;
|
|
}
|
|
},
|
|
|
|
updateAllUI(state) {
|
|
this.updatePanelBadge(state);
|
|
this.updatePanelPopup(state);
|
|
this.updateStateBroadcasters(state);
|
|
this.updateSyncButtonsTooltip(state);
|
|
this.updateSyncStatus(state);
|
|
},
|
|
|
|
updatePanelPopup(state) {
|
|
let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel");
|
|
// The localization string is for the signed in text, but it's the default text as well
|
|
let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext");
|
|
|
|
const status = state.status;
|
|
// Reset the status bar to its original state.
|
|
this.panelUILabel.setAttribute("label", defaultLabel);
|
|
this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext);
|
|
this.panelUIFooter.removeAttribute("fxastatus");
|
|
this.panelUIAvatar.style.removeProperty("list-style-image");
|
|
|
|
if (status == UIState.STATUS_NOT_CONFIGURED) {
|
|
return;
|
|
}
|
|
|
|
// At this point we consider sync to be configured (but still can be in an error state).
|
|
if (status == UIState.STATUS_LOGIN_FAILED) {
|
|
let tooltipDescription = this.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
|
|
let errorLabel = this.panelUIStatus.getAttribute("errorlabel");
|
|
this.panelUIFooter.setAttribute("fxastatus", "login-failed");
|
|
this.panelUILabel.setAttribute("label", errorLabel);
|
|
this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
|
|
return;
|
|
} else if (status == UIState.STATUS_NOT_VERIFIED) {
|
|
let tooltipDescription = this.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
|
|
let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel");
|
|
this.panelUIFooter.setAttribute("fxastatus", "unverified");
|
|
this.panelUILabel.setAttribute("label", unverifiedLabel);
|
|
this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription);
|
|
return;
|
|
}
|
|
|
|
// At this point we consider sync to be logged-in.
|
|
this.panelUIFooter.setAttribute("fxastatus", "signedin");
|
|
this.panelUILabel.setAttribute("label", state.displayName || state.email);
|
|
|
|
if (state.avatarURL) {
|
|
let bgImage = "url(\"" + state.avatarURL + "\")";
|
|
this.panelUIAvatar.style.listStyleImage = bgImage;
|
|
|
|
let img = new Image();
|
|
img.onerror = () => {
|
|
// Clear the image if it has trouble loading. Since this callback is asynchronous
|
|
// we check to make sure the image is still the same before we clear it.
|
|
if (this.panelUIAvatar.style.listStyleImage === bgImage) {
|
|
this.panelUIAvatar.style.removeProperty("list-style-image");
|
|
}
|
|
};
|
|
img.src = state.avatarURL;
|
|
}
|
|
},
|
|
|
|
updatePanelBadge(state) {
|
|
if (state.status == UIState.STATUS_LOGIN_FAILED ||
|
|
state.status == UIState.STATUS_NOT_VERIFIED) {
|
|
PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
|
|
} else {
|
|
PanelUI.removeNotification("fxa-needs-authentication");
|
|
}
|
|
},
|
|
|
|
updateStateBroadcasters(state) {
|
|
const status = state.status;
|
|
|
|
// Start off with a clean slate
|
|
document.getElementById("sync-reauth-state").hidden = true;
|
|
document.getElementById("sync-setup-state").hidden = true;
|
|
document.getElementById("sync-syncnow-state").hidden = true;
|
|
|
|
if (status == UIState.STATUS_LOGIN_FAILED) {
|
|
// unhiding this element makes the menubar show the login failure state.
|
|
document.getElementById("sync-reauth-state").hidden = false;
|
|
} else if (status == UIState.STATUS_NOT_CONFIGURED ||
|
|
status == UIState.STATUS_NOT_VERIFIED) {
|
|
document.getElementById("sync-setup-state").hidden = false;
|
|
} else {
|
|
document.getElementById("sync-syncnow-state").hidden = false;
|
|
}
|
|
},
|
|
|
|
updateSyncStatus(state) {
|
|
const broadcaster = document.getElementById("sync-status");
|
|
const syncingUI = broadcaster.getAttribute("syncstatus") == "active";
|
|
if (state.syncing != syncingUI) { // Do we need to update the UI?
|
|
state.syncing ? this.onActivityStart() : this.onActivityStop();
|
|
}
|
|
},
|
|
|
|
onMenuPanelCommand() {
|
|
switch (this.panelUIFooter.getAttribute("fxastatus")) {
|
|
case "signedin":
|
|
this.openPrefs("menupanel", "fxaSignedin");
|
|
break;
|
|
case "error":
|
|
if (this.panelUIFooter.getAttribute("fxastatus") == "unverified") {
|
|
this.openPrefs("menupanel", "fxaError");
|
|
} else {
|
|
this.openSignInAgainPage("menupanel");
|
|
}
|
|
break;
|
|
default:
|
|
this.openPrefs("menupanel", "fxa");
|
|
break;
|
|
}
|
|
|
|
PanelUI.hide();
|
|
},
|
|
|
|
openAccountsPage(action, urlParams = {}) {
|
|
let params = new URLSearchParams();
|
|
if (action) {
|
|
params.set("action", action);
|
|
}
|
|
for (let name in urlParams) {
|
|
if (urlParams[name] !== undefined) {
|
|
params.set(name, urlParams[name]);
|
|
}
|
|
}
|
|
let url = "about:accounts?" + params;
|
|
switchToTabHavingURI(url, true, {
|
|
replaceQueryString: true
|
|
});
|
|
},
|
|
|
|
openSignInAgainPage(entryPoint) {
|
|
this.openAccountsPage("reauth", { entrypoint: entryPoint });
|
|
},
|
|
|
|
async openDevicesManagementPage(entryPoint) {
|
|
let url = await fxAccounts.promiseAccountsManageDevicesURI(entryPoint);
|
|
switchToTabHavingURI(url, true, {
|
|
replaceQueryString: true
|
|
});
|
|
},
|
|
|
|
sendTabToDevice(url, clientId, title) {
|
|
Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title);
|
|
},
|
|
|
|
populateSendTabToDevicesMenu(devicesPopup, url, title) {
|
|
// remove existing menu items
|
|
while (devicesPopup.hasChildNodes()) {
|
|
devicesPopup.firstChild.remove();
|
|
}
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
const onTargetDeviceCommand = (event) => {
|
|
let clients = event.target.getAttribute("clientId") ?
|
|
[event.target.getAttribute("clientId")] :
|
|
this.remoteClients.map(client => client.id);
|
|
|
|
clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
|
|
}
|
|
|
|
function addTargetDevice(clientId, name) {
|
|
const targetDevice = document.createElement("menuitem");
|
|
targetDevice.addEventListener("command", onTargetDeviceCommand, true);
|
|
targetDevice.setAttribute("class", "sendtab-target");
|
|
targetDevice.setAttribute("clientId", clientId);
|
|
targetDevice.setAttribute("label", name);
|
|
fragment.appendChild(targetDevice);
|
|
}
|
|
|
|
const clients = this.remoteClients;
|
|
for (let client of clients) {
|
|
addTargetDevice(client.id, client.name);
|
|
}
|
|
|
|
// "All devices" menu item
|
|
if (clients.length > 1) {
|
|
const separator = document.createElement("menuseparator");
|
|
fragment.appendChild(separator);
|
|
const allDevicesLabel = this.fxaStrings.GetStringFromName("sendTabToAllDevices.menuitem");
|
|
addTargetDevice("", allDevicesLabel);
|
|
}
|
|
|
|
devicesPopup.appendChild(fragment);
|
|
},
|
|
|
|
isSendableURI(aURISpec) {
|
|
if (!aURISpec) {
|
|
return false;
|
|
}
|
|
// Disallow sending tabs with more than 65535 characters.
|
|
if (aURISpec.length > 65535) {
|
|
return false;
|
|
}
|
|
try {
|
|
// Filter out un-sendable URIs -- things like local files, object urls, etc.
|
|
const unsendableRegexp = new RegExp(
|
|
Services.prefs.getCharPref("services.sync.engine.tabs.filteredUrls"), "i");
|
|
return !unsendableRegexp.test(aURISpec);
|
|
} catch (e) {
|
|
// The preference has been removed, or is an invalid regexp, so we log an
|
|
// error and treat it as a valid URI -- and the more problematic case is
|
|
// the length, which we've already addressed.
|
|
Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
|
|
return true;
|
|
}
|
|
},
|
|
|
|
updateTabContextMenu(aPopupMenu, aTargetTab) {
|
|
if (!this.sendTabToDeviceEnabled || !this.weaveService.ready) {
|
|
return;
|
|
}
|
|
|
|
const targetURI = aTargetTab.linkedBrowser.currentURI.spec;
|
|
const showSendTab = this.remoteClients.length > 0 && this.isSendableURI(targetURI);
|
|
|
|
["context_sendTabToDevice", "context_sendTabToDevice_separator"]
|
|
.forEach(id => { document.getElementById(id).hidden = !showSendTab });
|
|
},
|
|
|
|
initPageContextMenu(contextMenu) {
|
|
if (!this.sendTabToDeviceEnabled || !this.weaveService.ready) {
|
|
return;
|
|
}
|
|
|
|
const remoteClientPresent = this.remoteClients.length > 0;
|
|
// showSendLink and showSendPage are mutually exclusive
|
|
let showSendLink = remoteClientPresent
|
|
&& (contextMenu.onSaveableLink || contextMenu.onPlainTextLink);
|
|
const showSendPage = !showSendLink && remoteClientPresent
|
|
&& !(contextMenu.isContentSelected ||
|
|
contextMenu.onImage || contextMenu.onCanvas ||
|
|
contextMenu.onVideo || contextMenu.onAudio ||
|
|
contextMenu.onLink || contextMenu.onTextInput)
|
|
&& this.isSendableURI(contextMenu.browser.currentURI.spec);
|
|
|
|
if (showSendLink) {
|
|
// This isn't part of the condition above since we don't want to try and
|
|
// send the page if a link is clicked on or selected but is not sendable.
|
|
showSendLink = this.isSendableURI(contextMenu.linkURL);
|
|
}
|
|
|
|
["context-sendpagetodevice", "context-sep-sendpagetodevice"]
|
|
.forEach(id => contextMenu.showItem(id, showSendPage));
|
|
["context-sendlinktodevice", "context-sep-sendlinktodevice"]
|
|
.forEach(id => contextMenu.showItem(id, showSendLink));
|
|
},
|
|
|
|
// Functions called by observers
|
|
onActivityStart() {
|
|
clearTimeout(this._syncAnimationTimer);
|
|
this._syncStartTime = Date.now();
|
|
|
|
let broadcaster = document.getElementById("sync-status");
|
|
broadcaster.setAttribute("syncstatus", "active");
|
|
broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncing2.label"));
|
|
broadcaster.setAttribute("disabled", "true");
|
|
},
|
|
|
|
_onActivityStop() {
|
|
if (!gBrowser)
|
|
return;
|
|
let broadcaster = document.getElementById("sync-status");
|
|
broadcaster.removeAttribute("syncstatus");
|
|
broadcaster.removeAttribute("disabled");
|
|
broadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
|
|
Services.obs.notifyObservers(null, "test:browser-sync:activity-stop");
|
|
},
|
|
|
|
onActivityStop() {
|
|
let now = Date.now();
|
|
let syncDuration = now - this._syncStartTime;
|
|
|
|
if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
|
|
let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
|
|
clearTimeout(this._syncAnimationTimer);
|
|
this._syncAnimationTimer = setTimeout(() => this._onActivityStop(), animationTime);
|
|
} else {
|
|
this._onActivityStop();
|
|
}
|
|
},
|
|
|
|
// doSync forces a sync - it *does not* return a promise as it is called
|
|
// via the various UI components.
|
|
doSync() {
|
|
if (!UIState.isReady()) {
|
|
return;
|
|
}
|
|
const state = UIState.get();
|
|
if (state.status == UIState.STATUS_SIGNED_IN) {
|
|
setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0);
|
|
}
|
|
Services.obs.notifyObservers(null, "cloudsync:user-sync");
|
|
},
|
|
|
|
openPrefs(entryPoint = "syncbutton", origin = undefined) {
|
|
window.openPreferences("paneSync", { origin, urlParams: { entrypoint: entryPoint } });
|
|
},
|
|
|
|
openSyncedTabsPanel() {
|
|
let placement = CustomizableUI.getPlacementOfWidget("sync-button");
|
|
let area = placement ? placement.area : CustomizableUI.AREA_NAVBAR;
|
|
let anchor = document.getElementById("sync-button") ||
|
|
document.getElementById("PanelUI-menu-button");
|
|
if (area == CustomizableUI.AREA_PANEL) {
|
|
// The button is in the panel, so we need to show the panel UI, then our
|
|
// subview.
|
|
PanelUI.show().then(() => {
|
|
PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
|
|
}).catch(Cu.reportError);
|
|
} else {
|
|
// It is placed somewhere else - just try and show it.
|
|
PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
|
|
}
|
|
},
|
|
|
|
/* After we are initialized we perform a once-only check for the sync
|
|
button being in "customize purgatory" and if so, move it to the panel.
|
|
This is done primarily for profiles created before SyncedTabs landed,
|
|
where the button defaulted to being in that purgatory.
|
|
We use a preference to ensure we only do it once, so people can still
|
|
customize it away and have it stick.
|
|
*/
|
|
maybeMoveSyncedTabsButton() {
|
|
if (gPhotonStructure) {
|
|
return;
|
|
}
|
|
const prefName = "browser.migrated-sync-button";
|
|
let migrated = Services.prefs.getBoolPref(prefName, false);
|
|
if (migrated) {
|
|
return;
|
|
}
|
|
if (!CustomizableUI.getPlacementOfWidget("sync-button")) {
|
|
CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
|
|
}
|
|
Services.prefs.setBoolPref(prefName, true);
|
|
},
|
|
|
|
/* Update the tooltip for the sync-status broadcaster (which will update the
|
|
Sync Toolbar button and the Sync spinner in the FxA hamburger area.)
|
|
If Sync is configured, the tooltip is when the last sync occurred,
|
|
otherwise the tooltip reflects the fact that Sync needs to be
|
|
(re-)configured.
|
|
*/
|
|
updateSyncButtonsTooltip(state) {
|
|
const status = state.status;
|
|
|
|
// This is a little messy as the Sync buttons are 1/2 Sync related and
|
|
// 1/2 FxA related - so for some strings we use Sync strings, but for
|
|
// others we reach into gSync for strings.
|
|
let tooltiptext;
|
|
if (status == UIState.STATUS_NOT_VERIFIED) {
|
|
// "needs verification"
|
|
tooltiptext = this.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
|
|
} else if (status == UIState.STATUS_NOT_CONFIGURED) {
|
|
// "needs setup".
|
|
tooltiptext = this.syncStrings.GetStringFromName("signInToSync.description");
|
|
} else if (status == UIState.STATUS_LOGIN_FAILED) {
|
|
// "need to reconnect/re-enter your password"
|
|
tooltiptext = this.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
|
|
} else {
|
|
// Sync appears configured - format the "last synced at" time.
|
|
tooltiptext = this.formatLastSyncDate(state.lastSync);
|
|
}
|
|
|
|
let broadcaster = document.getElementById("sync-status");
|
|
if (broadcaster) {
|
|
if (tooltiptext) {
|
|
broadcaster.setAttribute("tooltiptext", tooltiptext);
|
|
} else {
|
|
broadcaster.removeAttribute("tooltiptext");
|
|
}
|
|
}
|
|
},
|
|
|
|
get withinLastWeekFormat() {
|
|
delete this.withinLastWeekFormat;
|
|
return this.withinLastWeekFormat = new Intl.DateTimeFormat(undefined,
|
|
{weekday: "long", hour: "numeric", minute: "numeric"});
|
|
},
|
|
|
|
get oneWeekOrOlderFormat() {
|
|
delete this.oneWeekOrOlderFormat;
|
|
return this.oneWeekOrOlderFormat = new Intl.DateTimeFormat(undefined,
|
|
{month: "long", day: "numeric"});
|
|
},
|
|
|
|
formatLastSyncDate(date) {
|
|
let sixDaysAgo = (() => {
|
|
let tempDate = new Date();
|
|
tempDate.setDate(tempDate.getDate() - 6);
|
|
tempDate.setHours(0, 0, 0, 0);
|
|
return tempDate;
|
|
})();
|
|
|
|
// It may be confusing for the user to see "Last Sync: Monday" when the last
|
|
// sync was indeed a Monday, but 3 weeks ago.
|
|
let dateFormat = date < sixDaysAgo ? this.oneWeekOrOlderFormat : this.withinLastWeekFormat;
|
|
|
|
let lastSyncDateString = dateFormat.format(date);
|
|
return this.syncStrings.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
|
|
},
|
|
|
|
onClientsSynced() {
|
|
let broadcaster = document.getElementById("sync-syncnow-state");
|
|
if (broadcaster) {
|
|
if (Weave.Service.clientsEngine.stats.numClients > 1) {
|
|
broadcaster.setAttribute("devices-status", "multi");
|
|
} else {
|
|
broadcaster.setAttribute("devices-status", "single");
|
|
}
|
|
}
|
|
},
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Ci.nsIObserver,
|
|
Ci.nsISupportsWeakReference
|
|
])
|
|
};
|
|
|
|
XPCOMUtils.defineLazyGetter(gSync, "weaveService", function() {
|
|
return Components.classes["@mozilla.org/weave/service;1"]
|
|
.getService(Components.interfaces.nsISupports)
|
|
.wrappedJSObject;
|
|
});
|