diff --git a/apps/figcraft-web/src/main.rs b/apps/figcraft-web/src/main.rs index 587fd60b..35aebb02 100644 --- a/apps/figcraft-web/src/main.rs +++ b/apps/figcraft-web/src/main.rs @@ -13,8 +13,15 @@ //! (browsers block `ws://` from secure pages); serve it over `http://` on a trusted network to //! join from other machines. //! -//! URL query flag: `?webgl` forces the WebGL2 backend instead of WebGPU. +//! URL query: `?webgl` forces the WebGL2 backend instead of WebGPU; `?join=` joins that live session once the page has loaded, so a bookmark opens a shared file. +//! `join` takes the rest of the query: put it last. The link carries the session's secret, so the +//! URL (and the browser history) is as private as the link itself. +#[cfg(any(target_arch = "wasm32", test))] +mod query; +#[cfg(test)] +mod query_tests; #[cfg(target_arch = "wasm32")] mod web; diff --git a/apps/figcraft-web/src/query.rs b/apps/figcraft-web/src/query.rs new file mode 100644 index 00000000..be68f598 --- /dev/null +++ b/apps/figcraft-web/src/query.rs @@ -0,0 +1,10 @@ +//! The page's URL query: which flags it sets and the invite link it names. + +/// The invite link in `?join=` (`search` is `location.search`, with or without its `?`), still +/// escaped. `join` takes the rest of the query, so a pasted `figcraft://join?host=…&file=…&token=…` +/// keeps its own `&`s; put it last. +pub fn join_link(search: &str) -> Option<&str> { + let query = search.strip_prefix('?').unwrap_or(search); + let start = if query.starts_with("join=") { 0 } else { query.find("&join=")? + 1 }; + query.get(start + "join=".len()..).filter(|link| !link.is_empty()) +} diff --git a/apps/figcraft-web/src/query_tests.rs b/apps/figcraft-web/src/query_tests.rs new file mode 100644 index 00000000..9582682f --- /dev/null +++ b/apps/figcraft-web/src/query_tests.rs @@ -0,0 +1,21 @@ +//! `?join=` in the page's URL. + +use crate::query::join_link; + +#[test] +fn join_takes_the_rest_of_the_query() { + let link = "figcraft://join?host=192.168.1.20:47900&file=abc&token=AbCdEfGhIjKlMnOpQrStUv"; + assert_eq!(join_link(&format!("?join={link}")), Some(link), "a pasted link keeps its own &s"); + assert_eq!(join_link(&format!("?webgl&join={link}")), Some(link), "after other flags"); + assert_eq!(join_link("?join=figcraft%3A%2F%2Fjoin%3Fhost%3Dh"), Some("figcraft%3A%2F%2Fjoin%3Fhost%3Dh"), "escaped, decoded later"); + assert_eq!(join_link("join=K7M-2QX-9TD"), Some("K7M-2QX-9TD"), "a join code, no leading ?"); +} + +#[test] +fn no_join_without_the_parameter() { + assert_eq!(join_link(""), None); + assert_eq!(join_link("?webgl"), None); + assert_eq!(join_link("?join="), None); + assert_eq!(join_link("?rejoin=x"), None, "only the whole parameter name"); + assert_eq!(join_link("?xjoin=x&a=1"), None); +} diff --git a/apps/figcraft-web/src/web.rs b/apps/figcraft-web/src/web.rs index 8cadb013..d3c16441 100644 --- a/apps/figcraft-web/src/web.rs +++ b/apps/figcraft-web/src/web.rs @@ -40,7 +40,13 @@ pub fn start() { // on the socket wakes the page for a frame. let ctx = cc.egui_ctx.clone(); session.live.install(Box::new(figcraft_collab_net::NetLink::new(std::sync::Arc::new(move || ctx.request_repaint())))); - let fig = FigApp::new(session, Box::new(WebServices { reduce_motion, ctx: cc.egui_ctx.clone() })); + let mut fig = FigApp::new(session, Box::new(WebServices { reduce_motion, ctx: cc.egui_ctx.clone() })); + // `?join=`: join that session, as opening an invite link does on the desktop. + let search = query(); + if let Some(link) = crate::query::join_link(&search) { + let link = js_sys::decode_uri_component(link).ok().and_then(|s| s.as_string()).unwrap_or_else(|| link.to_string()); + figcraft_ui_egui::live::open_invite(&mut fig, &link); + } Ok(Box::new(WebShell { fig })) }), )