diff --git a/ui/src/components/app/document_title.rs b/ui/src/components/app/document_title.rs
index 70f14b27..3f799bc7 100644
--- a/ui/src/components/app/document_title.rs
+++ b/ui/src/components/app/document_title.rs
@@ -81,6 +81,39 @@ fn set_document_title(title: &str) {
}
}
+/// Send River's favicon to the parent shell via postMessage.
+/// The shell page uses a generic Freenet favicon by default; this overrides
+/// it with River's logo so the browser tab shows the correct branding.
+fn send_favicon_to_shell() {
+ let Some(window) = web_sys::window() else {
+ return;
+ };
+ // Read the resolved href from the element in our document.
+ // Dioxus renders this from asset!(), so the browser has already resolved it
+ // to an absolute URL that the parent shell can load.
+ let href = window
+ .document()
+ .and_then(|doc| doc.query_selector(r#"link[rel="icon"]"#).ok().flatten())
+ .and_then(|el| el.get_attribute("href"));
+ let Some(href) = href else {
+ return;
+ };
+ let msg = js_sys::Object::new();
+ let _ = js_sys::Reflect::set(
+ &msg,
+ &JsValue::from_str("__freenet_shell__"),
+ &JsValue::TRUE,
+ );
+ let _ = js_sys::Reflect::set(
+ &msg,
+ &JsValue::from_str("type"),
+ &JsValue::from_str("favicon"),
+ );
+ let _ = js_sys::Reflect::set(&msg, &JsValue::from_str("href"), &JsValue::from_str(&href));
+ let target = window.parent().ok().flatten().unwrap_or(window);
+ let _ = target.post_message(&msg, "*");
+}
+
/// Get the current room name (decrypted if private)
fn get_current_room_name() -> Option {
let current_room = CURRENT_ROOM.read();
@@ -257,6 +290,10 @@ pub fn init_document_title_manager() {
// Set initial title
update_document_title();
+ // Send our favicon to the parent shell so the browser tab shows the River logo
+ // instead of the default Freenet favicon. The shell accepts this via __freenet_shell__.
+ send_favicon_to_shell();
+
// Add visibility change listener
if let Some(document) = web_sys::window().and_then(|w| w.document()) {
let callback = Closure::wrap(Box::new(move || {