fix(desktop): Link clicking within App (#8493)

This commit is contained in:
Justin Tahara authored and GitHub committed 2026-02-17 18:17:16 +00:00
1 parent b2836c5a13
commit f6f2e2d7c0
1 file changed
+230 -29
+230 -29
View File
@@ -20,9 +20,9 @@ use tauri::Wry;
use tauri::{
webview::PageLoadPayload, AppHandle, Manager, Webview, WebviewUrl, WebviewWindowBuilder,
};
use url::Url;
#[cfg(target_os = "macos")]
use tokio::time::sleep;
use url::Url;
#[cfg(target_os = "macos")]
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
@@ -40,6 +40,136 @@ const TRAY_MENU_OPEN_APP_ID: &str = "tray_open_app";
const TRAY_MENU_OPEN_CHAT_ID: &str = "tray_open_chat";
const TRAY_MENU_SHOW_IN_BAR_ID: &str = "tray_show_in_menu_bar";
const TRAY_MENU_QUIT_ID: &str = "tray_quit";
const CHAT_LINK_INTERCEPT_SCRIPT: &str = r##"
(() => {
if (window.__ONYX_CHAT_LINK_INTERCEPT_INSTALLED__) {
return;
}
window.__ONYX_CHAT_LINK_INTERCEPT_INSTALLED__ = true;
function isChatSessionPage() {
try {
const currentUrl = new URL(window.location.href);
return (
currentUrl.pathname.startsWith("/app") &&
currentUrl.searchParams.has("chatId")
);
} catch {
return false;
}
}
function getAllowedNavigationUrl(rawUrl) {
try {
const parsed = new URL(String(rawUrl), window.location.href);
const scheme = parsed.protocol.toLowerCase();
if (!["http:", "https:", "mailto:", "tel:"].includes(scheme)) {
return null;
}
return parsed;
} catch {
return null;
}
}
async function openWithTauri(url) {
try {
const invoke =
window.__TAURI__?.core?.invoke || window.__TAURI_INTERNALS__?.invoke;
if (typeof invoke !== "function") {
return false;
}
await invoke("open_in_browser", { url });
return true;
} catch {
return false;
}
}
function handleChatNavigation(rawUrl) {
const parsedUrl = getAllowedNavigationUrl(rawUrl);
if (!parsedUrl) {
return false;
}
const safeUrl = parsedUrl.toString();
const scheme = parsedUrl.protocol.toLowerCase();
if (scheme === "mailto:" || scheme === "tel:") {
void openWithTauri(safeUrl).then((opened) => {
if (!opened) {
window.location.assign(safeUrl);
}
});
return true;
}
window.location.assign(safeUrl);
return true;
}
document.addEventListener(
"click",
(event) => {
if (!isChatSessionPage() || event.defaultPrevented) {
return;
}
const element = event.target;
if (!(element instanceof Element)) {
return;
}
const anchor = element.closest("a");
if (!(anchor instanceof HTMLAnchorElement)) {
return;
}
const target = (anchor.getAttribute("target") || "").toLowerCase();
if (target !== "_blank") {
return;
}
const href = anchor.getAttribute("href");
if (!href || href.startsWith("#")) {
return;
}
if (!handleChatNavigation(href)) {
return;
}
event.preventDefault();
event.stopPropagation();
},
true
);
const nativeWindowOpen = window.open;
window.open = function(url, target, features) {
const resolvedTarget = typeof target === "string" ? target.toLowerCase() : "";
const shouldNavigateInPlace = resolvedTarget === "" || resolvedTarget === "_blank";
if (
isChatSessionPage() &&
shouldNavigateInPlace &&
url != null &&
String(url).length > 0
) {
if (!handleChatNavigation(url)) {
return null;
}
return null;
}
if (typeof nativeWindowOpen === "function") {
return nativeWindowOpen.call(window, url, target, features);
}
return null;
};
})();
"##;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
@@ -177,22 +307,7 @@ fn trigger_new_window(app: &AppHandle) {
}
fn open_docs() {
let url = "https://docs.onyx.app";
#[cfg(target_os = "macos")]
{
let _ = Command::new("open").arg(url).status();
}
#[cfg(target_os = "linux")]
{
let _ = Command::new("xdg-open").arg(url).status();
}
#[cfg(target_os = "windows")]
{
let _ = Command::new("rundll32")
.arg("url.dll,FileProtocolHandler")
.arg(url)
.status();
}
let _ = open_in_default_browser("https://docs.onyx.app");
}
fn open_settings(app: &AppHandle) {
@@ -219,6 +334,68 @@ fn open_settings(app: &AppHandle) {
}
}
fn same_origin(left: &Url, right: &Url) -> bool {
left.scheme() == right.scheme()
&& left.host_str() == right.host_str()
&& left.port_or_known_default() == right.port_or_known_default()
}
fn is_chat_session_url(url: &Url) -> bool {
url.path().starts_with("/app") && url.query_pairs().any(|(key, _)| key == "chatId")
}
fn should_open_in_external_browser(current_url: &Url, destination_url: &Url) -> bool {
if !is_chat_session_url(current_url) {
return false;
}
match destination_url.scheme() {
"mailto" | "tel" => true,
"http" | "https" => !same_origin(current_url, destination_url),
_ => false,
}
}
fn open_in_default_browser(url: &str) -> bool {
#[cfg(target_os = "macos")]
{
return Command::new("open").arg(url).status().is_ok();
}
#[cfg(target_os = "linux")]
{
return Command::new("xdg-open").arg(url).status().is_ok();
}
#[cfg(target_os = "windows")]
{
return Command::new("rundll32")
.arg("url.dll,FileProtocolHandler")
.arg(url)
.status()
.is_ok();
}
#[allow(unreachable_code)]
false
}
#[tauri::command]
fn open_in_browser(url: String) -> Result<(), String> {
let parsed_url = Url::parse(&url).map_err(|_| "Invalid URL".to_string())?;
match parsed_url.scheme() {
"http" | "https" | "mailto" | "tel" => {}
_ => return Err("Unsupported URL scheme".to_string()),
}
if open_in_default_browser(parsed_url.as_str()) {
Ok(())
} else {
Err("Failed to open URL in default browser".to_string())
}
}
fn inject_chat_link_intercept(webview: &Webview) {
let _ = webview.eval(CHAT_LINK_INTERCEPT_SCRIPT);
}
// ============================================================================
// Tauri Commands
// ============================================================================
@@ -240,8 +417,8 @@ struct BootstrapState {
fn get_bootstrap_state(state: tauri::State<ConfigState>) -> BootstrapState {
let server_url = state.config.read().unwrap().server_url.clone();
let config_initialized = *state.config_initialized.read().unwrap();
let config_exists = config_initialized
&& get_config_path().map(|path| path.exists()).unwrap_or(false);
let config_exists =
config_initialized && get_config_path().map(|path| path.exists()).unwrap_or(false);
BootstrapState {
server_url,
@@ -462,7 +639,13 @@ fn setup_app_menu(app: &AppHandle) -> tauri::Result<()> {
true,
Some("CmdOrCtrl+Shift+N"),
)?;
let settings_item = MenuItem::with_id(app, "open_settings", "Settings...", true, Some("CmdOrCtrl+Comma"))?;
let settings_item = MenuItem::with_id(
app,
"open_settings",
"Settings...",
true,
Some("CmdOrCtrl+Comma"),
)?;
let docs_item = MenuItem::with_id(app, "open_docs", "Onyx Documentation", true, None::<&str>)?;
if let Some(file_menu) = menu
@@ -501,13 +684,7 @@ fn setup_app_menu(app: &AppHandle) -> tauri::Result<()> {
}
fn build_tray_menu(app: &AppHandle) -> tauri::Result<Menu<Wry>> {
let open_app = MenuItem::with_id(
app,
TRAY_MENU_OPEN_APP_ID,
"Open Onyx",
true,
None::<&str>,
)?;
let open_app = MenuItem::with_id(app, TRAY_MENU_OPEN_APP_ID, "Open Onyx", true, None::<&str>)?;
let open_chat = MenuItem::with_id(
app,
TRAY_MENU_OPEN_CHAT_ID,
@@ -598,6 +775,27 @@ fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(
tauri::plugin::Builder::<Wry>::new("chat-external-navigation-handler")
.on_navigation(|webview, destination_url| {
let Ok(current_url) = webview.url() else {
return true;
};
if should_open_in_external_browser(&current_url, destination_url) {
if !open_in_default_browser(destination_url.as_str()) {
eprintln!(
"Failed to open external URL in default browser: {}",
destination_url
);
}
return false;
}
true
})
.build(),
)
.plugin(tauri_plugin_window_state::Builder::default().build())
.manage(ConfigState {
config: RwLock::new(config),
@@ -609,6 +807,7 @@ fn main() {
get_bootstrap_state,
set_server_url,
get_config_path_cmd,
open_in_browser,
open_config_file,
open_config_directory,
navigate_to,
@@ -661,10 +860,12 @@ fn main() {
Ok(())
})
.on_page_load(|_webview: &Webview, _payload: &PageLoadPayload| {
.on_page_load(|webview: &Webview, _payload: &PageLoadPayload| {
inject_chat_link_intercept(webview);
// Re-inject titlebar after every navigation/page load (macOS only)
#[cfg(target_os = "macos")]
let _ = _webview.eval(TITLEBAR_SCRIPT);
let _ = webview.eval(TITLEBAR_SCRIPT);
})
.run(tauri::generate_context!())
.expect("error while running tauri application");