From 031b5d8d0a6dd1ae75a44e627138d7ba2309f23f Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:27:05 +0200 Subject: [PATCH] gh-15426: Fixed spaces wiping if zen is opened on other devices (gh-15458) --- .../browser/browser/zen-command-palette.ftl | 1 + src/zen/common/styles/zen-omnibox.css | 9 ++-- src/zen/sync/ZenSpacesSyncModel.sys.mjs | 34 +++++++++++++- src/zen/urlbar/ZenUBGlobalActions.sys.mjs | 6 ++- src/zen/urlbar/ZenUBSidebarProvider.sys.mjs | 46 ++++++++++++++----- 5 files changed, 76 insertions(+), 20 deletions(-) diff --git a/locales/en-US/browser/browser/zen-command-palette.ftl b/locales/en-US/browser/browser/zen-command-palette.ftl index 15b31201f..7bdca4205 100644 --- a/locales/en-US/browser/browser/zen-command-palette.ftl +++ b/locales/en-US/browser/browser/zen-command-palette.ftl @@ -39,3 +39,4 @@ zen-action-switch-to-dark-mode = Switch to Dark Mode zen-action-print = Print zen-action-focus-on = Focus on zen-action-extension = Extension +zen-action-open = Open diff --git a/src/zen/common/styles/zen-omnibox.css b/src/zen/common/styles/zen-omnibox.css index 1e8c199da..1565b77bb 100644 --- a/src/zen/common/styles/zen-omnibox.css +++ b/src/zen/common/styles/zen-omnibox.css @@ -653,11 +653,10 @@ --urlbarview-row-padding-block: 10px; color: light-dark(rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.7)) !important; - &:hover { - &, - & .urlbarView-favicon { - background-color: color-mix(in srgb, var(--zen-branding-bg-reverse) 5%, transparent 95%) !important; - } + &:hover, + &:hover .urlbarView-favicon, + .urlbarView-action-favicon { + background-color: color-mix(in srgb, var(--zen-branding-bg-reverse) 5%, transparent 95%) !important; } &[selected] { diff --git a/src/zen/sync/ZenSpacesSyncModel.sys.mjs b/src/zen/sync/ZenSpacesSyncModel.sys.mjs index 6ff03f7cd..9ff20efc0 100644 --- a/src/zen/sync/ZenSpacesSyncModel.sys.mjs +++ b/src/zen/sync/ZenSpacesSyncModel.sys.mjs @@ -145,6 +145,22 @@ export function recordDigest(kind, data) { class nsZenSpacesSyncModel { #file = null; #cache = null; + /** + * Ids applied from an incoming batch, keyed to the projection generation + * (sidebar lastCollected) that was current when they were applied. The + * projection only refreshes on the delayed session collection that runs + * after the sync, so within the same sync the just-applied ids are in the + * uploaded snapshot (noteApplied) but not yet in the projection. Without + * this, the outgoing diff would read that gap as local deletions and + * upload tombstones for records it just downloaded, wiping them on every + * other device (gh-15426). + */ + #appliedStamp = new Map(); + + /** The projection generation the current sidebar data belongs to. */ + #currentStamp() { + return lazy.ZenSessionStore.getSidebarData()?.lastCollected || 0; + } #data() { if (!this.#file) { @@ -743,6 +759,7 @@ class nsZenSpacesSyncModel { const uploaded = this.#data().uploaded; const current = this.#digestAll(); const pending = this.#pendingIds(); + const stamp = this.#currentStamp(); const now = Date.now() / 1000; const changes = {}; for (const [id, digest] of current) { @@ -751,7 +768,11 @@ class nsZenSpacesSyncModel { } } for (const id of Object.keys(uploaded)) { - if (!current.has(id) && !pending.has(id)) { + if ( + !current.has(id) && + !pending.has(id) && + this.#appliedStamp.get(id) !== stamp + ) { changes[id] = now; } } @@ -774,13 +795,18 @@ class nsZenSpacesSyncModel { const uploaded = this.#data().uploaded; const current = this.#digestAll(); const pending = this.#pendingIds(); + const stamp = this.#currentStamp(); for (const [id, digest] of current) { if (uploaded[id] !== digest) { return true; } } for (const id of Object.keys(uploaded)) { - if (!current.has(id) && !pending.has(id)) { + if ( + !current.has(id) && + !pending.has(id) && + this.#appliedStamp.get(id) !== stamp + ) { return true; } } @@ -825,8 +851,12 @@ class nsZenSpacesSyncModel { const data = this.#data(); if (!cleartext) { delete data.uploaded[id]; + this.#appliedStamp.delete(id); } else { data.uploaded[id] = recordDigest(cleartext.kind, cleartext.data); + // Hold back a tombstone for this id until the projection is recollected + // past the generation it was applied in (gh-15426). + this.#appliedStamp.set(id, this.#currentStamp()); } syncLog( `acknowledged incoming ${cleartext ? cleartext.kind : "tombstone"} ${id}` diff --git a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs index 3dae7ac16..40fb335d7 100644 --- a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs +++ b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs @@ -302,6 +302,10 @@ const globalActionsTemplate = [ }, ]; +export function formatValueSync(l10nId) { + return lazy.l10n.formatValueSync(l10nId); +} + export const globalActions = globalActionsTemplate.map(action => ({ isAvailable: window => { return ( @@ -317,6 +321,6 @@ export const globalActions = globalActionsTemplate.map(action => ({ extraPayload: {}, ...action, get label() { - return lazy.l10n.formatValueSync(action.l10nId); + return formatValueSync(action.l10nId); }, })); diff --git a/src/zen/urlbar/ZenUBSidebarProvider.sys.mjs b/src/zen/urlbar/ZenUBSidebarProvider.sys.mjs index 422d8a096..aa9837bd2 100644 --- a/src/zen/urlbar/ZenUBSidebarProvider.sys.mjs +++ b/src/zen/urlbar/ZenUBSidebarProvider.sys.mjs @@ -7,6 +7,7 @@ import { UrlbarUtils, } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs"; import { UrlbarShared } from "chrome://browser/content/urlbar/UrlbarShared.mjs"; +import { formatValueSync } from "resource:///modules/ZenUBGlobalActions.sys.mjs"; const lazy = {}; @@ -34,7 +35,6 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider { } async isActive(queryContext) { - // The sidebar data never holds tabs from private windows. return ( !queryContext.isPrivate && !!queryContext.tokens.length && @@ -49,12 +49,18 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider { return; } const tokens = queryContext.tokens.map(t => t.lowerCaseValue); - const matches = text => { + const matches = (percentage, text) => { text = text.toLowerCase(); - return tokens.every(token => text.includes(token)); + const matchCount = tokens.filter(token => text.includes(token)).length; + return matchCount / tokens.length >= percentage; }; - this.#addFolders(sidebar, matches, addCallback); - this.#addTabs(sidebar, matches, queryContext, addCallback); + this.#addFolders(sidebar, matches.bind(undefined, 0.7), addCallback); + this.#addTabs( + sidebar, + matches.bind(undefined, 0.4), + queryContext, + addCallback + ); } #addTabs(sidebar, matches, queryContext, addCallback) { @@ -123,7 +129,7 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider { continue; } const space = sidebar.spaces?.find(s => s.uuid == folder.workspaceId); - const path = []; + const path = [folder.name]; for ( let parent = folders.get(folder.parentId); parent; @@ -142,9 +148,9 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider { payload: { dynamicType: DYNAMIC_TYPE_NAME, zenFolderId: folder.id, - title: folder.name, - icon: - folder.userIcon || "chrome://browser/skin/zen-icons/folder.svg", + titleL10n: "zen-action-open", + userIcon: folder.userIcon, + icon: "chrome://browser/skin/zen-icons/folder.svg", path: path.join(" / "), }, }) @@ -156,13 +162,19 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider { return { icon: { attributes: { src: result.payload.icon } }, titleStrong: { - textContent: result.payload.title, + textContent: formatValueSync(result.payload.titleL10n), attributes: { dir: "ltr" }, }, path: { textContent: result.payload.path, attributes: { dir: "ltr", hidden: !result.payload.path }, }, + userIcon: { + attributes: { + src: result.payload.userIcon, + hidden: !result.payload.userIcon, + }, + }, }; } @@ -175,7 +187,7 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider { { name: "icon", tag: "img", - classList: ["urlbarView-favicon"], + classList: ["urlbarView-favicon", "urlbarView-action-favicon"], }, { name: "title", @@ -189,9 +201,19 @@ export class ZenUrlbarProviderSidebar extends UrlbarProvider { ], }, { - name: "path", tag: "span", classList: ["urlbarView-prettyName"], + children: [ + { + name: "userIcon", + tag: "img", + attributes: { hidden: true }, + }, + { + name: "path", + tag: "span", + }, + ], }, ], };