Merge pull request #1654 from mozilla/UX-UI-update

UX/UI Update
This commit is contained in:
Maxx Crawford
2020-05-22 14:07:20 -05:00
committed by GitHub
55 changed files with 1883 additions and 1110 deletions
+2 -1
View File
@@ -8,6 +8,7 @@ module.exports = {
"backgroundLogic": true,
"identityState": true,
"messageHandler": true,
"sync": true
"sync": true,
"Utils": true
}
};
+123 -23
View File
@@ -205,10 +205,33 @@ window.assignManager = {
return {};
}
const userContextId = this.getUserContextIdFromCookieStore(tab);
if (!siteSettings
|| userContextId === siteSettings.userContextId
|| this.storageArea.isExempted(options.url, tab.id)) {
return {};
// https://github.com/mozilla/multi-account-containers/issues/847
//
// Handle the case where this request's URL is not assigned to any particular
// container. We must do the following check:
//
// If the current tab's container is "unlocked", we can just go ahead
// and open the URL in the current tab, since an "unlocked" container accepts
// any-and-all sites.
//
// But if the current tab's container has been "locked" by the user, then we must
// re-open the page in the default container, because the user doesn't want random
// sites polluting their locked container.
//
// For example:
// - the current tab's container is locked and only allows "www.google.com"
// - the incoming request is for "www.amazon.com", which has no specific container assignment
// - in this case, we must re-open "www.amazon.com" in a new tab in the default container
const siteIsolatedReloadInDefault =
await this._maybeSiteIsolatedReloadInDefault(siteSettings, tab);
if (!siteIsolatedReloadInDefault) {
if (!siteSettings
|| userContextId === siteSettings.userContextId
|| this.storageArea.isExempted(options.url, tab.id)) {
return {};
}
}
const removeTab = backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|| (messageHandler.lastCreatedTab
@@ -257,15 +280,24 @@ window.assignManager = {
}
}
this.reloadPageInContainer(
options.url,
userContextId,
siteSettings.userContextId,
tab.index + 1,
tab.active,
siteSettings.neverAsk,
openTabId
);
if (siteIsolatedReloadInDefault) {
this.reloadPageInDefaultContainer(
options.url,
tab.index + 1,
tab.active,
openTabId
);
} else {
this.reloadPageInContainer(
options.url,
userContextId,
siteSettings.userContextId,
tab.index + 1,
tab.active,
siteSettings.neverAsk,
openTabId
);
}
this.calculateContextMenu(tab);
/* Removal of existing tabs:
@@ -299,6 +331,29 @@ window.assignManager = {
};
},
async _maybeSiteIsolatedReloadInDefault(siteSettings, tab) {
// Tab doesn't support cookies, so containers not supported either.
if (!("cookieStoreId" in tab)) {
return false;
}
// Requested page has been assigned to a specific container.
// I.e. it will be opened in that container anyway, so we don't need to check if the
// current tab's container is locked or not.
if (siteSettings) {
return false;
}
//tab is alredy reopening in the default container
if (tab.cookieStoreId === "firefox-default") {
return false;
}
// Requested page is not assigned to a specific container. If the current tab's container
// is locked, then the page must be reloaded in the default container.
const currentContainerState = await identityState.storageArea.get(tab.cookieStoreId);
return currentContainerState && currentContainerState.isIsolated;
},
init() {
browser.contextMenus.onClicked.addListener((info, tab) => {
info.bookmarkId ?
@@ -475,7 +530,6 @@ window.assignManager = {
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
let actionName;
// https://github.com/mozilla/testpilot-containers/issues/626
// Context menu has stored context IDs as strings, so we need to coerce
// the value to a string for accurate checking
@@ -500,16 +554,40 @@ window.assignManager = {
userContextId,
neverAsk: false
}, exemptedTabIds);
actionName = "added";
actionName = "assigned site to always open in this container";
} else {
// Remove assignment
await this.storageArea.remove(pageUrl);
actionName = "removed";
actionName = "removed from assigned sites list";
// remove site isolation if now empty
await this._maybeRemoveSiteIsolation(userContextId);
}
browser.tabs.sendMessage(tabId, {
text: `Successfully ${actionName} site to always open in this container`
});
const tab = await browser.tabs.get(tabId);
this.calculateContextMenu(tab);
if (tabId) {
const tab = await browser.tabs.get(tabId);
setTimeout(function(){
browser.tabs.sendMessage(tabId, {
text: `Successfully ${actionName}`
});
}, 1000);
this.calculateContextMenu(tab);
}
},
async _maybeRemoveSiteIsolation(userContextId) {
const assignments = await this.storageArea.getByContainer(userContextId);
const hasAssignments = assignments && Object.keys(assignments).length > 0;
if (hasAssignments) {
return;
}
await backgroundLogic.addRemoveSiteIsolation(
backgroundLogic.cookieStoreId(userContextId),
true
);
},
async _getAssignment(tab) {
@@ -589,13 +667,35 @@ window.assignManager = {
});
},
reloadPageInDefaultContainer(url, index, active, openerTabId) {
// To create a new tab in the default container, it is easiest just to omit the
// cookieStoreId entirely.
//
// Unfortunately, if you create a new tab WITHOUT a cookieStoreId but WITH an openerTabId,
// then the new tab automatically inherits the opener tab's cookieStoreId.
// I.e. it opens in the wrong container!
//
// So we have to explicitly pass in a cookieStoreId when creating the tab, since we
// are specifying the openerTabId. There doesn't seem to be any way
// to look up the default container's cookieStoreId programatically, so sadly
// we have to hardcode it here as "firefox-default". This is potentially
// not cross-browser compatible.
//
// Note that we could have just omitted BOTH cookieStoreId and openerTabId. But the
// drawback then is that if the user later closes the newly-created tab, the browser
// does not automatically return to the original opener tab. To get this desired behaviour,
// we MUST specify the openerTabId when creating the new tab.
const cookieStoreId = "firefox-default";
browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
},
reloadPageInContainer(url, currentUserContextId, userContextId, index, active, neverAsk = false, openerTabId = null) {
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
const loadPage = browser.extension.getURL("confirm-page.html");
// False represents assignment is not permitted
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
if (neverAsk) {
browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
return browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
} else {
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
let currentCookieStoreId;
@@ -603,7 +703,7 @@ window.assignManager = {
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
confirmUrl += `&currentCookieStoreId=${currentCookieStoreId}`;
}
browser.tabs.create({
return browser.tabs.create({
url: confirmUrl,
cookieStoreId: currentCookieStoreId,
openerTabId,
+34 -2
View File
@@ -6,7 +6,20 @@ const backgroundLogic = {
"about:home",
"about:blank"
]),
NUMBER_OF_KEYBOARD_SHORTCUTS: 10,
unhideQueue: [],
init() {
browser.commands.onCommand.addListener(function (command) {
for (let i=0; i < backgroundLogic.NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
const key = "open_container_" + i;
const cookieStoreId = identityState.keyboardShortcut[key];
if (command === key) {
if (cookieStoreId === "none") return;
browser.tabs.create({cookieStoreId});
}
}
});
},
async getExtensionInfo() {
const manifestPath = browser.extension.getURL("manifest.json");
@@ -123,6 +136,20 @@ const backgroundLogic = {
}
},
// https://github.com/mozilla/multi-account-containers/issues/847
async addRemoveSiteIsolation(cookieStoreId, remove = false) {
const containerState = await identityState.storageArea.get(cookieStoreId);
try {
if ("isIsolated" in containerState || remove) {
delete containerState.isIsolated;
} else {
containerState.isIsolated = "locked";
}
return await identityState.storageArea.set(cookieStoreId, containerState);
} catch (error) {
console.error(`No container: ${cookieStoreId}`);
}
},
async moveTabsToWindow(options) {
const requiredArguments = ["cookieStoreId", "windowId"];
@@ -229,7 +256,8 @@ const backgroundLogic = {
hasHiddenTabs: !!containerState.hiddenTabs.length,
hasOpenTabs: !!openTabs.length,
numberOfHiddenTabs: containerState.hiddenTabs.length,
numberOfOpenTabs: openTabs.length
numberOfOpenTabs: openTabs.length,
isIsolated: !!containerState.isIsolated
};
return;
});
@@ -329,6 +357,10 @@ const backgroundLogic = {
},
cookieStoreId(userContextId) {
if(userContextId === 0) return "firefox-default";
return `firefox-container-${userContextId}`;
}
};
};
backgroundLogic.init();
+33 -2
View File
@@ -1,4 +1,5 @@
window.identityState = {
keyboardShortcut: {},
storageArea: {
area: browser.storage.local,
@@ -27,7 +28,7 @@ window.identityState = {
await this.set(cookieStoreId, defaultContainerState);
return defaultContainerState;
}
throw new Error (`${cookieStoreId} not found`);
return false;
},
set(cookieStoreId, data) {
@@ -42,6 +43,29 @@ window.identityState = {
return this.area.remove([storeKey]);
},
async setKeyboardShortcut(shortcutId, cookieStoreId) {
identityState.keyboardShortcut[shortcutId] = cookieStoreId;
return this.area.set({[shortcutId]: cookieStoreId});
},
async loadKeyboardShortcuts () {
const identities = await browser.contextualIdentities.query({});
for (let i=0; i < backgroundLogic.NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
const key = "open_container_" + i;
const storageObject = await this.area.get(key);
if (storageObject[key]){
identityState.keyboardShortcut[key] = storageObject[key];
continue;
}
if (identities[i]) {
identityState.keyboardShortcut[key] = identities[i].cookieStoreId;
continue;
}
identityState.keyboardShortcut[key] = "none";
}
return identityState.keyboardShortcut;
},
/*
* Looks for abandoned identity keys in local storage, and makes sure all
* identities registered in the browser are also in local storage. (this
@@ -72,7 +96,8 @@ window.identityState = {
}
}
}
}
},
},
_createTabObject(tab) {
@@ -153,8 +178,14 @@ window.identityState = {
macAddonUUID: uuidv4()
};
},
init() {
this.storageArea.loadKeyboardShortcuts();
}
};
identityState.init();
function uuidv4() {
// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
+37 -4
View File
@@ -6,10 +6,17 @@ const messageHandler = {
init() {
// Handles messages from webextension code
browser.runtime.onMessage.addListener((m) => {
browser.runtime.onMessage.addListener(async (m) => {
let response;
let tab;
switch (m.method) {
case "getShortcuts":
response = identityState.storageArea.loadKeyboardShortcuts();
break;
case "setShortcut":
identityState.storageArea.setKeyboardShortcut(m.shortcut, m.cookieStoreId);
break;
case "resetSync":
response = sync.resetSync();
break;
@@ -25,6 +32,9 @@ const messageHandler = {
case "neverAsk":
assignManager._neverAsk(m);
break;
case "addRemoveSiteIsolation":
response = backgroundLogic.addRemoveSiteIsolation(m.cookieStoreId);
break;
case "getAssignment":
response = browser.tabs.get(m.tabId).then((tab) => {
return assignManager._getAssignment(tab);
@@ -36,9 +46,7 @@ const messageHandler = {
case "setOrRemoveAssignment":
// m.tabId is used for where to place the in content message
// m.url is the assignment to be removed/added
response = browser.tabs.get(m.tabId).then((tab) => {
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
});
response = assignManager._setOrRemoveAssignment(m.tabId, m.url, m.userContextId, m.value);
break;
case "sortTabs":
backgroundLogic.sortTabs();
@@ -73,6 +81,31 @@ const messageHandler = {
case "exemptContainerAssignment":
response = assignManager._exemptTab(m);
break;
case "reloadInContainer":
response = assignManager.reloadPageInContainer(
m.url,
m.currentUserContextId,
m.newUserContextId,
m.tabIndex,
m.active,
true
);
break;
case "assignAndReloadInContainer":
tab = await assignManager.reloadPageInContainer(
m.url,
m.currentUserContextId,
m.newUserContextId,
m.tabIndex,
m.active,
true
);
// m.tabId is used for where to place the in content message
// m.url is the assignment to be removed/added
response = browser.tabs.get(tab.id).then((tab) => {
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.newUserContextId, m.value);
});
break;
}
return response;
});
+51 -2
View File
@@ -1,3 +1,5 @@
const NUMBER_OF_KEYBOARD_SHORTCUTS = 10;
async function requestPermissions() {
const checkbox = document.querySelector("#bookmarksPermissions");
if (checkbox.checked) {
@@ -22,7 +24,7 @@ async function enableDisableSync() {
browser.runtime.sendMessage({ method: "resetSync" });
}
async function restoreOptions() {
async function setupOptions() {
const hasPermission = await browser.permissions.contains({permissions: ["bookmarks"]});
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
if (hasPermission) {
@@ -33,9 +35,56 @@ async function restoreOptions() {
} else {
document.querySelector("#syncCheck").checked = false;
}
setupContainerShortcutSelects();
}
async function setupContainerShortcutSelects () {
const keyboardShortcut = await browser.runtime.sendMessage({method: "getShortcuts"});
const identities = await browser.contextualIdentities.query({});
const fragment = document.createDocumentFragment();
const noneOption = document.createElement("option");
noneOption.value = "none";
noneOption.id = "none";
noneOption.textContent = "None";
fragment.append(noneOption);
document.addEventListener("DOMContentLoaded", restoreOptions);
for (const identity of identities) {
const option = document.createElement("option");
option.value = identity.cookieStoreId;
option.id = identity.cookieStoreId;
option.textContent = identity.name;
fragment.append(option);
}
for (let i=0; i < NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
const shortcutKey = "open_container_"+i;
const shortcutSelect = document.getElementById(shortcutKey);
shortcutSelect.appendChild(fragment.cloneNode(true));
if (keyboardShortcut && keyboardShortcut[shortcutKey]) {
const cookieStoreId = keyboardShortcut[shortcutKey];
shortcutSelect.querySelector("#" + cookieStoreId).selected = true;
}
}
}
function storeShortcutChoice (event) {
browser.runtime.sendMessage({
method: "setShortcut",
shortcut: event.target.id,
cookieStoreId: event.target.value
});
}
function resetOnboarding() {
browser.storage.local.set({"onboarding-stage": 0});
}
document.addEventListener("DOMContentLoaded", setupOptions);
document.querySelector("#bookmarksPermissions").addEventListener( "change", requestPermissions);
document.querySelector("#syncCheck").addEventListener( "change", enableDisableSync);
document.querySelector("button").addEventListener("click", resetOnboarding);
for (let i=0; i < NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
document.querySelector("#open_container_"+i)
.addEventListener("change", storeShortcutChoice);
}
+35
View File
@@ -0,0 +1,35 @@
async function init() {
const fragment = document.createDocumentFragment();
const identities = await browser.contextualIdentities.query({});
identities.forEach(identity => {
const tr = document.createElement("tr");
tr.classList.add("menu-item", "hover-highlight");
const td = document.createElement("td");
td.innerHTML = Utils.escaped`
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="${identity.icon}"
data-identity-color="${identity.color}">
</div>
</div>
<span class="menu-text">${identity.name}</span>`;
tr.appendChild(td);
fragment.appendChild(tr);
Utils.addEnterHandler(tr, async () => {
Utils.alwaysOpenInContainer(identity);
window.close();
});
});
const list = document.querySelector("#picker-identities-list");
list.innerHTML = "";
list.appendChild(fragment);
}
init();
+577 -428
View File
File diff suppressed because it is too large Load Diff
+107 -2
View File
@@ -1,11 +1,11 @@
const DEFAULT_FAVICON = "/img/blank-favicon.svg";
// TODO use export here instead of globals
window.Utils = {
const Utils = {
createFavIconElement(url) {
const imageElement = document.createElement("img");
imageElement.classList.add("icon", "offpage");
imageElement.classList.add("icon", "offpage", "menu-icon");
imageElement.src = url;
const loadListener = (e) => {
e.target.classList.remove("offpage");
@@ -18,6 +18,111 @@ window.Utils = {
imageElement.addEventListener("error", errorListener);
imageElement.addEventListener("load", loadListener);
return imageElement;
},
/**
* Escapes any occurances of &, ", <, > or / with XML entities.
*
* @param {string} str
* The string to escape.
* @return {string} The escaped string.
*/
escapeXML(str) {
const replacements = { "&": "&amp;", "\"": "&quot;", "'": "&apos;", "<": "&lt;", ">": "&gt;", "/": "&#x2F;" };
return String(str).replace(/[&"'<>/]/g, m => replacements[m]);
},
/**
* A tagged template function which escapes any XML metacharacters in
* interpolated values.
*
* @param {Array<string>} strings
* An array of literal strings extracted from the templates.
* @param {Array} values
* An array of interpolated values extracted from the template.
* @returns {string}
* The result of the escaped values interpolated with the literal
* strings.
*/
escaped(strings, ...values) {
const result = [];
for (const [i, string] of strings.entries()) {
result.push(string);
if (i < values.length)
result.push(this.escapeXML(values[i]));
}
return result.join("");
},
async currentTab() {
const activeTabs = await browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT });
if (activeTabs.length > 0) {
return activeTabs[0];
}
return false;
},
addEnterHandler(element, handler) {
element.addEventListener("click", (e) => {
handler(e);
});
element.addEventListener("keydown", (e) => {
if (e.keyCode === 13) {
e.preventDefault();
handler(e);
}
});
},
userContextId(cookieStoreId = "") {
const userContextId = cookieStoreId.replace("firefox-container-", "");
return (userContextId !== cookieStoreId) ? Number(userContextId) : false;
},
setOrRemoveAssignment(tabId, url, userContextId, value) {
return browser.runtime.sendMessage({
method: "setOrRemoveAssignment",
tabId,
url,
userContextId,
value
});
},
async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active) {
return await browser.runtime.sendMessage({
method: "reloadInContainer",
url,
currentUserContextId,
newUserContextId,
tabIndex,
active
});
},
async alwaysOpenInContainer(identity) {
const currentTab = await this.currentTab();
const assignedUserContextId = this.userContextId(identity.cookieStoreId);
if (currentTab.cookieStoreId !== identity.cookieStoreId) {
return await browser.runtime.sendMessage({
method: "assignAndReloadInContainer",
url: currentTab.url,
currentUserContextId: false,
newUserContextId: assignedUserContextId,
tabIndex: currentTab.index +1,
active:currentTab.active
});
}
await Utils.setOrRemoveAssignment(
currentTab.id,
currentTab.url,
assignedUserContextId,
false
);
}
};
window.Utils = Utils;