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
This commit is contained in:
Generated
-25
@@ -2267,17 +2267,9 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
"block2",
|
"block2",
|
||||||
"libc",
|
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-cloud-kit",
|
|
||||||
"objc2-core-data",
|
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-core-graphics",
|
|
||||||
"objc2-core-image",
|
|
||||||
"objc2-core-text",
|
|
||||||
"objc2-core-video",
|
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-quartz-core",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2297,7 +2289,6 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.1",
|
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
@@ -2358,19 +2349,6 @@ dependencies = [
|
|||||||
"objc2-core-graphics",
|
"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]]
|
[[package]]
|
||||||
name = "objc2-encode"
|
name = "objc2-encode"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@@ -3699,9 +3677,6 @@ name = "synapsis-desktop"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"objc2",
|
|
||||||
"objc2-app-kit",
|
|
||||||
"objc2-foundation",
|
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -24,23 +24,5 @@ serde_json = "1"
|
|||||||
reqwest = { version = "0.12", features = ["cookies", "json", "rustls-tls"] }
|
reqwest = { version = "0.12", features = ["cookies", "json", "rustls-tls"] }
|
||||||
tauri-plugin-notification = "2"
|
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]
|
[dev-dependencies]
|
||||||
http = "1"
|
http = "1"
|
||||||
|
|||||||
+2
-245
@@ -1,7 +1,5 @@
|
|||||||
use reqwest::{Client, Method, StatusCode, Url};
|
use reqwest::{Client, Method, StatusCode, Url};
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -12,15 +10,6 @@ struct AppState {
|
|||||||
client: Client,
|
client: Client,
|
||||||
notification_capability_sequence: AtomicU64,
|
notification_capability_sequence: AtomicU64,
|
||||||
entry_url: Mutex<Option<Url>>,
|
entry_url: Mutex<Option<Url>>,
|
||||||
node_registry: Mutex<NodeRegistry>,
|
|
||||||
node_registry_path: Mutex<Option<PathBuf>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct NodeRegistry {
|
|
||||||
nodes: Vec<String>,
|
|
||||||
active: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
@@ -109,56 +98,6 @@ fn endpoint(base: &Url, path: &str) -> Result<Url, String> {
|
|||||||
.map_err(|_| "The selected node address is invalid.".into())
|
.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<NodeRegistry, String> {
|
|
||||||
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<NodeRegistry, String> {
|
|
||||||
state
|
|
||||||
.node_registry
|
|
||||||
.lock()
|
|
||||||
.map(|registry| registry.clone())
|
|
||||||
.map_err(|_| "Couldn’t access the saved node list.".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn decode_response<T: DeserializeOwned>(response: reqwest::Response) -> Result<T, String> {
|
async fn decode_response<T: DeserializeOwned>(response: reqwest::Response) -> Result<T, String> {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let bytes = response.bytes().await.map_err(|error| error.to_string())?;
|
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 origin = base.origin().ascii_serialization();
|
||||||
let capability_id = state
|
let capability_id = state
|
||||||
.notification_capability_sequence
|
.notification_capability_sequence
|
||||||
@@ -359,7 +291,7 @@ fn navigate_to_node(state: &AppState, webview: &WebviewWindow, base: &Url) -> Re
|
|||||||
.add_capability(capability)
|
.add_capability(capability)
|
||||||
.map_err(|error| format!("Couldn’t enable native notifications for this node: {error}"))?;
|
.map_err(|error| format!("Couldn’t enable native notifications for this node: {error}"))?;
|
||||||
webview
|
webview
|
||||||
.navigate(base.clone())
|
.navigate(base)
|
||||||
.map_err(|error| format!("Couldn’t open the Synapsis web app: {error}"))
|
.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()
|
.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<tauri::AppHandle> = 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::<NSString>().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::<AppState>();
|
|
||||||
|
|
||||||
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<Self> {
|
|
||||||
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]
|
#[tauri::command]
|
||||||
async fn logout(state: State<'_, AppState>, node_url: String) -> Result<(), String> {
|
async fn logout(state: State<'_, AppState>, node_url: String) -> Result<(), String> {
|
||||||
let base = normalize_node(&node_url)?;
|
let base = normalize_node(&node_url)?;
|
||||||
@@ -548,41 +341,6 @@ pub fn run() {
|
|||||||
client,
|
client,
|
||||||
notification_capability_sequence: AtomicU64::new(0),
|
notification_capability_sequence: AtomicU64::new(0),
|
||||||
entry_url: Mutex::new(None),
|
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::<AppState>();
|
|
||||||
*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| {
|
.on_page_load(|webview, payload| {
|
||||||
if payload.event() != PageLoadEvent::Finished
|
if payload.event() != PageLoadEvent::Finished
|
||||||
@@ -632,8 +390,7 @@ pub fn run() {
|
|||||||
register,
|
register,
|
||||||
logout,
|
logout,
|
||||||
has_web_session,
|
has_web_session,
|
||||||
open_web_app,
|
open_web_app
|
||||||
get_saved_nodes
|
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running Synapsis");
|
.expect("error while running Synapsis");
|
||||||
|
|||||||
+5
-6
@@ -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-brand { top: 43px; left: 26px; }
|
||||||
.sidebar-wordmark { width: 137px; height: 26px; }
|
.sidebar-wordmark { width: 137px; height: 26px; }
|
||||||
.sidebar nav { display: flex; flex-direction: column; gap: 4px; }
|
.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 { gap: 13px; height: 44px; padding: 0 13px; font-size: 13px; }
|
||||||
.nav-item:hover, .nav-item.active { color: #fff; background: rgba(255,255,255,.055); }
|
.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; }
|
.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; }
|
.sidebar-bottom { margin-top: auto; }
|
||||||
.node-switcher { gap: 10px; padding: 11px 10px; }
|
.profile-button:hover { background: rgba(255,255,255,.05); }
|
||||||
.node-switcher:hover, .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; }
|
||||||
.node-switcher > span, .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; }
|
||||||
.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; }
|
.profile-button small { max-width: 130px; overflow: hidden; color: rgba(255,255,255,.3); font-size: 9px; 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 { 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; }
|
.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; }
|
.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; }
|
.home-content { min-width: 0; display: flex; flex-direction: column; }
|
||||||
|
|||||||
+6
-21
@@ -27,7 +27,6 @@ type Connection = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type AuthResponse = { user: User };
|
type AuthResponse = { user: User };
|
||||||
type NodeRegistry = { nodes: string[]; active?: string };
|
|
||||||
|
|
||||||
const DEFAULT_NODE = "synapsis.social";
|
const DEFAULT_NODE = "synapsis.social";
|
||||||
const SAVED_NODE_KEY = "synapsis.nodeUrl";
|
const SAVED_NODE_KEY = "synapsis.nodeUrl";
|
||||||
@@ -67,7 +66,6 @@ function Icon({ name, size = 20 }: { name: string; size?: number }) {
|
|||||||
bell: <><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"/><path d="M10 21h4"/></>,
|
bell: <><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"/><path d="M10 21h4"/></>,
|
||||||
message: <><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"/></>,
|
message: <><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"/></>,
|
||||||
plus: <><path d="M12 5v14M5 12h14"/></>,
|
plus: <><path d="M12 5v14M5 12h14"/></>,
|
||||||
node: <><circle cx="12" cy="12" r="2"/><circle cx="5" cy="6" r="2"/><circle cx="19" cy="6" r="2"/><circle cx="5" cy="18" r="2"/><circle cx="19" cy="18" r="2"/><path d="m7 7 3.5 3M17 7l-3.5 3M7 17l3.5-3M17 17l-3.5-3"/></>,
|
|
||||||
logout: <><path d="M10 17l5-5-5-5M15 12H3"/><path d="M14 3h5a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-5"/></>,
|
logout: <><path d="M10 17l5-5-5-5M15 12H3"/><path d="M14 3h5a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-5"/></>,
|
||||||
};
|
};
|
||||||
return <svg aria-hidden="true" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">{paths[name]}</svg>;
|
return <svg aria-hidden="true" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">{paths[name]}</svg>;
|
||||||
@@ -239,7 +237,6 @@ function App() {
|
|||||||
const [screen, setScreen] = useState<Screen>("node");
|
const [screen, setScreen] = useState<Screen>("node");
|
||||||
const [nodeInput, setNodeInput] = useState(DEFAULT_NODE);
|
const [nodeInput, setNodeInput] = useState(DEFAULT_NODE);
|
||||||
const [nodeUrl, setNodeUrl] = useState("");
|
const [nodeUrl, setNodeUrl] = useState("");
|
||||||
const [node, setNode] = useState<NodeInfo>({ name: "Synapsis" });
|
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [working, setWorking] = useState(false);
|
const [working, setWorking] = useState(false);
|
||||||
const [bootstrapping, setBootstrapping] = useState(true);
|
const [bootstrapping, setBootstrapping] = useState(true);
|
||||||
@@ -247,47 +244,42 @@ function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const launchParams = new URLSearchParams(window.location.search);
|
const launchParams = new URLSearchParams(window.location.search);
|
||||||
if (launchParams.has("signedOut") || launchParams.has("addNode")) {
|
if (launchParams.has("signedOut")) {
|
||||||
if (launchParams.has("signedOut")) localStorage.removeItem(SAVED_NODE_KEY);
|
localStorage.removeItem(SAVED_NODE_KEY);
|
||||||
window.history.replaceState({}, "", window.location.pathname);
|
window.history.replaceState({}, "", window.location.pathname);
|
||||||
setScreen("node");
|
setScreen("node");
|
||||||
setBootstrapping(false);
|
setBootstrapping(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isTauri()) {
|
const savedNode = localStorage.getItem(SAVED_NODE_KEY);
|
||||||
|
if (!savedNode || !isTauri()) {
|
||||||
setBootstrapping(false);
|
setBootstrapping(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const savedNodeUrl = savedNode;
|
||||||
async function restore() {
|
async function restore() {
|
||||||
let handedOffToWebApp = false;
|
|
||||||
try {
|
try {
|
||||||
const registry = await invoke<NodeRegistry>("get_saved_nodes");
|
|
||||||
const savedNodeUrl = registry.active || localStorage.getItem(SAVED_NODE_KEY);
|
|
||||||
if (!savedNodeUrl) return;
|
|
||||||
const hasWebSession = await invoke<boolean>("has_web_session", { nodeUrl: savedNodeUrl });
|
const hasWebSession = await invoke<boolean>("has_web_session", { nodeUrl: savedNodeUrl });
|
||||||
if (hasWebSession) {
|
if (hasWebSession) {
|
||||||
await openNodeWebApp(savedNodeUrl);
|
await openNodeWebApp(savedNodeUrl);
|
||||||
handedOffToWebApp = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const connection = await invoke<Connection>("connect", { nodeUrl: savedNodeUrl });
|
const connection = await invoke<Connection>("connect", { nodeUrl: savedNodeUrl });
|
||||||
setNodeUrl(connection.nodeUrl);
|
setNodeUrl(connection.nodeUrl);
|
||||||
setNode(connection.node);
|
|
||||||
setNodeInput(displayHost(connection.nodeUrl));
|
setNodeInput(displayHost(connection.nodeUrl));
|
||||||
setUser(connection.user);
|
setUser(connection.user);
|
||||||
if (connection.user) {
|
if (connection.user) {
|
||||||
await openNodeWebApp(connection.nodeUrl);
|
await openNodeWebApp(connection.nodeUrl);
|
||||||
handedOffToWebApp = true;
|
|
||||||
} else {
|
} else {
|
||||||
setScreen("login");
|
setScreen("login");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setScreen("node");
|
setScreen("node");
|
||||||
} finally {
|
} finally {
|
||||||
if (!handedOffToWebApp) setBootstrapping(false);
|
setBootstrapping(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void restore();
|
void restore();
|
||||||
@@ -305,7 +297,6 @@ function App() {
|
|||||||
const connection = await invoke<Connection>("connect", { nodeUrl: nodeInput });
|
const connection = await invoke<Connection>("connect", { nodeUrl: nodeInput });
|
||||||
localStorage.setItem(SAVED_NODE_KEY, connection.nodeUrl);
|
localStorage.setItem(SAVED_NODE_KEY, connection.nodeUrl);
|
||||||
setNodeUrl(connection.nodeUrl);
|
setNodeUrl(connection.nodeUrl);
|
||||||
setNode(connection.node);
|
|
||||||
setUser(connection.user);
|
setUser(connection.user);
|
||||||
if (connection.user) {
|
if (connection.user) {
|
||||||
await openNodeWebApp(connection.nodeUrl);
|
await openNodeWebApp(connection.nodeUrl);
|
||||||
@@ -377,11 +368,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseAnotherNode() {
|
|
||||||
setError("");
|
|
||||||
setScreen("node");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bootstrapping) {
|
if (bootstrapping) {
|
||||||
return <main className="launch-screen"><SignalField/><div className="launch-mark"><Wordmark className="launch-wordmark"/></div><span className="spinner" aria-label="Loading Synapsis"/></main>;
|
return <main className="launch-screen"><SignalField/><div className="launch-mark"><Wordmark className="launch-wordmark"/></div><span className="spinner" aria-label="Loading Synapsis"/></main>;
|
||||||
}
|
}
|
||||||
@@ -400,7 +386,6 @@ function App() {
|
|||||||
</nav>
|
</nav>
|
||||||
<button className="compose-button"><Icon name="plus"/><span>New post</span></button>
|
<button className="compose-button"><Icon name="plus"/><span>New post</span></button>
|
||||||
<div className="sidebar-bottom">
|
<div className="sidebar-bottom">
|
||||||
<button className="node-switcher" onClick={chooseAnotherNode}><Icon name="node"/><span><strong>{node.name || "Synapsis"}</strong><small>{displayHost(nodeUrl)}</small></span></button>
|
|
||||||
<button className="profile-button" onClick={logout} disabled={working} title="Log out">
|
<button className="profile-button" onClick={logout} disabled={working} title="Log out">
|
||||||
<span className="avatar">{name.slice(0, 1).toUpperCase()}</span><span><strong>{name}</strong><small>{user.handle.startsWith("@") ? user.handle : `@${user.handle}`}</small></span><Icon name="logout" size={17}/>
|
<span className="avatar">{name.slice(0, 1).toUpperCase()}</span><span><strong>{name}</strong><small>{user.handle.startsWith("@") ? user.handle : `@${user.handle}`}</small></span><Icon name="logout" size={17}/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user