Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Workspace-specific bookmarks #3215

Merged
71 changes: 60 additions & 11 deletions src/browser/base/zen-components/ZenWorkspaces.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
};
_hoveringSidebar = false;
_lastScrollTime = 0;
bookmarkMenus = [
"PlacesToolbar",
"bookmarks-menu-button",
"BMB_bookmarksToolbar",
"BMB_unsortedBookmarks",
"BMB_mobileBookmarks"
];

async init() {
if (!this.shouldHaveWorkspaces) {
Expand Down Expand Up @@ -61,6 +68,11 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}

Services.obs.addObserver(this, 'weave:engine:sync:finish');
Services.obs.addObserver(async function observe(subject) {
this._workspaceBookmarksCache = null;
await this.workspaceBookmarks();
this._invalidateBookmarkContainers();
}.bind(this), "workspace-bookmarks-updated");
}

initializeWorkspaceNavigation() {
Expand Down Expand Up @@ -128,7 +140,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}

// Change workspace based on scroll direction
const direction = event.deltaX > 0 ? -1 : 1;
const direction = event.deltaX > 0 ? 1 : -1;
await this.changeWorkspaceShortcut(direction);
this._lastScrollTime = currentTime;
}, { passive: true });
Expand Down Expand Up @@ -320,6 +332,21 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
return this._workspaceCache;
}

async workspaceBookmarks() {
if (this._workspaceBookmarksCache) {
return this._workspaceBookmarksCache;
}

const [bookmarks, lastChangeTimestamp] = await Promise.all([
ZenWorkspaceBookmarksStorage.getBookmarkGuidsByWorkspace(),
ZenWorkspaceBookmarksStorage.getLastChangeTimestamp(),
]);

this._workspaceBookmarksCache = { bookmarks, lastChangeTimestamp };

return this._workspaceCache;
}

