mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-09 20:12:49 +00:00
M9: a local view-only share's url is the view URL, not the edit URL
The previous fix made a local share's `link` follow the code shown, but its
top-level `url`, the session's WebSocket URL, still carried the edit token:
`collab.join {link: url}` from a view-only share joined with edit access and
the edit reached the host. The link layer now hands over the view way in as a
link and its URL (`SessionInfo::view`, a `WayIn`, in place of `view_link`), and
`collab.peers` gives a local share's `link` and `url` from the way in for the
code shown (`Live::invite`). Online shares are unchanged.
Regression test: `code_tests::a_local_view_share_s_link_and_url_only_let_people_look`
joins through the `link` and then the `url` over loopback; both are welcomed as
viewers whose edits never reach the host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2VLhC9d5v8VSsf4ng
This commit is contained in:
1 parent
a05201e21f
commit
b779e46d47
19 files changed
+99
-61
No files matched your search
@@ -91,7 +91,7 @@ fn info() -> SessionInfo {
|
||||
address: SERVER.into(),
|
||||
network: true,
|
||||
access: Access::Edit,
|
||||
view_link: None,
|
||||
view: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,31 +159,41 @@ fn a_local_view_code_lets_people_look_and_not_edit() {
|
||||
pump(&mut [&mut host, &mut viewer, &mut editor], "the end", |s| !s[1].live().status.active() && !s[2].live().status.active());
|
||||
}
|
||||
|
||||
/// A local network share's `link` is the one that goes with the code shown: started for looking
|
||||
/// only, whoever joins through it can't edit, and the session's line in `sessions` says the same.
|
||||
/// A local network share's `link` and `url` are the ones that go with the code shown: started for
|
||||
/// looking only, whoever joins through either can't edit, and the session's line in `sessions`
|
||||
/// says the same.
|
||||
#[test]
|
||||
fn a_local_view_share_s_link_only_lets_people_look() {
|
||||
let (mut host, mut viewer) = (session("Hana"), session("Ada"));
|
||||
fn a_local_view_share_s_link_and_url_only_let_people_look() {
|
||||
let mut host = session("Hana");
|
||||
let shared = host.execute("collab.share", &json!({"via": "local", "bind": "local", "access": "view"})).unwrap();
|
||||
let link = shared["link"].as_str().unwrap().to_string();
|
||||
viewer.execute("collab.join", &json!({"link": link})).unwrap();
|
||||
pump(&mut [&mut host, &mut viewer], "the link's join", |s| live(s[1]) && s[0].live().peers.len() == 1);
|
||||
let summary = viewer.live_summary();
|
||||
assert_eq!((&summary["access"], &summary["readOnly"]), (&json!("view"), &json!(true)), "joined with {link}");
|
||||
let before = layers(&host);
|
||||
let e = viewer.execute("shape.ellipse", &json!({"x": 80, "y": 10, "width": 40, "height": 30})).unwrap_err().to_string();
|
||||
assert!(e.contains("View only"), "{e}");
|
||||
host.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 40, "height": 30})).unwrap();
|
||||
pump(&mut [&mut host, &mut viewer], "the host's edit", |s| layers(s[1]) == before + 1);
|
||||
assert_eq!(layers(&host), before + 1, "nothing of the viewer's reached the host");
|
||||
// The link and the code name the same way in, wherever the session is listed.
|
||||
for key in ["link", "url"] {
|
||||
let way = shared[key].as_str().unwrap().to_string();
|
||||
let mut viewer = session("Ada");
|
||||
viewer.execute("collab.join", &json!({"link": way})).unwrap();
|
||||
pump(&mut [&mut host, &mut viewer], "the join", |s| live(s[1]) && s[0].live().peers.len() == 1);
|
||||
let summary = viewer.live_summary();
|
||||
assert_eq!((&summary["access"], &summary["readOnly"]), (&json!("view"), &json!(true)), "joined with {key} {way}");
|
||||
let before = layers(&host);
|
||||
let e = viewer.execute("shape.ellipse", &json!({"x": 80, "y": 10, "width": 40, "height": 30})).unwrap_err().to_string();
|
||||
assert!(e.contains("View only"), "{key}: {e}");
|
||||
host.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 40, "height": 30})).unwrap();
|
||||
pump(&mut [&mut host, &mut viewer], "the host's edit", |s| layers(s[1]) == before + 1);
|
||||
assert_eq!(layers(&host), before + 1, "nothing of the viewer's reached the host ({key})");
|
||||
viewer.execute("collab.leave", &json!({})).unwrap();
|
||||
pump(&mut [&mut host, &mut viewer], "the leave", |s| s[0].live().peers.is_empty());
|
||||
}
|
||||
// The link, the URL and the code name the same way in, wherever the session is listed.
|
||||
let Code::Local(local) = code::parse(shared["code"].as_str().unwrap()).unwrap() else { panic!("not a local code") };
|
||||
assert_eq!((&shared["link"], &shared["share"]["viewLink"]), (&json!(local.link()), &json!(local.link())));
|
||||
let url = shared["url"].as_str().unwrap();
|
||||
assert!(url.starts_with("ws://") && url.ends_with(&format!("&token={}", local.token())), "{url}");
|
||||
assert_eq!(shared["sessions"][0]["link"], shared["link"]);
|
||||
// Picking the edit code makes the link the edit one too.
|
||||
// Picking the edit code makes them the edit ones too.
|
||||
let edit = host.execute("collab.share", &json!({"access": "edit"})).unwrap();
|
||||
assert_eq!((&edit["link"], &edit["sessions"][0]["link"]), (&edit["share"]["editLink"], &edit["share"]["editLink"]));
|
||||
assert_ne!(edit["link"], shared["link"]);
|
||||
let edit_token = edit["link"].as_str().unwrap().rsplit("token=").next().unwrap();
|
||||
assert!(edit["url"].as_str().unwrap().ends_with(&format!("&token={edit_token}")), "{}", edit["url"]);
|
||||
assert_ne!((&edit["link"], &edit["url"]), (&shared["link"], &shared["url"]));
|
||||
assert_eq!(host.execute("collab.stop", &json!({})).unwrap()["ended"], true);
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ pub(crate) fn take(mut m: ServerMsg, cfg: &GuestConfig, inbox: &Inbox, seen: &mu
|
||||
address: inv.server.display(),
|
||||
network: !inv.loopback(),
|
||||
access,
|
||||
view_link: None,
|
||||
view: None,
|
||||
};
|
||||
seen.peers = peers.clone();
|
||||
seen.info = Some(info.clone());
|
||||
|
||||
@@ -9,7 +9,7 @@ use figcraft_collab::code;
|
||||
use figcraft_collab::message::{Access, Admin, AdminReply, ClientMsg, LibraryReply, LibraryReq, PluginReply, PluginReq};
|
||||
use figcraft_engine::live::{
|
||||
Bind, Event, HostInfo, HostOptions, LiveLink, NO_SESSION, NetReply, NetRequest, PageInfo, Presence, PublishOptions, Published, Resume, Role,
|
||||
SessionId, SessionInfo, Shared,
|
||||
SessionId, SessionInfo, Shared, WayIn,
|
||||
};
|
||||
|
||||
use crate::guest::{Guest, GuestConfig, Publish, upload_file};
|
||||
@@ -165,7 +165,7 @@ impl LiveLink for NetLink {
|
||||
core: figcraft_collab::Limits::default(),
|
||||
};
|
||||
let host = Host::start(cfg, inbox.clone()).map_err(|e| format!("can't listen on {ip}: {e}"))?;
|
||||
let view_link = view_token.map(|v| Invite::new(host.addr().to_string(), file.clone(), v).link());
|
||||
let view = view_token.map(|v| Invite::new(host.addr().to_string(), file.clone(), v)).map(|v| WayIn { link: v.link(), url: v.url() });
|
||||
let invite = Invite::new(host.addr().to_string(), file, token);
|
||||
let info = SessionInfo {
|
||||
role: Role::Host,
|
||||
@@ -179,7 +179,7 @@ impl LiveLink for NetLink {
|
||||
address: host.addr().to_string(),
|
||||
network: !ip.is_loopback(),
|
||||
access: Access::Edit,
|
||||
view_link,
|
||||
view,
|
||||
};
|
||||
self.started(session, Active::Host(host), inbox);
|
||||
Ok(info)
|
||||
|
||||
@@ -13,7 +13,7 @@ use serde_json::{Value, json};
|
||||
|
||||
use crate::live::{
|
||||
Access, Bind, DEFAULT_SERVER, Event, HostOptions, LiveLink, Presence, PublishOptions, Published, Role, SessionInfo, Shared, SharedLink, Status,
|
||||
join_target,
|
||||
WayIn, join_target,
|
||||
};
|
||||
use crate::{EngineError, Session};
|
||||
|
||||
@@ -39,6 +39,11 @@ const HOST: Local = Local { ip: Ipv4Addr::new(192, 168, 1, 20), port: 47_900, se
|
||||
/// The same session's view-only secret.
|
||||
const VIEW: Local = Local { secret: 0xBA9_8765_4321, ..HOST };
|
||||
|
||||
/// The WebSocket URL the scripted link gives with an invite `link` (the same secret).
|
||||
fn mock_url(link: &str) -> String {
|
||||
link.replace("figcraft://join?host=", "ws://")
|
||||
}
|
||||
|
||||
impl LiveLink for Link {
|
||||
/// Host as the network link does: a code's short secret when shared with a code, and the
|
||||
/// view-only link when asked for one.
|
||||
@@ -47,7 +52,7 @@ impl LiveLink for Link {
|
||||
s.session = session;
|
||||
// As the network link does: a short secret when the session is shared with a code.
|
||||
let link = if opts.short { HOST.link() } else { "figcraft://join?host=192.168.1.20:47900&file=f&token=AbCdEfGhIjKlMnOpQrStUv".to_string() };
|
||||
let view_link = opts.view.then(|| VIEW.link());
|
||||
let view = opts.view.then(|| WayIn { link: VIEW.link(), url: mock_url(&VIEW.link()) });
|
||||
s.hosted.push(opts);
|
||||
Ok(SessionInfo {
|
||||
role: Role::Host,
|
||||
@@ -56,12 +61,12 @@ impl LiveLink for Link {
|
||||
color: "#0D99FF".into(),
|
||||
file_name: "Shared".into(),
|
||||
pages: vec![],
|
||||
url: link.replace("figcraft://join?host=", "ws://"),
|
||||
url: mock_url(&link),
|
||||
link,
|
||||
address: "192.168.1.20:47900".into(),
|
||||
network: true,
|
||||
access: Access::Edit,
|
||||
view_link,
|
||||
view,
|
||||
})
|
||||
}
|
||||
fn join(&mut self, session: u64, link: &str, _: &str) -> Result<(), String> {
|
||||
@@ -152,7 +157,7 @@ fn on_the_server(s: &mut Session, script: &Arc<Mutex<Script>>, codes: Option<(&s
|
||||
address: "wss://share.example:443/".into(),
|
||||
network: true,
|
||||
access: Access::Edit,
|
||||
view_link: None,
|
||||
view: None,
|
||||
};
|
||||
let events = vec![Event::Published(published), Event::Shared(Ok(links(0, codes))), Event::Welcome { info, peers: vec![] }];
|
||||
script.lock().unwrap().events.extend(events);
|
||||
@@ -383,9 +388,9 @@ fn sharing_on_the_local_network_shows_a_code_that_carries_the_address() {
|
||||
// The link goes with the code: a view-only code's link doesn't edit.
|
||||
let r = ex(&mut s, "collab.share", json!({"access": "view"}));
|
||||
assert_eq!((&r["code"], &r["share"]["access"]), (&json!(view), &json!("view")));
|
||||
assert_eq!((&r["link"], &r["sessions"][0]["link"]), (&json!(VIEW.link()), &json!(VIEW.link())));
|
||||
assert_eq!((&r["link"], &r["url"], &r["sessions"][0]["link"]), (&json!(VIEW.link()), &json!(mock_url(&VIEW.link())), &json!(VIEW.link())));
|
||||
let r = ex(&mut s, "collab.share", json!({"access": "edit"}));
|
||||
assert_eq!((&r["code"], &r["link"]), (&json!(code::grouped(&HOST.code())), &json!(HOST.link())));
|
||||
assert_eq!((&r["code"], &r["link"], &r["url"]), (&json!(code::grouped(&HOST.code())), &json!(HOST.link()), &json!(mock_url(&HOST.link()))));
|
||||
assert!(err(&mut s, "collab.share", json!({"access": "comment"})).contains("share it online"));
|
||||
assert_eq!(script.lock().unwrap().hosted.len(), 1);
|
||||
assert_eq!(ex(&mut s, "collab.stop", json!({}))["ended"], true);
|
||||
@@ -393,7 +398,8 @@ fn sharing_on_the_local_network_shows_a_code_that_carries_the_address() {
|
||||
// Shared for looking only from the start: the edit code isn't the one shown.
|
||||
let r = ex(&mut s, "collab.share", json!({"via": "local", "access": "view"}));
|
||||
assert_eq!((&r["code"], &r["share"]["access"]), (&json!(view), &json!("view")));
|
||||
assert_eq!((&r["link"], &r["sessions"][0]["link"]), (&json!(VIEW.link()), &json!(VIEW.link())), "the edit link isn't handed out");
|
||||
assert_eq!((&r["link"], &r["url"]), (&json!(VIEW.link()), &json!(mock_url(&VIEW.link()))), "the edit link and URL aren't handed out");
|
||||
assert_eq!(r["sessions"][0]["link"], json!(VIEW.link()));
|
||||
ex(&mut s, "collab.stop", json!({}));
|
||||
assert!(err(&mut s, "collab.share", json!({"via": "local", "access": "comment"})).contains("share it online"));
|
||||
// Where to listen is the advanced part.
|
||||
|
||||
@@ -104,7 +104,7 @@ pub(crate) fn info(role: Role, me: u32) -> SessionInfo {
|
||||
address: "127.0.0.1:47900".into(),
|
||||
network: false,
|
||||
access: crate::live::Access::Edit,
|
||||
view_link: None,
|
||||
view: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ fn info(role: Role, name: &str, link: &str) -> SessionInfo {
|
||||
address: SERVER.into(),
|
||||
network: true,
|
||||
access: Access::Edit,
|
||||
view_link: None,
|
||||
view: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ pub(crate) use join::start_join;
|
||||
pub use join::{can_remove_people, specs};
|
||||
pub use types::{
|
||||
Bind, DEFAULT_SERVER, Event, HostOptions, LiveLink, MAX_NAME, MAX_PEERS, MAX_SELECTION, NO_HOSTING, NO_SESSION, NetReply, NetRequest, ONLINE_OFF,
|
||||
PEOPLE, PageInfo, PublishOptions, Published, Role, SessionId, SessionInfo, Share, Shared, SharedLink, Status, TOO_LARGE_TO_JOIN, Via,
|
||||
PEOPLE, PageInfo, PublishOptions, Published, Role, SessionId, SessionInfo, Share, Shared, SharedLink, Status, TOO_LARGE_TO_JOIN, Via, WayIn,
|
||||
join_target, parse_color,
|
||||
};
|
||||
|
||||
@@ -147,14 +147,15 @@ impl Live {
|
||||
self.info.as_ref().map(|i| i.role)
|
||||
}
|
||||
|
||||
/// The session's invite link as `collab.peers` gives it. A file shared on the local network
|
||||
/// gives the link that goes with the code shown: the session's own link is the edit way in,
|
||||
/// and a view-only share must not hand it out. Otherwise the session's own link (online, the
|
||||
/// owner's: `share` has the links for others).
|
||||
pub fn invite_link(&self) -> Option<&str> {
|
||||
/// The session's invite link and WebSocket URL as `collab.peers` gives them. A file shared
|
||||
/// on the local network gives the ones that go with the code shown: the session's own link
|
||||
/// and URL are the edit way in, and a view-only share must not hand them out. Otherwise the
|
||||
/// session's own (online, the owner's: `share` has the links for others).
|
||||
pub fn invite(&self) -> (Option<&str>, Option<&str>) {
|
||||
let info = self.info.as_ref();
|
||||
match &self.share {
|
||||
Some(sh) if sh.via == Via::Local => sh.link(),
|
||||
_ => self.info.as_ref().map(|i| i.link.as_str()),
|
||||
Some(sh) if sh.via == Via::Local => info.and_then(|i| i.way_in(sh.access)).unzip(),
|
||||
_ => (info.map(|i| i.link.as_str()), info.map(|i| i.url.as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ pub(super) fn share(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// that link's secret. (Online they arrive with the file.)
|
||||
let links = live.info.as_ref().filter(|_| via == Via::Local).map(|i| Shared {
|
||||
edit: local_link(&i.link),
|
||||
view: i.view_link.as_deref().map(local_link),
|
||||
view: i.view.as_ref().map(|v| local_link(&v.link)),
|
||||
comment: None,
|
||||
});
|
||||
live.share = Some(Share { via, server, access, links });
|
||||
|
||||
@@ -175,7 +175,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"collab.share",
|
||||
"Share with a join code",
|
||||
"{via?: \"online\" (default: the open file is kept on the server in Preferences, `pref.publishServer`, and the server hands out the codes) | \"local\" (this app hosts it for people on the same network; no server), access?: \"edit\" (default) | \"comment\" (online only: view and comment, not edit) | \"view\" (look only: their edits are refused): which code `code` is, server? (online: another server's address, with ?key=… when it needs its key), key? (online: the server's key, when the address has none), name? (online: the file's name there), port?, bind?: \"lan\" (default) | \"local\" (this machine only) | an IP address (local)} → the open file's session as `collab.peers` gives it: `code` is what others type to join (`collab.join {code}`, the Join field), `link` the invite link to give with it (local: the one that goes with `code`, so a view-only code's link lets people look only; online: your own link to the file, which edits: `share` has the link that goes with each code). Local: at once, the code carries the host's IPv4 address, port and secret (K7M2Q-…, 20 symbols), and `share` is {via, access, shared, editCode, viewCode, editLink, viewLink}: the edit and view-only ways in, each with its own secret (commentCode and commentLink null). Online: status \"connecting\" first; once live `code` is the server's (K7M-2QX-9TD) and `share` is {via, access, shared, editCode, viewCode, commentCode, editLink, viewLink, commentLink}: links made for others (commentCode and commentLink null from a server older than comment links) (the file's own links in `published` stay yours); `code` stays null when the server is older than join codes (give `share.editLink` / `share.viewLink` instead). Called again while shared: only switches which code `code` is (`access`). Called after `collab.stop` on a file that is still on the server: new links and codes for that same file (nothing is published twice). `collab.stop` stops sharing",
|
||||
"{via?: \"online\" (default: the open file is kept on the server in Preferences, `pref.publishServer`, and the server hands out the codes) | \"local\" (this app hosts it for people on the same network; no server), access?: \"edit\" (default) | \"comment\" (online only: view and comment, not edit) | \"view\" (look only: their edits are refused): which code `code` is, server? (online: another server's address, with ?key=… when it needs its key), key? (online: the server's key, when the address has none), name? (online: the file's name there), port?, bind?: \"lan\" (default) | \"local\" (this machine only) | an IP address (local)} → the open file's session as `collab.peers` gives it: `code` is what others type to join (`collab.join {code}`, the Join field), `link` the invite link to give with it and `url` its WebSocket URL (local: the ones that go with `code`, so a view-only code's link and URL let people look only; online: your own link to the file, which edits: `share` has the link that goes with each code). Local: at once, the code carries the host's IPv4 address, port and secret (K7M2Q-…, 20 symbols), and `share` is {via, access, shared, editCode, viewCode, editLink, viewLink}: the edit and view-only ways in, each with its own secret (commentCode and commentLink null). Online: status \"connecting\" first; once live `code` is the server's (K7M-2QX-9TD) and `share` is {via, access, shared, editCode, viewCode, commentCode, editLink, viewLink, commentLink}: links made for others (commentCode and commentLink null from a server older than comment links) (the file's own links in `published` stay yours); `code` stays null when the server is older than join codes (give `share.editLink` / `share.viewLink` instead). Called again while shared: only switches which code `code` is (`access`). Called after `collab.stop` on a file that is still on the server: new links and codes for that same file (nothing is published twice). `collab.stop` stops sharing",
|
||||
false,
|
||||
share
|
||||
),
|
||||
|
||||
@@ -283,6 +283,7 @@ impl Session {
|
||||
})
|
||||
.collect();
|
||||
let mine = synced.and_then(|d| d.text_edit.as_ref()).map(|t| editing(&editing_of(t)));
|
||||
let (link, url) = l.invite();
|
||||
let sessions: Vec<Value> = self
|
||||
.live
|
||||
.iter()
|
||||
@@ -295,7 +296,7 @@ impl Session {
|
||||
"reason": reason(o),
|
||||
"role": o.role(),
|
||||
"fileName": info.map(|i| i.file_name.as_str()),
|
||||
"link": o.invite_link(),
|
||||
"link": o.invite().0,
|
||||
"peers": o.peers.len(),
|
||||
})
|
||||
})
|
||||
@@ -313,8 +314,8 @@ impl Session {
|
||||
"color": info.map(|i| i.color.as_str()),
|
||||
"fileName": info.map(|i| i.file_name.as_str()),
|
||||
"pages": info.map(|i| &i.pages),
|
||||
"link": l.invite_link(),
|
||||
"url": info.map(|i| i.url.as_str()),
|
||||
"link": link,
|
||||
"url": url,
|
||||
"address": info.map(|i| i.address.as_str()),
|
||||
"network": info.map(|i| i.network),
|
||||
"access": l.client.as_ref().map(Client::access).or(info.map(|i| i.access)),
|
||||
|
||||
@@ -123,9 +123,29 @@ pub struct SessionInfo {
|
||||
/// comment link).
|
||||
pub access: Access,
|
||||
/// A session this app hosts with a way in for looking only ([`HostOptions::view`]): its
|
||||
/// invite link, which carries the view secret (`None`: everyone who joins edits, or you
|
||||
/// joined).
|
||||
pub view_link: Option<String>,
|
||||
/// invite link and WebSocket URL, which carry the view secret (`None`: everyone who joins
|
||||
/// edits, or you joined).
|
||||
pub view: Option<WayIn>,
|
||||
}
|
||||
|
||||
impl SessionInfo {
|
||||
/// The invite link and WebSocket URL that let people into a session this app hosts with
|
||||
/// `access` (its own link and URL edit). `None`: no way in gives that.
|
||||
pub fn way_in(&self, access: Access) -> Option<(&str, &str)> {
|
||||
match access {
|
||||
Access::Edit => Some((&self.link, &self.url)),
|
||||
Access::View => self.view.as_ref().map(|v| (v.link.as_str(), v.url.as_str())),
|
||||
Access::Comment => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A way into a session this app hosts: an invite link and the WebSocket URL it connects to,
|
||||
/// which carry the same secret.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct WayIn {
|
||||
pub link: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// A file you published to a server: where, and its first share links (they carry their tokens:
|
||||
@@ -395,7 +415,7 @@ pub struct HostOptions {
|
||||
/// Shared with a join code (`collab.share`): the session's secret is short enough to type.
|
||||
pub short: bool,
|
||||
/// Also make a second secret whose holders only look: they see every change, and the session
|
||||
/// refuses theirs ([`SessionInfo::view_link`]).
|
||||
/// refuses theirs ([`SessionInfo::view`]).
|
||||
pub view: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ impl LiveLink for Hosting {
|
||||
address,
|
||||
network: false,
|
||||
access: Access::Edit,
|
||||
view_link: None,
|
||||
view: None,
|
||||
})
|
||||
}
|
||||
fn join(&mut self, _: u64, _: &str, _: &str) -> Result<(), String> {
|
||||
|
||||
@@ -44,7 +44,7 @@ pub(crate) fn joined(h: &mut Harness, access: Access) {
|
||||
address: "127.0.0.1:47900".into(),
|
||||
network: false,
|
||||
access,
|
||||
view_link: None,
|
||||
view: None,
|
||||
};
|
||||
h.send(vec![LiveEvent::Welcome { info, peers: vec![] }, LiveEvent::Sync(w)]);
|
||||
h.frames(2);
|
||||
|
||||
@@ -13,7 +13,7 @@ use figcraft_engine::figcraft_collab::code::{self, Local};
|
||||
use figcraft_engine::figcraft_collab::message::ClientMsg;
|
||||
use figcraft_engine::live::{
|
||||
Access, DEFAULT_SERVER, Event as LiveEvent, HostOptions, LiveLink, NetReply, NetRequest, Peer, Presence, PublishOptions, Published, Role,
|
||||
SessionInfo, Shared as SharedLinks, SharedLink, Status,
|
||||
SessionInfo, Shared as SharedLinks, SharedLink, Status, WayIn,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -72,7 +72,7 @@ fn info(role: Role, link: &str) -> SessionInfo {
|
||||
address: "192.168.1.20:47900".into(),
|
||||
network: true,
|
||||
access: Access::Edit,
|
||||
view_link: None,
|
||||
view: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +83,9 @@ impl LiveLink for Link {
|
||||
let mut s = self.0.lock().unwrap();
|
||||
s.session = session;
|
||||
let (link, view) = if opts.port == Some(6) { (PLAIN.to_string(), PLAIN.replace("AbCd", "View")) } else { (HOST.link(), VIEW.link()) };
|
||||
let view_link = opts.view.then_some(view);
|
||||
let view = opts.view.then(|| WayIn { url: view.replace("figcraft://join?host=", "ws://"), link: view });
|
||||
s.hosted.push(opts);
|
||||
Ok(SessionInfo { view_link, ..info(Role::Host, &link) })
|
||||
Ok(SessionInfo { view, ..info(Role::Host, &link) })
|
||||
}
|
||||
fn join(&mut self, session: u64, link: &str, _: &str) -> Result<(), String> {
|
||||
let mut s = self.0.lock().unwrap();
|
||||
|
||||
@@ -131,7 +131,7 @@ impl Harness {
|
||||
address: "127.0.0.1:47900".into(),
|
||||
network: false,
|
||||
access: figcraft_engine::live::Access::Edit,
|
||||
view_link: None,
|
||||
view: None,
|
||||
};
|
||||
self.send(vec![LiveEvent::Welcome { info, peers: vec![Peer { client_id: 1, name: "Hana".into(), color: "#9747FF".into() }] }]);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ fn info(role: Role, name: &str) -> SessionInfo {
|
||||
address: "192.168.1.20:47900".into(),
|
||||
network: true,
|
||||
access: Access::Edit,
|
||||
view_link: None,
|
||||
view: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -603,8 +603,8 @@ resuming with its offline edits, and leaving the session going back to snapshot
|
||||
see every change and everyone's presence, the app makes the file read-only for them, and the
|
||||
session refuses any batch they send anyway, as a server does for its view links. The Share
|
||||
popover picks which code shows (Can edit / Can view, also before sharing starts, so the edit
|
||||
code need never be on screen; `collab.share {access}`, whose `link` is the one that goes with
|
||||
the code shown, so a view-only share never hands out the edit link); picking one later changes
|
||||
code need never be on screen; `collab.share {access}`, whose `link` and `url` are the ones that
|
||||
go with the code shown, so a view-only share never hands out the edit secret); picking one later changes
|
||||
nothing for who already joined. There is no comment code on the local network. A host without an IPv4
|
||||
address gets no codes and shares its links.
|
||||
- **In the browser** a code joins like a link (the page asks the server over its own WebSocket,
|
||||
|
||||
@@ -26,7 +26,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-06 | minor | M9: a file shared on the local network can be shared read-only. The session the app hosts has a second secret, so a view-only code next to the edit code (same address and port, a 44-bit secret of its own); whoever joins with it is welcomed as a viewer, sees every change and presence, gets the "View only" badge and only the moving tools, and the session refuses their edits. The Share popover offers Can edit / Can view under Local network before sharing starts (the edit code then never shows) and while sharing; `collab.share {via: "local", access: "view"}` does the same (its `link`, like its `code`, is the view-only way in), and `share` in `collab.peers` lists both codes and links. No comment code on the local network. Numbers unchanged. |
|
||||
| 2026-11-06 | minor | M9: a file shared on the local network can be shared read-only. The session the app hosts has a second secret, so a view-only code next to the edit code (same address and port, a 44-bit secret of its own); whoever joins with it is welcomed as a viewer, sees every change and presence, gets the "View only" badge and only the moving tools, and the session refuses their edits. The Share popover offers Can edit / Can view under Local network before sharing starts (the edit code then never shows) and while sharing; `collab.share {via: "local", access: "view"}` does the same (its `link` and `url`, like its `code`, are the view-only way in), and `share` in `collab.peers` lists both codes and links. No comment code on the local network. Numbers unchanged. |
|
||||
| 2026-10-31 | patch | M9, #1665: the document in a live session's welcome goes compressed (deflate) to clients whose hello announces `packed`, about seven times smaller on the wire (a synthetic file of 4,040 layers: 397 KB to 53 KB); older clients keep getting it uncompressed, and the client reads a packed document only up to the size an uncompressed welcome could have had. |
|
||||
| 2026-10-30 | minor | M9, part of #742: a team list on `figcraft-server`. With `team_files = "edit"` (or `comment`, `view`) everyone who reaches the server sees the files shared from it now (a link of theirs has a join code: Share ▸ Start sharing) and joins them by a team list link, `figcraft://join?server=…&file=…&team=1`, which carries no secret: the server hands out the file's team link while it stays shared (`ResolveTeam`, answered like a join code), and Stop sharing revokes it with the others, disconnecting whoever joined with it. Turning the list off or changing its access refuses the old team links at once. `collab.files {refresh}` asks the server in Preferences for its list (`Admin::Team`; not the public server FigCraft ships with), and Home ▸ Online shows it below your own files: a row joins its file, its menu copies the link for the app or, with the server's `web_url`, for the browser build (`<web_url>#join=…`). A browser build served beside its server names it with `<meta name="figcraft-server">`, the page's default for sharing, codes and the team list. No accounts: the list is for a server only your team reaches. Tests: `team_tests` (the server and apps over loopback), `code_tests` (links and the browser's driver), `online_tests` (the Online section), `query_tests` (the page's server). Percentages and hours unchanged |
|
||||
| 2026-10-30 | minor | M9, #1130: `figcraft-server` compares the SHA-256 digests of the presented and the kept server key instead of the raw strings, as `docs/server.md` says; the key file, `figcraft-server key` and rotation are unchanged. Numbers unchanged. |
|
||||
|
||||
Reference in new issue
Block a user