feat(web): join a live session from ?join= in the page URL

This commit is contained in:
sushi-killer
2026-10-17 01:45:08 +03:00
parent d1d8c3f652
commit a9bcc48df7
4 changed files with 46 additions and 2 deletions
+8 -1
View File
@@ -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=<invite link or join
//! code>` 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;
+10
View File
@@ -0,0 +1,10 @@
//! The page's URL query: which flags it sets and the invite link it names.
/// The invite link in `?join=<link>` (`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())
}
+21
View File
@@ -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);
}
+7 -1
View File
@@ -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=<link>`: 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 }))
}),
)