async onWorkspacesEnabledChanged() {
if (this.workspaceEnabled) {
throw Error("Shoud've had reloaded the window");
Expand All @@ -339,6 +366,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
if (this.workspaceEnabled) {
this._initializeWorkspaceCreationIcons();
this._initializeWorkspaceTabContextMenus();
await this.workspaceBookmarks();
window.addEventListener('TabBrowserInserted', this.onTabBrowserInserted.bind(this));
await SessionStore.promiseInitialized;
let workspaces = await this._workspaces();
Expand Down Expand Up @@ -754,8 +782,10 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {

if(clearCache) {
browser.ZenWorkspaces._workspaceCache = null;
browser.ZenWorkspaces._workspaceBookmarksCache = null;
}
let workspaces = await browser.ZenWorkspaces._workspaces();
await browser.ZenWorkspaces.workspaceBookmarks();
workspaceList.innerHTML = '';
workspaceList.parentNode.style.display = 'flex';
if (workspaces.workspaces.length <= 0) {
Expand Down Expand Up @@ -1252,16 +1282,23 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
}

// Reset bookmarks toolbar
const placesToolbar = document.getElementById("PlacesToolbar");
if (placesToolbar?._placesView) {
placesToolbar._placesView.invalidateContainer(placesToolbar._placesView._resultNode);
}
// Reset bookmarks
this._invalidateBookmarkContainers();

// Update workspace indicator
await this.updateWorkspaceIndicator();
}

_invalidateBookmarkContainers() {
for (let i = 0, len = this.bookmarkMenus.length; i < len; i++) {
const element = document.getElementById(this.bookmarkMenus[i]);
if (element && element._placesView) {
const placesView = element._placesView;
placesView.invalidateContainer(placesView._resultNode);
}
}
}

async updateWorkspaceIndicator() {
// Update current workspace indicator
const currentWorkspace = await this.getActiveWorkspace();
Expand Down Expand Up @@ -1534,12 +1571,24 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}

isBookmarkInAnotherWorkspace(bookmark) {
let tags = bookmark.tags;
// if any tag starts with "_workspace_id" and the workspace id doesnt match the active workspace id, return null
if (tags) {
for (let tag of tags.split(",")) {
return !!(tag.startsWith("zen_workspace_") && this.getActiveWorkspaceFromCache()?.uuid !== tag.split("_")[2]);
if (!this._workspaceBookmarksCache?.bookmarks) return false;
const bookmarkGuid = bookmark.bookmarkGuid;
const activeWorkspaceUuid = this.activeWorkspace;
let isInActiveWorkspace = false;
let isInOtherWorkspace = false;

for (const [workspaceUuid, bookmarkGuids] of Object.entries(this._workspaceBookmarksCache.bookmarks)) {
if (bookmarkGuids.includes(bookmarkGuid)) {
if (workspaceUuid === activeWorkspaceUuid) {
isInActiveWorkspace = true;
} else {
isInOtherWorkspace = true;
}
}
}

// Return true only if the bookmark is in another workspace and not in the active one
return isInOtherWorkspace && !isInActiveWorkspace;
}

})();
152 changes: 151 additions & 1 deletion src/browser/base/zen-components/ZenWorkspacesStorage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ var ZenWorkspacesStorage = {
async init() {
console.log('ZenWorkspacesStorage: Initializing...');
await this._ensureTable();
await ZenWorkspaceBookmarksStorage.init();
ZenWorkspaces._delayedStartup();
},

async _ensureTable() {
Expand Down Expand Up @@ -64,7 +66,6 @@ var ZenWorkspacesStorage = {
await ZenWorkspacesStorage.migrateWorkspacesFromJSON();
}

ZenWorkspaces._delayedStartup();
});
},

Expand Down Expand Up @@ -405,3 +406,152 @@ var ZenWorkspacesStorage = {
this._notifyWorkspacesChanged("zen-workspace-updated", Array.from(changedUUIDs));
},
};

// Integration of workspace-specific bookmarks into Places
var ZenWorkspaceBookmarksStorage = {
async init() {
await this._ensureTable();
},

async _ensureTable() {
await PlacesUtils.withConnectionWrapper('ZenWorkspaceBookmarksStorage.init', async (db) => {
// Create table using GUIDs instead of IDs
await db.execute(`
CREATE TABLE IF NOT EXISTS zen_bookmarks_workspaces (
id INTEGER PRIMARY KEY,
bookmark_guid TEXT NOT NULL,
workspace_uuid TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(bookmark_guid, workspace_uuid),
FOREIGN KEY(workspace_uuid) REFERENCES zen_workspaces(uuid) ON DELETE CASCADE,
FOREIGN KEY(bookmark_guid) REFERENCES moz_bookmarks(guid) ON DELETE CASCADE
)
`);

// Create index for fast lookups
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_bookmarks_workspaces_lookup
ON zen_bookmarks_workspaces(workspace_uuid, bookmark_guid)
`);

// Add changes tracking table
await db.execute(`
CREATE TABLE IF NOT EXISTS zen_bookmarks_workspaces_changes (
id INTEGER PRIMARY KEY,
bookmark_guid TEXT NOT NULL,
workspace_uuid TEXT NOT NULL,
change_type TEXT NOT NULL,
timestamp INTEGER NOT NULL,
UNIQUE(bookmark_guid, workspace_uuid),
FOREIGN KEY(workspace_uuid) REFERENCES zen_workspaces(uuid) ON DELETE CASCADE,
FOREIGN KEY(bookmark_guid) REFERENCES moz_bookmarks(guid) ON DELETE CASCADE
)
`);

// Create index for changes tracking
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_bookmarks_workspaces_changes
ON zen_bookmarks_workspaces_changes(bookmark_guid, workspace_uuid)
`);

});
},

/**
* Updates the last change timestamp in the metadata table.
* @param {Object} db - The database connection.
*/
async updateLastChangeTimestamp(db) {
const now = Date.now();
await db.execute(`
INSERT OR REPLACE INTO moz_meta (key, value)
VALUES ('zen_bookmarks_workspaces_last_change', :now)
`, { now });
},

/**
* Gets the timestamp of the last change.
* @returns {Promise<number>} The timestamp of the last change.
*/
async getLastChangeTimestamp() {
const db = await PlacesUtils.promiseDBConnection();
const result = await db.executeCached(`
SELECT value FROM moz_meta WHERE key = 'zen_bookmarks_workspaces_last_change'
`);
return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0;
},

async getBookmarkWorkspaces(bookmarkGuid) {
const db = await PlacesUtils.promiseDBConnection();

const rows = await db.execute(`
SELECT workspace_uuid
FROM zen_bookmarks_workspaces
WHERE bookmark_guid = :bookmark_guid
`, { bookmark_guid: bookmarkGuid });

return rows.map(row => row.getResultByName("workspace_uuid"));
},

/**
* Get all bookmark GUIDs organized by workspace UUID.
* @returns {Promise<Object>} A dictionary with workspace UUIDs as keys and arrays of bookmark GUIDs as values.
* @example
* // Returns:
* {
* "workspace-uuid-1": ["bookmark-guid-1", "bookmark-guid-2"],
* "workspace-uuid-2": ["bookmark-guid-3"]
* }
*/
async getBookmarkGuidsByWorkspace() {
const db = await PlacesUtils.promiseDBConnection();

const rows = await db.execute(`
SELECT workspace_uuid, GROUP_CONCAT(bookmark_guid) as bookmark_guids
FROM zen_bookmarks_workspaces
GROUP BY workspace_uuid
`);

const result = {};
for (const row of rows) {
const workspaceUuid = row.getResultByName("workspace_uuid");
const bookmarkGuids = row.getResultByName("bookmark_guids");
result[workspaceUuid] = bookmarkGuids ? bookmarkGuids.split(',') : [];
}

return result;
},

/**
* Get all changed bookmarks with their change types.
* @returns {Promise<Object>} An object mapping bookmark+workspace pairs to their change data.
*/
async getChangedIDs() {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.execute(`
SELECT bookmark_guid, workspace_uuid, change_type, timestamp
FROM zen_bookmarks_workspaces_changes
`);

const changes = {};
for (const row of rows) {
const key = `${row.getResultByName('bookmark_guid')}:${row.getResultByName('workspace_uuid')}`;
changes[key] = {
type: row.getResultByName('change_type'),
timestamp: row.getResultByName('timestamp')
};
}
return changes;
},

/**
* Clear all recorded changes.
*/
async clearChangedIDs() {
await PlacesUtils.withConnectionWrapper('ZenWorkspaceBookmarksStorage.clearChangedIDs', async (db) => {
await db.execute(`DELETE FROM zen_bookmarks_workspaces_changes`);
});
},

};
Loading