mirror of
https://github.com/zen-browser/desktop.git
synced 2026-09-06 21:40:45 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abc69a1fcb | ||
|
|
8df45e5c9a | ||
|
|
58e83c137f |
@@ -68,6 +68,10 @@ zen-settings-workspaces-sync =
|
|||||||
.label = Sync your sidebar across devices
|
.label = Sync your sidebar across devices
|
||||||
.description = Keep your workspaces, pinned tabs and folders in sync on all your devices through your Mozilla account.
|
.description = Keep your workspaces, pinned tabs and folders in sync on all your devices through your Mozilla account.
|
||||||
|
|
||||||
|
zen-settings-normal-tabs-sync =
|
||||||
|
.label = Include unpinned tabs
|
||||||
|
.description = Also sync the regular tabs of each space, not just your pinned tabs and folders.
|
||||||
|
|
||||||
zen-settings-workspaces-sync-unpinned-tabs =
|
zen-settings-workspaces-sync-unpinned-tabs =
|
||||||
.label = Sync only pinned tabs in workspaces
|
.label = Sync only pinned tabs in workspaces
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,9 @@
|
|||||||
|
|
||||||
- name: zen.spaces-sync.debug
|
- name: zen.spaces-sync.debug
|
||||||
value: false
|
value: false
|
||||||
|
|
||||||
|
- name: zen.spaces-sync.normal-tabs
|
||||||
|
value: false
|
||||||
|
|
||||||
|
- name: services.sync.prefs.sync.zen.spaces-sync.normal-tabs
|
||||||
|
value: true
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
diff --git a/browser/components/preferences/config/account-sync.mjs b/browser/components/preferences/config/account-sync.mjs
|
diff --git a/browser/components/preferences/config/account-sync.mjs b/browser/components/preferences/config/account-sync.mjs
|
||||||
index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac3342975bd241a 100644
|
index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..7e3c2f6852b77192b4505bb3d5a7b58586da6548 100644
|
||||||
--- a/browser/components/preferences/config/account-sync.mjs
|
--- a/browser/components/preferences/config/account-sync.mjs
|
||||||
+++ b/browser/components/preferences/config/account-sync.mjs
|
+++ b/browser/components/preferences/config/account-sync.mjs
|
||||||
@@ -46,6 +46,7 @@ Preferences.addAll([
|
@@ -46,6 +46,8 @@ Preferences.addAll([
|
||||||
{ id: "services.sync.engine.creditcards", type: "bool" },
|
{ id: "services.sync.engine.creditcards", type: "bool" },
|
||||||
{ id: "services.sync.engine.addons", type: "bool" },
|
{ id: "services.sync.engine.addons", type: "bool" },
|
||||||
{ id: "services.sync.engine.prefs", type: "bool" },
|
{ id: "services.sync.engine.prefs", type: "bool" },
|
||||||
+ { id: "services.sync.engine.spaces", type: "bool" },
|
+ { id: "services.sync.engine.spaces", type: "bool" },
|
||||||
|
+ { id: "zen.spaces-sync.normal-tabs", type: "bool" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -557,6 +558,7 @@ const SYNC_ENGINE_SETTINGS = [
|
@@ -557,6 +559,7 @@ const SYNC_ENGINE_SETTINGS = [
|
||||||
type: "payments",
|
type: "payments",
|
||||||
},
|
},
|
||||||
{ id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
|
{ id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
|
||||||
@@ -18,7 +19,7 @@ index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac33429
|
|||||||
{ id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
|
{ id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -564,6 +566,12 @@ SYNC_ENGINE_SETTINGS.forEach(({ id, pref }) => {
|
@@ -564,6 +567,18 @@ SYNC_ENGINE_SETTINGS.forEach(({ id, pref }) => {
|
||||||
Preferences.addSetting({ id, pref });
|
Preferences.addSetting({ id, pref });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,11 +28,17 @@ index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac33429
|
|||||||
+ id: "zenSyncWorkspaces",
|
+ id: "zenSyncWorkspaces",
|
||||||
+ pref: "services.sync.engine.spaces",
|
+ pref: "services.sync.engine.spaces",
|
||||||
+});
|
+});
|
||||||
|
+Preferences.addSetting({
|
||||||
|
+ id: "zenSyncNormalTabs",
|
||||||
|
+ pref: "zen.spaces-sync.normal-tabs",
|
||||||
|
+ deps: ["zenSyncWorkspaces"],
|
||||||
|
+ visible: deps => !!deps.zenSyncWorkspaces.value,
|
||||||
|
+});
|
||||||
+
|
+
|
||||||
Preferences.addSetting({
|
Preferences.addSetting({
|
||||||
id: "syncEngines",
|
id: "syncEngines",
|
||||||
deps: SYNC_ENGINE_SETTINGS.map(({ id }) => id),
|
deps: SYNC_ENGINE_SETTINGS.map(({ id }) => id),
|
||||||
@@ -995,6 +1003,20 @@ SettingGroupManager.registerGroups({
|
@@ -995,6 +1010,27 @@ SettingGroupManager.registerGroups({
|
||||||
id: "syncEnginesList",
|
id: "syncEnginesList",
|
||||||
control: "sync-engines-list",
|
control: "sync-engines-list",
|
||||||
},
|
},
|
||||||
@@ -46,6 +53,13 @@ index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac33429
|
|||||||
+ controlAttrs: {
|
+ controlAttrs: {
|
||||||
+ inputlayout: "inline-end",
|
+ inputlayout: "inline-end",
|
||||||
+ },
|
+ },
|
||||||
|
+ items: [
|
||||||
|
+ {
|
||||||
|
+ id: "zenSyncNormalTabs",
|
||||||
|
+ l10nId: "zen-settings-normal-tabs-sync",
|
||||||
|
+ control: "moz-checkbox",
|
||||||
|
+ },
|
||||||
|
+ ],
|
||||||
+ },
|
+ },
|
||||||
+ ],
|
+ ],
|
||||||
+ },
|
+ },
|
||||||
|
|||||||
31
src/zen/common/styles/squircle-exclusions.inc.css
Normal file
31
src/zen/common/styles/squircle-exclusions.inc.css
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Upstream elements that take their radius from --border-radius-circle (9999px).
|
||||||
|
They are meant to read as circles or pills, so a superellipse only makes them
|
||||||
|
look lumpy. */
|
||||||
|
.tab-icon-overlay:not([crashed]):is([soundplaying], [muted], [activemedia-blocked]),
|
||||||
|
.tab-note-icon-overlay,
|
||||||
|
.tab-group-overflow-count,
|
||||||
|
.tab-group-editor-swatch,
|
||||||
|
.tab-preview-container-indicator,
|
||||||
|
#tabbrowser-tabs[orient='vertical']:not([expanded]) .tab-close-button,
|
||||||
|
.all-tabs-item > toolbarbutton.all-tabs-container-indicator::before,
|
||||||
|
.urlbarView-button,
|
||||||
|
.downloadButton,
|
||||||
|
#downloads-indicator-progress-inner,
|
||||||
|
#smartwindow-ask-button-inner,
|
||||||
|
#PanelUI-fxa-menu-manage-account-button,
|
||||||
|
.fxa-menu-manage-account-avatar,
|
||||||
|
[fxastatus='signedin'] #fxa-avatar-image,
|
||||||
|
#profile-icon-image,
|
||||||
|
.appMenu-update-banner-pulse-ring,
|
||||||
|
:is(#appMenu-zoomReduce-button2, #appMenu-zoomEnlarge-button2) > .toolbarbutton-icon,
|
||||||
|
.color-swatches .userContext-icon,
|
||||||
|
.reusable-relay-masks > button,
|
||||||
|
.radio-check {
|
||||||
|
corner-shape: round;
|
||||||
|
}
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
) !important;
|
) !important;
|
||||||
--focus-outline-color: var(--color-accent-primary) !important;
|
--focus-outline-color: var(--color-accent-primary) !important;
|
||||||
|
|
||||||
--toolbarbutton-icon-fill-attention: var(--toolbar-color) !important;
|
--toolbarbutton-icon-fill-attention: var(--toolbar-color, currentColor) !important;
|
||||||
--toolbarbutton-icon-fill: currentColor !important;
|
--toolbarbutton-icon-fill: currentColor !important;
|
||||||
|
|
||||||
--button-background-color-primary: var(--in-content-primary-button-background) !important;
|
--button-background-color-primary: var(--in-content-primary-button-background) !important;
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
--zen-squircle-value: 1.3;
|
--zen-squircle-value: 1.3;
|
||||||
|
|
||||||
@media (-moz-platform: windows) {
|
@media (-moz-platform: windows) {
|
||||||
--zen-squircle-value: 2.1;
|
--zen-squircle-value: 2.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
--tab-icon-overlay-stroke: light-dark(white, black) !important;
|
--tab-icon-overlay-stroke: light-dark(white, black) !important;
|
||||||
@@ -361,4 +361,6 @@
|
|||||||
.zen-squircle-after::after {
|
.zen-squircle-after::after {
|
||||||
corner-shape: superellipse(var(--zen-squircle-value));
|
corner-shape: superellipse(var(--zen-squircle-value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%include squircle-exclusions.inc.css
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
// Windows defaults to 8px
|
// Windows defaults to 8px
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--zen-border-radius",
|
"--zen-border-radius",
|
||||||
"8px"
|
"9px"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
--zen-compact-float: var(--zen-element-separation);
|
--zen-compact-float: var(--zen-element-separation);
|
||||||
|
|
||||||
@media (-moz-mac-tahoe-theme) {
|
@media (-moz-mac-tahoe-theme) {
|
||||||
--zen-compact-mode-no-padding-radius-fix: -2px;
|
--zen-compact-mode-no-padding-radius-fix: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[zen-no-padding='true'] & {
|
:root[zen-no-padding='true'] & {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
&,
|
&,
|
||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
border-radius: calc((var(--zen-native-inner-radius) - var(--zen-compact-mode-no-padding-radius-fix, 0px)) * var(--zen-squircle-value));
|
border-radius: calc((var(--zen-native-inner-radius) - var(--zen-compact-mode-no-padding-radius-fix, 0px)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2341,9 +2341,13 @@ class nsZenWorkspaces {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
tabToSelect = lastSelectedTab;
|
tabToSelect = lastSelectedTab;
|
||||||
|
} else if (!onInit && !tabToSelect) {
|
||||||
|
// Create new tab if needed and no suitable tab was found
|
||||||
|
tabToSelect = this._emptyTab;
|
||||||
}
|
}
|
||||||
// Find first suitable tab
|
// Find first suitable tab
|
||||||
else {
|
// If we found a tab to select, select it
|
||||||
|
if (!tabToSelect || tabToSelect.closing) {
|
||||||
tabToSelect = gBrowser.visibleTabs.find(tab => !tab.pinned);
|
tabToSelect = gBrowser.visibleTabs.find(tab => !tab.pinned);
|
||||||
if (!tabToSelect && gBrowser.visibleTabs.length) {
|
if (!tabToSelect && gBrowser.visibleTabs.length) {
|
||||||
tabToSelect = gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1];
|
tabToSelect = gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1];
|
||||||
@@ -2354,12 +2358,6 @@ class nsZenWorkspaces {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we found a tab to select, select it
|
|
||||||
if (!onInit && !tabToSelect) {
|
|
||||||
// Create new tab if needed and no suitable tab was found
|
|
||||||
const newTab = this.selectEmptyTab();
|
|
||||||
tabToSelect = newTab;
|
|
||||||
}
|
|
||||||
if (tabToSelect && !onInit) {
|
if (tabToSelect && !onInit) {
|
||||||
tabToSelect._visuallySelected = true;
|
tabToSelect._visuallySelected = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,10 +75,9 @@ class ZenSpacesSyncStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracker for the Spaces engine. There is no per-event bookkeeping: every
|
* Tracker for the Spaces engine. The model diffs the current projections
|
||||||
* time fresh sidebar data is collected (or a container changes), the model
|
* against the last-uploaded snapshot. The score is only bumped when
|
||||||
* diffs the current projections against the last-uploaded snapshot; the
|
* something actually differs.
|
||||||
* score is only bumped when something actually differs.
|
|
||||||
*/
|
*/
|
||||||
class ZenSpacesSyncTracker extends Tracker {
|
class ZenSpacesSyncTracker extends Tracker {
|
||||||
_ignoreAll = false;
|
_ignoreAll = false;
|
||||||
|
|||||||
@@ -11,8 +11,17 @@ import {
|
|||||||
ZenSpacesSyncModel,
|
ZenSpacesSyncModel,
|
||||||
} from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs";
|
} from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs";
|
||||||
|
|
||||||
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||||
|
|
||||||
const lazy = {};
|
const lazy = {};
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
lazy,
|
||||||
|
"syncNormalTabs",
|
||||||
|
"zen.spaces-sync.normal-tabs",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
|
SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
|
||||||
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
|
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
|
||||||
@@ -84,6 +93,11 @@ class nsZenSpacesSyncApplier {
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (data.pinned === false && !lazy.syncNormalTabs) {
|
||||||
|
// Normal-tab syncing is off here: leave the record untouched and
|
||||||
|
// unacknowledged, like an unknown kind.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const entry = { key: record.id, data, record };
|
const entry = { key: record.id, data, record };
|
||||||
switch (record.cleartext.kind) {
|
switch (record.cleartext.kind) {
|
||||||
case RECORD_KINDS.CONTAINER:
|
case RECORD_KINDS.CONTAINER:
|
||||||
@@ -567,7 +581,9 @@ class nsZenSpacesSyncApplier {
|
|||||||
if (data.workspaceUuid) {
|
if (data.workspaceUuid) {
|
||||||
tab.setAttribute("zen-workspace-id", data.workspaceUuid);
|
tab.setAttribute("zen-workspace-id", data.workspaceUuid);
|
||||||
}
|
}
|
||||||
win.gBrowser.pinTab(tab);
|
if (data.pinned !== false) {
|
||||||
|
win.gBrowser.pinTab(tab);
|
||||||
|
}
|
||||||
if (data.workspaceUuid) {
|
if (data.workspaceUuid) {
|
||||||
win.gZenWorkspaces.moveTabToWorkspace(tab, data.workspaceUuid);
|
win.gZenWorkspaces.moveTabToWorkspace(tab, data.workspaceUuid);
|
||||||
}
|
}
|
||||||
@@ -593,7 +609,10 @@ class nsZenSpacesSyncApplier {
|
|||||||
const identityChanged =
|
const identityChanged =
|
||||||
initial?.entry?.url !== data.url ||
|
initial?.entry?.url !== data.url ||
|
||||||
(initial?.entry?.title || "") !== (data.title || "");
|
(initial?.entry?.title || "") !== (data.title || "");
|
||||||
if (identityChanged || syncableIconUrl(initial?.image || "") !== icon) {
|
if (
|
||||||
|
data.pinned !== false &&
|
||||||
|
(identityChanged || syncableIconUrl(initial?.image || "") !== icon)
|
||||||
|
) {
|
||||||
lazy.ZenWindowSync.setPinnedInitialState(
|
lazy.ZenWindowSync.setPinnedInitialState(
|
||||||
tab,
|
tab,
|
||||||
{ url: data.url, title: data.title || "" },
|
{ url: data.url, title: data.title || "" },
|
||||||
@@ -745,6 +764,17 @@ class nsZenSpacesSyncApplier {
|
|||||||
// The split record governs placement of its members.
|
// The split record governs placement of its members.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const wantPinned = data.pinned !== false;
|
||||||
|
if (wantPinned !== tab.pinned) {
|
||||||
|
if (wantPinned) {
|
||||||
|
win.gBrowser.pinTab(tab);
|
||||||
|
} else {
|
||||||
|
win.gBrowser.unpinTab(tab);
|
||||||
|
// Pin identity would otherwise freeze the projection of what is now
|
||||||
|
// a normal tab.
|
||||||
|
delete tab._zenPinnedInitialState;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.#applyFolderMembership(win, tab, data.folderId);
|
this.#applyFolderMembership(win, tab, data.folderId);
|
||||||
if (
|
if (
|
||||||
!data.folderId &&
|
!data.folderId &&
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
lazy,
|
||||||
|
"syncNormalTabs",
|
||||||
|
"zen.spaces-sync.normal-tabs",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug logging for the whole Spaces sync pipeline.
|
* Debug logging for the whole Spaces sync pipeline.
|
||||||
*
|
*
|
||||||
@@ -258,19 +265,31 @@ class nsZenSpacesSyncModel {
|
|||||||
|
|
||||||
/* Mark: projections */
|
/* Mark: projections */
|
||||||
|
|
||||||
#isSyncableTab(tabData) {
|
#isTabRecordMaterial(tabData) {
|
||||||
return !!(
|
return !!(
|
||||||
tabData &&
|
tabData &&
|
||||||
tabData.zenSyncId &&
|
tabData.zenSyncId &&
|
||||||
(tabData.pinned || tabData.zenEssential) &&
|
|
||||||
!tabData.zenIsEmpty &&
|
!tabData.zenIsEmpty &&
|
||||||
!tabData.zenIsGlance &&
|
!tabData.zenIsGlance &&
|
||||||
!tabData.zenLiveFolderItemId
|
!tabData.zenLiveFolderItemId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#isSyncableTab(tabData) {
|
||||||
|
return (
|
||||||
|
this.#isTabRecordMaterial(tabData) &&
|
||||||
|
(!!tabData.pinned || !!tabData.zenEssential || lazy.syncNormalTabs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#tabIdentity(tabData) {
|
#tabIdentity(tabData) {
|
||||||
const initial = tabData._zenPinnedInitialState;
|
// Pin identity freezes url/title at pin time. A normal tab's identity
|
||||||
|
// follows its live entry (a leftover initial state from a past pin must
|
||||||
|
// not shadow it).
|
||||||
|
const initial =
|
||||||
|
tabData.pinned || tabData.zenEssential
|
||||||
|
? tabData._zenPinnedInitialState
|
||||||
|
: null;
|
||||||
let url = initial?.entry?.url;
|
let url = initial?.entry?.url;
|
||||||
let title = initial?.entry?.title;
|
let title = initial?.entry?.title;
|
||||||
if (!url || url === "about:blank") {
|
if (!url || url === "about:blank") {
|
||||||
@@ -473,6 +492,7 @@ class nsZenSpacesSyncModel {
|
|||||||
create: true,
|
create: true,
|
||||||
}),
|
}),
|
||||||
essential,
|
essential,
|
||||||
|
pinned: !!(tab.pinned || tab.zenEssential),
|
||||||
workspaceUuid: essential ? null : tab.zenWorkspace || null,
|
workspaceUuid: essential ? null : tab.zenWorkspace || null,
|
||||||
folderId: ctx.folderOf(tab.groupId || null),
|
folderId: ctx.folderOf(tab.groupId || null),
|
||||||
staticLabel:
|
staticLabel:
|
||||||
@@ -496,6 +516,28 @@ class nsZenSpacesSyncModel {
|
|||||||
const ctx = this.#projectionContext(sidebar);
|
const ctx = this.#projectionContext(sidebar);
|
||||||
const { tabs, folders, splits, splitParents, splitWs } = ctx;
|
const { tabs, folders, splits, splitParents, splitWs } = ctx;
|
||||||
|
|
||||||
|
if (!lazy.syncNormalTabs) {
|
||||||
|
// Items excluded only by the normal-tabs option are held back, not
|
||||||
|
// deleted. Flipping the option off must not tombstone them remotely.
|
||||||
|
const held = new Set();
|
||||||
|
for (const tab of ctx.allTabs) {
|
||||||
|
if (this.#isTabRecordMaterial(tab) && !this.#isSyncableTab(tab)) {
|
||||||
|
held.add(tab.zenSyncId);
|
||||||
|
pending.add(tab.zenSyncId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const split of sidebar.splitViewData || []) {
|
||||||
|
if (
|
||||||
|
split?.groupId &&
|
||||||
|
!ctx.splitIds.has(split.groupId) &&
|
||||||
|
Array.isArray(split.tabs) &&
|
||||||
|
split.tabs.some(id => held.has(id))
|
||||||
|
) {
|
||||||
|
pending.add(split.groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const identity of lazy.ContextualIdentityService.getPublicIdentities()) {
|
for (const identity of lazy.ContextualIdentityService.getPublicIdentities()) {
|
||||||
if (!identity.name) {
|
if (!identity.name) {
|
||||||
continue;
|
continue;
|
||||||
@@ -568,12 +610,15 @@ class nsZenSpacesSyncModel {
|
|||||||
|
|
||||||
this.#projectTabs(map, ctx);
|
this.#projectTabs(map, ctx);
|
||||||
|
|
||||||
|
const tabById = new Map(tabs.map(t => [t.zenSyncId, t]));
|
||||||
for (const split of splits) {
|
for (const split of splits) {
|
||||||
|
const member = tabById.get(split.tabs[0]);
|
||||||
map.set(split.groupId, {
|
map.set(split.groupId, {
|
||||||
kind: RECORD_KINDS.SPLIT,
|
kind: RECORD_KINDS.SPLIT,
|
||||||
data: {
|
data: {
|
||||||
splitId: split.groupId,
|
splitId: split.groupId,
|
||||||
gridType: split.gridType || "grid",
|
gridType: split.gridType || "grid",
|
||||||
|
pinned: !!(member?.pinned || member?.zenEssential),
|
||||||
tabs: [...split.tabs],
|
tabs: [...split.tabs],
|
||||||
workspaceUuid: splitWs.get(split.groupId) ?? null,
|
workspaceUuid: splitWs.get(split.groupId) ?? null,
|
||||||
folderId: splitParents.get(split.groupId) || null,
|
folderId: splitParents.get(split.groupId) || null,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ BROWSER_CHROME_MANIFESTS += [
|
|||||||
"site_control/browser.toml",
|
"site_control/browser.toml",
|
||||||
"space_routing/browser.toml",
|
"space_routing/browser.toml",
|
||||||
"spaces/browser.toml",
|
"spaces/browser.toml",
|
||||||
|
"spaces_sync/browser.toml",
|
||||||
"split_view/browser.toml",
|
"split_view/browser.toml",
|
||||||
"tabs/browser.toml",
|
"tabs/browser.toml",
|
||||||
"ub-actions/browser.toml",
|
"ub-actions/browser.toml",
|
||||||
|
|||||||
17
src/zen/tests/spaces_sync/browser.toml
Normal file
17
src/zen/tests/spaces_sync/browser.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 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/.
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
prefs = [
|
||||||
|
"zen.window-sync.enabled=true",
|
||||||
|
"zen.urlbar.replace-newtab=false",
|
||||||
|
"zen.spaces-sync.first-sync-animation-shown=true",
|
||||||
|
]
|
||||||
|
support-files = [
|
||||||
|
"head.js",
|
||||||
|
]
|
||||||
|
|
||||||
|
["browser_normal_tabs_apply.js"]
|
||||||
|
|
||||||
|
["browser_normal_tabs_projection.js"]
|
||||||
129
src/zen/tests/spaces_sync/browser_normal_tabs_apply.js
Normal file
129
src/zen/tests/spaces_sync/browser_normal_tabs_apply.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
add_task(async function test_ApplySkippedWhileOptionOff() {
|
||||||
|
await gZenWorkspaces.promiseInitialized;
|
||||||
|
const id = "test-spaces-sync-normal-off";
|
||||||
|
const failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(
|
||||||
|
failed,
|
||||||
|
[],
|
||||||
|
"A skipped normal-tab record should not be reported as failed"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
!document.getElementById(id),
|
||||||
|
"No tab should materialize while the option is off"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_ApplyCreatesUnpinnedTab() {
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
const id = "test-spaces-sync-normal-on";
|
||||||
|
const failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The record should apply cleanly");
|
||||||
|
|
||||||
|
const tab = document.getElementById(id);
|
||||||
|
Assert.ok(gBrowser.isTab(tab), "The tab should materialize");
|
||||||
|
Assert.ok(!tab.pinned, "The tab should not be pinned");
|
||||||
|
Assert.equal(
|
||||||
|
tab.getAttribute("zen-workspace-id"),
|
||||||
|
gZenWorkspaces.activeWorkspace,
|
||||||
|
"The tab should land in the record's workspace"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
!tab._zenPinnedInitialState,
|
||||||
|
"A normal tab should carry no pin identity"
|
||||||
|
);
|
||||||
|
|
||||||
|
ZenSpacesSyncModel.noteApplied(id, null);
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_UpdateRetargetsUnloadedNormalTab() {
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
const id = "test-spaces-sync-retarget";
|
||||||
|
let failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false, url: "https://example.com/first" }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The record should apply cleanly");
|
||||||
|
const tab = document.getElementById(id);
|
||||||
|
Assert.ok(!tab.linkedPanel, "The synced tab should stay unloaded");
|
||||||
|
|
||||||
|
failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false, url: "https://example.com/second" }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The navigation record should apply cleanly");
|
||||||
|
Assert.ok(!tab.linkedPanel, "The tab should stay unloaded after retarget");
|
||||||
|
const state = JSON.parse(SessionStore.getTabState(tab));
|
||||||
|
Assert.equal(
|
||||||
|
state.entries.at(-1)?.url,
|
||||||
|
"https://example.com/second",
|
||||||
|
"The unloaded tab's state should point at the new url"
|
||||||
|
);
|
||||||
|
|
||||||
|
ZenSpacesSyncModel.noteApplied(id, null);
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_UpdateRetargetsDiscardedNormalTab() {
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
const tab = await openSyncableTab("https://example.com/?loadedfirst");
|
||||||
|
const id = tab.id;
|
||||||
|
await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
|
||||||
|
gBrowser.discardBrowser(tab);
|
||||||
|
Assert.ok(!tab.linkedPanel, "The tab should be discarded");
|
||||||
|
|
||||||
|
const failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false, url: "https://example.com/?remotenav" }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The navigation record should apply cleanly");
|
||||||
|
Assert.ok(!tab.linkedPanel, "The tab should stay unloaded after retarget");
|
||||||
|
const state = JSON.parse(SessionStore.getTabState(tab));
|
||||||
|
Assert.equal(
|
||||||
|
state.entries.at(-1)?.url,
|
||||||
|
"https://example.com/?remotenav",
|
||||||
|
"The discarded tab's state should point at the new url"
|
||||||
|
);
|
||||||
|
|
||||||
|
ZenSpacesSyncModel.noteApplied(id, null);
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_ApplyPinStateTransition() {
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
const id = "test-spaces-sync-transition";
|
||||||
|
let failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: true }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The pinned record should apply cleanly");
|
||||||
|
const tab = document.getElementById(id);
|
||||||
|
Assert.ok(gBrowser.isTab(tab), "The tab should materialize");
|
||||||
|
Assert.ok(tab.pinned, "A pinned record should materialize pinned");
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => tab._zenPinnedInitialState,
|
||||||
|
"waiting for the pin identity to settle"
|
||||||
|
);
|
||||||
|
|
||||||
|
failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The demoting record should apply cleanly");
|
||||||
|
Assert.ok(!tab.pinned, "A record demotion should unpin the tab");
|
||||||
|
Assert.ok(
|
||||||
|
!tab._zenPinnedInitialState,
|
||||||
|
"Demotion should clear the pin identity"
|
||||||
|
);
|
||||||
|
|
||||||
|
ZenSpacesSyncModel.noteApplied(id, null);
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
});
|
||||||
81
src/zen/tests/spaces_sync/browser_normal_tabs_projection.js
Normal file
81
src/zen/tests/spaces_sync/browser_normal_tabs_projection.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
add_task(async function test_ProjectionGatedByPref() {
|
||||||
|
await gZenWorkspaces.promiseInitialized;
|
||||||
|
const pinnedTab = await openSyncableTab("https://example.com/?pinned", {
|
||||||
|
pinned: true,
|
||||||
|
});
|
||||||
|
const normalTab = await openSyncableTab("https://example.com/?normal");
|
||||||
|
|
||||||
|
let projections = await collectProjections();
|
||||||
|
const pinnedRecord = projections.get(pinnedTab.id);
|
||||||
|
Assert.ok(pinnedRecord, "Pinned tab should project with the option off");
|
||||||
|
Assert.equal(
|
||||||
|
pinnedRecord.data.pinned,
|
||||||
|
true,
|
||||||
|
"Pinned tab record should carry pinned: true"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
!projections.get(normalTab.id),
|
||||||
|
"Normal tab should not project with the option off"
|
||||||
|
);
|
||||||
|
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
ZenSpacesSyncModel.invalidate();
|
||||||
|
projections = ZenSpacesSyncModel.projections();
|
||||||
|
|
||||||
|
const record = projections.get(normalTab.id);
|
||||||
|
Assert.ok(record, "Normal tab should project with the option on");
|
||||||
|
Assert.equal(
|
||||||
|
record.data.pinned,
|
||||||
|
false,
|
||||||
|
"Normal tab record should carry pinned: false"
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
record.data.workspaceUuid,
|
||||||
|
gZenWorkspaces.activeWorkspace,
|
||||||
|
"Normal tab record should name its workspace"
|
||||||
|
);
|
||||||
|
const space = projections.get(gZenWorkspaces.activeWorkspace);
|
||||||
|
Assert.ok(
|
||||||
|
space?.data?.children?.includes(normalTab.id),
|
||||||
|
"The space's child order should include the normal tab"
|
||||||
|
);
|
||||||
|
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
BrowserTestUtils.removeTab(normalTab);
|
||||||
|
BrowserTestUtils.removeTab(pinnedTab);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_DisablingOptionHoldsBackInsteadOfTombstoning() {
|
||||||
|
await gZenWorkspaces.promiseInitialized;
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
const normalTab = await openSyncableTab("https://example.com/?heldback");
|
||||||
|
|
||||||
|
await collectProjections();
|
||||||
|
ZenSpacesSyncModel.markUploaded([normalTab.id]);
|
||||||
|
let changes = ZenSpacesSyncModel.computeChangedIDs();
|
||||||
|
Assert.ok(
|
||||||
|
!(normalTab.id in changes),
|
||||||
|
"An uploaded normal tab should report no pending change"
|
||||||
|
);
|
||||||
|
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
ZenSpacesSyncModel.invalidate();
|
||||||
|
Assert.ok(
|
||||||
|
!ZenSpacesSyncModel.projections().get(normalTab.id),
|
||||||
|
"Normal tab should stop projecting once the option is off"
|
||||||
|
);
|
||||||
|
changes = ZenSpacesSyncModel.computeChangedIDs();
|
||||||
|
Assert.ok(
|
||||||
|
!(normalTab.id in changes),
|
||||||
|
"Disabling the option should not tombstone the tab"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drop the snapshot entry so this task leaves no sync state behind.
|
||||||
|
ZenSpacesSyncModel.noteApplied(normalTab.id, null);
|
||||||
|
BrowserTestUtils.removeTab(normalTab);
|
||||||
|
});
|
||||||
74
src/zen/tests/spaces_sync/head.js
Normal file
74
src/zen/tests/spaces_sync/head.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { ZenSpacesSyncModel } = ChromeUtils.importESModule(
|
||||||
|
"resource:///modules/zen/ZenSpacesSyncModel.sys.mjs"
|
||||||
|
);
|
||||||
|
const { ZenSpacesSyncApplier } = ChromeUtils.importESModule(
|
||||||
|
"resource:///modules/zen/ZenSpacesSyncApplier.sys.mjs"
|
||||||
|
);
|
||||||
|
const { SessionSaver } = ChromeUtils.importESModule(
|
||||||
|
"resource:///modules/sessionstore/SessionSaver.sys.mjs"
|
||||||
|
);
|
||||||
|
const { TabStateFlusher } = ChromeUtils.importESModule(
|
||||||
|
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
|
||||||
|
);
|
||||||
|
const { TestUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/TestUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
const { E10SUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://gre/modules/E10SUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
const NORMAL_TABS_PREF = "zen.spaces-sync.normal-tabs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a full session save (which collects the sidebar data) and returns
|
||||||
|
* fresh sync projections built from it.
|
||||||
|
*/
|
||||||
|
async function collectProjections() {
|
||||||
|
await SessionSaver.run();
|
||||||
|
ZenSpacesSyncModel.invalidate();
|
||||||
|
return ZenSpacesSyncModel.projections();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSyncableTab(url, { pinned = false } = {}) {
|
||||||
|
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||||
|
if (pinned) {
|
||||||
|
gBrowser.pinTab(tab);
|
||||||
|
}
|
||||||
|
await TabStateFlusher.flush(tab.linkedBrowser);
|
||||||
|
ok(tab.id, "the tab should have a sync id");
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabRecord(
|
||||||
|
id,
|
||||||
|
{ pinned, url = "https://example.com/", current = null, workspaceUuid } = {}
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
deleted: false,
|
||||||
|
cleartext: {
|
||||||
|
kind: "tab",
|
||||||
|
data: {
|
||||||
|
tabId: id,
|
||||||
|
url,
|
||||||
|
title: "Synced tab",
|
||||||
|
current,
|
||||||
|
icon: null,
|
||||||
|
containerGuid: null,
|
||||||
|
essential: false,
|
||||||
|
pinned,
|
||||||
|
workspaceUuid: workspaceUuid ?? gZenWorkspaces.activeWorkspace,
|
||||||
|
folderId: null,
|
||||||
|
staticLabel: null,
|
||||||
|
hasStaticIcon: false,
|
||||||
|
defaultContainer: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user