diff --git a/android/src/app.rs b/android/src/app.rs index fc84d26..f61f441 100644 --- a/android/src/app.rs +++ b/android/src/app.rs @@ -2026,7 +2026,7 @@ impl SleekApp { } #[cfg(not(target_os = "android"))] { - self.state.status_line = "Browser opened for sign-in — look for the Chromium window (Alt+Tab if covered).".into(); + self.state.status_line = "Browser opened for sign-in — look for the browser window (Alt+Tab if covered).".into(); // Fullscreen Sleek sits above everything on Fluxbox/VNC; yield first. self.yield_to_oauth_browser(ctx); } @@ -3057,15 +3057,10 @@ impl eframe::App for SleekApp { ) .show_separator_line(false) .show(ctx, |ui| { - let panel_h = ui.available_height(); - ScrollArea::vertical() - .auto_shrink([false, false]) - .max_height(panel_h) - .id_salt("chats_master_scroll") - .show(ui, |ui| { - let act = ui::chats_tab(ui, &th, &mut self.state); - self.handle_chats_action(act); - }); + // chats_tab keeps the join card pinned and scrolls only + // the conversation list below it. + let act = ui::chats_tab(ui, &th, &mut self.state, true); + self.handle_chats_action(act); }); } @@ -3107,6 +3102,14 @@ impl eframe::App for SleekApp { } } + // Chats owns its own list scroll area so the join card remains + // pinned instead of moving with the outer page scroll. + if registered && self.state.tab == Tab::Chats { + let act = ui::chats_tab(ui, &th, &mut self.state, true); + self.handle_chats_action(act); + return; + } + let panel_h = ui.available_height(); ScrollArea::vertical() .auto_shrink([false, false]) @@ -3119,10 +3122,9 @@ impl eframe::App for SleekApp { // Route::Chat is rendered above without this outer scroll. if registered { match self.state.tab { - Tab::Chats => { - let act = ui::chats_tab(ui, &th, &mut self.state); - self.handle_chats_action(act); - } + // Handled by the early return above (chats_tab has its own + // scroll area); unreachable here whenever `registered`. + Tab::Chats => {} Tab::Discover => { match ui::discover_tab(ui, &th, &mut self.state) { DiscoverAction::None => {} @@ -3164,7 +3166,19 @@ impl eframe::App for SleekApp { ui.add_space(sp.xl * 2.0); title(ui, &th, "Connecting"); ui.add_space(sp.md); - dim_label(ui, &th, &self.state.status_line); + let mut status_lines = self.state.status_line.lines(); + if let Some(message) = status_lines.next() { + dim_label(ui, &th, message); + } + for line in status_lines { + let url = line.trim(); + if url.starts_with("http://") || url.starts_with("https://") { + ui.hyperlink_to("Open login link", url); + ui.label(egui::RichText::new(url).small()); + } else if !url.is_empty() { + dim_label(ui, &th, url); + } + } ui.add_space(sp.lg); if button(ui, &th, "Cancel").clicked() { self.do_intentional_disconnect(); diff --git a/android/src/auth.rs b/android/src/auth.rs index e9c2db2..e751780 100644 --- a/android/src/auth.rs +++ b/android/src/auth.rs @@ -539,7 +539,7 @@ fn open_system_browser(url: &str) -> Result<()> { } } -/// Desktop open: prefer `$BROWSER`, then Chromium (Codespace VNC), then `open`. +/// Desktop open: prefer `$BROWSER`, then the configured URL handler, then Chromium. /// /// Codespace / desktop-lite has no default browser; Chromium from nix needs /// `--no-sandbox` because user namespaces are blocked. @@ -558,7 +558,25 @@ fn open_desktop_browser(url: &str) -> Result<()> { } } - // 2) Chromium / Chrome with container-friendly flags (VNC / Docker). + // 2) Prefer the desktop URL handler. This uses the user's configured + // browser (for example the installed Chrome Flatpak), whereas probing + // Chromium binaries first can select a browser that is unavailable or + // cannot start in restricted VM environments. + if let Ok(opener) = std::env::var("SLEEK_XDG_OPEN") { + if !opener.is_empty() { + match Command::new(&opener).arg(url).spawn() { + Ok(_) => return Ok(()), + Err(e) => log::warn!("SLEEK_XDG_OPEN={opener} failed: {e}"), + } + } + } else if which_bin("xdg-open").is_some() { + match Command::new("xdg-open").arg(url).spawn() { + Ok(_) => return Ok(()), + Err(e) => log::debug!("xdg-open failed: {e}"), + } + } + + // 3) Chromium / Chrome with container-friendly flags (VNC / Docker). let chromes = ["chromium", "chromium-browser", "google-chrome", "google-chrome-stable"]; for bin in chromes { if which_bin(bin).is_some() { @@ -582,7 +600,7 @@ fn open_desktop_browser(url: &str) -> Result<()> { } } - // 3) Firefox (nix profile); sandbox often fails in Codespaces. + // 4) Firefox (nix profile); sandbox often fails in Codespaces. if which_bin("firefox").is_some() { match Command::new("firefox") .env("MOZ_DISABLE_CONTENT_SANDBOX", "1") @@ -595,7 +613,7 @@ fn open_desktop_browser(url: &str) -> Result<()> { } } - // 4) Generic opener (xdg-open / open). + // 5) Generic opener (xdg-open / open). open::that(url).context("open browser") } @@ -623,9 +641,9 @@ pub async fn bluesky_login_mobile( let url = login_url(auth_broker, handle, None); log::info!("bluesky mobile login url: {url}"); match open_system_browser(&url) { - Ok(()) => on_status( - "Browser opened — complete Bluesky sign-in; Sleek will resume via freeq://".into(), - ), + Ok(()) => on_status(format!( + "Browser opened — complete Bluesky sign-in; Sleek will resume via freeq://\n{url}" + )), Err(e) => { log::warn!("failed to open browser: {e}; url={url}"); on_status(format!( @@ -664,7 +682,7 @@ pub async fn bluesky_login_loopback( // backend and used to leave the UI stuck on "Opening browser…". log::info!("bluesky login url: {url}"); match open_system_browser(&url) { - Ok(()) => on_status("Browser opened — complete sign-in, then return here".into()), + Ok(()) => on_status(format!("Browser opened — complete sign-in, then return here\n{url}")), Err(e) => { log::warn!("failed to open browser: {e}; url={url}"); on_status(format!( diff --git a/android/src/ui/chats.rs b/android/src/ui/chats.rs index 4cc193d..2302218 100644 --- a/android/src/ui/chats.rs +++ b/android/src/ui/chats.rs @@ -12,11 +12,14 @@ pub enum ChatsAction { Join(String), } -pub fn chats_tab(ui: &mut egui::Ui, th: &Theme, state: &mut AppState) -> ChatsAction { +pub fn chats_tab( + ui: &mut egui::Ui, + th: &Theme, + state: &mut AppState, + pinned_join: bool, +) -> ChatsAction { let mut action = ChatsAction::None; let sp = &th.spacing; - let p = &th.palette; - // Quick join card(ui, th, |ui| { title_2(ui, th, "Join channel"); @@ -48,6 +51,25 @@ pub fn chats_tab(ui: &mut egui::Ui, th: &Theme, state: &mut AppState) -> ChatsAc }); ui.add_space(sp.md); + if pinned_join { + // In the wide master panel, keep the join controls pinned and scroll + // only the conversation list. The narrow layout already has one + // outer scroll area, so do not nest another scrollbar there. + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .id_salt("chats_conversation_scroll") + .show(ui, |ui| action = chats_list(ui, th, state)); + } else { + action = chats_list(ui, th, state); + } + + action +} + +pub fn chats_list(ui: &mut egui::Ui, th: &Theme, state: &mut AppState) -> ChatsAction { + let mut action = ChatsAction::None; + let sp = &th.spacing; + let p = &th.palette; let conversations: Vec<(String, String, bool, u32, String, String, i64)> = state .sorted_conversations() .into_iter() diff --git a/android/src/ui/connect.rs b/android/src/ui/connect.rs index cc98009..64d1631 100644 --- a/android/src/ui/connect.rs +++ b/android/src/ui/connect.rs @@ -9,6 +9,27 @@ use vidya::{ use crate::state::{AppState, ConnectMode, ConnectionState}; use crate::ui::widgets::{avatar_circle, card, text_edit_clipboard_menu}; +fn oauth_status(ui: &mut egui::Ui, th: &Theme, status: &str) { + // OAuth fallback messages append the broker URL on a separate line. Keep + // it clickable so sign-in remains possible when automatic browser launch + // is unavailable. + let mut lines = status.lines(); + if let Some(message) = lines.next() { + dim_label(ui, th, message); + } + for line in lines { + let url = line.trim(); + if url.starts_with("http://") || url.starts_with("https://") { + ui.horizontal_wrapped(|ui| { + ui.hyperlink_to("Open login link", url); + ui.label(egui::RichText::new(url).small()); + }); + } else if !url.is_empty() { + dim_label(ui, th, url); + } + } +} + pub fn connect_screen(ui: &mut egui::Ui, th: &Theme, state: &mut AppState) -> ConnectAction { let mut action = ConnectAction::None; let sp = &th.spacing; @@ -51,7 +72,7 @@ pub fn connect_screen(ui: &mut egui::Ui, th: &Theme, state: &mut AppState) -> Co } if !state.status_line.is_empty() && loading { - dim_label(ui, th, &state.status_line); + oauth_status(ui, th, &state.status_line); ui.add_space(sp.sm); }