diff --git a/prefs/browser.yaml b/prefs/browser.yaml
index 30aa603e9c..f76c33f5c8 100644
--- a/prefs/browser.yaml
+++ b/prefs/browser.yaml
@@ -3,7 +3,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
- name: browser.startup.page
- value: 3
+ value: 0
+ locked: true
- name: browser.sessionstore.restore_pinned_tabs_on_demand
value: true
diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml
index 91b136f2ec..0ae64eab21 100644
--- a/src/browser/base/content/zen-assets.inc.xhtml
+++ b/src/browser/base/content/zen-assets.inc.xhtml
@@ -58,3 +58,4 @@
+
diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn
index fc39083763..ab73371949 100644
--- a/src/browser/base/content/zen-assets.jar.inc.mn
+++ b/src/browser/base/content/zen-assets.jar.inc.mn
@@ -42,6 +42,7 @@
content/browser/zen-components/ZenWorkspaceIcons.mjs (../../zen/workspaces/ZenWorkspaceIcons.mjs)
content/browser/zen-components/ZenWorkspace.mjs (../../zen/workspaces/ZenWorkspace.mjs)
content/browser/zen-components/ZenWorkspaces.mjs (../../zen/workspaces/ZenWorkspaces.mjs)
+ content/browser/zen-components/ZenWindowSyncing.mjs (../../zen/workspaces/ZenWindowSyncing.mjs)
content/browser/zen-components/ZenWorkspaceCreation.mjs (../../zen/workspaces/ZenWorkspaceCreation.mjs)
content/browser/zen-components/ZenWorkspacesStorage.mjs (../../zen/workspaces/ZenWorkspacesStorage.mjs)
content/browser/zen-components/ZenWorkspacesSync.mjs (../../zen/workspaces/ZenWorkspacesSync.mjs)
diff --git a/src/zen/sessionstore/ZenSessionFile.sys.mjs b/src/zen/sessionstore/ZenSessionFile.sys.mjs
new file mode 100644
index 0000000000..c50960ae10
--- /dev/null
+++ b/src/zen/sessionstore/ZenSessionFile.sys.mjs
@@ -0,0 +1,27 @@
+// 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/.
+
+const FILE_NAME = 'zen-sessions.jsonlz4';
+
+export class nsZenSessionFile {
+ #path;
+
+ #windows;
+
+ constructor() {
+ this.#path = PathUtils.join(profileDir, FILE_NAME);
+ }
+
+ async read() {
+ try {
+ return await IOUtils.readJSON(this.#path, { compress: true });
+ } catch (e) {
+ return {};
+ }
+ }
+
+ async write(data) {
+ await IOUtils.writeJSON(this.#path, data, { compress: true });
+ }
+}
diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs
new file mode 100644
index 0000000000..68aa829e40
--- /dev/null
+++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs
@@ -0,0 +1,50 @@
+// 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 {
+ cancelIdleCallback,
+ clearTimeout,
+ requestIdleCallback,
+ setTimeout,
+} from 'resource://gre/modules/Timer.sys.mjs';
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ZenSessionFile: 'resource://gre/modules/ZenSessionFile.sys.mjs',
+ PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs',
+ RunState: 'resource:///modules/sessionstore/RunState.sys.mjs',
+});
+
+class nsZenSessionManager {
+ #file;
+
+ constructor() {
+ this.#file = null;
+ }
+
+ get file() {
+ if (!this.#file) {
+ this.#file = lazy.ZenSessionFile;
+ }
+ return this.#file;
+ }
+
+ /**
+ * Saves the current session state. Collects data and writes to disk.
+ *
+ * @param forceUpdateAllWindows (optional)
+ * Forces us to recollect data for all windows and will bypass and
+ * update the corresponding caches.
+ */
+ saveState(forceUpdateAllWindows = false) {
+ if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ // Don't save (or even collect) anything in permanent private
+ // browsing mode
+ return Promise.resolve();
+ }
+ }
+}
+
+export const ZenSessionStore = new nsZenSessionManager();
diff --git a/src/zen/sessionstore/ZenSessionWindow.sys.mjs b/src/zen/sessionstore/ZenSessionWindow.sys.mjs
new file mode 100644
index 0000000000..4600702349
--- /dev/null
+++ b/src/zen/sessionstore/ZenSessionWindow.sys.mjs
@@ -0,0 +1,35 @@
+// 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/.
+
+export class ZenSessionWindow {
+ #id;
+ #selectedWorkspace;
+ #selectedTab;
+
+ constructor(id) {
+ this.#id = id;
+ this.#selectedWorkspace = null;
+ this.#selectedTab = null;
+ }
+
+ get id() {
+ return this.#id;
+ }
+
+ get selectedWorkspace() {
+ return this.#selectedWorkspace;
+ }
+
+ set selectedWorkspace(workspace) {
+ this.#selectedWorkspace = workspace;
+ }
+
+ get selectedTab() {
+ return this.#selectedTab;
+ }
+
+ set selectedTab(tab) {
+ this.#selectedTab = tab;
+ }
+}
diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs
index f6e7b44f5f..4366090a2f 100644
--- a/src/zen/tabs/ZenPinnedTabManager.mjs
+++ b/src/zen/tabs/ZenPinnedTabManager.mjs
@@ -97,6 +97,7 @@
}
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 {
@@ -1511,6 +1512,7 @@
}
async onTabLabelChanged(tab) {
+ tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { detail: { tab } }));
if (!this._pinsCache) {
return;
}
diff --git a/src/zen/workspaces/ZenWindowSyncing.mjs b/src/zen/workspaces/ZenWindowSyncing.mjs
new file mode 100644
index 0000000000..857f9fc9cf
--- /dev/null
+++ b/src/zen/workspaces/ZenWindowSyncing.mjs
@@ -0,0 +1,308 @@
+// 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/.
+{
+ class nsZenWorkspaceWindowSync extends nsZenMultiWindowFeature {
+ #ignoreNextEvents = false;
+ #waitForPromise = null;
+
+ constructor() {
+ super();
+ if (!window.closed) {
+ this.init();
+ }
+ }
+
+ async init() {
+ await gZenWorkspaces.promiseInitialized;
+ this.#makeSureAllTabsHaveIds();
+ this.#setUpEventListeners();
+ }
+
+ #makeSureAllTabsHaveIds() {
+ const allTabs = gZenWorkspaces.allStoredTabs;
+ for (const tab of allTabs) {
+ if (!tab.hasAttribute('zen-sync-id') && !tab.hasAttribute('zen-empty-tab')) {
+ const tabId = gZenUIManager.generateUuidv4();
+ tab.setAttribute('zen-sync-id', tabId);
+ }
+ }
+ }
+
+ #setUpEventListeners() {
+ const kEvents = [
+ 'TabClose',
+ 'TabOpen',
+ 'TabMove',
+
+ 'TabPinned',
+ 'TabUnpinned',
+
+ 'TabAddedToEssentials',
+ 'TabRemovedFromEssentials',
+
+ 'TabHide',
+ 'TabShow',
+
+ 'ZenTabIconChanged',
+ 'ZenTabLabelChanged',
+
+ 'TabGroupCreate',
+ 'TabGroupRemoved',
+ 'TabGrouped',
+ 'TabUngrouped',
+ 'TabGroupMoved',
+ ];
+ const eventListener = this.#handleEvent.bind(this);
+ for (const event of kEvents) {
+ window.addEventListener(event, eventListener);
+ }
+
+ window.addEventListener('unload', () => {
+ for (const event of kEvents) {
+ window.removeEventListener(event, eventListener);
+ }
+ });
+ }
+
+ #handleEvent(event) {
+ this.#propagateToOtherWindows(event);
+ }
+
+ async #propagateToOtherWindows(event) {
+ if (this.#ignoreNextEvents) {
+ return;
+ }
+ if (this.#waitForPromise) {
+ await this.#waitForPromise;
+ }
+ this.#waitForPromise = new Promise((resolve) => {
+ this.foreachWindowAsActive(async (browser) => {
+ if (browser.gZenWorkspaceWindowSync && !this.windowIsActive(browser)) {
+ await browser.gZenWorkspaceWindowSync.onExternalTabEvent(event);
+ }
+ }).then(() => {
+ resolve();
+ });
+ });
+ }
+
+ async onExternalTabEvent(event) {
+ this.#ignoreNextEvents = true;
+ switch (event.type) {
+ case 'TabClose':
+ this.#onTabClose(event);
+ break;
+ case 'TabOpen':
+ await this.#onTabOpen(event);
+ break;
+ case 'TabPinned':
+ this.#onTabPinned(event);
+ break;
+ case 'TabUnpinned':
+ this.#onTabUnpinned(event);
+ break;
+ case 'TabAddedToEssentials':
+ this.#onTabAddedToEssentials(event);
+ break;
+ case 'TabRemovedFromEssentials':
+ this.#onTabRemovedFromEssentials(event);
+ break;
+ case 'TabHide':
+ this.#onTabHide(event);
+ break;
+ case 'TabShow':
+ this.#onTabShow(event);
+ break;
+ case 'TabMove':
+ case 'TabGroupMoved':
+ this.#onTabMove(event);
+ break;
+ case 'ZenTabIconChanged':
+ this.#onTabIconChanged(event);
+ break;
+ case 'ZenTabLabelChanged':
+ this.#onTabLabelChanged(event);
+ break;
+ case 'TabGroupCreate':
+ this.#onTabGroupCreate(event);
+ break;
+ case 'TabGroupRemoved':
+ case 'TabGrouped':
+ case 'TabUngrouped':
+ // Tab grouping changes are automatically synced by Firefox
+ break;
+ default:
+ console.warn(`Unhandled event type: ${event.type}`);
+ break;
+ }
+ this.#ignoreNextEvents = false;
+ }
+
+ #getTabId(tab) {
+ return tab.getAttribute('zen-sync-id');
+ }
+
+ #getTabWithId(tabId) {
+ for (const tab of gZenWorkspaces.allStoredTabs) {
+ if (this.#getTabId(tab) === tabId) {
+ return tab;
+ }
+ }
+ return null;
+ }
+
+ #onTabClose(event) {
+ const targetTab = event.target;
+ const tabId = this.#getTabId(targetTab);
+ const tabToClose = this.#getTabWithId(tabId);
+ if (tabToClose) {
+ gBrowser.removeTab(tabToClose);
+ }
+ }
+
+ #onTabPinned(event) {
+ const targetTab = event.target;
+ if (targetTab.hasAttribute('zen-essential')) {
+ return this.#onTabAddedToEssentials(event);
+ }
+ const tabId = this.#getTabId(targetTab);
+ const elementIndex = targetTab.elementIndex;
+ const tabToPin = this.#getTabWithId(tabId);
+ if (tabToPin) {
+ gBrowser.pinTab(tabToPin);
+ gBrowser.moveTabTo(tabToPin, { elementIndex, forceUngrouped: !!targetTab.group });
+ }
+ }
+
+ #onTabUnpinned(event) {
+ const targetTab = event.target;
+ const tabId = this.#getTabId(targetTab);
+ const tabToUnpin = this.#getTabWithId(tabId);
+ if (tabToUnpin) {
+ gBrowser.unpinTab(tabToUnpin);
+ }
+ }
+
+ #onTabIconChanged(event) {
+ this.#updateTabIconAndLabel(event);
+ }
+
+ #onTabLabelChanged(event) {
+ this.#updateTabIconAndLabel(event);
+ }
+
+ #updateTabIconAndLabel(event) {
+ const targetTab = event.target;
+ const tabId = this.#getTabId(targetTab);
+ const tabToChange = this.#getTabWithId(tabId);
+ if (tabToChange && tabToChange.hasAttribute('pending')) {
+ gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab));
+ gBrowser._setTabLabel(tabToChange, targetTab.label);
+ }
+ }
+
+ #onTabAddedToEssentials(event) {
+ const targetTab = event.target;
+ const tabId = this.#getTabId(targetTab);
+ const tabToAdd = this.#getTabWithId(tabId);
+ if (tabToAdd) {
+ gZenPinnedTabManager.addToEssentials(tabToAdd);
+ }
+ }
+
+ #onTabRemovedFromEssentials(event) {
+ const targetTab = event.target;
+ const tabId = this.#getTabId(targetTab);
+ const tabToRemove = this.#getTabWithId(tabId);
+ if (tabToRemove) {
+ gZenPinnedTabManager.removeFromEssentials(tabToRemove);
+ }
+ }
+
+ #onTabHide(event) {
+ const targetTab = event.target;
+ const tabId = this.#getTabId(targetTab);
+ const tabToHide = this.#getTabWithId(tabId);
+ if (tabToHide) {
+ gBrowser.hideTab(tabToHide);
+ }
+ }
+
+ #onTabShow(event) {
+ const targetTab = event.target;
+ const tabId = this.#getTabId(targetTab);
+ const tabToShow = this.#getTabWithId(tabId);
+ if (tabToShow) {
+ gBrowser.showTab(tabToShow);
+ }
+ }
+
+ #onTabMove(event) {
+ const targetTab = event.target;
+ const tabId = this.#getTabId(targetTab);
+ const tabToMove = this.#getTabWithId(tabId);
+ const workspaceId = targetTab.getAttribute('zen-workspace-id');
+ const isEssential = targetTab.hasAttribute('zen-essential');
+ if (tabToMove) {
+ let tabSibling = targetTab.previousElementSibling;
+ let isFirst = false;
+ if (!tabSibling?.hasAttribute('zen-sync-id')) {
+ isFirst = true;
+ }
+ gBrowser.zenHandleTabMove(tabToMove, () => {
+ if (isFirst) {
+ let container;
+ if (isEssential) {
+ container = gZenWorkspaces.getEssentialsSection(tabToMove);
+ } else {
+ const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId);
+ container = tabToMove.pinned
+ ? workspaceElement.pinnedTabsContainer
+ : workspaceElement.tabsContainer;
+ }
+ container.insertBefore(tabToMove, container.firstChild);
+ } else {
+ let relativeTab = gZenWorkspaces.allStoredTabs.find((tab) => {
+ return this.#getTabId(tab) === this.#getTabId(tabSibling);
+ });
+ if (relativeTab) {
+ relativeTab.after(tabToMove);
+ }
+ }
+ });
+ }
+ }
+
+ async #onTabOpen(event) {
+ const targetTab = event.target;
+ const isPinned = targetTab.pinned;
+ const isEssential = isPinned && targetTab.hasAttribute('zen-essential');
+ if (!this.#getTabId(targetTab) && !targetTab.hasAttribute('zen-empty-tab')) {
+ const tabId = gZenUIManager.generateUuidv4();
+ targetTab.setAttribute('zen-sync-id', tabId);
+ }
+ const duplicatedTab = gBrowser.addTrustedTab(targetTab.linkedBrowser.currentURI.spec, {
+ createLazyBrowser: true,
+ essential: isEssential,
+ pinned: isPinned,
+ });
+ if (!isEssential) {
+ gZenWorkspaces.moveTabToWorkspace(
+ duplicatedTab,
+ targetTab.getAttribute('zen-workspace-id')
+ );
+ }
+ duplicatedTab.setAttribute('zen-pin-id', targetTab.getAttribute('zen-pin-id'));
+ duplicatedTab.setAttribute('zen-sync-id', targetTab.getAttribute('zen-sync-id'));
+ }
+
+ #onTabGroupCreate(event) {
+ const targetGroup = event.target;
+ const isSplitView = targetGroup.classList.contains('zen-split-view');
+ const isFolder = targetGroup.isZenFolder;
+ }
+ }
+
+ window.gZenWorkspaceWindowSync = new nsZenWorkspaceWindowSync();
+}