diff --git a/src/browser/components/preferences/preferences-js.patch b/src/browser/components/preferences/preferences-js.patch index 918e727ef..3e3ad8c86 100644 --- a/src/browser/components/preferences/preferences-js.patch +++ b/src/browser/components/preferences/preferences-js.patch @@ -1,8 +1,8 @@ diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js -index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a9c5bb3f8 100644 +index a76699af75252bec31e17b562463c37236cbbfe5..6355a6ee92754846ffcdd123ac49545d520470c7 100644 --- a/browser/components/preferences/preferences.js +++ b/browser/components/preferences/preferences.js -@@ -131,6 +131,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () { +@@ -132,6 +132,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () { styleSheets: [ "chrome://browser/skin/preferences/dialog.css", "chrome://browser/skin/preferences/preferences.css", @@ -10,7 +10,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a ], resizeCallback: async ({ title, frame }) => { // Search within main document and highlight matched keyword. -@@ -437,6 +438,8 @@ const CONFIG_PANES = Object.freeze({ +@@ -462,6 +463,8 @@ const CONFIG_PANES = Object.freeze({ tabsBrowsing: { l10nId: "tabs-browsing-section", groupIds: [ @@ -19,7 +19,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a "browserLayout", "tabs", "pageNavigation", -@@ -483,7 +486,7 @@ function register_module(categoryName, categoryObject) { +@@ -509,7 +512,7 @@ function register_module(categoryName, categoryObject) { } this._initted = true; let template = document.getElementById("template-" + categoryName); @@ -28,7 +28,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a // Replace the template element with the nodes inside of it. template.replaceWith(template.content); -@@ -527,6 +530,10 @@ function init_all() { +@@ -553,6 +556,10 @@ function init_all() { register_module("paneHome", gHomePane); register_module("paneSearch", gSearchPane); register_module("panePrivacy", gPrivacyPane); @@ -39,15 +39,3 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a // Restore the cached Firefox Labs nav button visibility so it shows // immediately when recipes are expected to be available, before -@@ -656,9 +663,9 @@ async function gotoPref( - let redesignEnabled = srdSectionPrefs.all; - let categories = document.getElementById("categories"); - const kDefaultCategoryInternalName = redesignEnabled -- ? "paneSync" -+ ? "paneTabsBrowsing" - : "paneGeneral"; -- const kDefaultCategory = redesignEnabled ? "sync" : "general"; -+ const kDefaultCategory = redesignEnabled ? "tabsBrowsing" : "general"; - let hash = document.location.hash; - let category = aCategory || hash.substring(1) || kDefaultCategoryInternalName; - diff --git a/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs b/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs index 035f1f76d..39f791b57 100644 --- a/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs +++ b/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs @@ -368,7 +368,7 @@ class nsZenLiveFoldersManager { }, ], content: { - width: "310px", + width: "312px", position: "callout", title_logo: { imageURL: icon, diff --git a/src/zen/sync/ZenSpacesSyncApplier.sys.mjs b/src/zen/sync/ZenSpacesSyncApplier.sys.mjs index 555abd2ce..e2d562f5c 100644 --- a/src/zen/sync/ZenSpacesSyncApplier.sys.mjs +++ b/src/zen/sync/ZenSpacesSyncApplier.sys.mjs @@ -6,6 +6,7 @@ import { canonicalJSON, LAYOUT_RECORD_ID, RECORD_KINDS, + syncableIconUrl, ZenSpacesSyncModel, } from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs"; @@ -379,6 +380,13 @@ class nsZenSpacesSyncApplier { folder.label = data.name; } + if (data.live && !folder.isLiveFolder) { + // The folder synced before its provider config was available and + // materialized as a plain folder; upgrade it so this profile + // projects the config too instead of clobbering it with live: null. + folder.isLiveFolder = true; + } + if ((folder.iconURL || null) !== (data.icon || null)) { win.gZenFolders.setFolderUserIcon(folder, data.icon || null); // Window sync picks this up and mirrors the icon everywhere else. @@ -508,7 +516,7 @@ class nsZenSpacesSyncApplier { lazy.ZenWindowSync.setPinnedInitialState( tab, { url: data.url, title: data.title || "" }, - data.icon || undefined + syncableIconUrl(data.icon) || undefined ); } @@ -518,12 +526,15 @@ class nsZenSpacesSyncApplier { } else { tab.removeAttribute("zenDefaultUserContextId"); } + // Records from before icon normalization can still carry wrapped or + // remote urls setIcon would refuse. + const icon = syncableIconUrl(data.icon); tab.zenStaticLabel = typeof data.staticLabel === "string" ? data.staticLabel : undefined; - tab.zenStaticIcon = data.hasStaticIcon && data.icon ? data.icon : undefined; - if (data.icon) { + tab.zenStaticIcon = data.hasStaticIcon && icon ? icon : undefined; + if (icon) { try { - win.gBrowser.setIcon(tab, data.icon); + win.gBrowser.setIcon(tab, icon); // The tab is lazy, so its session state cache keeps the (empty) // image it was created with: it is only cleared once a restore // finishes, which never happens for lazy tabs, and it would hide @@ -547,15 +558,16 @@ class nsZenSpacesSyncApplier { * @param {object} data */ #updateTabIdentity(win, tab, data) { + const icon = syncableIconUrl(data.icon); const initial = tab._zenPinnedInitialState; const identityChanged = initial?.entry?.url !== data.url || (initial?.entry?.title || "") !== (data.title || ""); - if (identityChanged || (initial?.image || "") !== (data.icon || "")) { + if (identityChanged || syncableIconUrl(initial?.image || "") !== icon) { lazy.ZenWindowSync.setPinnedInitialState( tab, { url: data.url, title: data.title || "" }, - data.icon || undefined + icon || undefined ); } if (identityChanged && !tab.linkedPanel) { @@ -580,10 +592,12 @@ class nsZenSpacesSyncApplier { } } } - tab.zenStaticIcon = data.hasStaticIcon && data.icon ? data.icon : undefined; - if (data.icon && tab.getAttribute("image") !== data.icon) { + tab.zenStaticIcon = data.hasStaticIcon && icon ? icon : undefined; + // Compare normalized: setIcon rewraps svg data: icons into + // moz-remote-image urls when writing the attribute. + if (icon && syncableIconUrl(tab.getAttribute("image")) !== icon) { try { - win.gBrowser.setIcon(tab, data.icon); + win.gBrowser.setIcon(tab, icon); } catch (e) { console.error("ZenSpacesSync: failed to set tab icon", e); } @@ -619,7 +633,7 @@ class nsZenSpacesSyncApplier { }, ]; state.index = 1; - state.image = data.icon || undefined; + state.image = syncableIconUrl(data.icon) || undefined; delete state.scroll; win.SessionStore.setTabState(tab, state); } catch (e) { diff --git a/src/zen/sync/ZenSpacesSyncModel.sys.mjs b/src/zen/sync/ZenSpacesSyncModel.sys.mjs index 3d08be58d..0ec8dd760 100644 --- a/src/zen/sync/ZenSpacesSyncModel.sys.mjs +++ b/src/zen/sync/ZenSpacesSyncModel.sys.mjs @@ -36,6 +36,36 @@ const BUILTIN_GUID_PREFIX = "builtin-"; const STORE_FILE_NAME = "zen-spaces-sync.json"; const STORE_VERSION = 1; +// Everything setIcon accepts without a loading principal. +const LOCAL_ICON_PROTOCOLS = ["data:", "chrome:", "about:", "resource:"]; + +/** + * Normalizes a tab icon for syncing. The image attribute can hold a + * moz-remote-image: wrapper around the actual (svg data:) favicon, carrying + * a per-process contentParentId that means nothing on another device — sync + * the wrapped URL instead. Anything else setIcon would reject as remote is + * dropped. + * + * @param {?string} icon + * @returns {string} + */ +export function syncableIconUrl(icon) { + if (!icon || typeof icon !== "string") { + return ""; + } + if (icon.startsWith("moz-remote-image:")) { + try { + const uri = Services.io.newURI(icon); + icon = new URLSearchParams(uri.query).get("url") || ""; + } catch (e) { + return ""; + } + } + return LOCAL_ICON_PROTOCOLS.some(protocol => icon.startsWith(protocol)) + ? icon + : ""; +} + function sortedClone(value) { if (Array.isArray(value)) { return value.map(sortedClone); @@ -201,7 +231,7 @@ class nsZenSpacesSyncModel { if (!url || url === "about:blank") { return null; } - const icon = initial?.image || tabData.image || ""; + const icon = syncableIconUrl(initial?.image || tabData.image || ""); return { url, title: title || "", icon }; } @@ -357,6 +387,7 @@ class nsZenSpacesSyncModel { } const map = new Map(); + const pending = new Set(); const ctx = this.#projectionContext(sidebar); const { tabs, folders, splits, splitIds, splitParents, splitWs } = ctx; @@ -425,6 +456,10 @@ class nsZenSpacesSyncModel { } catch (e) { console.error("ZenSpacesSync: failed to project live folder", e); } + if (!live) { + pending.add(fid); + continue; + } } map.set(fid, { kind: RECORD_KINDS.FOLDER, @@ -486,10 +521,20 @@ class nsZenSpacesSyncModel { }); } - this.#cache = { stamp, map }; + this.#cache = { stamp, map, pending }; return map; } + /** + * Ids that are locally present but deliberately not projected this cycle + * (live folders whose provider config isn't available yet). The diff must + * not read their absence as a deletion. + */ + #pendingIds() { + this.projections(); + return this.#cache.pending; + } + #digestCache = null; /** @@ -536,6 +581,7 @@ class nsZenSpacesSyncModel { computeChangedIDs() { const uploaded = this.#data().uploaded; const current = this.#digestAll(); + const pending = this.#pendingIds(); const now = Date.now() / 1000; const changes = {}; for (const [id, digest] of current) { @@ -544,7 +590,7 @@ class nsZenSpacesSyncModel { } } for (const id of Object.keys(uploaded)) { - if (!current.has(id)) { + if (!current.has(id) && !pending.has(id)) { changes[id] = now; } } @@ -554,13 +600,14 @@ class nsZenSpacesSyncModel { hasPendingChanges() { const uploaded = this.#data().uploaded; const current = this.#digestAll(); + const pending = this.#pendingIds(); for (const [id, digest] of current) { if (uploaded[id] !== digest) { return true; } } for (const id of Object.keys(uploaded)) { - if (!current.has(id)) { + if (!current.has(id) && !pending.has(id)) { return true; } }