diff --git a/locales/en-US/browser/browser/zen-workspaces.ftl b/locales/en-US/browser/browser/zen-workspaces.ftl
index 67f43f7fe6..9eea8c1be1 100644
--- a/locales/en-US/browser/browser/zen-workspaces.ftl
+++ b/locales/en-US/browser/browser/zen-workspaces.ftl
@@ -66,6 +66,10 @@ zen-panel-ui-gradient-click-to-add = Click to add a color
zen-workspace-creation-name =
.placeholder = Space Name
+zen-move-tab-to-workspace-button =
+ .label = Move To...
+ .tooltiptext = Move all tabs in this window to a Space
+
zen-workspaces-panel-context-reorder =
.label = Reorder Spaces
diff --git a/prefs/zen/session-store.yaml b/prefs/zen/session-store.yaml
new file mode 100644
index 0000000000..ede2bf75f5
--- /dev/null
+++ b/prefs/zen/session-store.yaml
@@ -0,0 +1,9 @@
+# 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/.
+
+- name: zen.session-store.backup-file
+ value: true
+
+- name: zen.session-store.log
+ value: true
diff --git a/prefs/zen/window-sync.yaml b/prefs/zen/window-sync.yaml
new file mode 100644
index 0000000000..010d357f28
--- /dev/null
+++ b/prefs/zen/window-sync.yaml
@@ -0,0 +1,9 @@
+# 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/.
+
+- name: zen.window-sync.enabled
+ value: true
+
+- name: zen.window-sync.log
+ value: true
diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml
index 47f083c49c..fb96936332 100644
--- a/src/browser/base/content/zen-assets.inc.xhtml
+++ b/src/browser/base/content/zen-assets.inc.xhtml
@@ -45,7 +45,6 @@
# Scripts used all over the browser
-
diff --git a/src/browser/base/content/zen-panels/popups.inc b/src/browser/base/content/zen-panels/popups.inc
index d94f76ae0c..b4ecd5e149 100644
--- a/src/browser/base/content/zen-panels/popups.inc
+++ b/src/browser/base/content/zen-panels/popups.inc
@@ -59,3 +59,8 @@
+
+
diff --git a/src/browser/components/sessionstore/SessionFile-sys-mjs.patch b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch
new file mode 100644
index 0000000000..895c4313fa
--- /dev/null
+++ b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch
@@ -0,0 +1,21 @@
+diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs
+index 31140cb8be3b529a0952ca8dc55165690b0e2120..605c9e0aa84da0a2d3171a0573e8cd95e27bd0c4 100644
+--- a/browser/components/sessionstore/SessionFile.sys.mjs
++++ b/browser/components/sessionstore/SessionFile.sys.mjs
+@@ -22,6 +22,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
+ RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs",
++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
+ });
+
+ const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
+@@ -380,7 +381,7 @@ var SessionFileInternal = {
+ this._readOrigin = result.origin;
+
+ result.noFilesFound = noFilesFound;
+-
++ await lazy.ZenSessionStore.readFile();
+ return result;
+ },
+
diff --git a/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch b/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch
new file mode 100644
index 0000000000..e6956ea61b
--- /dev/null
+++ b/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch
@@ -0,0 +1,20 @@
+diff --git a/browser/components/sessionstore/SessionSaver.sys.mjs b/browser/components/sessionstore/SessionSaver.sys.mjs
+index 9141793550f7c7ff6aa63d4c85bf571b4499e2d0..f00314ebf75ac826e1c9cca8af264ff8aae106c0 100644
+--- a/browser/components/sessionstore/SessionSaver.sys.mjs
++++ b/browser/components/sessionstore/SessionSaver.sys.mjs
+@@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
+ SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
+ });
+
+ /*
+@@ -305,6 +306,7 @@ var SessionSaverInternal = {
+ this._maybeClearCookiesAndStorage(state);
+
+ Glean.sessionRestore.collectData.stopAndAccumulate(timerId);
++ lazy.ZenSessionStore.saveState(state);
+ return this._writeState(state);
+ },
+
diff --git a/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch
new file mode 100644
index 0000000000..b106193cf3
--- /dev/null
+++ b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch
@@ -0,0 +1,21 @@
+diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs
+index be23213ae9ec7e59358a17276c6c3764d38d9996..ca5a8ccc916ceeab5140f1278d15233cefbe5815 100644
+--- a/browser/components/sessionstore/SessionStartup.sys.mjs
++++ b/browser/components/sessionstore/SessionStartup.sys.mjs
+@@ -40,6 +40,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
+ StartupPerformance:
+ "resource:///modules/sessionstore/StartupPerformance.sys.mjs",
+ sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
+ });
+
+ const STATE_RUNNING_STR = "running";
+@@ -179,6 +180,8 @@ export var SessionStartup = {
+ this._initialState = parsed;
+ }
+
++ lazy.ZenSessionStore.onFileRead(this._initialState);
++
+ if (this._initialState == null) {
+ // No valid session found.
+ this._sessionType = this.NO_SESSION;
diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
index 7efaefb173..b1c2074d47 100644
--- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
+++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
-index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb22176c60c4e 100644
+index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..e1d95e97819718d70fab23850a8810dd216db7a8 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -127,6 +127,8 @@ const TAB_EVENTS = [
@@ -11,7 +11,15 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
];
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-@@ -1911,6 +1913,8 @@ var SessionStoreInternal = {
+@@ -196,6 +198,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
+ TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
+ });
+
+ ChromeUtils.defineLazyGetter(lazy, "blankURI", () => {
+@@ -1911,6 +1914,8 @@ var SessionStoreInternal = {
case "TabPinned":
case "TabUnpinned":
case "SwapDocShells":
@@ -20,7 +28,42 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
this.saveStateDelayed(win);
break;
case "TabGroupCreate":
-@@ -2384,11 +2388,9 @@ var SessionStoreInternal = {
+@@ -2020,6 +2025,10 @@ var SessionStoreInternal = {
+ this._windows[aWindow.__SSi].isTaskbarTab = true;
+ }
+
++ if (aWindow.document.documentElement.hasAttribute("zen-unsynced-window")) {
++ this._windows[aWindow.__SSi].isZenUnsynced = true;
++ }
++
+ let tabbrowser = aWindow.gBrowser;
+
+ // add tab change listeners to all already existing tabs
+@@ -2151,7 +2160,6 @@ var SessionStoreInternal = {
+ if (closedWindowState) {
+ let newWindowState;
+ if (
+- AppConstants.platform == "macosx" ||
+ !lazy.SessionStartup.willRestore()
+ ) {
+ // We want to split the window up into pinned tabs and unpinned tabs.
+@@ -2215,6 +2223,15 @@ var SessionStoreInternal = {
+ });
+ this._shouldRestoreLastSession = false;
+ }
++ else if (!aInitialState && isRegularWindow) {
++ let windowPromises = [];
++ for (let window of this._browserWindows) {
++ windowPromises.push(lazy.TabStateFlusher.flushWindow(window));
++ }
++ Promise.all(windowPromises).finally(() => {
++ lazy.ZenSessionStore.restoreNewWindow(aWindow, this);
++ });
++ }
+
+ if (this._restoreLastWindow && aWindow.toolbar.visible) {
+ // always reset (if not a popup window)
+@@ -2384,11 +2401,9 @@ var SessionStoreInternal = {
tabbrowser.selectedTab.label;
}
@@ -32,7 +75,25 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
// Store the window's close date to figure out when each individual tab
// was closed. This timestamp should allow re-arranging data based on how
-@@ -3373,7 +3375,7 @@ var SessionStoreInternal = {
+@@ -2465,7 +2480,7 @@ var SessionStoreInternal = {
+ // 2) Flush the window.
+ // 3) When the flush is complete, revisit our decision to store the window
+ // in _closedWindows, and add/remove as necessary.
+- if (!winData.isPrivate && !winData.isTaskbarTab) {
++ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) {
+ this.maybeSaveClosedWindow(winData, isLastWindow);
+ }
+
+@@ -2486,7 +2501,7 @@ var SessionStoreInternal = {
+
+ // Save non-private windows if they have at
+ // least one saveable tab or are the last window.
+- if (!winData.isPrivate && !winData.isTaskbarTab) {
++ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) {
+ this.maybeSaveClosedWindow(winData, isLastWindow);
+
+ if (!isLastWindow && winData.closedId > -1) {
+@@ -3373,7 +3388,7 @@ var SessionStoreInternal = {
if (!isPrivateWindow && tabState.isPrivate) {
return;
}
@@ -41,12 +102,12 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
return;
}
-@@ -4089,6 +4091,12 @@ var SessionStoreInternal = {
+@@ -4089,6 +4104,12 @@ var SessionStoreInternal = {
Math.min(tabState.index, tabState.entries.length)
);
tabState.pinned = false;
+ tabState.zenEssential = false;
-+ tabState.zenPinnedId = null;
++ tabState.zenSyncId = null;
+ tabState.zenIsGlance = false;
+ tabState.zenGlanceId = null;
+ tabState.zenHasStaticLabel = false;
@@ -54,7 +115,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
if (inBackground === false) {
aWindow.gBrowser.selectedTab = newTab;
-@@ -4525,6 +4533,7 @@ var SessionStoreInternal = {
+@@ -4525,6 +4546,7 @@ var SessionStoreInternal = {
// Append the tab if we're opening into a different window,
tabIndex: aSource == aTargetWindow ? pos : Infinity,
pinned: state.pinned,
@@ -62,7 +123,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
userContextId: state.userContextId,
skipLoad: true,
preferredRemoteType,
-@@ -5374,7 +5383,7 @@ var SessionStoreInternal = {
+@@ -5374,7 +5396,7 @@ var SessionStoreInternal = {
for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
let tab = tabbrowser.tabs[i];
@@ -71,7 +132,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
removableTabs.push(tab);
}
}
-@@ -5434,7 +5443,7 @@ var SessionStoreInternal = {
+@@ -5434,7 +5456,7 @@ var SessionStoreInternal = {
}
let workspaceID = aWindow.getWorkspaceID();
@@ -80,7 +141,16 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
winData.workspaceID = workspaceID;
}
},
-@@ -5625,11 +5634,12 @@ var SessionStoreInternal = {
+@@ -5483,7 +5505,7 @@ var SessionStoreInternal = {
+
+ // collect the data for all windows
+ for (ix in this._windows) {
+- if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab) {
++ if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab || this._windows[ix].isZenUnsynced) {
+ // window data is still in _statesToRestore
+ continue;
+ }
+@@ -5625,11 +5647,12 @@ var SessionStoreInternal = {
}
let tabbrowser = aWindow.gBrowser;
@@ -94,7 +164,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
// update the internal state data for this window
for (let tab of tabs) {
if (tab == aWindow.FirefoxViewHandler.tab) {
-@@ -5640,6 +5650,7 @@ var SessionStoreInternal = {
+@@ -5640,6 +5663,7 @@ var SessionStoreInternal = {
tabsData.push(tabData);
}
@@ -102,7 +172,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
// update tab group state for this window
winData.groups = [];
for (let tabGroup of aWindow.gBrowser.tabGroups) {
-@@ -5652,7 +5663,7 @@ var SessionStoreInternal = {
+@@ -5652,7 +5676,7 @@ var SessionStoreInternal = {
// a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab,
// since it's only inserted into the tab strip after it's selected).
if (aWindow.FirefoxViewHandler.tab?.selected) {
@@ -111,7 +181,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
winData.title = tabbrowser.tabs[0].label;
}
winData.selected = selectedIndex;
-@@ -5765,8 +5776,8 @@ var SessionStoreInternal = {
+@@ -5765,8 +5789,8 @@ var SessionStoreInternal = {
// selectTab represents.
let selectTab = 0;
if (overwriteTabs) {
@@ -122,7 +192,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
selectTab = Math.min(selectTab, winData.tabs.length);
}
-@@ -5809,6 +5820,8 @@ var SessionStoreInternal = {
+@@ -5809,6 +5833,8 @@ var SessionStoreInternal = {
winData.tabs,
winData.groups ?? []
);
@@ -131,7 +201,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
this._log.debug(
`restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs`
);
-@@ -6372,6 +6385,25 @@ var SessionStoreInternal = {
+@@ -6372,6 +6398,25 @@ var SessionStoreInternal = {
// Most of tabData has been restored, now continue with restoring
// attributes that may trigger external events.
@@ -145,8 +215,8 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
+ if (tabData.zenHasStaticLabel) {
+ tab.setAttribute("zen-has-static-label", "true");
+ }
-+ if (tabData.zenPinnedId) {
-+ tab.setAttribute("zen-pin-id", tabData.zenPinnedId);
++ if (tabData.zenSyncId) {
++ tab.setAttribute("id", tabData.zenSyncId);
+ }
+ if (tabData.zenDefaultUserContextId) {
+ tab.setAttribute("zenDefaultUserContextId", true);
@@ -157,7 +227,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
if (tabData.pinned) {
tabbrowser.pinTab(tab);
-@@ -7290,7 +7322,7 @@ var SessionStoreInternal = {
+@@ -7290,7 +7335,7 @@ var SessionStoreInternal = {
let groupsToSave = new Map();
for (let tIndex = 0; tIndex < window.tabs.length; ) {
@@ -166,7 +236,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
// Adjust window.selected
if (tIndex + 1 < window.selected) {
window.selected -= 1;
-@@ -7305,7 +7337,7 @@ var SessionStoreInternal = {
+@@ -7305,7 +7350,7 @@ var SessionStoreInternal = {
);
// We don't want to increment tIndex here.
continue;
diff --git a/src/browser/components/sessionstore/TabState-sys-mjs.patch b/src/browser/components/sessionstore/TabState-sys-mjs.patch
index 2100e23343..0a7c56bc42 100644
--- a/src/browser/components/sessionstore/TabState-sys-mjs.patch
+++ b/src/browser/components/sessionstore/TabState-sys-mjs.patch
@@ -1,13 +1,13 @@
diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs
-index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a12f2060b 100644
+index 82721356d191055bec0d4b0ca49e481221988801..e1d2c5ca0cbe5431df01f8d6411f88e3325e0ee9 100644
--- a/browser/components/sessionstore/TabState.sys.mjs
+++ b/browser/components/sessionstore/TabState.sys.mjs
-@@ -85,7 +85,22 @@ class _TabState {
+@@ -85,7 +85,23 @@ class _TabState {
tabData.groupId = tab.group.id;
}
+ tabData.zenWorkspace = tab.getAttribute("zen-workspace-id");
-+ tabData.zenPinnedId = tab.getAttribute("zen-pin-id");
++ tabData.zenSyncId = tab.getAttribute("id");
+ tabData.zenEssential = tab.getAttribute("zen-essential");
+ tabData.pinned = tabData.pinned || tabData.zenEssential;
+ tabData.zenDefaultUserContextId = tab.getAttribute("zenDefaultUserContextId");
@@ -17,6 +17,7 @@ index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a
+ tabData.zenHasStaticLabel = tab.hasAttribute("zen-has-static-label");
+ tabData.zenGlanceId = tab.getAttribute("glance-id");
+ tabData.zenIsGlance = tab.hasAttribute("zen-glance-tab");
++ tabData._zenIsActiveTab = tab._zenContentsVisible;
+
tabData.searchMode = tab.ownerGlobal.gURLBar.getSearchMode(browser, true);
+ if (tabData.searchMode?.source === tab.ownerGlobal.UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS) {
diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch
index 3f27154b7f..dd0edd05f5 100644
--- a/src/browser/components/tabbrowser/content/tab-js.patch
+++ b/src/browser/components/tabbrowser/content/tab-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
-index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbfd6214f77 100644
+index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7600f564a 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -121,15 +121,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbf
on_click(event) {
if (event.button != 0) {
return;
-@@ -575,6 +599,7 @@
- )
- );
- } else {
-+ gZenPinnedTabManager._removePinnedAttributes(this, true);
- gBrowser.removeTab(this, {
- animate: true,
- triggeringEvent: event,
-@@ -587,6 +612,14 @@
+@@ -587,6 +611,14 @@
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
@@ -144,7 +136,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbf
}
on_dblclick(event) {
-@@ -610,6 +643,8 @@
+@@ -610,6 +642,8 @@
animate: true,
triggeringEvent: event,
});
diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch
index 8a71eaaae6..0950c539de 100644
--- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch
+++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
-index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f53bc059b 100644
+index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..9a619171c7f1f7ba43c2c661bdad370f83733bf0 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -386,6 +386,7 @@
@@ -10,7 +10,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
const browsers = [];
if (this.#activeSplitView) {
for (const tab of this.#activeSplitView.tabs) {
-@@ -450,15 +451,64 @@
+@@ -450,15 +451,66 @@
return this.tabContainer.visibleTabs;
}
@@ -18,6 +18,8 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
+ return this.#handleTabMove(...args);
+ }
+
++ get zenTabProgressListener() { return TabProgressListener; }
++
+ get _numVisiblePinTabsWithoutCollapsed() {
+ let i = 0;
+ for (let item of this.tabContainer.ariaFocusableItems) {
@@ -77,7 +79,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
set selectedTab(val) {
if (
gSharedTabWarning.willShowSharedTabWarning(val) ||
-@@ -613,6 +663,7 @@
+@@ -613,6 +665,7 @@
this.tabpanels.appendChild(panel);
let tab = this.tabs[0];
@@ -85,7 +87,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
tab.linkedPanel = uniqueId;
this._selectedTab = tab;
this._selectedBrowser = browser;
-@@ -898,13 +949,17 @@
+@@ -898,13 +951,17 @@
}
this.showTab(aTab);
@@ -104,7 +106,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab.setAttribute("pinned", "true");
this._updateTabBarForPinnedTabs();
-@@ -917,11 +972,15 @@
+@@ -917,11 +974,15 @@
}
this.#handleTabMove(aTab, () => {
@@ -121,7 +123,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
});
aTab.style.marginInlineStart = "";
-@@ -1098,6 +1157,8 @@
+@@ -1098,6 +1159,8 @@
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
@@ -130,7 +132,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (
aIconURL &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
-@@ -1107,6 +1168,9 @@
+@@ -1107,6 +1170,9 @@
);
return;
}
@@ -140,7 +142,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
-@@ -1379,7 +1443,6 @@
+@@ -1379,7 +1445,6 @@
// Preview mode should not reset the owner
if (!this._previewMode && !oldTab.selected) {
@@ -148,7 +150,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
-@@ -1470,6 +1533,7 @@
+@@ -1470,6 +1535,7 @@
if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
@@ -156,7 +158,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps.
if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
-@@ -1622,6 +1686,9 @@
+@@ -1622,6 +1688,9 @@
}
let activeEl = document.activeElement;
@@ -166,17 +168,20 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// If focus is on the old tab, move it to the new tab.
if (activeEl == oldTab) {
newTab.focus();
-@@ -1945,7 +2012,8 @@
+@@ -1945,7 +2014,11 @@
}
_setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
- if (!aLabel || aLabel.includes("about:reader?")) {
++ if (!aTab._zenContentsVisible && !aTab._labelIsInitialTitle) {
++ return false;
++ }
+ gZenPinnedTabManager.onTabLabelChanged(aTab);
+ if (!aLabel || aLabel.includes("about:reader?") || aTab.hasAttribute("zen-has-static-label")) {
return false;
}
-@@ -2053,7 +2121,7 @@
+@@ -2053,7 +2126,7 @@
newIndex = this.selectedTab._tPos + 1;
}
@@ -185,7 +190,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (this.isTabGroupLabel(targetTab)) {
throw new Error(
"Replacing a tab group label with a tab is not supported"
-@@ -2328,6 +2396,7 @@
+@@ -2328,6 +2401,7 @@
uriIsAboutBlank,
userContextId,
skipLoad,
@@ -193,7 +198,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} = {}) {
let b = document.createXULElement("browser");
// Use the JSM global to create the permanentKey, so that if the
-@@ -2401,8 +2470,7 @@
+@@ -2401,8 +2475,7 @@
// we use a different attribute name for this?
b.setAttribute("name", name);
}
@@ -203,7 +208,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
b.setAttribute("transparent", "true");
}
-@@ -2567,7 +2635,7 @@
+@@ -2567,7 +2640,7 @@
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
@@ -212,7 +217,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab.linkedPanel = uniqueId;
// Inject the into the DOM if necessary.
-@@ -2626,8 +2694,8 @@
+@@ -2626,8 +2699,8 @@
// If we transitioned from one browser to two browsers, we need to set
// hasSiblings=false on both the existing browser and the new browser.
if (this.tabs.length == 2) {
@@ -223,7 +228,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
}
-@@ -2814,7 +2882,6 @@
+@@ -2814,7 +2887,6 @@
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
tabIndex: tab._tPos + 1,
userContextId: tab.userContextId,
@@ -231,16 +236,17 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
focusUrlBar: true,
});
resolve(this.selectedBrowser);
-@@ -2923,6 +2990,8 @@
+@@ -2923,6 +2995,9 @@
schemelessInput,
hasValidUserGestureActivation = false,
textDirectiveUserActivation = false,
+ _forZenEmptyTab,
+ essential,
++ zenWorkspaceId,
} = {}
) {
// all callers of addTab that pass a params object need to pass
-@@ -2933,10 +3002,17 @@
+@@ -2933,10 +3008,17 @@
);
}
@@ -258,7 +264,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab;
-@@ -2944,6 +3020,7 @@
+@@ -2944,6 +3026,7 @@
if (this.selectedTab.owner) {
this.selectedTab.owner = null;
}
@@ -266,14 +272,16 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Find the tab that opened this one, if any. This is used for
// determining positioning, and inherited attributes such as the
-@@ -2996,6 +3073,19 @@
+@@ -2996,6 +3079,21 @@
noInitialLabel,
skipBackgroundNotify,
});
+ if (hasZenDefaultUserContextId) {
+ t.setAttribute("zenDefaultUserContextId", "true");
+ }
-+ if (zenForcedWorkspaceId !== undefined) {
++ if (zenWorkspaceId) {
++ t.setAttribute("zen-workspace-id", zenWorkspaceId);
++ } else if (zenForcedWorkspaceId !== undefined) {
+ t.setAttribute("zen-workspace-id", zenForcedWorkspaceId);
+ t.setAttribute("change-workspace", "")
+ }
@@ -286,7 +294,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (insertTab) {
// Insert the tab into the tab container in the correct position.
this.#insertTabAtIndex(t, {
-@@ -3004,6 +3094,7 @@
+@@ -3004,6 +3102,7 @@
ownerTab,
openerTab,
pinned,
@@ -294,7 +302,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
bulkOrderedOpen,
tabGroup: tabGroup ?? openerTab?.group,
});
-@@ -3022,6 +3113,7 @@
+@@ -3022,6 +3121,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -302,7 +310,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}));
if (focusUrlBar) {
-@@ -3146,6 +3238,12 @@
+@@ -3146,6 +3246,12 @@
}
}
@@ -315,7 +323,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Additionally send pinned tab events
if (pinned) {
this.#notifyPinnedStatus(t);
-@@ -3349,10 +3447,10 @@
+@@ -3349,10 +3455,10 @@
isAdoptingGroup = false,
isUserTriggered = false,
telemetryUserCreateSource = "unknown",
@@ -327,7 +335,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
if (!color) {
-@@ -3373,9 +3471,14 @@
+@@ -3373,9 +3479,14 @@
label,
isAdoptingGroup
);
@@ -344,7 +352,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
group.addTabs(tabs);
-@@ -3496,7 +3599,7 @@
+@@ -3496,7 +3607,7 @@
}
this.#handleTabMove(tab, () =>
@@ -353,7 +361,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
}
-@@ -3698,6 +3801,7 @@
+@@ -3698,6 +3809,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -361,7 +369,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
) {
// If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
-@@ -3767,6 +3871,7 @@
+@@ -3767,6 +3879,7 @@
openWindowInfo,
name,
skipLoad,
@@ -369,7 +377,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
});
}
-@@ -3955,7 +4060,7 @@
+@@ -3955,7 +4068,7 @@
// Add a new tab if needed.
if (!tab) {
let createLazyBrowser =
@@ -378,7 +386,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
let url = "about:blank";
if (tabData.entries?.length) {
-@@ -3992,8 +4097,10 @@
+@@ -3992,8 +4105,10 @@
insertTab: false,
skipLoad: true,
preferredRemoteType,
@@ -390,7 +398,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (select) {
tabToSelect = tab;
}
-@@ -4005,7 +4112,8 @@
+@@ -4005,7 +4120,8 @@
this.pinTab(tab);
// Then ensure all the tab open/pinning information is sent.
this._fireTabOpen(tab, {});
@@ -400,7 +408,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
let { groupId } = tabData;
const tabGroup = tabGroupWorkingData.get(groupId);
// if a tab refers to a tab group we don't know, skip any group
-@@ -4019,7 +4127,10 @@
+@@ -4019,7 +4135,10 @@
tabGroup.stateData.id,
tabGroup.stateData.color,
tabGroup.stateData.collapsed,
@@ -412,7 +420,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
tabsFragment.appendChild(tabGroup.node);
}
-@@ -4064,9 +4175,23 @@
+@@ -4064,9 +4183,23 @@
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
@@ -428,15 +436,15 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
+ gZenWorkspaces._initialTab._shouldRemove = true;
+ }
+ }
-+ }
+ }
+ else {
+ gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab;
- }
++ }
+ this._hasAlreadyInitializedZenSessionStore = true;
if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert();
-@@ -4257,11 +4382,14 @@
+@@ -4257,11 +4390,14 @@
if (ownerTab) {
tab.owner = ownerTab;
}
@@ -452,7 +460,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (
!bulkOrderedOpen &&
((openerTab &&
-@@ -4273,7 +4401,7 @@
+@@ -4273,7 +4409,7 @@
let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
@@ -461,7 +469,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
tabGroup = previousTab.group;
}
if (
-@@ -4284,7 +4412,7 @@
+@@ -4284,7 +4420,7 @@
) {
elementIndex = Infinity;
} else if (previousTab.visible) {
@@ -470,7 +478,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else if (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0;
}
-@@ -4312,14 +4440,14 @@
+@@ -4312,14 +4448,14 @@
}
// Ensure index is within bounds.
if (tab.pinned) {
@@ -489,7 +497,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
-@@ -4330,7 +4458,7 @@
+@@ -4330,7 +4466,7 @@
this.tabContainer._invalidateCachedTabs();
@@ -498,7 +506,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (this.isTab(itemAfter) && itemAfter.group == tabGroup) {
// Place at the front of, or between tabs in, the same tab group
this.tabContainer.insertBefore(tab, itemAfter);
-@@ -4358,7 +4486,11 @@
+@@ -4358,7 +4494,11 @@
const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -510,7 +518,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
this._updateTabsAfterInsert();
-@@ -4366,6 +4498,7 @@
+@@ -4366,6 +4506,7 @@
if (pinned) {
this._updateTabBarForPinnedTabs();
}
@@ -518,17 +526,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
TabBarVisibility.update();
}
-@@ -4655,6 +4788,9 @@
- return;
- }
-
-+ for (let tab of selectedTabs) {
-+ gZenPinnedTabManager._removePinnedAttributes(tab, true);
-+ }
- this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource });
- }
-
-@@ -4916,6 +5052,7 @@
+@@ -4916,6 +5057,7 @@
telemetrySource,
} = {}
) {
@@ -536,7 +534,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
-@@ -5005,6 +5142,7 @@
+@@ -5005,6 +5147,7 @@
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
@@ -544,7 +542,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} catch (e) {
console.error(e);
}
-@@ -5043,6 +5181,12 @@
+@@ -5043,6 +5186,12 @@
aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start();
}
@@ -557,7 +555,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Handle requests for synchronously removing an already
// asynchronously closing tab.
if (!animate && aTab.closing) {
-@@ -5057,6 +5201,9 @@
+@@ -5057,6 +5206,9 @@
// state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab);
@@ -567,7 +565,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
-@@ -5105,7 +5252,13 @@
+@@ -5105,7 +5257,13 @@
// We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
@@ -582,7 +580,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
return;
}
-@@ -5239,7 +5392,7 @@
+@@ -5239,7 +5397,7 @@
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
@@ -591,7 +589,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here,
-@@ -5263,6 +5416,7 @@
+@@ -5263,6 +5421,7 @@
newTab = true;
}
@@ -599,7 +597,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
-@@ -5303,13 +5457,7 @@
+@@ -5303,13 +5462,7 @@
aTab._mouseleave();
if (newTab) {
@@ -614,7 +612,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else {
TabBarVisibility.update();
}
-@@ -5442,6 +5590,7 @@
+@@ -5442,6 +5595,7 @@
this.tabs[i]._tPos = i;
}
@@ -622,7 +620,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (!this._windowIsClosing) {
// update tab close buttons state
this.tabContainer._updateCloseButtons();
-@@ -5663,6 +5812,7 @@
+@@ -5663,6 +5817,7 @@
}
let excludeTabs = new Set(aExcludeTabs);
@@ -630,7 +628,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
-@@ -5675,13 +5825,13 @@
+@@ -5675,13 +5830,13 @@
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
@@ -646,7 +644,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
let tab = this.tabContainer.findNextTab(aTab, {
-@@ -5697,7 +5847,7 @@
+@@ -5697,7 +5852,7 @@
}
if (tab) {
@@ -655,7 +653,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
// If no qualifying visible tab was found, see if there is a tab in
-@@ -5718,7 +5868,7 @@
+@@ -5718,7 +5873,7 @@
});
}
@@ -664,7 +662,47 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
_blurTab(aTab) {
-@@ -6124,10 +6274,10 @@
+@@ -5729,7 +5884,7 @@
+ * @returns {boolean}
+ * False if swapping isn't permitted, true otherwise.
+ */
+- swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
++ swapBrowsersAndCloseOther(aOurTab, aOtherTab, zenCloseOther = true) {
+ // Do not allow transfering a private tab to a non-private window
+ // and vice versa.
+ if (
+@@ -5783,6 +5938,7 @@
+ // fire the beforeunload event in the process. Close the other
+ // window if this was its last tab.
+ if (
++ zenCloseOther &&
+ !remoteBrowser._beginRemoveTab(aOtherTab, {
+ adoptedByTab: aOurTab,
+ closeWindowWithLastTab: true,
+@@ -5794,7 +5950,7 @@
+ // If this is the last tab of the window, hide the window
+ // immediately without animation before the docshell swap, to avoid
+ // about:blank being painted.
+- let [closeWindow] = aOtherTab._endRemoveArgs;
++ let [closeWindow] = !zenCloseOther ? [false] : aOtherTab._endRemoveArgs;
+ if (closeWindow) {
+ let win = aOtherTab.ownerGlobal;
+ win.windowUtils.suppressAnimation(true);
+@@ -5918,11 +6074,13 @@
+ }
+
+ // Finish tearing down the tab that's going away.
++ if (zenCloseOther) {
+ if (closeWindow) {
+ aOtherTab.ownerGlobal.close();
+ } else {
+ remoteBrowser._endRemoveTab(aOtherTab);
+ }
++ }
+
+ this.setTabTitle(aOurTab);
+
+@@ -6124,10 +6282,10 @@
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
}
@@ -677,15 +715,27 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
-@@ -6186,6 +6336,7 @@
+@@ -6185,7 +6343,8 @@
+ *
* @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab
*/
- replaceTabWithWindow(aTab, aOptions) {
+- replaceTabWithWindow(aTab, aOptions) {
++ replaceTabWithWindow(aTab, aOptions, zenForceSync = false) {
+ if (!this.isTab(aTab)) return; // TODO: Handle tab groups
if (this.tabs.length == 1) {
return null;
}
-@@ -6319,7 +6470,7 @@
+@@ -6213,7 +6372,8 @@
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ options,
+- aTab
++ aTab,
++ zenForceSync ? "zen-synced" : "zen-unsynced"
+ );
+ }
+
+@@ -6319,7 +6479,7 @@
* `true` if element is a ``
*/
isTabGroup(element) {
@@ -694,7 +744,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
/**
-@@ -6404,8 +6555,8 @@
+@@ -6404,8 +6564,8 @@
}
// Don't allow mixing pinned and unpinned tabs.
@@ -705,7 +755,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
-@@ -6431,10 +6582,16 @@
+@@ -6431,10 +6591,16 @@
this.#handleTabMove(
element,
() => {
@@ -724,7 +774,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (neighbor && this.isTab(element) && tabIndex > element._tPos) {
neighbor.after(element);
} else {
-@@ -6492,23 +6649,28 @@
+@@ -6492,23 +6658,28 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -759,7 +809,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else if (!element.pinned && targetElement && targetElement.pinned) {
// If the caller asks to move an unpinned element next to a pinned
// tab, move the unpinned element to be the first unpinned element
-@@ -6521,14 +6683,34 @@
+@@ -6521,14 +6692,34 @@
// move the tab group right before the first unpinned tab.
// 4. Moving a tab group and the first unpinned tab is grouped:
// move the tab group right before the first unpinned tab's tab group.
@@ -795,7 +845,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
-@@ -6537,7 +6719,7 @@
+@@ -6537,7 +6728,7 @@
element,
() => {
if (moveBefore) {
@@ -804,7 +854,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else if (targetElement) {
targetElement.after(element);
} else {
-@@ -6607,10 +6789,10 @@
+@@ -6607,10 +6798,10 @@
* @param {TabMetricsContext} [metricsContext]
*/
moveTabToGroup(aTab, aGroup, metricsContext) {
@@ -817,7 +867,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
return;
}
if (aTab.group && aTab.group.id === aGroup.id) {
-@@ -6656,6 +6838,7 @@
+@@ -6656,6 +6847,7 @@
let state = {
tabIndex: tab._tPos,
@@ -825,7 +875,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
-@@ -6682,7 +6865,7 @@
+@@ -6682,7 +6874,7 @@
let changedTabGroup =
previousTabState.tabGroupId != currentTabState.tabGroupId;
@@ -834,7 +884,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
tab.dispatchEvent(
new CustomEvent("TabMove", {
bubbles: true,
-@@ -6723,6 +6906,10 @@
+@@ -6723,6 +6915,10 @@
moveActionCallback();
@@ -845,7 +895,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Clear tabs cache after moving nodes because the order of tabs may have
// changed.
this.tabContainer._invalidateCachedTabs();
-@@ -7623,7 +7810,7 @@
+@@ -7623,7 +7819,7 @@
// preventDefault(). It will still raise the window if appropriate.
break;
}
@@ -854,7 +904,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
window.focus();
aEvent.preventDefault();
break;
-@@ -7640,7 +7827,6 @@
+@@ -7640,7 +7836,6 @@
}
case "TabGroupCollapse":
aEvent.target.tabs.forEach(tab => {
@@ -862,7 +912,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
});
break;
case "TabGroupCreateByUser":
-@@ -8589,6 +8775,7 @@
+@@ -8589,6 +8784,7 @@
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
@@ -870,7 +920,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
}
-@@ -9623,7 +9810,7 @@ var TabContextMenu = {
+@@ -9623,7 +9819,7 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;
@@ -879,11 +929,3 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Build Ask Chat items
TabContextMenu.GenAI.buildTabMenu(
document.getElementById("context_askChat"),
-@@ -9943,6 +10130,7 @@ var TabContextMenu = {
- )
- );
- } else {
-+ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true);
- gBrowser.removeTab(this.contextTab, {
- animate: true,
- ...gBrowser.TabMetrics.userTriggeredContext(
diff --git a/src/zen/ZenComponents.manifest b/src/zen/ZenComponents.manifest
index 74c0232b85..0f38828a14 100644
--- a/src/zen/ZenComponents.manifest
+++ b/src/zen/ZenComponents.manifest
@@ -13,3 +13,4 @@
category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
#include common/Components.manifest
+#include sessionstore/SessionComponents.manifest
diff --git a/src/zen/common/modules/ZenSessionStore.mjs b/src/zen/common/modules/ZenSessionStore.mjs
index daa0d0962c..77aed2ebf2 100644
--- a/src/zen/common/modules/ZenSessionStore.mjs
+++ b/src/zen/common/modules/ZenSessionStore.mjs
@@ -17,8 +17,9 @@ class ZenSessionStore extends nsZenPreloadedFeature {
if (tabData.zenWorkspace) {
tab.setAttribute('zen-workspace-id', tabData.zenWorkspace);
}
- if (tabData.zenPinnedId) {
- tab.setAttribute('zen-pin-id', tabData.zenPinnedId);
+ // Keep for now, for backward compatibility for window sync to work.
+ if (tabData.zenSyncId || tabData.zenPinnedId) {
+ tab.setAttribute('id', tabData.zenSyncId || tabData.zenPinnedId);
}
if (tabData.zenHasStaticLabel) {
tab.setAttribute('zen-has-static-label', 'true');
diff --git a/src/zen/common/modules/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs
index e16fc982f7..517696d30b 100644
--- a/src/zen/common/modules/ZenUIManager.mjs
+++ b/src/zen/common/modules/ZenUIManager.mjs
@@ -811,7 +811,6 @@ window.gZenVerticalTabsManager = {
!aItem.isConnected ||
gZenUIManager.testingEnabled ||
!gZenStartup.isReady ||
- !gZenPinnedTabManager.hasInitializedPins ||
aItem.group?.hasAttribute('split-view-group')
) {
return;
@@ -1310,14 +1309,6 @@ window.gZenVerticalTabsManager = {
} else {
gBrowser.setTabTitle(this._tabEdited);
}
- if (this._tabEdited.getAttribute('zen-pin-id')) {
- // Update pin title in storage
- await gZenPinnedTabManager.updatePinTitle(
- this._tabEdited,
- this._tabEdited.label,
- !!newName
- );
- }
// Maybe add some confetti here?!?
gZenUIManager.motion.animate(
diff --git a/src/zen/common/styles/zen-browser-container.css b/src/zen/common/styles/zen-browser-container.css
index a1715df59b..c75e00d2e2 100644
--- a/src/zen/common/styles/zen-browser-container.css
+++ b/src/zen/common/styles/zen-browser-container.css
@@ -60,3 +60,15 @@
}
}
}
+
+.zen-pseudo-browser-image {
+ position: absolute;
+ inset: 0;
+ opacity: 0.4;
+ pointer-events: none;
+ z-index: 2;
+}
+
+browser[zen-pseudo-hidden='true'] {
+ -moz-subtree-hidden-only-visually: 1 !important;
+}
diff --git a/src/zen/common/styles/zen-theme.css b/src/zen/common/styles/zen-theme.css
index 93f4f8808d..c1f40a074d 100644
--- a/src/zen/common/styles/zen-theme.css
+++ b/src/zen/common/styles/zen-theme.css
@@ -231,6 +231,13 @@
--toolbox-textcolor: currentColor !important;
}
+ &[zen-unsynced-window='true'] {
+ --zen-main-browser-background: linear-gradient(130deg, light-dark(rgb(240, 230, 200), rgb(30, 25, 20)) 0%, light-dark(rgb(220, 200, 150), rgb(50, 45, 40)) 100%);
+ --zen-main-browser-background-toolbar: var(--zen-main-browser-background);
+ --zen-primary-color: light-dark(rgb(200, 100, 20), rgb(220, 120, 30)) !important;
+ --toolbox-textcolor: currentColor !important;
+ }
+
--toolbar-field-background-color: var(--zen-colors-input-bg) !important;
--arrowpanel-background: var(--zen-dialog-background) !important;
diff --git a/src/zen/common/sys/ZenCustomizableUI.sys.mjs b/src/zen/common/sys/ZenCustomizableUI.sys.mjs
index bd7efc760d..42fd818dab 100644
--- a/src/zen/common/sys/ZenCustomizableUI.sys.mjs
+++ b/src/zen/common/sys/ZenCustomizableUI.sys.mjs
@@ -116,6 +116,9 @@ export const ZenCustomizableUI = new (class {
#initCreateNewButton(window) {
const button = window.document.getElementById('zen-create-new-button');
button.addEventListener('command', (event) => {
+ if (window.gZenWorkspaces.privateWindowOrDisabled) {
+ return window.document.getElementById('cmd_newNavigatorTab').doCommand();
+ }
if (button.hasAttribute('open')) {
return;
}
diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs
index 2209e67138..6c1d2dbc78 100644
--- a/src/zen/folders/ZenFolder.mjs
+++ b/src/zen/folders/ZenFolder.mjs
@@ -150,7 +150,6 @@ class ZenFolder extends MozTabbrowserTabGroup {
for (let tab of this.allItems.reverse()) {
tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab;
if (tab.hasAttribute('zen-empty-tab')) {
- await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
gBrowser.removeTab(tab);
} else {
gBrowser.ungroupTab(tab);
@@ -160,7 +159,6 @@ class ZenFolder extends MozTabbrowserTabGroup {
async delete() {
for (const tab of this.allItemsRecursive) {
- await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
if (tab.hasAttribute('zen-empty-tab')) {
// Manually remove the empty tabs as removeTabs() inside removeTabGroup
// does ignore them.
diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs
index 8fdf63fec7..80535e6931 100644
--- a/src/zen/folders/ZenFolders.mjs
+++ b/src/zen/folders/ZenFolders.mjs
@@ -101,20 +101,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
.querySelector('menupopup');
changeFolderSpace.innerHTML = '';
for (const workspace of [...gZenWorkspaces._workspaceCache.workspaces].reverse()) {
- const item = document.createXULElement('menuitem');
- item.className = 'zen-workspace-context-menu-item';
- item.setAttribute('zen-workspace-id', workspace.uuid);
- item.setAttribute('disabled', workspace.uuid === gZenWorkspaces.activeWorkspace);
- let name = workspace.name;
- const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
- if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
- name = `${workspace.icon} ${name}`;
- }
- item.setAttribute('label', name);
- if (iconIsSvg) {
- item.setAttribute('image', workspace.icon);
- item.classList.add('zen-workspace-context-icon');
- }
+ const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace);
item.addEventListener('command', (event) => {
if (!this.#lastFolderContextMenu) return;
this.changeFolderToSpace(
@@ -506,9 +493,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
tabs = [emptyTab, ...filteredTabs];
const folder = this._createFolderNode(options);
- if (options.initialPinId) {
- folder.setAttribute('zen-pin-id', options.initialPinId);
- }
if (options.insertAfter) {
options.insertAfter.after(folder);
@@ -860,7 +844,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
.open(group.icon, { onlySvgIcons: true })
.then((icon) => {
this.setFolderUserIcon(group, icon);
- group.dispatchEvent(new CustomEvent('ZenFolderIconChanged', { bubbles: true }));
+ group.dispatchEvent(new CustomEvent('TabGroupUpdate', { bubbles: true }));
})
.catch((err) => {
console.error(err);
@@ -938,7 +922,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (!parentFolder && folder.hasAttribute('split-view-group')) continue;
const emptyFolderTabs = folder.tabs
.filter((tab) => tab.hasAttribute('zen-empty-tab'))
- .map((tab) => tab.getAttribute('zen-pin-id'));
+ .map((tab) => tab.getAttribute('id'));
let prevSiblingInfo = null;
const prevSibling = folder.previousElementSibling;
@@ -947,9 +931,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (prevSibling) {
if (gBrowser.isTabGroup(prevSibling)) {
prevSiblingInfo = { type: 'group', id: prevSibling.id };
- } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-pin-id')) {
- const zenPinId = prevSibling.getAttribute('zen-pin-id');
- prevSiblingInfo = { type: 'tab', id: zenPinId };
+ } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('id')) {
+ prevSiblingInfo = { type: 'tab', id: prevSibling.getAttribute('id') };
} else {
prevSiblingInfo = { type: 'start', id: null };
}
@@ -967,7 +950,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
prevSiblingInfo: prevSiblingInfo,
emptyTabIds: emptyFolderTabs,
userIcon: userIcon?.getAttribute('href'),
- pinId: folder.getAttribute('zen-pin-id'),
// note: We shouldn't be using the workspace-id anywhere, we are just
// remembering it for the pinned tabs manager to use it later.
workspaceId: folder.getAttribute('zen-workspace-id'),
@@ -994,10 +976,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
tabFolderWorkingData.set(folderData.id, workingData);
const oldGroup = document.getElementById(folderData.id);
- folderData.emptyTabIds.forEach((zenPinId) => {
- oldGroup
- ?.querySelector(`tab[zen-pin-id="${zenPinId}"]`)
- ?.setAttribute('zen-empty-tab', true);
+ folderData.emptyTabIds.forEach((id) => {
+ oldGroup?.querySelector(`tab[id="${id}"]`)?.setAttribute('zen-empty-tab', true);
});
if (oldGroup) {
if (!folderData.splitViewGroup) {
@@ -1009,7 +989,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
saveOnWindowClose: folderData.saveOnWindowClose,
workspaceId: folderData.workspaceId,
});
- folder.setAttribute('zen-pin-id', folderData.pinId);
+ folder.setAttribute('id', folderData.id);
workingData.node = folder;
oldGroup.before(folder);
} else {
@@ -1041,9 +1021,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (parentWorkingData && parentWorkingData.node) {
switch (stateData?.prevSiblingInfo?.type) {
case 'tab': {
- const tab = parentWorkingData.node.querySelector(
- `[zen-pin-id="${stateData.prevSiblingInfo.id}"]`
- );
+ const tab = document.getElementById(stateData.prevSiblingInfo.id);
tab.after(node);
break;
}
diff --git a/src/zen/moz.build b/src/zen/moz.build
index 21915a69ab..eb681597f0 100644
--- a/src/zen/moz.build
+++ b/src/zen/moz.build
@@ -13,4 +13,5 @@ DIRS += [
"tests",
"urlbar",
"toolkit",
+ "sessionstore",
]
diff --git a/src/zen/sessionstore/SessionComponents.manifest b/src/zen/sessionstore/SessionComponents.manifest
new file mode 100644
index 0000000000..8e3b0f9b08
--- /dev/null
+++ b/src/zen/sessionstore/SessionComponents.manifest
@@ -0,0 +1,10 @@
+# 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/.
+
+# Browser global components initializing before UI startup
+category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.init
+category browser-before-ui-startup resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.init
+
+# App shutdown consumers
+category browser-quit-application-granted resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.uninit
diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs
new file mode 100644
index 0000000000..2f14f53500
--- /dev/null
+++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs
@@ -0,0 +1,226 @@
+/* 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/. */
+
+import { JSONFile } from 'resource://gre/modules/JSONFile.sys.mjs';
+import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs';
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs',
+ BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs',
+ TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs',
+ SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs',
+ SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs',
+ setTimeout: 'resource://gre/modules/Timer.sys.mjs',
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.session-store.log', true);
+
+// Note that changing this hidden pref will make the previous session file
+// unused, causing a new session file to be created on next write.
+const SHOULD_COMPRESS_FILE = Services.prefs.getBoolPref('zen.session-store.compress-file', true);
+const SHOULD_BACKUP_FILE = Services.prefs.getBoolPref('zen.session-store.backup-file', true);
+
+const FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json';
+const MIGRATION_PREF = 'zen.ui.migration.session-manager-restore';
+
+/**
+ * Class representing the sidebar object stored in the session file.
+ * This object holds all the data related to tabs, groups, folders
+ * and split view state.
+ */
+class nsZenSidebarObject {
+ #sidebar = {};
+
+ get data() {
+ return { ...this.#sidebar };
+ }
+
+ set data(data) {
+ console.log(data);
+ this.#sidebar = data;
+ }
+}
+
+export class nsZenSessionManager {
+ #file;
+ #sidebarObject = new nsZenSidebarObject();
+
+ // Called from SessionComponents.manifest on app-startup
+ init() {
+ let profileDir = Services.dirsvc.get('ProfD', Ci.nsIFile).path;
+ let backupFile = null;
+ if (SHOULD_BACKUP_FILE) {
+ backupFile = PathUtils.join(profileDir, 'zen-sessions-backup', FILE_NAME);
+ }
+ let filePath = PathUtils.join(profileDir, FILE_NAME);
+ this.#file = new JSONFile({
+ path: filePath,
+ compression: SHOULD_COMPRESS_FILE ? 'lz4' : undefined,
+ backupFile,
+ });
+ }
+
+ log(...args) {
+ if (lazy.gShouldLog) {
+ console.info('ZenSessionManager:', ...args);
+ }
+ }
+
+ async readFile() {
+ console.log(await this.#file.load());
+ try {
+ this.#sidebar = (await this.#file.load()) || {};
+ } catch (e) {
+ console.error('ZenSessionManager: Failed to read session file', e);
+ this.#sidebar = {};
+ }
+ }
+
+ onFileRead(initialState) {
+ // For the first time after migration, we restore the tabs
+ // That where going to be restored by SessionStore. The sidebar
+ // object will always be empty after migration because we haven't
+ // gotten the opportunity to save the session yet.
+ if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) {
+ Services.prefs.setBoolPref(MIGRATION_PREF, true);
+ return;
+ }
+ // Restore all windows with the same sidebar object, this will
+ // guarantee that all tabs, groups, folders and split view data
+ // are properly synced across all windows.
+ this.log(`Restoring Zen session data into ${initialState.windows?.length || 0} windows`);
+ for (const winData of initialState.windows || []) {
+ this.restoreWindowData(winData);
+ }
+ }
+
+ get #sidebar() {
+ return { ...this.#sidebarObject.data };
+ }
+
+ set #sidebar(data) {
+ this.#sidebarObject.data = data;
+ }
+
+ /**
+ * Saves the current session state. Collects data and writes to disk.
+ *
+ * @param state
+ * The current session state.
+ */
+ saveState(state) {
+ if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || !state?.windows?.length) {
+ // Don't save (or even collect) anything in permanent private
+ // browsing mode. We also don't want to save if there are no windows.
+ return;
+ }
+ this.#collectWindowData(state);
+ // This would save the data to disk asynchronously.
+ this.#file.data = this.#sidebar;
+ this.#file.saveSoon();
+ this.log(`Saving Zen session data with ${this.#sidebar.tabs?.length || 0} tabs`);
+ }
+
+ /**
+ * Collects session data for a given window.
+ *
+ * @param state
+ * The current session state.
+ */
+ #collectWindowData(state) {
+ let sidebarData = this.#sidebar;
+ if (!sidebarData) {
+ sidebarData = {};
+ }
+
+ sidebarData.lastCollected = Date.now();
+ this.#collectTabsData(sidebarData, state);
+ this.#sidebar = sidebarData;
+ }
+
+ #filterUnusedTabs(tabs) {
+ return tabs.filter((tab) => {
+ // We need to ignore empty tabs with no group association
+ // as they are not useful to restore.
+ return !(tab.zenIsEmpty && !tab.groupId);
+ });
+ }
+
+ /**
+ * Collects session data for all tabs in a given window.
+ *
+ * @param sidebarData
+ * The sidebar data object to populate.
+ * @param state
+ * The current session state.
+ */
+ #collectTabsData(sidebarData, state) {
+ const tabIdRelationMap = new Map();
+ for (const window of state.windows) {
+ // Only accept the tabs with `_zenIsActiveTab` set to true from
+ // every window. We do this to avoid collecting tabs with invalid
+ // state when multiple windows are open. Note that if we a tab without
+ // this flag set in any other window, we just add it anyway.
+ for (const tabData of window.tabs) {
+ if (!tabIdRelationMap.has(tabData.zenSyncId) || tabData._zenIsActiveTab) {
+ tabIdRelationMap.set(tabData.zenSyncId, tabData);
+ }
+ }
+ }
+
+ sidebarData.tabs = this.#filterUnusedTabs(Array.from(tabIdRelationMap.values()));
+
+ sidebarData.folders = state.windows[0].folders;
+ sidebarData.splitViewData = state.windows[0].splitViewData;
+ sidebarData.groups = state.windows[0].groups;
+ }
+
+ restoreWindowData(aWindowData) {
+ const sidebar = this.#sidebar;
+ console.log(sidebar);
+ if (!sidebar) {
+ return;
+ }
+ aWindowData.tabs = sidebar.tabs || [];
+ aWindowData.splitViewData = sidebar.splitViewData;
+ aWindowData.folders = sidebar.folders;
+ aWindowData.groups = sidebar.groups;
+ }
+
+ restoreNewWindow(aWindow, SessionStoreInternal) {
+ if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) {
+ return;
+ }
+ this.log('Restoring new window with Zen session data');
+ aWindow._zenPromiseNewWindowRestored = new Promise((resolve) => {
+ lazy.setTimeout(() => {
+ const state = lazy.SessionStore.getCurrentState(true);
+ const windows = (state.windows || []).find(
+ (win) => !win.isPrivate && !win.isPopup && !win.isTaskbarTab && !win.isZenUnsynced
+ );
+ let windowToClone = windows[0];
+ let newWindow = Cu.cloneInto(windowToClone, {});
+ if (windows.length < 2) {
+ // We only want to restore the sidebar object if we found
+ // only one normal window to clone from (which is the one
+ // we are opening).
+ this.log('Restoring sidebar data into new window');
+ this.restoreWindowData(newWindow);
+ }
+ newWindow.tabs = this.#filterUnusedTabs(newWindow.tabs || []);
+ delete newWindow.selected;
+ const newState = { windows: [newWindow] };
+ this.log(`Cloning window with ${newWindow.tabs.length} tabs`);
+ SessionStoreInternal.restoreWindows(aWindow, newState, {
+ firstWindow: true,
+ });
+ resolve();
+ });
+ });
+ }
+}
+
+export const ZenSessionStore = new nsZenSessionManager();
diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs
new file mode 100644
index 0000000000..89248877d4
--- /dev/null
+++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs
@@ -0,0 +1,908 @@
+/* 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/. */
+
+import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs';
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs',
+ SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs',
+ TabStateFlusher: 'resource:///modules/sessionstore/TabStateFlusher.sys.mjs',
+ ZenSessionStore: 'resource:///modules/zen/ZenSessionManager.sys.mjs',
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gWindowSyncEnabled', 'zen.window-sync.enabled');
+XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.window-sync.log', true);
+
+const OBSERVING = ['browser-window-before-show'];
+const INSTANT_EVENTS = ['unload'];
+const EVENTS = [
+ 'TabOpen',
+ 'TabClose',
+
+ 'ZenTabIconChanged',
+ 'ZenTabLabelChanged',
+
+ 'TabMove',
+ 'TabPinned',
+ 'TabUnpinned',
+ 'TabAddedToEssentials',
+ 'TabRemovedFromEssentials',
+
+ 'TabGroupUpdate',
+ 'TabGroupCreate',
+ 'TabGroupRemoved',
+ 'TabGroupMoved',
+
+ 'TabSelect',
+
+ 'focus',
+ ...INSTANT_EVENTS,
+];
+
+// Flags acting as an enum for sync types.
+const SYNC_FLAG_LABEL = 1 << 0;
+const SYNC_FLAG_ICON = 1 << 1;
+const SYNC_FLAG_MOVE = 1 << 2;
+
+class nsZenWindowSync {
+ constructor() {}
+
+ /**
+ * Context about the currently handled event.
+ * Used to avoid re-entrancy issues.
+ *
+ * We do still wan't to keep a stack of these in order
+ * to handle consequtive events properly. For example,
+ * loading a webpage will call IconChanged and TitleChanged
+ * events one after another.
+ */
+ #eventHandlingContext = {
+ window: null,
+ eventCount: 0,
+ lastHandlerPromise: Promise.resolve(),
+ };
+
+ /**
+ * Last focused window.
+ * Used to determine which window to sync tab contents visibility from.
+ */
+ #lastFocusedWindow = null;
+
+ /**
+ * Last selected tab.
+ * Used to determine if we should run another sync operation
+ * when switching browser views.
+ */
+ #lastSelectedTab = null;
+
+ /**
+ * Iterator that yields all currently opened browser windows.
+ * (Might miss the most recent one.)
+ * This list is in focus order, but may include minimized windows
+ * before non-minimized windows.
+ */
+ #browserWindows = {
+ *[Symbol.iterator]() {
+ for (let window of lazy.BrowserWindowTracker.orderedWindows) {
+ if (
+ window.__SSi &&
+ !window.closed &&
+ window.gZenStartup.isReady &&
+ !window.gZenWorkspaces?.privateWindowOrDisabled
+ ) {
+ yield window;
+ }
+ }
+ },
+ };
+
+ init() {
+ if (!lazy.gWindowSyncEnabled) {
+ return;
+ }
+ for (let topic of OBSERVING) {
+ Services.obs.addObserver(this, topic);
+ }
+ lazy.SessionStore.promiseAllWindowsRestored.then(() => {
+ this.#onSessionStoreInitialized();
+ });
+ }
+
+ uninit() {
+ for (let topic of OBSERVING) {
+ Services.obs.removeObserver(this, topic);
+ }
+ }
+
+ log(...args) {
+ if (lazy.gShouldLog) {
+ console.info('ZenWindowSync:', ...args);
+ }
+ }
+
+ /**
+ * Called when a browser window is about to be shown.
+ * Adds event listeners for the specified events.
+ *
+ * @param {Window} aWindow - The browser window that is about to be shown.
+ */
+ #onWindowBeforeShow(aWindow) {
+ // There are 2 possibilities to know if we are trying to open
+ // a new *unsynced* window:
+ // 1. We are passing `zen-unsynced` in the window arguments.
+ // 2. We are trying to open a link in a new window where other synced
+ // windows already exist
+ let forcedSync = false;
+ let hasUnsyncedArg = false;
+ for (let arg of aWindow.arguments) {
+ if (arg === 'zen-synced') {
+ forcedSync = true;
+ } else if (arg === 'zen-unsynced') {
+ hasUnsyncedArg = true;
+ }
+ }
+ if (
+ !forcedSync &&
+ (hasUnsyncedArg ||
+ (typeof aWindow.arguments[0] === 'string' &&
+ aWindow.arguments.length > 1 &&
+ [...this.#browserWindows].length > 0))
+ ) {
+ this.log('Not syncing new window due to unsynced argument or existing synced windows');
+ aWindow.document.documentElement.setAttribute('zen-unsynced-window', 'true');
+ return;
+ }
+ aWindow.gZenWindowSync = this;
+ for (let eventName of EVENTS) {
+ aWindow.addEventListener(eventName, this, true);
+ }
+ }
+
+ /** * Generates a unique tab ID.
+ *
+ * @returns {string} A unique tab ID.
+ */
+ get #newTabSyncId() {
+ // Note: If this changes, make sure to also update the
+ // getExtTabGroupIdForInternalTabGroupId implementation in
+ // browser/components/extensions/parent/ext-browser.js.
+ // See: Bug 1960104 - Improve tab group ID generation in addTabGroup
+ // This is implemented from gBrowser.addTabGroup.
+ return `${Date.now()}-${Math.round(Math.random() * 100)}`;
+ }
+
+ /**
+ * Called when the session store has finished initializing for a window.
+ *
+ * @param {Window} aWindow - The browser window that has initialized session store.
+ */
+ #onSessionStoreInitialized() {
+ // For every tab we have in where there's no sync ID, we need to
+ // assign one and sync it to other windows.
+ // This should only happen really when updating from an older version
+ // that didn't have this feature.
+ this.#runOnAllWindows(null, (aWindow) => {
+ const { gBrowser } = aWindow;
+ for (let tab of gBrowser.tabs) {
+ if (!tab.id) {
+ tab.id = this.#newTabSyncId;
+ lazy.TabStateFlusher.flush(tab.linkedBrowser);
+ }
+ }
+ });
+ }
+
+ /**
+ * Runs a callback function on all browser windows except the specified one.
+ *
+ * @param {Window} aWindow - The browser window to exclude.
+ * @param {Function} aCallback - The callback function to run on each window.
+ * @returns {any} The value returned by the callback function, if any.
+ */
+ #runOnAllWindows(aWindow, aCallback) {
+ for (let window of this.#browserWindows) {
+ if (window !== aWindow) {
+ let value = aCallback(window);
+ if (value) {
+ return value;
+ }
+ }
+ }
+ return null;
+ }
+
+ observe(aSubject, aTopic) {
+ switch (aTopic) {
+ case 'browser-window-before-show': {
+ this.#onWindowBeforeShow(aSubject);
+ break;
+ }
+ }
+ }
+
+ handleEvent(aEvent) {
+ const window = aEvent.currentTarget.ownerGlobal;
+ if (!window.gZenStartup.isReady || window.gZenWorkspaces?.privateWindowOrDisabled) {
+ return;
+ }
+ if (INSTANT_EVENTS.includes(aEvent.type)) {
+ return this.#handleNextEvent(aEvent);
+ }
+ if (this.#eventHandlingContext.window && this.#eventHandlingContext.window !== window) {
+ // We're already handling an event for another window.
+ // To avoid re-entrancy issues, we skip this event.
+ return;
+ }
+ const lastHandlerPromise = this.#eventHandlingContext.lastHandlerPromise;
+ this.#eventHandlingContext.eventCount++;
+ this.#eventHandlingContext.window = window;
+ let resolveNewPromise;
+ this.#eventHandlingContext.lastHandlerPromise = new Promise((resolve) => {
+ resolveNewPromise = resolve;
+ });
+ // Wait for the last handler to finish before processing the next event.
+ lastHandlerPromise.then(() => {
+ try {
+ this.#handleNextEvent(aEvent);
+ } finally {
+ if (--this.#eventHandlingContext.eventCount === 0) {
+ this.#eventHandlingContext.window = null;
+ }
+ resolveNewPromise();
+ }
+ });
+ }
+
+ /**
+ * Handles the next event by calling the appropriate handler method.
+ *
+ * @param {Event} aEvent - The event to handle.
+ */
+ #handleNextEvent(aEvent) {
+ const handler = `on_${aEvent.type}`;
+ if (typeof this[handler] === 'function') {
+ this[handler](aEvent);
+ } else {
+ console.warn(`ZenWindowSync: No handler for event type: ${aEvent.type}`);
+ }
+ }
+
+ /**
+ * Retrieves a item element from a window by its ID.
+ *
+ * @param {Window} aWindow - The window containing the item.
+ * @param {string} aItemId - The ID of the item to retrieve.
+ * @returns {MozTabbrowserTab|MozTabbrowserTabGroup|null} The item element if found, otherwise null.
+ */
+ #getItemFromWindow(aWindow, aItemId) {
+ return aWindow.document.getElementById(aItemId);
+ }
+
+ /**
+ * Synchronizes the icon and label of the target tab with the original tab.
+ *
+ * @param {Object} aOriginalTab - The original tab to copy from.
+ * @param {Object} aTargetTab - The target tab to copy to.
+ * @param {Window} aWindow - The window containing the tabs.
+ * @param {number} flags - The sync flags indicating what to synchronize.
+ */
+ #syncItemWithOriginal(aOriginalItem, aTargetItem, aWindow, flags = 0) {
+ if (!aOriginalItem || !aTargetItem) {
+ return;
+ }
+ const { gBrowser, gZenFolders } = aWindow;
+ if (flags & SYNC_FLAG_ICON) {
+ if (gBrowser.isTab(aOriginalItem)) {
+ gBrowser.setIcon(aTargetItem, gBrowser.getIcon(aOriginalItem));
+ } else if (aOriginalItem.isZenFolder) {
+ // Icons are a zen-only feature for tab groups.
+ gZenFolders.setFolderUserIcon(aTargetItem, aOriginalItem.iconURL);
+ }
+ }
+ if (flags & SYNC_FLAG_LABEL) {
+ if (gBrowser.isTab(aOriginalItem)) {
+ gBrowser._setTabLabel(aTargetItem, aOriginalItem.label);
+ } else if (gBrowser.isTabGroup(aOriginalItem)) {
+ aTargetItem.label = aOriginalItem.label;
+ }
+ }
+ if (flags & SYNC_FLAG_MOVE && !aTargetItem.hasAttribute('zen-empty-tab')) {
+ const workspaceId = aOriginalItem.getAttribute('zen-workspace-id');
+ if (workspaceId) {
+ aTargetItem.setAttribute('zen-workspace-id', workspaceId);
+ } else {
+ aTargetItem.removeAttribute('zen-workspace-id');
+ }
+ this.#syncItemPosition(aOriginalItem, aTargetItem, aWindow);
+ }
+ if (gBrowser.isTab(aTargetItem)) {
+ lazy.TabStateFlusher.flush(aTargetItem.linkedBrowser);
+ }
+ }
+
+ /**
+ * Synchronizes the position of the target item with the original item.
+ *
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aOriginalItem - The original item to copy from.
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTargetItem - The target item to copy to.
+ * @param {Window} aWindow - The window containing the items.
+ */
+ #syncItemPosition(aOriginalItem, aTargetItem, aWindow) {
+ const { gBrowser, gZenPinnedTabManager } = aWindow;
+ const originalIsEssential = aOriginalItem.hasAttribute('zen-essential');
+ const targetIsEssential = aTargetItem.hasAttribute('zen-essential');
+ const originalIsPinned = aOriginalItem.pinned;
+ const targetIsPinned = aTargetItem.pinned;
+
+ const isGroup = gBrowser.isTabGroup(aOriginalItem);
+ const isTab = !isGroup;
+
+ if (isTab) {
+ if (originalIsEssential !== targetIsEssential) {
+ if (originalIsEssential) {
+ gZenPinnedTabManager.addToEssentials(aTargetItem);
+ } else {
+ gZenPinnedTabManager.removeEssentials(aTargetItem, /* unpin= */ !targetIsPinned);
+ }
+ } else if (originalIsPinned !== targetIsPinned) {
+ if (originalIsPinned) {
+ gBrowser.pinTab(aTargetItem);
+ } else {
+ gBrowser.unpinTab(aTargetItem);
+ }
+ }
+ }
+
+ this.#moveItemToMatchOriginal(aOriginalItem, aTargetItem, aWindow, {
+ isEssential: originalIsEssential,
+ isPinned: originalIsPinned,
+ });
+ }
+
+ /**
+ * Moves the target item to match the position of the original item.
+ *
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aOriginalItem - The original item to match.
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTargetItem - The target item to move.
+ * @param {Window} aWindow - The window containing the items.
+ */
+ #moveItemToMatchOriginal(aOriginalItem, aTargetItem, aWindow, { isEssential, isPinned }) {
+ const { gBrowser, gZenWorkspaces } = aWindow;
+ const originalSibling = aOriginalItem.previousElementSibling;
+ let isFirstTab = true;
+ if (gBrowser.isTabGroup(originalSibling) || gBrowser.isTab(originalSibling)) {
+ isFirstTab =
+ !originalSibling.hasAttribute('id') || originalSibling.hasAttribute('zen-empty-tab');
+ }
+
+ gBrowser.zenHandleTabMove(aOriginalItem, () => {
+ if (isFirstTab) {
+ let container;
+ const parentGroup = aOriginalItem.group;
+ if (parentGroup?.hasAttribute('id')) {
+ container = this.#getItemFromWindow(aWindow, parentGroup.getAttribute('id'));
+ if (container) {
+ if (container?.tabs?.length) {
+ // First tab in folders is the empty tab placeholder.
+ container.tabs[0].after(aTargetItem);
+ } else {
+ container.appendChild(aTargetItem);
+ }
+ return;
+ }
+ }
+ if (isEssential) {
+ container = gZenWorkspaces.getEssentialsSection(aTargetItem);
+ } else {
+ const workspaceId = aTargetItem.getAttribute('zen-workspace-id');
+ const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId);
+ container = isPinned
+ ? workspaceElement?.pinnedTabsContainer
+ : workspaceElement?.tabsContainer;
+ }
+ if (container) {
+ container.insertBefore(aTargetItem, container.firstChild);
+ }
+ return;
+ }
+ const relativeTab = this.#getItemFromWindow(aWindow, originalSibling.id);
+ if (relativeTab) {
+ relativeTab.after(aTargetItem);
+ }
+ });
+ }
+
+ /**
+ * Synchronizes a item across all browser windows.
+ *
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aItem - The item to synchronize.
+ * @param {number} flags - The sync flags indicating what to synchronize.
+ */
+ #syncItemForAllWindows(aItem, flags = 0) {
+ const window = aItem.ownerGlobal;
+ this.#runOnAllWindows(window, (win) => {
+ this.#syncItemWithOriginal(aItem, this.#getItemFromWindow(win, aItem.id), win, flags);
+ });
+ }
+
+ /**
+ * Swaps the browser docshells between two tabs.
+ *
+ * @param {Object} aOurTab - The tab in the current window.
+ * @param {Object} aOtherTab - The tab in the other window.
+ */
+ async #swapBrowserDocShellsAsync(aOurTab, aOtherTab) {
+ await this.#styleSwapedBrowsers(aOurTab, aOtherTab);
+ this.#swapBrowserDocSheellsInner(aOurTab, aOtherTab);
+ }
+
+ /**
+ * Restores the tab progress listener for a given tab.
+ *
+ * @param {Object} aTab - The tab to restore the progress listener for.
+ * @param {Function} callback - The callback function to execute while the listener is removed.
+ * @param {boolean} onClose - Indicates if the swap is done during a tab close operation.
+ */
+ #withRestoreTabProgressListener(aTab, callback, onClose = false) {
+ const otherTabBrowser = aTab.ownerGlobal.gBrowser;
+ const otherBrowser = aTab.linkedBrowser;
+
+ // We aren't closing the other tab so, we also need to swap its tablisteners.
+ let filter = otherTabBrowser._tabFilters.get(aTab);
+ let tabListener = otherTabBrowser._tabListeners.get(aTab);
+ try {
+ otherBrowser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(tabListener);
+ } catch {
+ /* ignore errors, we might have already removed them */
+ }
+
+ try {
+ callback();
+ } catch (e) {
+ console.error(e);
+ }
+
+ // Restore the listeners for the swapped in tab.
+ if (!onClose) {
+ tabListener = new otherTabBrowser.zenTabProgressListener(aTab, otherBrowser, false, false);
+ otherTabBrowser._tabListeners.set(aTab, tabListener);
+
+ const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
+ filter.addProgressListener(tabListener, notifyAll);
+ otherBrowser.webProgress.addProgressListener(filter, notifyAll);
+ }
+ }
+
+ /**
+ * Swaps the browser docshells between two tabs.
+ *
+ * @param {Object} aOurTab - The tab in the current window.
+ * @param {Object} aOtherTab - The tab in the other window.
+ * @param {boolean} focus - Indicates if the tab should be focused after the swap.
+ * @param {boolean} onClose - Indicates if the swap is done during a tab close operation.
+ */
+ #swapBrowserDocSheellsInner(aOurTab, aOtherTab, focus = true, onClose = false) {
+ // Load about:blank if by any chance we loaded the previous tab's URL.
+ // TODO: We should maybe start using a singular about:blank preloaded view
+ // to avoid loading a full blank page each time and wasting resources.
+ // We do need to do this though instead of just unloading the browser because
+ // firefox doesn't expect an unloaded + selected tab, so we need to get
+ // around this limitation somehow.
+ if (!onClose && aOurTab.linkedBrowser?.currentURI.spec !== 'about:blank') {
+ aOurTab.linkedBrowser.loadURI(Services.io.newURI('about:blank'), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
+ });
+ }
+ // Running `swapBrowsersAndCloseOther` doesn't expect us to use the tab after
+ // the operation, so it doesn't really care about cleaning up the other tab.
+ // We need to make a new tab progress listener for the other tab after the swap.
+ this.#withRestoreTabProgressListener(
+ aOtherTab,
+ () => {
+ aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false);
+ },
+ onClose
+ );
+ const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture'];
+ // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted',
+ // but we need to manually remove some attributes from the other tab.
+ for (let attr of kAttributesToRemove) {
+ aOtherTab.removeAttribute(attr);
+ }
+ if (focus) {
+ // Recalculate the focus in order to allow the user to continue typing
+ // inside the web contentx area without having to click outside and back in.
+ aOurTab.linkedBrowser.blur();
+ aOurTab.ownerGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab);
+ }
+ // Ensure the tab's state is flushed after the swap. By doing this,
+ // we can re-schedule another session store delayed process to fire.
+ // It's also important to note that if we don't flush the state here,
+ // we would start recieving invalid history changes from the the incorrect
+ // browser view that was just swapped out.
+ lazy.TabStateFlusher.flush(aOurTab.linkedBrowser);
+ }
+
+ /**
+ * Styles the swapped browsers to ensure proper visibility and layout.
+ *
+ * @param {Object} aOurTab - The tab in the current window.
+ * @param {Object} aOtherTab - The tab in the other window.
+ * @param {boolean} onClose - Indicates if the styling is done during a tab close operation.
+ */
+ async #styleSwapedBrowsers(aOurTab, aOtherTab, onClose = false) {
+ const ourBrowser = aOurTab.linkedBrowser;
+ const otherBrowser = aOtherTab.linkedBrowser;
+
+ if (!onClose) {
+ const browserBlob = await aOtherTab.ownerGlobal.PageThumbs.captureToBlob(
+ aOtherTab.linkedBrowser,
+ {
+ fullScale: true,
+ fullViewport: true,
+ }
+ );
+
+ let mySrc = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(browserBlob);
+ reader.onloadend = function () {
+ // result includes identifier 'data:image/png;base64,' plus the base64 data
+ resolve(reader.result);
+ };
+ reader.onerror = function () {
+ reject(new Error('Failed to read blob as data URL'));
+ };
+ });
+
+ const [img, loadPromise] = this.#createPseudoImageForBrowser(otherBrowser, mySrc);
+ // Run a reflow to ensure the image is rendered before hiding the browser.
+ void img.getBoundingClientRect();
+ await loadPromise;
+ otherBrowser.setAttribute('zen-pseudo-hidden', 'true');
+ }
+
+ this.#maybeRemovePseudoImageForBrowser(ourBrowser);
+ ourBrowser.removeAttribute('zen-pseudo-hidden');
+ }
+
+ /**
+ * Create and insert a new pseudo image for a browser element.
+ *
+ * @param {Object} aBrowser - The browser element to create the pseudo image for.
+ * @param {string} aSrc - The source URL of the image.
+ * @returns {Object} The created pseudo image element.
+ */
+ #createPseudoImageForBrowser(aBrowser, aSrc) {
+ const doc = aBrowser.ownerDocument;
+ const img = doc.createElement('img');
+ img.className = 'zen-pseudo-browser-image';
+ aBrowser.after(img);
+ const loadPromise = new Promise((resolve) => {
+ img.onload = () => resolve();
+ img.src = aSrc;
+ });
+ return [img, loadPromise];
+ }
+
+ /**
+ * Removes the pseudo image element for a browser if it exists.
+ *
+ * @param {Object} aBrowser - The browser element to remove the pseudo image for.
+ */
+ #maybeRemovePseudoImageForBrowser(aBrowser) {
+ const elements = aBrowser.parentNode?.querySelectorAll('.zen-pseudo-browser-image');
+ if (elements) {
+ elements.forEach((element) => element.remove());
+ }
+ }
+
+ /**
+ * Retrieves the active tab, where the web contents are being viewed
+ * from other windows by its ID.
+ *
+ * @param {Window} aWindow - The window to exclude.
+ * @param {string} aTabId - The ID of the tab to retrieve.
+ * @param {Function} filter - A function to filter the tabs.
+ * @returns {Object|null} The active tab from other windows if found, otherwise null.
+ */
+ #getActiveTabFromOtherWindows(aWindow, aTabId, filter = (tab) => tab?._zenContentsVisible) {
+ return this.#runOnAllWindows(aWindow, (win) => {
+ const tab = this.#getItemFromWindow(win, aTabId);
+ if (filter(tab)) {
+ return tab;
+ }
+ });
+ }
+
+ /**
+ * Moves all active tabs from the specified window to other windows.
+ *
+ * @param {Window} aWindow - The window to move active tabs from.
+ */
+ #moveAllActiveTabsToOtherWindows(aWindow) {
+ const mostRecentWindow = [...this.#browserWindows].find((win) => win !== aWindow);
+ if (!mostRecentWindow || !aWindow.gZenWorkspaces) {
+ return;
+ }
+ const activeTabsOnClosedWindow = aWindow.gZenWorkspaces.allStoredTabs.filter(
+ (tab) => tab._zenContentsVisible
+ );
+ for (let tab of activeTabsOnClosedWindow) {
+ const targetTab = this.#getItemFromWindow(mostRecentWindow, tab.id);
+ if (targetTab) {
+ targetTab._zenContentsVisible = true;
+ this.#swapBrowserDocSheellsInner(targetTab, tab, targetTab.selected, /* onClose =*/ true);
+ // We can animate later, whats important is to always stay on the same
+ // process and avoid async operations here to avoid the closed window
+ // being unloaded before the swap is done.
+ this.#styleSwapedBrowsers(targetTab, tab, /* onClose =*/ true);
+ }
+ }
+ }
+
+ /**
+ * Handles tab switch or window focus events to synchronize tab contents visibility.
+ *
+ * @param {Window} aWindow - The window that triggered the event.
+ * @param {Object} aPreviousTab - The previously selected tab.
+ */
+ #onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null) {
+ const selectedTab = aWindow.gBrowser.selectedTab;
+ if (aPreviousTab?._zenContentsVisible) {
+ const otherTabToShow = this.#getActiveTabFromOtherWindows(
+ aWindow,
+ aPreviousTab.id,
+ (tab) => tab?.selected
+ );
+ if (otherTabToShow) {
+ otherTabToShow._zenContentsVisible = true;
+ delete aPreviousTab._zenContentsVisible;
+ this.#swapBrowserDocShellsAsync(otherTabToShow, aPreviousTab);
+ }
+ }
+ if (selectedTab._zenContentsVisible) {
+ return;
+ }
+ const otherSelectedTab = this.#getActiveTabFromOtherWindows(aWindow, selectedTab.id);
+ selectedTab._zenContentsVisible = true;
+ if (otherSelectedTab) {
+ delete otherSelectedTab._zenContentsVisible;
+ this.#swapBrowserDocShellsAsync(selectedTab, otherSelectedTab);
+ }
+ }
+
+ /**
+ * Delegates generic sync events to synchronize tabs across windows.
+ *
+ * @param {Event} aEvent - The event to delegate.
+ * @param {number} flags - The sync flags indicating what to synchronize.
+ */
+ #delegateGenericSyncEvent(aEvent, flags = 0) {
+ const item = aEvent.target;
+ this.#syncItemForAllWindows(item, flags);
+ }
+
+ /* Mark: Public API */
+
+ shouldLoadTab(aTab) {
+ if (!lazy.gWindowSyncEnabled) {
+ // Since we are never going to sync the tab, we can always load it.
+ return true;
+ }
+ if (aTab._zenContentsVisible) {
+ // This tab is already active in this window.
+ return true;
+ }
+ // We don't want to trigger a new browser kick-off if there's
+ // another window where this tab is already active.
+ return !this.#getActiveTabFromOtherWindows(
+ aTab.ownerGlobal,
+ aTab.id,
+ (tab) => tab?._zenContentsVisible
+ );
+ }
+
+ moveTabsToSyncedWorkspace(aWindow, aWorkspaceId) {
+ const tabsToMove = aWindow.gZenWorkspaces.allStoredTabs.filter(
+ (tab) => !tab.hasAttribute('zen-empty-tab')
+ );
+ const selectedTab = aWindow.gBrowser.selectedTab;
+ let win = [...this.#browserWindows][0];
+ const moveAllTabsToWindow = (allowSelected = false) => {
+ const { gBrowser, gZenWorkspaces } = win;
+ win.focus();
+ let tabIndex = 0;
+ let success = true;
+ for (const tab of tabsToMove) {
+ if (tab !== selectedTab || allowSelected) {
+ const newTab = gBrowser.adoptTab(tab, { tabIndex });
+ if (!newTab) {
+ // The adoption failed. Restore "fadein" and don't increase the index.
+ tab.setAttribute('fadein', 'true');
+ success = false;
+ continue;
+ }
+ gZenWorkspaces.moveTabToWorkspace(newTab, aWorkspaceId);
+ ++tabIndex;
+ }
+ }
+ if (success) {
+ aWindow.close();
+ }
+ };
+ if (!win) {
+ win = aWindow.gBrowser.replaceTabWithWindow(selectedTab, {}, /* zenForceSync = */ true);
+ win.gZenWorkspaces.promiseInitialized.then(() => {
+ moveAllTabsToWindow();
+ });
+ return;
+ }
+ moveAllTabsToWindow(true);
+ }
+
+ /* Mark: Event Handlers */
+
+ on_TabOpen(aEvent) {
+ const tab = aEvent.target;
+ const window = tab.ownerGlobal;
+ if (tab.selected) {
+ tab._zenContentsVisible = true;
+ }
+ if (tab.id) {
+ // This tab was opened as part of a sync operation.
+ return;
+ }
+ tab.id = this.#newTabSyncId;
+ this.#runOnAllWindows(window, (win) => {
+ const newTab = win.gBrowser.addTrustedTab('about:blank', {
+ animate: true,
+ createLazyBrowser: true,
+ zenWorkspaceId: tab.getAttribute('zen-workspace-id') || '',
+ _forZenEmptyTab: tab.hasAttribute('zen-empty-tab'),
+ });
+ newTab.id = tab.id;
+ this.#syncItemWithOriginal(
+ tab,
+ newTab,
+ win,
+ SYNC_FLAG_ICON | SYNC_FLAG_LABEL | SYNC_FLAG_MOVE
+ );
+ });
+ }
+
+ on_ZenTabIconChanged(aEvent) {
+ if (!aEvent.target?._zenContentsVisible) {
+ // No need to sync icon changes for tabs that aren't active in this window.
+ return;
+ }
+ return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON);
+ }
+
+ on_ZenTabLabelChanged(aEvent) {
+ if (!aEvent.target?._zenContentsVisible) {
+ // No need to sync label changes for tabs that aren't active in this window.
+ return;
+ }
+ return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_LABEL);
+ }
+
+ on_TabMove(aEvent) {
+ return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_MOVE);
+ }
+
+ on_TabPinned(aEvent) {
+ return this.on_TabMove(aEvent);
+ }
+
+ on_TabUnpinned(aEvent) {
+ return this.on_TabMove(aEvent);
+ }
+
+ on_TabAddedToEssentials(aEvent) {
+ return this.on_TabMove(aEvent);
+ }
+
+ on_TabRemovedFromEssentials(aEvent) {
+ return this.on_TabMove(aEvent);
+ }
+
+ on_TabClose(aEvent) {
+ const tab = aEvent.target;
+ const window = tab.ownerGlobal;
+ this.#runOnAllWindows(window, (win) => {
+ const targetTab = this.#getItemFromWindow(win, tab.id);
+ if (targetTab) {
+ win.gBrowser.removeTab(targetTab, { animate: true });
+ }
+ });
+ }
+
+ on_focus(aEvent) {
+ const { ownerGlobal: window } = aEvent.target;
+ if (
+ !window.gBrowser ||
+ this.#lastFocusedWindow?.deref() === window ||
+ window.closing ||
+ !window.toolbar.visible
+ ) {
+ return;
+ }
+ this.#lastFocusedWindow = new WeakRef(window);
+ this.#lastSelectedTab = new WeakRef(window.gBrowser.selectedTab);
+ this.#onTabSwitchOrWindowFocus(window);
+ }
+
+ on_TabSelect(aEvent) {
+ const tab = aEvent.target;
+ if (this.#lastSelectedTab?.deref() === tab) {
+ return;
+ }
+ this.#lastSelectedTab = new WeakRef(tab);
+ const previousTab = aEvent.detail.previousTab;
+ this.#onTabSwitchOrWindowFocus(aEvent.target.ownerGlobal, previousTab);
+ }
+
+ on_unload(aEvent) {
+ const window = aEvent.target.ownerGlobal;
+ for (let eventName of EVENTS) {
+ window.removeEventListener(eventName, this);
+ }
+ delete window.gZenWindowSync;
+ this.#moveAllActiveTabsToOtherWindows(window);
+ }
+
+ on_TabGroupCreate(aEvent) {
+ const tabGroup = aEvent.target;
+ if (tabGroup.id) {
+ // This tab group was opened as part of a sync operation.
+ console.log('Duplicate!');
+ }
+ const window = tabGroup.ownerGlobal;
+ const isFolder = tabGroup.isZenFolder;
+ const isSplitView = tabGroup.hasAttribute('split-view-group');
+ // Tab groups already have an ID upon creation.
+ this.#runOnAllWindows(window, (win) => {
+ const newGroup = isFolder
+ ? win.gZenFolders.createFolder([], {})
+ : win.gBrowser.addTabGroup({ splitView: isSplitView });
+ newGroup.id = tabGroup.id;
+ this.#syncItemWithOriginal(
+ tabGroup,
+ newGroup,
+ win,
+ SYNC_FLAG_ICON | SYNC_FLAG_LABEL | SYNC_FLAG_MOVE
+ );
+ });
+ }
+
+ on_TabGroupRemoved(aEvent) {
+ const tabGroup = aEvent.target;
+ const window = tabGroup.ownerGlobal;
+ this.#runOnAllWindows(window, (win) => {
+ const targetGroup = this.#getItemFromWindow(win, tabGroup.id);
+ if (targetGroup) {
+ if (targetGroup.isZenFolder) {
+ targetGroup.delete();
+ } else {
+ win.gBrowser.removeTabGroup(targetGroup, { isUserTriggered: true });
+ }
+ }
+ });
+ }
+
+ on_TabGroupMoved(aEvent) {
+ return this.on_TabMove(aEvent);
+ }
+
+ on_TabGroupUpdate(aEvent) {
+ return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON | SYNC_FLAG_LABEL);
+ }
+}
+
+export const ZenWindowSync = new nsZenWindowSync();
diff --git a/src/zen/sessionstore/moz.build b/src/zen/sessionstore/moz.build
new file mode 100644
index 0000000000..188f4c27ce
--- /dev/null
+++ b/src/zen/sessionstore/moz.build
@@ -0,0 +1,8 @@
+# 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/.
+
+EXTRA_JS_MODULES.zen += [
+ "ZenSessionManager.sys.mjs",
+ "ZenWindowSync.sys.mjs",
+]
diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs
index c922d11663..3ef23aab13 100644
--- a/src/zen/tabs/ZenPinnedTabManager.mjs
+++ b/src/zen/tabs/ZenPinnedTabManager.mjs
@@ -7,23 +7,7 @@ import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components
const lazy = {};
class ZenPinnedTabsObserver {
- static ALL_EVENTS = [
- 'TabPinned',
- 'TabUnpinned',
- 'TabMove',
- 'TabGroupCreate',
- 'TabGroupRemoved',
- 'TabGroupMoved',
- 'ZenFolderRenamed',
- 'ZenFolderIconChanged',
- 'TabGroupCollapse',
- 'TabGroupExpand',
- 'TabGrouped',
- 'TabUngrouped',
- 'ZenFolderChangedWorkspace',
- 'TabAddedToEssentials',
- 'TabRemovedFromEssentials',
- ];
+ static ALL_EVENTS = ['TabPinned', 'TabUnpinned'];
#listeners = [];
@@ -76,7 +60,6 @@ class ZenPinnedTabsObserver {
}
class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
- hasInitializedPins = false;
promiseInitializedPinned = new Promise((resolve) => {
this._resolvePinnedInitializedInternal = resolve;
});
@@ -103,19 +86,10 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
onTabIconChanged(tab, url = null) {
+ tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } }));
const iconUrl = url ?? tab.iconImage.src;
- if (!iconUrl && tab.hasAttribute('zen-pin-id')) {
- try {
- setTimeout(async () => {
- const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI);
- if (favicon) {
- gBrowser.setIcon(tab, favicon);
- }
- });
- } catch {
- // Handle error
- }
- } else {
+ if (tab.hasAttribute('zen-essential')) {
+ tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
if (tab.hasAttribute('zen-essential')) {
tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
}
@@ -150,260 +124,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return lazy.zenTabsEssentialsMax;
}
- async refreshPinnedTabs({ init = false } = {}) {
- if (!this.enabled) {
- return;
- }
- await ZenPinnedTabsStorage.promiseInitialized;
- await this.#initializePinsCache();
- setTimeout(async () => {
- // Execute in a separate task to avoid blocking the main thread
- await SessionStore.promiseAllWindowsRestored;
- await gZenWorkspaces.promiseInitialized;
- await this.#initializePinnedTabs(init);
- if (init) {
- this._hasFinishedLoading = true;
- }
- }, 100);
- }
-
- async #initializePinsCache() {
- try {
- // Get pin data
- const pins = await ZenPinnedTabsStorage.getPins();
-
- // Enhance pins with favicons
- this._pinsCache = await Promise.all(
- pins.map(async (pin) => {
- try {
- if (pin.isGroup) {
- return pin; // Skip groups for now
- }
- const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url));
- return {
- ...pin,
- iconUrl: image || null,
- };
- } catch {
- // If favicon fetch fails, continue without icon
- return {
- ...pin,
- iconUrl: null,
- };
- }
- })
- );
- } catch (ex) {
- console.error('Failed to initialize pins cache:', ex);
- this._pinsCache = [];
- }
-
- this.log(`Initialized pins cache with ${this._pinsCache.length} pins`);
- return this._pinsCache;
- }
-
- #finishedInitializingPins() {
- if (this.hasInitializedPins) {
- return;
- }
- this._resolvePinnedInitializedInternal();
- delete this._resolvePinnedInitializedInternal;
- this.hasInitializedPins = true;
- }
-
- async #initializePinnedTabs(init = false) {
- const pins = this._pinsCache;
- if (!pins?.length || !init) {
- this.#finishedInitializingPins();
- return;
- }
-
- const pinnedTabsByUUID = new Map();
- const pinsToCreate = new Set(pins.map((p) => p.uuid));
-
- // First pass: identify existing tabs and remove those without pins
- for (let tab of gZenWorkspaces.allStoredTabs) {
- const pinId = tab.getAttribute('zen-pin-id');
- if (!pinId) {
- continue;
- }
-
- if (pinsToCreate.has(pinId)) {
- // This is a valid pinned tab that matches a pin
- pinnedTabsByUUID.set(pinId, tab);
- pinsToCreate.delete(pinId);
-
- if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl && init) {
- this._resetTabToStoredState(tab);
- }
- } else {
- // This is a pinned tab that no longer has a corresponding pin
- gBrowser.removeTab(tab);
- }
- }
-
- for (const group of gZenWorkspaces.allTabGroups) {
- const pinId = group.getAttribute('zen-pin-id');
- if (!pinId) {
- continue;
- }
- if (pinsToCreate.has(pinId)) {
- // This is a valid pinned group that matches a pin
- pinsToCreate.delete(pinId);
- }
- }
-
- // Second pass: For every existing tab, update its label
- // and set 'zen-has-static-label' attribute if it's been edited
- for (let pin of pins) {
- const tab = pinnedTabsByUUID.get(pin.uuid);
- if (!tab) {
- continue;
- }
-
- tab.removeAttribute('zen-has-static-label'); // So we can set it again
- if (pin.title && pin.editedTitle) {
- gBrowser._setTabLabel(tab, pin.title, { beforeTabOpen: true });
- tab.setAttribute('zen-has-static-label', 'true');
- }
- }
-
- const groups = new Map();
- const pendingTabsInsideGroups = {};
-
- // Third pass: create new tabs for pins that don't have tabs
- for (let pin of pins) {
- try {
- if (!pinsToCreate.has(pin.uuid)) {
- continue; // Skip pins that already have tabs
- }
-
- if (pin.isGroup) {
- const tabs = [];
- // If there's already existing tabs, let's use them
- for (const [uuid, existingTab] of pinnedTabsByUUID) {
- const pinObject = this._pinsCache.find((p) => p.uuid === uuid);
- if (pinObject && pinObject.parentUuid === pin.uuid) {
- tabs.push(existingTab);
- }
- }
- // We still need to iterate through pending tabs since the database
- // query doesn't guarantee the order of insertion
- for (const [parentUuid, folderTabs] of Object.entries(pendingTabsInsideGroups)) {
- if (parentUuid === pin.uuid) {
- tabs.push(...folderTabs);
- }
- }
- const group = gZenFolders.createFolder(tabs, {
- label: pin.title,
- collapsed: pin.isFolderCollapsed,
- initialPinId: pin.uuid,
- workspaceId: pin.workspaceUuid,
- insertAfter:
- groups.get(pin.parentUuid)?.querySelector('.tab-group-container')?.lastChild || null,
- });
- gZenFolders.setFolderUserIcon(group, pin.folderIcon);
- groups.set(pin.uuid, group);
- continue;
- }
-
- let params = {
- skipAnimation: true,
- allowInheritPrincipal: false,
- skipBackgroundNotify: true,
- userContextId: pin.containerTabId || 0,
- createLazyBrowser: true,
- skipLoad: true,
- noInitialLabel: false,
- };
-
- // Create and initialize the tab
- let newTab = gBrowser.addTrustedTab(pin.url, params);
- newTab.setAttribute('zenDefaultUserContextId', true);
-
- // Set initial label/title
- if (pin.title) {
- gBrowser.setInitialTabTitle(newTab, pin.title);
- }
-
- // Set the icon if we have it cached
- if (pin.iconUrl) {
- gBrowser.setIcon(newTab, pin.iconUrl);
- }
-
- newTab.setAttribute('zen-pin-id', pin.uuid);
-
- if (pin.workspaceUuid) {
- newTab.setAttribute('zen-workspace-id', pin.workspaceUuid);
- }
-
- if (pin.isEssential) {
- newTab.setAttribute('zen-essential', 'true');
- }
-
- if (pin.editedTitle) {
- newTab.setAttribute('zen-has-static-label', 'true');
- }
-
- // Initialize browser state if needed
- if (!newTab.linkedBrowser._remoteAutoRemoved) {
- let state = {
- entries: [
- {
- url: pin.url,
- title: pin.title,
- triggeringPrincipal_base64: E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
- },
- ],
- userContextId: pin.containerTabId || 0,
- image: pin.iconUrl,
- };
-
- SessionStore.setTabState(newTab, state);
- }
-
- this.log(`Created new pinned tab for pin ${pin.uuid} (isEssential: ${pin.isEssential})`);
- gBrowser.pinTab(newTab);
-
- if (pin.parentUuid) {
- const parentGroup = groups.get(pin.parentUuid);
- if (parentGroup) {
- parentGroup.querySelector('.tab-group-container').appendChild(newTab);
- } else {
- if (pendingTabsInsideGroups[pin.parentUuid]) {
- pendingTabsInsideGroups[pin.parentUuid].push(newTab);
- } else {
- pendingTabsInsideGroups[pin.parentUuid] = [newTab];
- }
- }
- } else {
- if (!pin.isEssential) {
- const container = gZenWorkspaces.workspaceElement(
- pin.workspaceUuid
- )?.pinnedTabsContainer;
- if (container) {
- container.insertBefore(newTab, container.lastChild);
- }
- } else {
- gZenWorkspaces.getEssentialsSection(pin.containerTabId).appendChild(newTab);
- }
- }
-
- gBrowser.tabContainer._invalidateCachedTabs();
- newTab.initialize();
- } catch (ex) {
- console.error('Failed to initialize pinned tabs:', ex);
- }
- }
-
- setTimeout(() => {
- this.#finishedInitializingPins();
- }, 0);
-
- gBrowser._updateTabBarForPinnedTabs();
- gZenUIManager.updateTabsToolbar();
- }
-
_onPinnedTabEvent(action, event) {
if (!this.enabled) return;
const tab = event.target;
@@ -413,238 +133,22 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
switch (action) {
case 'TabPinned':
- case 'TabAddedToEssentials':
tab._zenClickEventListener = this._zenClickEventListener;
tab.addEventListener('click', tab._zenClickEventListener);
- this._setPinnedAttributes(tab);
break;
- case 'TabRemovedFromEssentials':
- if (tab.pinned) {
- this.#onTabMove(tab);
- break;
- }
// [Fall through]
case 'TabUnpinned':
- this._removePinnedAttributes(tab);
if (tab._zenClickEventListener) {
tab.removeEventListener('click', tab._zenClickEventListener);
delete tab._zenClickEventListener;
}
break;
- case 'TabMove':
- this.#onTabMove(tab);
- break;
- case 'TabGroupCreate':
- this.#onTabGroupCreate(event);
- break;
- case 'TabGroupRemoved':
- this.#onTabGroupRemoved(event);
- break;
- case 'TabGroupMoved':
- this.#onTabGroupMoved(event);
- break;
- case 'ZenFolderRenamed':
- case 'ZenFolderIconChanged':
- case 'TabGroupCollapse':
- case 'TabGroupExpand':
- case 'ZenFolderChangedWorkspace':
- this.#updateGroupInfo(event.originalTarget, action);
- break;
- case 'TabGrouped':
- this.#onTabGrouped(event);
- break;
- case 'TabUngrouped':
- this.#onTabUngrouped(event);
- break;
default:
console.warn('ZenPinnedTabManager: Unhandled tab event', action);
break;
}
}
- async #onTabGroupCreate(event) {
- const group = event.originalTarget;
- if (!group.isZenFolder) {
- return;
- }
- if (group.hasAttribute('zen-pin-id')) {
- return; // Group already exists in storage
- }
- const workspaceId = group.getAttribute('zen-workspace-id');
- let id = await ZenPinnedTabsStorage.createGroup(
- group.name,
- group.iconURL,
- group.collapsed,
- workspaceId,
- group.getAttribute('zen-pin-id'),
- group._pPos
- );
- group.setAttribute('zen-pin-id', id);
- for (const tab of group.tabs) {
- // Only add it if the tab is directly under the group
- if (
- tab.pinned &&
- tab.hasAttribute('zen-pin-id') &&
- tab.group === group &&
- this.hasInitializedPins
- ) {
- const tabPinId = tab.getAttribute('zen-pin-id');
- await ZenPinnedTabsStorage.addTabToGroup(tabPinId, id, /* position */ tab._pPos);
- }
- }
- await this.refreshPinnedTabs();
- }
-
- async #onTabGrouped(event) {
- const tab = event.detail;
- const group = tab.group;
- if (!group.isZenFolder) {
- return;
- }
- const pinId = group.getAttribute('zen-pin-id');
- const tabPinId = tab.getAttribute('zen-pin-id');
- const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
- if (!tabPin || !tabPin.group) {
- return;
- }
- ZenPinnedTabsStorage.addTabToGroup(tabPinId, pinId, /* position */ tab._pPos);
- }
-
- async #onTabUngrouped(event) {
- const tab = event.detail;
- const group = tab.group;
- if (!group?.isZenFolder) {
- return;
- }
- const tabPinId = tab.getAttribute('zen-pin-id');
- const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
- if (!tabPin) {
- return;
- }
- ZenPinnedTabsStorage.removeTabFromGroup(tabPinId, /* position */ tab._pPos);
- }
-
- async #updateGroupInfo(group, action) {
- if (!group?.isZenFolder) {
- return;
- }
- const pinId = group.getAttribute('zen-pin-id');
- const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
- if (groupPin) {
- groupPin.title = group.name;
- groupPin.folderIcon = group.iconURL;
- groupPin.isFolderCollapsed = group.collapsed;
- groupPin.position = group._pPos;
- groupPin.parentUuid = group.group?.getAttribute('zen-pin-id') || null;
- groupPin.workspaceUuid = group.getAttribute('zen-workspace-id') || null;
- await this.savePin(groupPin);
- switch (action) {
- case 'ZenFolderRenamed':
- case 'ZenFolderIconChanged':
- case 'TabGroupCollapse':
- case 'TabGroupExpand':
- break;
- default:
- for (const item of group.allItems) {
- if (gBrowser.isTabGroup(item)) {
- await this.#updateGroupInfo(item, action);
- } else {
- await this.#onTabMove(item);
- }
- }
- }
- }
- }
-
- async #onTabGroupRemoved(event) {
- const group = event.originalTarget;
- if (!group.isZenFolder) {
- return;
- }
- await ZenPinnedTabsStorage.removePin(group.getAttribute('zen-pin-id'));
- group.removeAttribute('zen-pin-id');
- }
-
- async #onTabGroupMoved(event) {
- const group = event.originalTarget;
- if (!group.isZenFolder) {
- return;
- }
- const newIndex = group._pPos;
- const pinId = group.getAttribute('zen-pin-id');
- if (!pinId) {
- return;
- }
- for (const tab of group.allItemsRecursive) {
- if (tab.pinned && tab.getAttribute('zen-pin-id') === pinId) {
- const pin = this._pinsCache.find((p) => p.uuid === pinId);
- if (pin) {
- pin.position = tab._pPos;
- pin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
- pin.workspaceUuid = group.getAttribute('zen-workspace-id');
- await this.savePin(pin, false);
- }
- break;
- }
- }
- const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
- if (groupPin) {
- groupPin.position = newIndex;
- groupPin.parentUuid = group.group?.getAttribute('zen-pin-id');
- groupPin.workspaceUuid = group.getAttribute('zen-workspace-id');
- await this.savePin(groupPin);
- }
- }
-
- async #onTabMove(tab) {
- if (!tab.pinned || !this._pinsCache) {
- return;
- }
-
- const allTabs = [...gBrowser.tabs, ...gBrowser.tabGroups];
- for (let i = 0; i < allTabs.length; i++) {
- const otherTab = allTabs[i];
- if (
- otherTab.pinned &&
- otherTab.getAttribute('zen-pin-id') !== tab.getAttribute('zen-pin-id')
- ) {
- const actualPin = this._pinsCache.find(
- (pin) => pin.uuid === otherTab.getAttribute('zen-pin-id')
- );
- if (!actualPin) {
- continue;
- }
- actualPin.position = otherTab._pPos;
- actualPin.workspaceUuid = otherTab.getAttribute('zen-workspace-id');
- actualPin.parentUuid = otherTab.group?.getAttribute('zen-pin-id') || null;
- await this.savePin(actualPin, false);
- }
- }
-
- const actualPin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
-
- if (!actualPin) {
- return;
- }
- actualPin.position = tab._pPos;
- actualPin.isEssential = tab.hasAttribute('zen-essential');
- actualPin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
- actualPin.workspaceUuid = tab.getAttribute('zen-workspace-id') || null;
-
- // There was a bug where the title and hasStaticLabel attribute were not being set
- // This is a workaround to fix that
- if (tab.hasAttribute('zen-has-static-label')) {
- actualPin.editedTitle = true;
- actualPin.title = tab.label;
- }
- await this.savePin(actualPin);
- tab.dispatchEvent(
- new CustomEvent('ZenPinnedTabMoved', {
- detail: { tab },
- })
- );
- }
-
async _onTabClick(e) {
const tab = e.target?.closest('tab');
if (e.button === 1 && tab) {
@@ -672,106 +176,10 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return;
}
- const browser = tab.linkedBrowser;
-
- const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
-
- if (!pin) {
- return;
- }
-
- const userContextId = tab.getAttribute('usercontextid');
-
- pin.title = tab.label || browser.contentTitle;
- pin.url = browser.currentURI.spec;
- pin.workspaceUuid = tab.getAttribute('zen-workspace-id');
- pin.userContextId = userContextId ? parseInt(userContextId, 10) : 0;
-
- await this.savePin(pin);
this.resetPinChangedUrl(tab);
- await this.refreshPinnedTabs();
gZenUIManager.showToast('zen-pinned-tab-replaced');
}
- async _setPinnedAttributes(tab) {
- if (
- tab.hasAttribute('zen-pin-id') ||
- !this._hasFinishedLoading ||
- tab.hasAttribute('zen-empty-tab')
- ) {
- return;
- }
-
- this.log(`Setting pinned attributes for tab ${tab.linkedBrowser.currentURI.spec}`);
- const browser = tab.linkedBrowser;
-
- const uuid = gZenUIManager.generateUuidv4();
- const userContextId = tab.getAttribute('usercontextid');
-
- let entry = null;
-
- if (tab.getAttribute('zen-pinned-entry')) {
- entry = JSON.parse(tab.getAttribute('zen-pinned-entry'));
- }
-
- await this.savePin({
- uuid,
- title: entry?.title || tab.label || browser.contentTitle,
- url: entry?.url || browser.currentURI.spec,
- containerTabId: userContextId ? parseInt(userContextId, 10) : 0,
- workspaceUuid: tab.getAttribute('zen-workspace-id'),
- isEssential: tab.getAttribute('zen-essential') === 'true',
- parentUuid: tab.group?.getAttribute('zen-pin-id') || null,
- position: tab._pPos,
- });
-
- tab.setAttribute('zen-pin-id', uuid);
- tab.dispatchEvent(
- new CustomEvent('ZenPinnedTabCreated', {
- detail: { tab },
- })
- );
-
- // This is used while migrating old pins to new system - we don't want to refresh when migrating
- if (tab.getAttribute('zen-pinned-entry')) {
- tab.removeAttribute('zen-pinned-entry');
- return;
- }
- this.onLocationChange(browser);
- await this.refreshPinnedTabs();
- }
-
- async _removePinnedAttributes(tab, isClosing = false) {
- tab.removeAttribute('zen-has-static-label');
- if (!tab.getAttribute('zen-pin-id') || this._temporarilyUnpiningEssential) {
- return;
- }
-
- if (Services.startup.shuttingDown || window.skipNextCanClose) {
- return;
- }
-
- this.log(`Removing pinned attributes for tab ${tab.getAttribute('zen-pin-id')}`);
- await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
- this.resetPinChangedUrl(tab);
-
- if (!isClosing) {
- tab.removeAttribute('zen-pin-id');
- tab.removeAttribute('zen-essential'); // Just in case
-
- if (!tab.hasAttribute('zen-workspace-id') && gZenWorkspaces.workspaceEnabled) {
- const workspace = await gZenWorkspaces.getActiveWorkspace();
- tab.setAttribute('zen-workspace-id', workspace.uuid);
- }
- }
- await this.refreshPinnedTabs();
- tab.dispatchEvent(
- new CustomEvent('ZenPinnedTabRemoved', {
- detail: { tab },
- })
- );
- }
-
_initClosePinnedTabShortcut() {
let cmdClose = document.getElementById('cmd_close');
@@ -780,21 +188,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
- async savePin(pin, notifyObservers = true) {
- if (!this.hasInitializedPins && !gZenUIManager.testingEnabled) {
- return;
- }
- const existingPin = this._pinsCache.find((p) => p.uuid === pin.uuid);
- if (existingPin) {
- Object.assign(existingPin, pin);
- } else {
- // We shouldn't need it, but just in case there's
- // a race condition while making new pinned tabs.
- this._pinsCache.push(pin);
- }
- await ZenPinnedTabsStorage.savePin(pin, notifyObservers);
- }
-
async onCloseTabShortcut(
event,
selectedTab = gBrowser.selectedTab,
@@ -841,7 +234,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
switch (behavior) {
case 'close': {
for (const tab of pinnedTabs) {
- this._removePinnedAttributes(tab, true);
gBrowser.removeTab(tab, { animate: true });
}
break;
@@ -1017,12 +409,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
tab.removeAttribute('zen-workspace-id');
}
if (tab.pinned && tab.hasAttribute('zen-pin-id')) {
- const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
- if (pin) {
- pin.isEssential = true;
- pin.workspaceUuid = null;
- this.savePin(pin);
- }
gBrowser.zenHandleTabMove(tab, () => {
if (tab.ownerGlobal !== window) {
tab = gBrowser.adoptTab(tab, {
@@ -1415,11 +801,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true';
}
- async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = true) {
+ async updatePinTitle(tab, newTitle, isEdited = true) {
const uuid = tab.getAttribute('zen-pin-id');
- await ZenPinnedTabsStorage.updatePinTitle(uuid, newTitle, isEdited, notifyObservers);
-
- await this.refreshPinnedTabs();
const browsers = Services.wm.getEnumerator('navigator:browser');
@@ -1564,6 +947,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
async onTabLabelChanged(tab) {
+ tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { bubbles: true, detail: { tab } }));
if (!this._pinsCache) {
return;
}
diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs
deleted file mode 100644
index a407dad353..0000000000
--- a/src/zen/tabs/ZenPinnedTabsStorage.mjs
+++ /dev/null
@@ -1,660 +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/.
-
-window.ZenPinnedTabsStorage = {
- _saveCache: [],
-
- async init() {
- await this._ensureTable();
- },
-
- async _ensureTable() {
- await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => {
- // Create the pins table if it doesn't exist
- await db.execute(`
- CREATE TABLE IF NOT EXISTS zen_pins (
- id INTEGER PRIMARY KEY,
- uuid TEXT UNIQUE NOT NULL,
- title TEXT NOT NULL,
- url TEXT,
- container_id INTEGER,
- workspace_uuid TEXT,
- position INTEGER NOT NULL DEFAULT 0,
- is_essential BOOLEAN NOT NULL DEFAULT 0,
- is_group BOOLEAN NOT NULL DEFAULT 0,
- created_at INTEGER NOT NULL,
- updated_at INTEGER NOT NULL
- )
- `);
-
- const columns = await db.execute(`PRAGMA table_info(zen_pins)`);
- const columnNames = columns.map((row) => row.getResultByName('name'));
-
- // Helper function to add column if it doesn't exist
- const addColumnIfNotExists = async (columnName, definition) => {
- if (!columnNames.includes(columnName)) {
- await db.execute(`ALTER TABLE zen_pins ADD COLUMN ${columnName} ${definition}`);
- }
- };
-
- await addColumnIfNotExists('edited_title', 'BOOLEAN NOT NULL DEFAULT 0');
- await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0');
- await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL');
- await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL');
-
- await db.execute(`
- CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid)
- `);
-
- await db.execute(`
- CREATE TABLE IF NOT EXISTS zen_pins_changes (
- uuid TEXT PRIMARY KEY,
- timestamp INTEGER NOT NULL
- )
- `);
-
- await db.execute(`
- CREATE INDEX IF NOT EXISTS idx_zen_pins_changes_uuid ON zen_pins_changes(uuid)
- `);
-
- this._resolveInitialized();
- });
- },
-
- /**
- * Private helper method to notify observers with a list of changed UUIDs.
- * @param {string} event - The observer event name.
- * @param {Array} uuids - Array of changed workspace UUIDs.
- */
- _notifyPinsChanged(event, uuids) {
- if (uuids.length === 0) return; // No changes to notify
-
- // Convert the array of UUIDs to a JSON string
- const data = JSON.stringify(uuids);
-
- Services.obs.notifyObservers(null, event, data);
- },
-
- async savePin(pin, notifyObservers = true) {
- // If we find the exact same pin in the cache, skip saving
- const existingIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === pin.uuid);
- const copy = { ...pin };
- if (existingIndex !== -1) {
- const existingPin = this._saveCache[existingIndex];
- const isSame = Object.keys(pin).every((key) => pin[key] === existingPin[key]);
- if (isSame) {
- return; // No changes, skip saving
- } else {
- // Update the cached pin
- this._saveCache[existingIndex] = { ...copy };
- }
- } else {
- // Add to cache
- this._saveCache.push(copy);
- }
-
- const changedUUIDs = new Set();
-
- await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => {
- await db.executeTransaction(async () => {
- const now = Date.now();
-
- let newPosition;
- if ('position' in pin && Number.isFinite(pin.position)) {
- newPosition = pin.position;
- } else {
- // Get the maximum position within the same parent group (or null for root level)
- const maxPositionResult = await db.execute(
- `
- SELECT MAX("position") as max_position
- FROM zen_pins
- WHERE COALESCE(folder_parent_uuid, '') = COALESCE(:folder_parent_uuid, '')
- `,
- { folder_parent_uuid: pin.parentUuid || null }
- );
- const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
- newPosition = maxPosition + 1000;
- }
-
- // Insert or replace the pin
- await db.executeCached(
- `
- INSERT OR REPLACE INTO zen_pins (
- uuid, title, url, container_id, workspace_uuid, position,
- is_essential, is_group, folder_parent_uuid, edited_title, created_at,
- updated_at, is_folder_collapsed, folder_icon
- ) VALUES (
- :uuid, :title, :url, :container_id, :workspace_uuid, :position,
- :is_essential, :is_group, :folder_parent_uuid, :edited_title,
- COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now),
- :now, :is_folder_collapsed, :folder_icon
- )
- `,
- {
- uuid: pin.uuid,
- title: pin.title,
- url: pin.isGroup ? '' : pin.url,
- container_id: pin.containerTabId || null,
- workspace_uuid: pin.workspaceUuid || null,
- position: newPosition,
- is_essential: pin.isEssential || false,
- is_group: pin.isGroup || false,
- folder_parent_uuid: pin.parentUuid || null,
- edited_title: pin.editedTitle || false,
- now,
- folder_icon: pin.folderIcon || null,
- is_folder_collapsed: pin.isFolderCollapsed || false,
- }
- );
-
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid: pin.uuid,
- timestamp: Math.floor(now / 1000),
- }
- );
-
- changedUUIDs.add(pin.uuid);
- await this.updateLastChangeTimestamp(db);
- });
- });
-
- if (notifyObservers) {
- this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
- }
- },
-
- async getPins() {
- const db = await PlacesUtils.promiseDBConnection();
- const rows = await db.executeCached(`
- SELECT * FROM zen_pins
- ORDER BY position ASC
- `);
- return rows.map((row) => ({
- uuid: row.getResultByName('uuid'),
- title: row.getResultByName('title'),
- url: row.getResultByName('url'),
- containerTabId: row.getResultByName('container_id'),
- workspaceUuid: row.getResultByName('workspace_uuid'),
- position: row.getResultByName('position'),
- isEssential: Boolean(row.getResultByName('is_essential')),
- isGroup: Boolean(row.getResultByName('is_group')),
- parentUuid: row.getResultByName('folder_parent_uuid'),
- editedTitle: Boolean(row.getResultByName('edited_title')),
- folderIcon: row.getResultByName('folder_icon'),
- isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')),
- }));
- },
-
- /**
- * Create a new group
- * @param {string} title - The title of the group
- * @param {string} workspaceUuid - The workspace UUID (optional)
- * @param {string} parentUuid - The parent group UUID (optional, null for root level)
- * @param {number} position - The position of the group (optional, will auto-calculate if not provided)
- * @param {boolean} notifyObservers - Whether to notify observers (default: true)
- * @returns {Promise} The UUID of the created group
- */
- async createGroup(
- title,
- icon = null,
- isCollapsed = false,
- workspaceUuid = null,
- parentUuid = null,
- position = null,
- notifyObservers = true
- ) {
- if (!title || typeof title !== 'string') {
- throw new Error('Group title is required and must be a string');
- }
-
- const groupUuid = gZenUIManager.generateUuidv4();
-
- const groupPin = {
- uuid: groupUuid,
- title,
- folderIcon: icon || null,
- isFolderCollapsed: isCollapsed || false,
- workspaceUuid,
- parentUuid,
- position,
- isGroup: true,
- isEssential: false,
- editedTitle: true, // Group titles are always considered edited
- };
-
- await this.savePin(groupPin, notifyObservers);
- return groupUuid;
- },
-
- /**
- * Add an existing tab/pin to a group
- * @param {string} tabUuid - The UUID of the tab to add to the group
- * @param {string} groupUuid - The UUID of the target group
- * @param {number} position - The position within the group (optional, will append if not provided)
- * @param {boolean} notifyObservers - Whether to notify observers (default: true)
- */
- async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) {
- if (!tabUuid || !groupUuid) {
- throw new Error('Both tabUuid and groupUuid are required');
- }
-
- const changedUUIDs = new Set();
-
- await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => {
- await db.executeTransaction(async () => {
- // Verify the group exists and is actually a group
- const groupCheck = await db.execute(
- `SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`,
- { groupUuid }
- );
-
- if (groupCheck.length === 0) {
- throw new Error(`Group with UUID ${groupUuid} does not exist`);
- }
-
- if (!groupCheck[0].getResultByName('is_group')) {
- throw new Error(`Pin with UUID ${groupUuid} is not a group`);
- }
-
- const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, {
- tabUuid,
- });
-
- if (tabCheck.length === 0) {
- throw new Error(`Tab with UUID ${tabUuid} does not exist`);
- }
-
- const now = Date.now();
- let newPosition;
-
- if (position !== null && Number.isFinite(position)) {
- newPosition = position;
- } else {
- // Get the maximum position within the group
- const maxPositionResult = await db.execute(
- `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`,
- { groupUuid }
- );
- const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
- newPosition = maxPosition + 1000;
- }
-
- await db.execute(
- `
- UPDATE zen_pins
- SET folder_parent_uuid = :groupUuid,
- position = :newPosition,
- updated_at = :now
- WHERE uuid = :tabUuid
- `,
- {
- tabUuid,
- groupUuid,
- newPosition,
- now,
- }
- );
-
- changedUUIDs.add(tabUuid);
-
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid: tabUuid,
- timestamp: Math.floor(now / 1000),
- }
- );
-
- await this.updateLastChangeTimestamp(db);
- });
- });
-
- if (notifyObservers) {
- this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
- }
- },
-
- /**
- * Remove a tab from its group (move to root level)
- * @param {string} tabUuid - The UUID of the tab to remove from its group
- * @param {number} newPosition - The new position at root level (optional, will append if not provided)
- * @param {boolean} notifyObservers - Whether to notify observers (default: true)
- */
- async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) {
- if (!tabUuid) {
- throw new Error('tabUuid is required');
- }
-
- const changedUUIDs = new Set();
-
- await PlacesUtils.withConnectionWrapper(
- 'ZenPinnedTabsStorage.removeTabFromGroup',
- async (db) => {
- await db.executeTransaction(async () => {
- // Verify the tab exists and is in a group
- const tabCheck = await db.execute(
- `SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`,
- { tabUuid }
- );
-
- if (tabCheck.length === 0) {
- throw new Error(`Tab with UUID ${tabUuid} does not exist`);
- }
-
- if (!tabCheck[0].getResultByName('folder_parent_uuid')) {
- return;
- }
-
- const now = Date.now();
- let finalPosition;
-
- if (newPosition !== null && Number.isFinite(newPosition)) {
- finalPosition = newPosition;
- } else {
- // Get the maximum position at root level (where folder_parent_uuid is null)
- const maxPositionResult = await db.execute(
- `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL`
- );
- const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
- finalPosition = maxPosition + 1000;
- }
-
- // Update the tab to be at root level
- await db.execute(
- `
- UPDATE zen_pins
- SET folder_parent_uuid = NULL,
- position = :newPosition,
- updated_at = :now
- WHERE uuid = :tabUuid
- `,
- {
- tabUuid,
- newPosition: finalPosition,
- now,
- }
- );
-
- changedUUIDs.add(tabUuid);
-
- // Record the change
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid: tabUuid,
- timestamp: Math.floor(now / 1000),
- }
- );
-
- await this.updateLastChangeTimestamp(db);
- });
- }
- );
-
- if (notifyObservers) {
- this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
- }
- },
-
- async removePin(uuid, notifyObservers = true) {
- const cachedIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === uuid);
- if (cachedIndex !== -1) {
- this._saveCache.splice(cachedIndex, 1);
- }
-
- const changedUUIDs = [uuid];
-
- await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => {
- await db.executeTransaction(async () => {
- // Get all child UUIDs first for change tracking
- const children = await db.execute(
- `SELECT uuid FROM zen_pins WHERE folder_parent_uuid = :uuid`,
- {
- uuid,
- }
- );
-
- // Add child UUIDs to changedUUIDs array
- for (const child of children) {
- changedUUIDs.push(child.getResultByName('uuid'));
- }
-
- // Delete the pin/group itself
- await db.execute(`DELETE FROM zen_pins WHERE uuid = :uuid`, { uuid });
-
- // Record the changes
- const now = Math.floor(Date.now() / 1000);
- for (const changedUuid of changedUUIDs) {
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid: changedUuid,
- timestamp: now,
- }
- );
- }
-
- await this.updateLastChangeTimestamp(db);
- });
- });
-
- if (notifyObservers) {
- this._notifyPinsChanged('zen-pin-removed', changedUUIDs);
- }
- },
-
- async wipeAllPins() {
- await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => {
- await db.execute(`DELETE FROM zen_pins`);
- await db.execute(`DELETE FROM zen_pins_changes`);
- await this.updateLastChangeTimestamp(db);
- });
- },
-
- async markChanged(uuid) {
- await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => {
- const now = Date.now();
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid,
- timestamp: Math.floor(now / 1000),
- }
- );
- });
- },
-
- async getChangedIDs() {
- const db = await PlacesUtils.promiseDBConnection();
- const rows = await db.execute(`
- SELECT uuid, timestamp FROM zen_pins_changes
- `);
- const changes = {};
- for (const row of rows) {
- changes[row.getResultByName('uuid')] = row.getResultByName('timestamp');
- }
- return changes;
- },
-
- async clearChangedIDs() {
- await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => {
- await db.execute(`DELETE FROM zen_pins_changes`);
- });
- },
-
- shouldReorderPins(before, current, after) {
- const minGap = 1; // Minimum allowed gap between positions
- return (
- (before !== null && current - before < minGap) || (after !== null && after - current < minGap)
- );
- },
-
- async reorderAllPins(db, changedUUIDs) {
- const pins = await db.execute(`
- SELECT uuid
- FROM zen_pins
- ORDER BY position ASC
- `);
-
- for (let i = 0; i < pins.length; i++) {
- const newPosition = (i + 1) * 1000; // Use large increments
- await db.execute(
- `
- UPDATE zen_pins
- SET position = :newPosition
- WHERE uuid = :uuid
- `,
- { newPosition, uuid: pins[i].getResultByName('uuid') }
- );
- changedUUIDs.add(pins[i].getResultByName('uuid'));
- }
- },
-
- async updateLastChangeTimestamp(db) {
- const now = Date.now();
- await db.execute(
- `
- INSERT OR REPLACE INTO moz_meta (key, value)
- VALUES ('zen_pins_last_change', :now)
- `,
- { now }
- );
- },
-
- async getLastChangeTimestamp() {
- const db = await PlacesUtils.promiseDBConnection();
- const result = await db.executeCached(`
- SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change'
- `);
- return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0;
- },
-
- async updatePinPositions(pins) {
- const changedUUIDs = new Set();
-
- await PlacesUtils.withConnectionWrapper(
- 'ZenPinnedTabsStorage.updatePinPositions',
- async (db) => {
- await db.executeTransaction(async () => {
- const now = Date.now();
-
- for (let i = 0; i < pins.length; i++) {
- const pin = pins[i];
- const newPosition = (i + 1) * 1000;
-
- await db.execute(
- `
- UPDATE zen_pins
- SET position = :newPosition
- WHERE uuid = :uuid
- `,
- { newPosition, uuid: pin.uuid }
- );
-
- changedUUIDs.add(pin.uuid);
-
- // Record the change
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid: pin.uuid,
- timestamp: Math.floor(now / 1000),
- }
- );
- }
-
- await this.updateLastChangeTimestamp(db);
- });
- }
- );
-
- this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
- },
-
- async updatePinTitle(uuid, newTitle, isEdited = true, notifyObservers = true) {
- if (!uuid || typeof newTitle !== 'string') {
- throw new Error('Invalid parameters: uuid and newTitle are required');
- }
-
- const changedUUIDs = new Set();
-
- await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinTitle', async (db) => {
- await db.executeTransaction(async () => {
- const now = Date.now();
-
- // Update the pin's title and edited_title flag
- const result = await db.execute(
- `
- UPDATE zen_pins
- SET title = :newTitle,
- edited_title = :isEdited,
- updated_at = :now
- WHERE uuid = :uuid
- `,
- {
- uuid,
- newTitle,
- isEdited,
- now,
- }
- );
-
- // Only proceed with change tracking if a row was actually updated
- if (result.rowsAffected > 0) {
- changedUUIDs.add(uuid);
-
- // Record the change
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid,
- timestamp: Math.floor(now / 1000),
- }
- );
-
- await this.updateLastChangeTimestamp(db);
- }
- });
- });
-
- if (notifyObservers && changedUUIDs.size > 0) {
- this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
- }
- },
-
- async __dropTables() {
- await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.__dropTables', async (db) => {
- await db.execute(`DROP TABLE IF EXISTS zen_pins`);
- await db.execute(`DROP TABLE IF EXISTS zen_pins_changes`);
- });
- },
-};
-
-ZenPinnedTabsStorage.promiseInitialized = new Promise((resolve) => {
- ZenPinnedTabsStorage._resolveInitialized = resolve;
- ZenPinnedTabsStorage.init();
-});
diff --git a/src/zen/tabs/jar.inc.mn b/src/zen/tabs/jar.inc.mn
index 2acab4816b..192d0d33d2 100644
--- a/src/zen/tabs/jar.inc.mn
+++ b/src/zen/tabs/jar.inc.mn
@@ -2,7 +2,6 @@
# 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/.
- content/browser/zen-components/ZenPinnedTabsStorage.mjs (../../zen/tabs/ZenPinnedTabsStorage.mjs)
content/browser/zen-components/ZenPinnedTabManager.mjs (../../zen/tabs/ZenPinnedTabManager.mjs)
* content/browser/zen-styles/zen-tabs.css (../../zen/tabs/zen-tabs.css)
content/browser/zen-styles/zen-tabs/vertical-tabs.css (../../zen/tabs/zen-tabs/vertical-tabs.css)
\ No newline at end of file
diff --git a/src/zen/workspaces/ZenWorkspace.mjs b/src/zen/workspaces/ZenWorkspace.mjs
index ce6baa122b..05078a0b01 100644
--- a/src/zen/workspaces/ZenWorkspace.mjs
+++ b/src/zen/workspaces/ZenWorkspace.mjs
@@ -37,6 +37,14 @@ class nsZenWorkspace extends MozXULElement {
`;
}
+ static get moveTabToButtonMarkup() {
+ return `
+
+ `;
+ }
+
static get inheritedAttributes() {
return {
'.zen-workspace-tabs-section': 'zen-workspace-id=id',
@@ -88,6 +96,20 @@ class nsZenWorkspace extends MozXULElement {
gZenWorkspaces.changeWorkspaceIcon();
});
+ if (!gZenWorkspaces.currentWindowIsSyncing) {
+ let actionsButton = this.indicator.querySelector('.zen-workspaces-actions');
+ const moveTabToFragment = window.MozXULElement.parseXULToFragment(
+ nsZenWorkspace.moveTabToButtonMarkup
+ );
+ actionsButton.after(moveTabToFragment);
+ actionsButton.setAttribute('hidden', 'true');
+ actionsButton = actionsButton.nextElementSibling;
+ actionsButton.addEventListener('command', (event) => {
+ event.stopPropagation();
+ this.#openMoveTabsToWorkspacePanel(event.target);
+ });
+ }
+
this.scrollbox._getScrollableElements = () => {
const children = [...this.pinnedTabsContainer.children, ...this.tabsContainer.children];
if (Services.prefs.getBoolPref('zen.view.show-newtab-button-top', false)) {
@@ -209,7 +231,7 @@ class nsZenWorkspace extends MozXULElement {
if (newName === '') {
return;
}
- let workspaces = (await gZenWorkspaces._workspaces()).workspaces;
+ let workspaces = (await gZenWorkspaces.getWorkspaces()).workspaces;
let workspaceData = workspaces.find((workspace) => workspace.uuid === this.workspaceUuid);
workspaceData.name = newName;
await gZenWorkspaces.saveWorkspace(workspaceData);
@@ -256,6 +278,37 @@ class nsZenWorkspace extends MozXULElement {
this.style.removeProperty('--toolbox-textcolor');
this.style.removeProperty('--zen-primary-color');
}
+
+ #openMoveTabsToWorkspacePanel(button) {
+ button = button.closest('toolbarbutton');
+ if (!button) return;
+
+ const popup = document.getElementById('zenMoveTabsToSyncedWorkspacePopup');
+ popup.innerHTML = '';
+
+ gZenWorkspaces.getWorkspaces(true).then((workspaces) => {
+ for (const workspace of workspaces.workspaces) {
+ const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace);
+ item.addEventListener('command', async () => {
+ const { ZenWindowSync } = ChromeUtils.importESModule(
+ 'resource:///modules/zen/ZenWindowSync.sys.mjs'
+ );
+ ZenWindowSync.moveTabsToSyncedWorkspace(window, workspace.uuid);
+ });
+ popup.appendChild(item);
+ }
+
+ button.setAttribute('open', 'true');
+ popup.addEventListener(
+ 'popuphidden',
+ () => {
+ button.removeAttribute('open');
+ },
+ { once: true }
+ );
+ popup.openPopup(button, 'after_start', 0, 0, true /* isContextMenu */);
+ });
+ }
}
customElements.define('zen-workspace', nsZenWorkspace);
diff --git a/src/zen/workspaces/ZenWorkspaceIcons.mjs b/src/zen/workspaces/ZenWorkspaceIcons.mjs
index f9186c43f2..d56f6ecea5 100644
--- a/src/zen/workspaces/ZenWorkspaceIcons.mjs
+++ b/src/zen/workspaces/ZenWorkspaceIcons.mjs
@@ -126,7 +126,7 @@ class nsZenWorkspaceIcons extends MozXULElement {
}
async #updateIcons() {
- const workspaces = await gZenWorkspaces._workspaces();
+ const workspaces = await gZenWorkspaces.getWorkspaces();
this.innerHTML = '';
for (const workspace of workspaces.workspaces) {
const button = this.#createWorkspaceIcon(workspace);
diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs
index 0816696b6e..e24ddcac60 100644
--- a/src/zen/workspaces/ZenWorkspaces.mjs
+++ b/src/zen/workspaces/ZenWorkspaces.mjs
@@ -128,7 +128,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
this.addPopupListeners();
await this.#waitForPromises();
- await this._workspaces();
+ await this.getWorkspaces();
await this.afterLoadInit();
}
@@ -318,7 +318,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async _createDefaultWorkspaceIfNeeded() {
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
if (!workspaces.workspaces.length) {
await this.createAndSaveWorkspace('Space', null, true);
this._workspaceCache = null;
@@ -413,7 +413,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
await new Promise((resolve) => {
setTimeout(async () => {
const tabs = gBrowser.tabContainer.allTabs;
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
for (const workspace of workspaces.workspaces) {
await this._createWorkspaceTabsSection(workspace, tabs);
}
@@ -835,8 +835,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return PrivateBrowsingUtils.isWindowPrivate(window);
}
+ get currentWindowIsSyncing() {
+ return !document.documentElement.hasAttribute('zen-unsynced-window') && !this.isPrivateWindow;
+ }
+
get privateWindowOrDisabled() {
- return this.isPrivateWindow || !this.shouldHaveWorkspaces;
+ return !this.shouldHaveWorkspaces || !this.currentWindowIsSyncing;
}
get workspaceEnabled() {
@@ -860,17 +864,17 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
}
- async _workspaces() {
- if (this._workspaceCache) {
+ async getWorkspaces(lieToMe = false) {
+ if (this._workspaceCache && !lieToMe) {
return this._workspaceCache;
}
- if (this.isPrivateWindow) {
+ if (!this.currentWindowIsSyncing && !lieToMe) {
this._workspaceCache = {
- workspaces: this._privateWorkspace ? [this._privateWorkspace] : [],
+ workspaces: this._tempWorkspace ? [this._tempWorkspace] : [],
lastChangeTimestamp: 0,
};
- this._activeWorkspace = this._privateWorkspace?.uuid;
+ this._activeWorkspace = this._tempWorkspace?.uuid;
return this._workspaceCache;
}
@@ -883,21 +887,23 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// Get the active workspace ID from preferences
const activeWorkspaceId = this.activeWorkspace;
- if (activeWorkspaceId) {
- const activeWorkspace = this.getWorkspaceFromId(activeWorkspaceId);
- // Set the active workspace ID to the first one if the one with selected id doesn't exist
- if (!activeWorkspace) {
+ if (!lieToMe) {
+ if (activeWorkspaceId) {
+ const activeWorkspace = this.getWorkspaceFromId(activeWorkspaceId);
+ // Set the active workspace ID to the first one if the one with selected id doesn't exist
+ if (!activeWorkspace) {
+ this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid;
+ }
+ } else {
+ // Set the active workspace ID to the first one if active workspace doesn't exist
this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid;
}
- } else {
- // Set the active workspace ID to the first one if active workspace doesn't exist
- this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid;
}
+
// sort by position
this._workspaceCache.workspaces.sort(
(a, b) => (a.position ?? Infinity) - (b.position ?? Infinity)
);
-
return this._workspaceCache;
}
@@ -928,6 +934,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
let activeWorkspace = await this.getActiveWorkspace();
this.activeWorkspace = activeWorkspace?.uuid;
await gZenSessionStore.promiseInitialized;
+ await window._zenPromiseNewWindowRestored;
+ delete window._zenPromiseNewWindowRestored;
try {
if (activeWorkspace) {
window.gZenThemePicker = new nsZenThemePicker();
@@ -939,7 +947,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
await this.workspaceBookmarks();
await this.initializeTabsStripSections();
this._initializeEmptyTab();
- await gZenPinnedTabManager.refreshPinnedTabs({ init: true });
await this.changeWorkspace(activeWorkspace, { onInit: true });
this.#fixTabPositions();
this.onWindowResize();
@@ -1112,13 +1119,15 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
shouldCloseWindow() {
return (
- !window.toolbar.visible || Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab')
+ !window.toolbar.visible ||
+ Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab') ||
+ this.privateWindowOrDisabled
);
}
async #clearAnyZombieTabs() {
const tabs = this.allStoredTabs;
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
for (let tab of tabs) {
const workspaceID = tab.getAttribute('zen-workspace-id');
if (
@@ -1206,6 +1215,24 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
);
}
+ generateMenuItemForWorkspace(workspace) {
+ const item = document.createXULElement('menuitem');
+ item.className = 'zen-workspace-context-menu-item';
+ item.setAttribute('zen-workspace-id', workspace.uuid);
+ item.setAttribute('disabled', workspace.uuid === this.activeWorkspace);
+ let name = workspace.name;
+ const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
+ if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
+ name = `${workspace.icon} ${name}`;
+ }
+ item.setAttribute('label', name);
+ if (iconIsSvg) {
+ item.setAttribute('image', workspace.icon);
+ item.classList.add('zen-workspace-context-icon');
+ }
+ return item;
+ }
+
#contextMenuData = null;
updateWorkspaceActionsMenu(event) {
if (event.target.id !== 'zenWorkspaceMoreActions') {
@@ -1249,20 +1276,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (!this.#contextMenuData.workspaceId) {
separator.hidden = false;
for (const workspace of [...this._workspaceCache.workspaces].reverse()) {
- const item = document.createXULElement('menuitem');
- item.className = 'zen-workspace-context-menu-item';
- item.setAttribute('zen-workspace-id', workspace.uuid);
- item.setAttribute('disabled', workspace.uuid === this.activeWorkspace);
- let name = workspace.name;
- const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
- if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
- name = `${workspace.icon} ${name}`;
- }
- item.setAttribute('label', name);
- if (iconIsSvg) {
- item.setAttribute('image', workspace.icon);
- item.classList.add('zen-workspace-context-icon');
- }
+ const item = this.generateMenuItemForWorkspace(workspace);
item.addEventListener('command', (e) => {
this.changeWorkspaceWithID(e.target.closest('menuitem').getAttribute('zen-workspace-id'));
});
@@ -1307,7 +1321,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async removeWorkspace(windowID) {
- let workspacesData = await this._workspaces();
+ let workspacesData = await this.getWorkspaces();
await this.changeWorkspace(
workspacesData.workspaces.find((workspace) => workspace.uuid !== windowID)
);
@@ -1330,7 +1344,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async getActiveWorkspace() {
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
return (
workspaces.workspaces.find((workspace) => workspace.uuid === this.activeWorkspace) ??
workspaces.workspaces[0]
@@ -1364,7 +1378,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
browser.gZenWorkspaces._workspaceCache = null;
browser.gZenWorkspaces._workspaceBookmarksCache = null;
}
- let workspaces = await browser.gZenWorkspaces._workspaces();
+ let workspaces = await browser.gZenWorkspaces.getWorkspaces();
browser.document
.getElementById('cmd_zenCtxDeleteWorkspace')
.setAttribute('disabled', workspaces.workspaces.length <= 1);
@@ -1389,7 +1403,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async _propagateWorkspaceData({ ignoreStrip = false, clearCache = true, onInit = false } = {}) {
- const currentWindowIsPrivate = this.isPrivateWindow;
+ const currentWindowIsPrivate = !this.currentWindowIsSyncing;
if (onInit) {
if (currentWindowIsPrivate) return;
return await this._propagateWorkspaceDataForWindow(this.ownerWindow, {
@@ -1402,7 +1416,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// For example, when the window is in private browsing mode.
if (
!browser.gZenWorkspaces.workspaceEnabled ||
- browser.gZenWorkspaces.isPrivateWindow !== currentWindowIsPrivate
+ !browser.gZenWorkspaces.currentWindowIsSyncing !== currentWindowIsPrivate
) {
return;
}
@@ -1417,7 +1431,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (this.privateWindowOrDisabled) {
return;
}
- const workspaces = (await this._workspaces()).workspaces;
+ const workspaces = (await this.getWorkspaces()).workspaces;
const workspace = workspaces.find((w) => w.uuid === id);
if (!workspace) {
console.warn(`Workspace with ID ${id} not found for reordering.`);
@@ -1443,7 +1457,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async moveWorkspace(draggedWorkspaceId, targetWorkspaceId) {
- const workspaces = (await this._workspaces()).workspaces;
+ const workspaces = (await this.getWorkspaces()).workspaces;
const draggedIndex = workspaces.findIndex((w) => w.uuid === draggedWorkspaceId);
const draggedWorkspace = workspaces.splice(draggedIndex, 1)[0];
const targetIndex = workspaces.findIndex((w) => w.uuid === targetWorkspaceId);
@@ -1478,11 +1492,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
!tab.hasAttribute('zen-empty-tab') &&
!tab.hasAttribute('zen-essential')
);
- for (const tab of tabs) {
- if (tab.pinned) {
- await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
- }
- }
gBrowser.removeTabs(tabs, {
animate: false,
skipSessionStore: true,
@@ -1661,7 +1670,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return;
}
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
gZenFolders.cancelPopupTimer();
// Refresh tab cache
@@ -1777,7 +1786,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return;
}
this._organizingWorkspaceStrip = true;
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
let workspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === workspace.uuid);
if (!justMove) {
this._fixIndicatorsNames(workspaces);
@@ -1911,7 +1920,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const kGlobalAnimationDuration = 0.2;
this._animatingChange = true;
const animations = [];
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
const newWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === newWorkspace.uuid);
const isGoingLeft = newWorkspaceIndex <= previousWorkspaceIndex;
const clonedEssentials = [];
@@ -2214,7 +2223,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
tab,
currentWorkspace.uuid,
currentWorkspace.containerTabId,
- await this._workspaces()
+ await this.getWorkspaces()
);
}
@@ -2273,7 +2282,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const lastSelectedTab = this._lastSelectedWorkspaceTabs[workspace.uuid];
const containerId = workspace.containerTabId?.toString();
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
// Save current tab as last selected for old workspace if it shouldn't be visible in new workspace
if (oldWorkspaceId && oldWorkspaceId !== workspace.uuid) {
@@ -2406,7 +2415,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async _updateWorkspacesChangeContextMenu() {
if (gZenWorkspaces.privateWindowOrDisabled) return;
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
const menuPopup = document.getElementById('context-zen-change-workspace-tab-menu-popup');
if (!menuPopup) {
@@ -2456,8 +2465,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (!this.workspaceEnabled) {
return;
}
- if (this.isPrivateWindow) {
- name = 'Private ' + name;
+ if (!this.currentWindowIsSyncing) {
+ name = this.isPrivateWindow ? 'Private ' + name : 'Temporary';
}
// get extra tabs remaning (e.g. on new profiles) and just move them to the new workspace
const extraTabs = Array.from(gBrowser.tabContainer.arrowScrollbox.children).filter(
@@ -2474,8 +2483,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
!dontChange,
containerTabId
);
- if (this.isPrivateWindow) {
- this._privateWorkspace = workspaceData;
+ if (!this.currentWindowIsSyncing) {
+ this._tempWorkspace = workspaceData;
} else {
await this.saveWorkspace(workspaceData, dontChange);
}
@@ -2710,7 +2719,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// Context menu management
async contextChangeContainerTab(event) {
this._organizingWorkspaceStrip = true;
- let workspaces = await this._workspaces();
+ let workspaces = await this.getWorkspaces();
let workspace = workspaces.workspaces.find(
(workspace) => workspace.uuid === (this.#contextMenuData?.workspaceId || this.activeWorkspace)
);
@@ -2764,7 +2773,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async changeWorkspaceShortcut(offset = 1, whileScrolling = false) {
// Cycle through workspaces
- let workspaces = await this._workspaces();
+ let workspaces = await this.getWorkspaces();
let activeWorkspace = await this.getActiveWorkspace();
let workspaceIndex = workspaces.workspaces.indexOf(activeWorkspace);
@@ -2828,7 +2837,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(
tabs[tabs.length - 1]
);
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
await this.changeWorkspace(
workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID)
);
@@ -2887,7 +2896,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async shortcutSwitchTo(index) {
- const workspaces = await this._workspaces();
+ const workspaces = await this.getWorkspaces();
// The index may be out of bounds, if it doesnt exist, don't do anything
if (index >= workspaces.workspaces.length || index < 0) {
return;
diff --git a/src/zen/workspaces/create-workspace-form.css b/src/zen/workspaces/create-workspace-form.css
index 33d79eca7b..35d14e0215 100644
--- a/src/zen/workspaces/create-workspace-form.css
+++ b/src/zen/workspaces/create-workspace-form.css
@@ -33,7 +33,7 @@ zen-workspace-creation {
width: calc(100% - 10px);
margin: auto;
gap: 3.2rem;
- margin-top: 1.2rem;
+ margin-top: 0.6rem;
height: 100%;
& .zen-workspace-creation-form {
diff --git a/src/zen/workspaces/zen-workspaces.css b/src/zen/workspaces/zen-workspaces.css
index 674a9cd44e..10da559c14 100644
--- a/src/zen/workspaces/zen-workspaces.css
+++ b/src/zen/workspaces/zen-workspaces.css
@@ -181,7 +181,11 @@
height: calc(100% - var(--zen-toolbox-padding) * 2);
}
- :root:not([zen-private-window]) & {
+ :root[zen-private-window] & {
+ pointer-events: none;
+ }
+
+ :root:not([zen-unsynced-window]) & {
&:hover,
&[open='true'] {
&::before {
@@ -228,15 +232,19 @@
font-weight: 600;
align-items: center;
margin: 0;
+
+ :root[zen-unsynced-window] & {
+ pointer-events: none;
+ }
}
.zen-workspaces-actions {
margin-left: auto !important;
- opacity: 0;
- visibility: collapse;
transition: opacity 0.1s;
order: 5;
--toolbarbutton-inner-padding: 6px !important;
+ opacity: 0;
+ visibility: collapse;
& image {
border-radius: max(calc(var(--border-radius-medium) - 4px), 4px) !important;
@@ -248,9 +256,24 @@
:root[zen-renaming-tab='true'] & {
display: none;
}
+
+ :root[zen-unsynced-window] & {
+ .toolbarbutton-text {
+ display: flex;
+ font-size: 10px;
+ min-height: 22px;
+ }
+
+ .toolbarbutton-icon {
+ display: none;
+ }
+ }
}
- :root:not([zen-private-window]) &:hover .zen-workspaces-actions,
+ :root[zen-unsynced-window='true']
+ #navigator-toolbox[zen-has-implicit-hover='true']
+ & .zen-workspaces-actions,
+ :root:not([zen-unsynced-window]) &:hover .zen-workspaces-actions,
& .zen-workspaces-actions[open='true'] {
visibility: visible;
pointer-events: auto;
diff --git a/src/zen/zen.globals.js b/src/zen/zen.globals.js
index 197089441a..06e07eaa47 100644
--- a/src/zen/zen.globals.js
+++ b/src/zen/zen.globals.js
@@ -26,7 +26,6 @@ export default [
'ZEN_KEYSET_ID',
'gZenPinnedTabManager',
- 'ZenPinnedTabsStorage',
'gZenEmojiPicker',
'gZenSessionStore',