mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
fix(desktop): Link clicking within App (#8493)
This commit is contained in:
1 parent
b2836c5a13
commit
f6f2e2d7c0
1 file changed
+230
-29
+230
-29
@@ -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(¤t_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");
|
||||
|
||||
Reference in new issue
Block a user