From d04b9633dc88f06a1fca0feae048e9d57e255a19 Mon Sep 17 00:00:00 2001 From: cyph3rasi Date: Wed, 15 Jul 2026 18:38:32 -0700 Subject: [PATCH] Remove the node switcher UI, native title-bar integration, registry persistence, and dedicated dependencies Hop-State: A_06FPH4TF27M92NB8GVKNGKG Hop-Proposal: R_06FPH4T0F0KDZV6BB8BQ0J8 Hop-Task: T_06FPH2162KK06P10J2MY3SR Hop-Attempt: AT_06FPH2162H2HW7STZZWQFJR --- src-tauri/Cargo.lock | 25 ----- src-tauri/Cargo.toml | 18 ---- src-tauri/src/lib.rs | 247 +------------------------------------------ src/App.css | 11 +- src/App.tsx | 27 ++--- 5 files changed, 13 insertions(+), 315 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fec0d91..7396846 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2267,17 +2267,9 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.1", "block2", - "libc", "objc2", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", "objc2-foundation", - "objc2-quartz-core", ] [[package]] @@ -2297,7 +2289,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.13.1", "objc2", "objc2-foundation", ] @@ -2358,19 +2349,6 @@ dependencies = [ "objc2-core-graphics", ] -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -3699,9 +3677,6 @@ name = "synapsis-desktop" version = "0.1.0" dependencies = [ "http", - "objc2", - "objc2-app-kit", - "objc2-foundation", "reqwest 0.12.28", "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6e5494c..d221f7f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,23 +24,5 @@ serde_json = "1" reqwest = { version = "0.12", features = ["cookies", "json", "rustls-tls"] } tauri-plugin-notification = "2" -[target.'cfg(target_os = "macos")'.dependencies] -objc2 = "0.6" -objc2-app-kit = { version = "0.3", features = [ - "NSButton", - "NSCell", - "NSControl", - "NSLayoutConstraint", - "NSMenu", - "NSMenuItem", - "NSPopUpButton", - "NSResponder", - "NSTitlebarAccessoryViewController", - "NSView", - "NSViewController", - "NSWindow", -] } -objc2-foundation = { version = "0.3", features = ["NSArray", "NSString"] } - [dev-dependencies] http = "1" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d3404f5..33960ea 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,5 @@ use reqwest::{Client, Method, StatusCode, Url}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::fs; -use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Mutex; use std::time::Duration; @@ -12,15 +10,6 @@ struct AppState { client: Client, notification_capability_sequence: AtomicU64, entry_url: Mutex>, - node_registry: Mutex, - node_registry_path: Mutex>, -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct NodeRegistry { - nodes: Vec, - active: Option, } #[derive(Debug, Default, Deserialize, Serialize)] @@ -109,56 +98,6 @@ fn endpoint(base: &Url, path: &str) -> Result { .map_err(|_| "The selected node address is invalid.".into()) } -fn load_node_registry(path: &Path) -> NodeRegistry { - fs::read(path) - .ok() - .and_then(|bytes| serde_json::from_slice(&bytes).ok()) - .unwrap_or_default() -} - -fn persist_node_registry(state: &AppState, registry: &NodeRegistry) -> Result<(), String> { - let path = state - .node_registry_path - .lock() - .map_err(|_| "Couldn’t access the saved node list.".to_string())? - .clone(); - let Some(path) = path else { - return Ok(()); - }; - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(|error| format!("Couldn’t save the node list: {error}"))?; - } - let bytes = serde_json::to_vec_pretty(registry) - .map_err(|error| format!("Couldn’t encode the node list: {error}"))?; - fs::write(path, bytes).map_err(|error| format!("Couldn’t save the node list: {error}")) -} - -fn remember_node(state: &AppState, node: &Url) -> Result { - let node_url = node.as_str().to_owned(); - let snapshot = { - let mut registry = state - .node_registry - .lock() - .map_err(|_| "Couldn’t access the saved node list.".to_string())?; - registry.nodes.retain(|saved| saved != &node_url); - registry.nodes.push(node_url.clone()); - registry.active = Some(node_url); - registry.clone() - }; - persist_node_registry(state, &snapshot)?; - Ok(snapshot) -} - -#[tauri::command] -fn get_saved_nodes(state: State<'_, AppState>) -> Result { - state - .node_registry - .lock() - .map(|registry| registry.clone()) - .map_err(|_| "Couldn’t access the saved node list.".to_string()) -} - async fn decode_response(response: reqwest::Response) -> Result { let status = response.status(); let bytes = response.bytes().await.map_err(|error| error.to_string())?; @@ -343,13 +282,6 @@ async fn open_web_app( ); } } - let registry = remember_node(&state, &base)?; - #[cfg(target_os = "macos")] - macos_titlebar::install(&webview, ®istry)?; - navigate_to_node(&state, &webview, &base) -} - -fn navigate_to_node(state: &AppState, webview: &WebviewWindow, base: &Url) -> Result<(), String> { let origin = base.origin().ascii_serialization(); let capability_id = state .notification_capability_sequence @@ -359,7 +291,7 @@ fn navigate_to_node(state: &AppState, webview: &WebviewWindow, base: &Url) -> Re .add_capability(capability) .map_err(|error| format!("Couldn’t enable native notifications for this node: {error}"))?; webview - .navigate(base.clone()) + .navigate(base) .map_err(|error| format!("Couldn’t open the Synapsis web app: {error}")) } @@ -377,145 +309,6 @@ fn remote_notification_capability(origin: &str, sequence: u64) -> String { .to_string() } -#[cfg(target_os = "macos")] -mod macos_titlebar { - use super::*; - use objc2::rc::Retained; - use objc2::runtime::AnyObject; - use objc2::{define_class, msg_send, sel, MainThreadOnly}; - use objc2_app_kit::{ - NSControlStateValueOn, NSLayoutAttribute, NSMenu, NSMenuItem, NSPopUpButton, - NSTitlebarAccessoryViewController, NSView, NSWindow, - }; - use objc2_foundation::{MainThreadMarker, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString}; - use std::sync::OnceLock; - - static APP_HANDLE: OnceLock = OnceLock::new(); - - define_class!( - #[unsafe(super = NSTitlebarAccessoryViewController)] - #[thread_kind = MainThreadOnly] - #[ivars = ()] - struct NodeTitlebarController; - - impl NodeTitlebarController { - #[unsafe(method(selectNode:))] - fn select_node(&self, sender: &NSMenuItem) { - let Some(value) = sender - .representedObject() - .and_then(|object| object.downcast::().ok()) - .map(|string| string.to_string()) - else { - return; - }; - let Some(app) = APP_HANDLE.get() else { - return; - }; - let Some(webview) = app.get_webview_window("main") else { - return; - }; - let state = app.state::(); - - if value == "__add_node__" { - let Ok(saved_entry_url) = state.entry_url.lock() else { - return; - }; - let Some(mut entry_url) = saved_entry_url.clone() else { - return; - }; - entry_url.set_query(Some("addNode=1")); - let _ = webview.navigate(entry_url); - return; - } - - let Ok(base) = normalize_node(&value) else { - return; - }; - let Ok(registry) = remember_node(&state, &base) else { - return; - }; - let _ = install(&webview, ®istry); - let _ = navigate_to_node(&state, &webview, &base); - } - } - - unsafe impl NSObjectProtocol for NodeTitlebarController {} - ); - - impl NodeTitlebarController { - fn new(mtm: MainThreadMarker) -> Retained { - let this = Self::alloc(mtm).set_ivars(()); - unsafe { msg_send![super(this), init] } - } - } - - pub fn initialize(app: &tauri::AppHandle) { - let _ = APP_HANDLE.set(app.clone()); - } - - pub fn install(webview: &WebviewWindow, registry: &NodeRegistry) -> Result<(), String> { - let mtm = MainThreadMarker::new() - .ok_or_else(|| "The node switcher must be created on the main thread.".to_string())?; - let controller = NodeTitlebarController::new(mtm); - let menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), &NSString::from_str("Nodes")); - - for node_url in ®istry.nodes { - let Ok(url) = Url::parse(node_url) else { - continue; - }; - let title = url.host_str().unwrap_or(node_url); - let item = unsafe { - menu.addItemWithTitle_action_keyEquivalent( - &NSString::from_str(title), - Some(sel!(selectNode:)), - &NSString::new(), - ) - }; - unsafe { - item.setTarget(Some(&*controller as &AnyObject)); - item.setRepresentedObject(Some(&*NSString::from_str(node_url) as &AnyObject)); - } - if registry.active.as_deref() == Some(node_url) { - item.setState(NSControlStateValueOn); - } - } - - if !registry.nodes.is_empty() { - menu.addItem(&NSMenuItem::separatorItem(mtm)); - } - let add_item = unsafe { - menu.addItemWithTitle_action_keyEquivalent( - &NSString::from_str("Add Node…"), - Some(sel!(selectNode:)), - &NSString::new(), - ) - }; - unsafe { - add_item.setTarget(Some(&*controller as &AnyObject)); - add_item.setRepresentedObject(Some(&*NSString::from_str("__add_node__") as &AnyObject)); - } - let button = - NSPopUpButton::pullDownButtonWithTitle_menu(&NSString::from_str("Nodes"), &menu); - button.setBordered(false); - button.setFrame(NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(86.0, 28.0))); - controller.setView(unsafe { &*(&*button as *const NSPopUpButton as *const NSView) }); - controller.setLayoutAttribute(NSLayoutAttribute::Right); - controller.setAutomaticallyAdjustsSize(false); - - let window = unsafe { - let pointer = webview - .ns_window() - .map_err(|error| format!("Couldn’t access the desktop title bar: {error}"))?; - &*(pointer as *const NSWindow) - }; - while window.titlebarAccessoryViewControllers().count() > 0 { - window.removeTitlebarAccessoryViewControllerAtIndex(0); - } - window.addTitlebarAccessoryViewController(&controller); - Ok(()) - } -} - #[tauri::command] async fn logout(state: State<'_, AppState>, node_url: String) -> Result<(), String> { let base = normalize_node(&node_url)?; @@ -548,41 +341,6 @@ pub fn run() { client, notification_capability_sequence: AtomicU64::new(0), entry_url: Mutex::new(None), - node_registry: Mutex::new(NodeRegistry::default()), - node_registry_path: Mutex::new(None), - }) - .setup(|app| { - let webview = app - .get_webview_window("main") - .ok_or_else(|| "Couldn’t find the main Synapsis window.".to_string())?; - let registry_path = app - .path() - .app_data_dir() - .map_err(|error| format!("Couldn’t find the app data folder: {error}"))? - .join("nodes.json"); - let registry = load_node_registry(®istry_path); - let state = app.state::(); - *state - .entry_url - .lock() - .map_err(|_| "Couldn’t remember the app entry screen.".to_string())? = - Some(webview.url()?); - *state - .node_registry_path - .lock() - .map_err(|_| "Couldn’t prepare the saved node list.".to_string())? = - Some(registry_path); - *state - .node_registry - .lock() - .map_err(|_| "Couldn’t prepare the saved node list.".to_string())? = - registry.clone(); - #[cfg(target_os = "macos")] - { - macos_titlebar::initialize(app.handle()); - macos_titlebar::install(&webview, ®istry)?; - } - Ok(()) }) .on_page_load(|webview, payload| { if payload.event() != PageLoadEvent::Finished @@ -632,8 +390,7 @@ pub fn run() { register, logout, has_web_session, - open_web_app, - get_saved_nodes + open_web_app ]) .run(tauri::generate_context!()) .expect("error while running Synapsis"); diff --git a/src/App.css b/src/App.css index 06fa3d4..ac034e0 100644 --- a/src/App.css +++ b/src/App.css @@ -76,16 +76,15 @@ label { display: block; margin: 0 0 9px; color: rgba(255,255,255,.75); font-size .sidebar-brand { top: 43px; left: 26px; } .sidebar-wordmark { width: 137px; height: 26px; } .sidebar nav { display: flex; flex-direction: column; gap: 4px; } -.nav-item, .node-switcher, .profile-button { display: flex; align-items: center; width: 100%; border: 0; border-radius: 8px; background: transparent; color: rgba(255,255,255,.52); cursor: pointer; } +.nav-item, .profile-button { display: flex; align-items: center; width: 100%; border: 0; border-radius: 8px; background: transparent; color: rgba(255,255,255,.52); cursor: pointer; } .nav-item { gap: 13px; height: 44px; padding: 0 13px; font-size: 13px; } .nav-item:hover, .nav-item.active { color: #fff; background: rgba(255,255,255,.055); } .compose-button { height: 44px; display: flex; align-items: center; justify-content: center; gap: 9px; margin-top: 22px; border: 0; border-radius: 8px; color: #0a0b0b; background: #f4f4ef; font-size: 12px; font-weight: 700; cursor: pointer; } .sidebar-bottom { margin-top: auto; } -.node-switcher { gap: 10px; padding: 11px 10px; } -.node-switcher:hover, .profile-button:hover { background: rgba(255,255,255,.05); } -.node-switcher > span, .profile-button > span:not(.avatar) { min-width: 0; flex: 1; display: flex; flex-direction: column; align-items: flex-start; } -.node-switcher strong, .profile-button strong { max-width: 130px; overflow: hidden; color: rgba(255,255,255,.83); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } -.node-switcher small, .profile-button small { max-width: 130px; overflow: hidden; color: rgba(255,255,255,.3); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; } +.profile-button:hover { background: rgba(255,255,255,.05); } +.profile-button > span:not(.avatar) { min-width: 0; flex: 1; display: flex; flex-direction: column; align-items: flex-start; } +.profile-button strong { max-width: 130px; overflow: hidden; color: rgba(255,255,255,.83); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +.profile-button small { max-width: 130px; overflow: hidden; color: rgba(255,255,255,.3); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; } .profile-button { gap: 10px; margin-top: 7px; padding: 9px; border-top: 1px solid rgba(255,255,255,.06); border-radius: 0 0 8px 8px; text-align: left; } .avatar { width: 30px; height: 30px; display: grid; flex: 0 0 auto; place-items: center; border: 1px solid rgba(255,255,255,.22); border-radius: 50%; background: rgba(255,255,255,.08); font-size: 11px; font-weight: 700; } .home-content { min-width: 0; display: flex; flex-direction: column; } diff --git a/src/App.tsx b/src/App.tsx index cae01c2..8066c53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,7 +27,6 @@ type Connection = { }; type AuthResponse = { user: User }; -type NodeRegistry = { nodes: string[]; active?: string }; const DEFAULT_NODE = "synapsis.social"; const SAVED_NODE_KEY = "synapsis.nodeUrl"; @@ -67,7 +66,6 @@ function Icon({ name, size = 20 }: { name: string; size?: number }) { bell: <>, message: <>, plus: <>, - node: <>, logout: <>, }; return ; @@ -239,7 +237,6 @@ function App() { const [screen, setScreen] = useState("node"); const [nodeInput, setNodeInput] = useState(DEFAULT_NODE); const [nodeUrl, setNodeUrl] = useState(""); - const [node, setNode] = useState({ name: "Synapsis" }); const [user, setUser] = useState(null); const [working, setWorking] = useState(false); const [bootstrapping, setBootstrapping] = useState(true); @@ -247,47 +244,42 @@ function App() { useEffect(() => { const launchParams = new URLSearchParams(window.location.search); - if (launchParams.has("signedOut") || launchParams.has("addNode")) { - if (launchParams.has("signedOut")) localStorage.removeItem(SAVED_NODE_KEY); + if (launchParams.has("signedOut")) { + localStorage.removeItem(SAVED_NODE_KEY); window.history.replaceState({}, "", window.location.pathname); setScreen("node"); setBootstrapping(false); return; } - if (!isTauri()) { + const savedNode = localStorage.getItem(SAVED_NODE_KEY); + if (!savedNode || !isTauri()) { setBootstrapping(false); return; } + const savedNodeUrl = savedNode; async function restore() { - let handedOffToWebApp = false; try { - const registry = await invoke("get_saved_nodes"); - const savedNodeUrl = registry.active || localStorage.getItem(SAVED_NODE_KEY); - if (!savedNodeUrl) return; const hasWebSession = await invoke("has_web_session", { nodeUrl: savedNodeUrl }); if (hasWebSession) { await openNodeWebApp(savedNodeUrl); - handedOffToWebApp = true; return; } const connection = await invoke("connect", { nodeUrl: savedNodeUrl }); setNodeUrl(connection.nodeUrl); - setNode(connection.node); setNodeInput(displayHost(connection.nodeUrl)); setUser(connection.user); if (connection.user) { await openNodeWebApp(connection.nodeUrl); - handedOffToWebApp = true; } else { setScreen("login"); } } catch { setScreen("node"); } finally { - if (!handedOffToWebApp) setBootstrapping(false); + setBootstrapping(false); } } void restore(); @@ -305,7 +297,6 @@ function App() { const connection = await invoke("connect", { nodeUrl: nodeInput }); localStorage.setItem(SAVED_NODE_KEY, connection.nodeUrl); setNodeUrl(connection.nodeUrl); - setNode(connection.node); setUser(connection.user); if (connection.user) { await openNodeWebApp(connection.nodeUrl); @@ -377,11 +368,6 @@ function App() { } } - function chooseAnotherNode() { - setError(""); - setScreen("node"); - } - if (bootstrapping) { return
; } @@ -400,7 +386,6 @@ function App() {
-