v0.0.8 - Multi-window tab management: Open in New Window moves tabs, per-window active notebook, favicon display, Download Website + Open Link in New Window context menu items; update nostr-core to v0.1.3

This commit is contained in:
Laan Tungir
2026-09-27 18:16:48 -04:00
parent b55526ccf5
commit 4aec3db05b
7 changed files with 328 additions and 80 deletions
Generated
+8 -10
View File
@@ -1957,12 +1957,13 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "nostr-core"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"aes",
"base64",
"bech32",
"block-modes",
"cbc",
"chacha20",
"chacha20poly1305",
"chrono",
@@ -1980,7 +1981,7 @@ dependencies = [
[[package]]
name = "nostr-nips"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"aes",
"block-modes",
@@ -2002,7 +2003,7 @@ dependencies = [
[[package]]
name = "nostr-relay"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"futures-util",
"nostr-core",
@@ -2020,7 +2021,7 @@ dependencies = [
[[package]]
name = "nostr-services"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"nostr-core",
"nostr-relay",
@@ -2035,10 +2036,8 @@ dependencies = [
[[package]]
name = "nostr-signer"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"aes",
"cbc",
"hex",
"libc",
"nostr-core",
@@ -2046,7 +2045,6 @@ dependencies = [
"serde",
"serde_json",
"serialport",
"sha2",
"thiserror 2.0.20",
"tokio",
"tracing",
@@ -3057,7 +3055,7 @@ dependencies = [
[[package]]
name = "sovereign_browser"
version = "0.0.7"
version = "0.0.8"
dependencies = [
"anyhow",
"base64",
@@ -3223,7 +3221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
+1 -1
View File
@@ -6,7 +6,7 @@ members = [
[package]
name = "sovereign_browser"
version = "0.0.7"
version = "0.0.8"
edition = "2021"
license = "MIT"
description = "A Linux x86 web browser built on WebKitGTK with Nostr identity"
+1 -1
View File
@@ -1 +1 @@
0.0.7
0.0.8
+301 -65
View File
@@ -63,6 +63,27 @@ static G_MAIN_NOTEBOOK: Lazy<Mutex<Option<glib::thread_guard::ThreadGuard<gtk::N
static G_MAIN_WINDOW: Lazy<Mutex<Option<glib::thread_guard::ThreadGuard<gtk::Window>>>> =
Lazy::new(|| Mutex::new(None));
/// The currently focused window's notebook. Updated on window focus-in so
/// that "active" operations (new tab, close, reload, …) target the window
/// the user is actually looking at. Falls back to the main notebook.
/// Mirrors the C `g_active_notebook` (tab_manager.c:128).
static G_ACTIVE_NOTEBOOK: Lazy<Mutex<Option<glib::thread_guard::ThreadGuard<gtk::Notebook>>>> =
Lazy::new(|| Mutex::new(None));
/// Auxiliary (non-main) top-level windows, so we can revert the active
/// notebook to the main window when an aux window is destroyed.
/// Mirrors the C `g_aux_windows` (tab_manager.c:1380).
static G_AUX_WINDOWS: Lazy<Mutex<Vec<glib::thread_guard::ThreadGuard<gtk::Window>>>> =
Lazy::new(|| Mutex::new(Vec::new()));
/// Per-tab widget map: tab id → (notebook, webview). GTK widgets are not
/// `Send`, so they are wrapped in ThreadGuards. This lets close/reload/
/// duplicate resolve the tab's *own* notebook (which may be an auxiliary
/// window's notebook) rather than assuming the main notebook.
static G_TAB_WIDGETS: Lazy<
Mutex<std::collections::HashMap<i32, (glib::thread_guard::ThreadGuard<gtk::Notebook>, glib::thread_guard::ThreadGuard<WebView>)>>,
> = Lazy::new(|| Mutex::new(std::collections::HashMap::new()));
/// The custom inspector window (created on `open-window` signal, destroyed
/// on `closed`). Stored in a ThreadGuard so it can live in a static.
static G_INSPECTOR_WINDOW: Lazy<Mutex<Option<glib::thread_guard::ThreadGuard<gtk::Window>>>> =
@@ -101,10 +122,25 @@ pub fn tab_manager_set_suppress_quit_on_last_tab(suppress: bool) {
pub fn tab_manager_set_main_window(window: &gtk::Window, notebook: &gtk::Notebook) {
*G_MAIN_NOTEBOOK.lock().unwrap() = Some(glib::thread_guard::ThreadGuard::new(notebook.clone()));
*G_MAIN_WINDOW.lock().unwrap() = Some(glib::thread_guard::ThreadGuard::new(window.clone()));
// The main window starts as the active window. Focus-in reverts the
// active notebook to the main one when the user returns to it.
set_active_notebook(notebook);
let notebook_focus = notebook.clone();
window.connect_focus_in_event(move |_w, _e| {
set_active_notebook(&notebook_focus);
glib::Propagation::Proceed
});
}
/// Get the active notebook (focused window's notebook, or main).
///
/// Mirrors the C `get_effective_notebook()` (tab_manager.c:2765): prefer the
/// focused window's notebook, falling back to the main notebook.
pub fn tab_manager_get_active_notebook() -> Option<gtk::Notebook> {
if let Some(g) = G_ACTIVE_NOTEBOOK.lock().unwrap().as_ref() {
return Some(g.get_ref().clone());
}
tab_manager_get_main_notebook()
}
@@ -113,6 +149,49 @@ pub fn tab_manager_get_main_notebook() -> Option<gtk::Notebook> {
G_MAIN_NOTEBOOK.lock().unwrap().as_ref().map(|g| g.get_ref().clone())
}
/// Record the focused window's notebook as the active one.
fn set_active_notebook(notebook: &gtk::Notebook) {
*G_ACTIVE_NOTEBOOK.lock().unwrap() =
Some(glib::thread_guard::ThreadGuard::new(notebook.clone()));
}
/// Revert the active notebook to the main window's notebook.
fn revert_active_notebook_to_main() {
let main = tab_manager_get_main_notebook();
*G_ACTIVE_NOTEBOOK.lock().unwrap() =
main.map(glib::thread_guard::ThreadGuard::new);
}
/// Register a tab's notebook + webview so close/reload/duplicate can resolve
/// the tab's own notebook (which may belong to an auxiliary window).
fn register_tab_widgets(tab_id: i32, notebook: &gtk::Notebook, webview: &WebView) {
G_TAB_WIDGETS.lock().unwrap().insert(
tab_id,
(
glib::thread_guard::ThreadGuard::new(notebook.clone()),
glib::thread_guard::ThreadGuard::new(webview.clone()),
),
);
}
/// Look up a tab's notebook by id.
fn tab_notebook(tab_id: i32) -> Option<gtk::Notebook> {
G_TAB_WIDGETS
.lock()
.unwrap()
.get(&tab_id)
.map(|(nb, _)| nb.get_ref().clone())
}
/// Look up a tab's webview by id.
fn tab_webview(tab_id: i32) -> Option<WebView> {
G_TAB_WIDGETS
.lock()
.unwrap()
.get(&tab_id)
.map(|(_, wv)| wv.get_ref().clone())
}
/// Get the main window.
pub fn tab_manager_get_main_window() -> Option<gtk::Window> {
G_MAIN_WINDOW.lock().unwrap().as_ref().map(|g| g.get_ref().clone())
@@ -407,6 +486,10 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
page.show_all();
tab_label.show_all();
// Record the tab's notebook + webview so close/reload/duplicate can
// resolve the tab's own notebook (aux windows have their own notebook).
register_tab_widgets(tab_id, notebook, &webview);
// Apply the current chrome visibility so a tab opened while the
// toolbars are hidden stays hidden. Mirrors the C version
// (tab_manager.c:3235). The tab strip itself is per-notebook
@@ -440,11 +523,23 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
});
// Favicon change — populate the favicon image from the page favicon.
// (cairo::Surface → Pixbuf conversion is not exposed in this gdk version,
// so we keep the favicon widget but leave it empty.)
let _favicon_clone = favicon.clone();
webview.connect_favicon_notify(move |_wv| {
// favicon population skipped (API limitation)
// WebKitGTK's favicon database (enabled in web_context) fetches and
// caches favicons; `notify::favicon` fires when one is ready and
// `webview.favicon()` returns a cairo surface. Mirrors the C
// `on_favicon_changed` (tab_manager.c:948).
let favicon_clone = favicon.clone();
webview.connect_favicon_notify(move |wv| {
if let Some(surface) = wv.favicon() {
if let Ok(img) = cairo::ImageSurface::try_from(surface) {
let w = img.width();
let h = img.height();
if w > 0 && h > 0 {
if let Some(pixbuf) = gdk::pixbuf_get_from_surface(&img, 0, 0, w, h) {
favicon_clone.set_from_pixbuf(Some(&pixbuf));
}
}
}
}
});
// Load progress.
@@ -662,6 +757,7 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
None
};
let is_link = hit_test.context_is_link();
if let Some(url) = url {
let url_for_action = url.clone();
let nb_clone = notebook_clone_ctx.clone();
@@ -674,7 +770,7 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
}
});
let label = if hit_test.context_is_link() {
let label = if is_link {
"Open Link in New Tab"
} else {
"Open Page in New Tab"
@@ -685,6 +781,29 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
None,
);
context_menu.insert(&item, 0);
// For links, also add our own "Open Link in New Window" that
// opens a real top-level window (WebKit's stock item fires the
// `create` signal, which we route to a new *tab*). Mirrors the
// C version (tab_manager.c:1995).
if is_link {
let url_for_win = url.clone();
let nb_win = notebook_clone_ctx.clone();
let win_action = gio::SimpleAction::new("open-in-new-window", None);
win_action.connect_activate(move |_, _| {
if let Some(wv) = tab_manager_get_active_webview(&nb_win) {
if let Some(ctx) = wv.web_context() {
tab_manager_new_window(Some(&url_for_win), &ctx);
}
}
});
let win_item = webkit2gtk::ContextMenuItem::from_gaction(
&win_action,
"Open Link in New Window",
None,
);
context_menu.insert(&win_item, 1);
}
}
// "Toggle Toolbars" — lets the user restore the hidden chrome
@@ -713,15 +832,48 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
}
/// Close a tab by ID.
///
/// The `notebook` argument is only a fallback: the tab's *own* notebook is
/// resolved from the per-tab widget map, because a tab may live in an
/// auxiliary window's notebook (whose page numbering differs from the main
/// notebook). Mirrors the C `tab_manager_close_tab()` (tab_manager.c:3902).
pub fn tab_manager_close_tab(notebook: &gtk::Notebook, tab_id: i32) {
// Resolve the tab's own notebook (fall back to the passed one).
let nb = tab_notebook(tab_id).unwrap_or_else(|| notebook.clone());
let mut state = G_TAB_STATE.lock().unwrap();
if let Some(pos) = state.tabs.iter().position(|t| t.id == tab_id) {
state.tabs.remove(pos);
if pos < notebook.n_pages() as usize {
notebook.remove_page(Some(pos as u32));
// Find the page index within the tab's own notebook.
if let Some(wv) = tab_webview(tab_id) {
let n = nb.n_pages();
for i in 0..n {
if let Some(page) = nb.nth_page(Some(i)) {
if find_webview(&page).map(|w| w == wv).unwrap_or(false) {
nb.remove_page(Some(i));
break;
}
}
}
}
// If no tabs remain and not suppressed, quit.
drop(state);
// Drop the widget map entry.
G_TAB_WIDGETS.lock().unwrap().remove(&tab_id);
// If this was an auxiliary window's last tab, close that window.
let main_nb = tab_manager_get_main_notebook();
let is_main = main_nb.as_ref().map(|m| m == &nb).unwrap_or(false);
if !is_main && nb.n_pages() == 0 {
if let Some(win) = nb.toplevel().and_then(|w| w.downcast::<gtk::Window>().ok()) {
win.close();
}
}
// If no tabs remain anywhere and not suppressed, quit.
let state = G_TAB_STATE.lock().unwrap();
if state.tabs.is_empty() && !state.suppress_quit_on_last_tab {
drop(state);
gtk::main_quit();
}
}
@@ -738,7 +890,24 @@ pub fn tab_manager_close_active() {
}
/// Get the active tab ID.
///
/// Resolves the tab from the active notebook's current page (works across
/// windows), falling back to the last-focused tab id. Mirrors the C
/// `tab_manager_get_active()` (tab_manager.c:3993).
pub fn tab_manager_get_active_tab() -> i32 {
if let Some(nb) = tab_manager_get_active_notebook() {
let cur = nb.current_page().unwrap_or(0);
if let Some(page) = nb.nth_page(Some(cur)) {
if let Some(wv) = find_webview(&page) {
let widgets = G_TAB_WIDGETS.lock().unwrap();
for (id, (_, w)) in widgets.iter() {
if w.get_ref() == &wv {
return *id;
}
}
}
}
}
let state = G_TAB_STATE.lock().unwrap();
state.active_tab_id
}
@@ -966,8 +1135,8 @@ pub fn tab_manager_open_file() {
/// Create a new blank window.
pub fn tab_manager_new_window_blank() {
// Get the context from the main window's active webview.
if let Some(nb) = tab_manager_get_main_notebook() {
// Get the context from the active window's active webview.
if let Some(nb) = tab_manager_get_active_notebook() {
if let Some(wv) = tab_manager_get_active_webview(&nb) {
if let Some(ctx) = wv.web_context() {
tab_manager_new_window(None, &ctx);
@@ -979,7 +1148,11 @@ pub fn tab_manager_new_window_blank() {
}
/// Create a new top-level window with a single tab.
pub fn tab_manager_new_window(url: Option<&str>, ctx: &WebContext) {
///
/// Returns the new window's webview (or `None` if creation failed), so
/// callers such as "Open in New Window" can close the original tab after
/// the move. Mirrors the C `tab_manager_new_window()` (tab_manager.c:1263).
pub fn tab_manager_new_window(url: Option<&str>, ctx: &WebContext) -> Option<WebView> {
let window = gtk::Window::new(gtk::WindowType::Toplevel);
window.set_title("sovereign browser");
window.set_default_size(1024, 768);
@@ -995,15 +1168,65 @@ pub fn tab_manager_new_window(url: Option<&str>, ctx: &WebContext) {
setup_notebook_action_widgets(&notebook);
// Create the first tab.
tab_manager_new_tab(&notebook, ctx, url);
let tab_id = tab_manager_new_tab(&notebook, ctx, url);
if tab_id < 0 {
window.close();
return None;
}
let webview = tab_webview(tab_id);
// On destroy, do not quit the app (only the main window quits).
window.connect_destroy(|_| {
// no-op: aux window close does not quit
// Focus-in updates the active notebook so "active" operations target
// this window. Mirrors the C `on_window_focus_in` (tab_manager.c:1367).
let notebook_focus = notebook.clone();
window.connect_focus_in_event(move |_w, _e| {
set_active_notebook(&notebook_focus);
glib::Propagation::Proceed
});
// On destroy, revert the active notebook to the main window, drop this
// window from the aux list, and remove its tabs from the global state
// (their widgets are gone with the window). Does NOT quit the app.
let window_weak = window.clone();
let notebook_destroy = notebook.clone();
window.connect_destroy(move |_| {
let mut aux = G_AUX_WINDOWS.lock().unwrap();
aux.retain(|g| g.get_ref() != &window_weak);
drop(aux);
// Remove tabs that belonged to this window's notebook.
let orphan_ids: Vec<i32> = {
let widgets = G_TAB_WIDGETS.lock().unwrap();
widgets
.iter()
.filter(|(_, (nb, _))| nb.get_ref() == &notebook_destroy)
.map(|(id, _)| *id)
.collect()
};
if !orphan_ids.is_empty() {
let mut state = G_TAB_STATE.lock().unwrap();
state.tabs.retain(|t| !orphan_ids.contains(&t.id));
drop(state);
let mut widgets = G_TAB_WIDGETS.lock().unwrap();
for id in &orphan_ids {
widgets.remove(id);
}
}
revert_active_notebook_to_main();
});
// Register this window as an auxiliary window.
G_AUX_WINDOWS
.lock()
.unwrap()
.push(glib::thread_guard::ThreadGuard::new(window.clone()));
// This new window is now the active one.
set_active_notebook(&notebook);
window.show_all();
window.present();
webview
}
/// Show the tab context menu (right-click on a tab label).
@@ -1046,30 +1269,15 @@ fn show_tab_context_menu(notebook: &gtk::Notebook, tab_id: i32, event: &gdk::Eve
menu.append(&gtk::SeparatorMenuItem::new());
let item_new_win = gtk::MenuItem::with_label("Open in New Window");
let notebook_clone_nw = notebook.clone();
item_new_win.connect_activate(move |_| {
let state = G_TAB_STATE.lock().unwrap();
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
drop(state);
if let Some(wv) = tab_manager_get_active_webview(&notebook_clone_nw) {
if let Some(ctx) = wv.web_context() {
tab_manager_new_window(url.as_deref(), &ctx);
}
}
// Moves the tab to a new window (closes the original).
tab_manager_open_in_new_window(tab_id);
});
menu.append(&item_new_win);
let item_dup = gtk::MenuItem::with_label("Duplicate Tab");
let notebook_clone5 = notebook.clone();
item_dup.connect_activate(move |_| {
let state = G_TAB_STATE.lock().unwrap();
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
drop(state);
if let Some(wv) = tab_manager_get_active_webview(&notebook_clone5) {
if let Some(ctx) = wv.web_context() {
tab_manager_new_tab(&notebook_clone5, &ctx, url.as_deref());
}
}
tab_manager_duplicate(tab_id);
});
menu.append(&item_dup);
@@ -1079,6 +1287,23 @@ fn show_tab_context_menu(notebook: &gtk::Notebook, tab_id: i32, event: &gdk::Eve
});
menu.append(&item_reload);
menu.append(&gtk::SeparatorMenuItem::new());
// Download Website — mirrors the C context menu (tab_manager.c:818).
let item_dl = gtk::MenuItem::with_label("Download Website");
item_dl.connect_activate(move |_| {
let url = tab_webview(tab_id)
.and_then(|wv| wv.uri().map(|u| u.to_string()))
.unwrap_or_default();
if !url.is_empty() {
let _ = crate::site_downloader::site_downloader_download(
&url,
"/tmp/sovereign_download",
);
}
});
menu.append(&item_dl);
menu.show_all();
menu.popup_at_pointer(Some(event));
}
@@ -1109,51 +1334,62 @@ pub fn tab_manager_close_to_right(notebook: &gtk::Notebook, tab_id: i32) {
/// Reload a specific tab by ID.
pub fn tab_manager_reload_tab(tab_id: i32) {
if let Some(nb) = tab_manager_get_active_notebook() {
let n = nb.n_pages();
for i in 0..n {
if let Some(page) = nb.nth_page(Some(i)) {
if let Some(wv) = find_webview(&page) {
// Match by tab id via the state.
let state = G_TAB_STATE.lock().unwrap();
let matches = state.tabs.get(i as usize).map(|t| t.id == tab_id).unwrap_or(false);
drop(state);
if matches {
wv.reload();
return;
}
}
}
}
if let Some(wv) = tab_webview(tab_id) {
wv.reload_bypass_cache();
}
}
/// Duplicate a tab by ID.
/// Duplicate a tab by ID (opens the same URL in a new tab in the same window).
pub fn tab_manager_duplicate(tab_id: i32) {
let state = G_TAB_STATE.lock().unwrap();
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
drop(state);
if let Some(nb) = tab_manager_get_active_notebook() {
if let Some(wv) = tab_manager_get_active_webview(&nb) {
if let Some(ctx) = wv.web_context() {
tab_manager_new_tab(&nb, &ctx, url.as_deref());
}
// Duplicate into the tab's own notebook (its window).
let nb = match tab_notebook(tab_id) {
Some(n) => n,
None => return,
};
if let Some(wv) = tab_webview(tab_id) {
if let Some(ctx) = wv.web_context() {
tab_manager_new_tab(&nb, &ctx, url.as_deref());
}
}
}
/// Open a tab in a new window by ID.
///
/// This *moves* the tab: the URL is opened in a brand-new top-level window
/// and the original tab is closed. Mirrors the C
/// `tab_manager_open_in_new_window()` (tab_manager.c:4141).
pub fn tab_manager_open_in_new_window(tab_id: i32) {
let state = G_TAB_STATE.lock().unwrap();
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
drop(state);
if let Some(nb) = tab_manager_get_active_notebook() {
if let Some(wv) = tab_manager_get_active_webview(&nb) {
if let Some(ctx) = wv.web_context() {
tab_manager_new_window(url.as_deref(), &ctx);
}
}
// Capture the URL and the tab's own notebook before moving.
let (url, nb) = {
let state = G_TAB_STATE.lock().unwrap();
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
(url, tab_notebook(tab_id))
};
let nb = match nb {
Some(n) => n,
None => return,
};
// Get a WebContext from the tab's webview (or the notebook's active one).
let ctx = tab_webview(tab_id)
.and_then(|wv| wv.web_context())
.or_else(|| tab_manager_get_active_webview(&nb).and_then(|wv| wv.web_context()));
let ctx = match ctx {
Some(c) => c,
None => return,
};
// Open the URL in a new window. If creation fails, keep the original tab.
if tab_manager_new_window(url.as_deref(), &ctx).is_none() {
return;
}
// Close the original tab — "Open in New Window" moves the tab rather
// than duplicating it.
tab_manager_close_tab(&nb, tab_id);
}
/// Build the hamburger menu button.
+2 -2
View File
@@ -1,7 +1,7 @@
//! Version information for sovereign_browser
/// The current version of sovereign_browser (with leading 'v').
pub const VERSION: &str = "v0.0.7";
pub const VERSION: &str = "v0.0.8";
/// Major version number.
pub const VERSION_MAJOR: u32 = 0;
@@ -10,4 +10,4 @@ pub const VERSION_MAJOR: u32 = 0;
pub const VERSION_MINOR: u32 = 0;
/// Patch version number.
pub const VERSION_PATCH: u32 = 7;
pub const VERSION_PATCH: u32 = 8;
+7
View File
@@ -27,6 +27,13 @@ pub fn web_context_create() -> WebContext {
sec_mgr.register_uri_scheme_as_secure("nostr");
}
// Enable the favicon database so `notify::favicon` fires and
// `webview.favicon()` returns a surface. Without a directory the
// database is disabled and tab favicons never appear. Mirrors the C
// `webkit_web_context_set_favicon_database_directory()` (web_context.c:237).
let fav_dir = std::env::temp_dir().join("sovereign_browser-favicons");
ctx.set_favicon_database_directory(fav_dir.to_str());
ctx
}
+8 -1
View File
@@ -12,7 +12,7 @@ pub fn webkit_data_get_dir() -> PathBuf {
}
/// Configure the website data manager for a profile.
pub fn webkit_data_configure(_web_context: &WebContext, pubkey_hex: &str) {
pub fn webkit_data_configure(web_context: &WebContext, pubkey_hex: &str) {
let data_dir = webkit_data_get_dir().join(pubkey_hex);
std::fs::create_dir_all(&data_dir).ok();
@@ -24,4 +24,11 @@ pub fn webkit_data_configure(_web_context: &WebContext, pubkey_hex: &str) {
let _manager = WebsiteDataManager::builder()
.local_storage_directory(data_dir.join("localstorage").to_str().unwrap_or(""))
.build();
// Per-user favicon database so a switched user doesn't see the previous
// user's visited-site icons in the tab strip. Mirrors the C
// `webkit_web_context_set_favicon_database_directory()` (web_context.c:237).
let fav_dir = data_dir.join("favicons");
std::fs::create_dir_all(&fav_dir).ok();
web_context.set_favicon_database_directory(fav_dir.to_str());
}