Merge pull request #1369 from mojtabajafarzadeh/fix/share-key-and-large-file-publish

follow-up: server publish timeout, key-to-server binding, and "Go to main component" in the context menu
This commit is contained in:
BFlat
2026-10-27 00:13:25 +07:00
committed by GitHub
5 changed files with 71 additions and 12 deletions
+8 -1
View File
@@ -956,11 +956,18 @@ fn serve(mut stream: TcpStream, conn: u64, hub: &Sender<HubIn>, limits: NetLimit
// Sessions and the operator may use the last connections; one-shot requests
// (hosting, libraries, browsing plugins) leave them free.
let session = matches!(first, ClientMsg::Hello(_) | ClientMsg::Manage(_));
// A publish uploads a whole file; a large one on a slow link takes the server a
// while to receive and answer. The manage/hosting connections that carry uploads
// wait the longer request-reply timeout, not the short request idle.
let idle = match first {
ClientMsg::Manage(_) | ClientMsg::OpenHosting(_) => limits.request_reply_timeout,
ClientMsg::Hello(_) => limits.idle_timeout,
_ => limits.request_idle,
};
let max = match &first {
ClientMsg::OpenPlugins(o) if o.key.is_empty() => limits.max_request,
_ => limits.max_client_message,
};
let idle = if session { limits.idle_timeout } else { limits.request_idle };
let reserved = !session && open.0.load(Ordering::Relaxed) > limits.max_connections.saturating_sub(limits.reserve_for_sessions);
// (In flight is only taken when the connection goes on: the hub gives it back.)
if reserved || !shared.in_flight.take(bytes, limits.max_in_flight) {
+17
View File
@@ -55,6 +55,23 @@ fn the_context_menu_groups_the_copy_commands_under_copy_as() {
assert!(u.app.ui.context_menu.is_none(), "the menu closes");
}
#[test]
fn the_context_menu_offers_go_to_main_for_an_instance() {
// Right-clicking an instance reaches its main component: the menu offers "Go to main
// component" instead of "Create component" (which is for plain layers).
let mut s = Session::new();
let f = id(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 100, "height": 60, "name": "Button"})).unwrap());
let comp = id(&s.execute("component.create", &json!({"ids": [f]})).unwrap());
let inst = id(&s.execute("instance.create", &json!({"component": comp, "x": 0, "y": 200})).unwrap());
let mut u = Ui::new(s);
u.frame(vec![]);
u.app.ui.context_menu = Some((Pos2::new(700.0, 300.0), vec![inst]));
u.frame(vec![]);
let shapes = flat(&u.frame_output(vec![]).shapes);
assert!(has_text(&shapes, "Go to main component"), "an instance's menu reaches its main");
assert!(!has_text(&shapes, "Create component"), "not offered for an instance");
}
#[test]
fn copying_a_link_with_nothing_selected_copies_nothing() {
let mut s = Session::new();
+18 -10
View File
@@ -98,10 +98,11 @@ pub struct LiveUi {
pub advanced: bool,
pub this_computer: bool,
pub port: String,
/// The server key for online sharing (a server that needs one; empty: none). Passed to
/// `collab.share` so a keyed server can be used from the Share popover without the Publish
/// dialog.
pub share_key: String,
/// The server key for online sharing, and the server address it was entered for (a server
/// that needs one; `None`: none). Passed to `collab.share` so a keyed server can be used
/// from the Share popover without the Publish dialog. The key goes out only to the server it
/// belongs to: switching the server in Preferences must not leak the old one to it.
pub share_key: Option<(String, String)>,
/// What is typed in the join field of the Share popover and the home screen.
pub code: String,
/// The join started from the home screen: what it is doing shows under the field, and its
@@ -750,8 +751,14 @@ fn start_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
if let (true, Ok(port)) = (local, app.ui.live.port.trim().parse::<u16>()) {
params["port"] = json!(port);
}
if !local && !app.ui.live.share_key.trim().is_empty() {
params["key"] = json!(app.ui.live.share_key.trim());
// The key goes out only to the server it belongs to (the server in Preferences): changing
// the server must not send the old server's key to a new one.
if !local
&& let Some((at, key)) = app.ui.live.share_key.as_ref()
&& *at == server
&& !key.is_empty()
{
params["key"] = json!(key);
}
app.run("collab.share", params);
}
@@ -770,12 +777,13 @@ fn start_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
} else {
widgets::label(ui, tr("Another server, or one that needs its key. The usual server is set in Preferences."), t.text_secondary);
// The server key (a server that needs one; the address in Preferences is without it).
// It is remembered for the server it was typed for, so it never leaks to another one.
ui.horizontal(|ui| {
widgets::label(ui, tr("Server key"), t.text);
if let Some(key) =
widgets::secret_field_named(ui, egui::Id::unique("live_share_key"), &app.ui.live.share_key, 200.0, tr("Server key"))
{
app.ui.live.share_key = key.trim().chars().filter(|c| !c.is_whitespace() && !c.is_control()).take(128).collect();
let shown = app.ui.live.share_key.as_ref().and_then(|(at, key)| (*at == server).then_some(key.as_str())).unwrap_or_default();
if let Some(key) = widgets::secret_field_named(ui, egui::Id::unique("live_share_key"), shown, 200.0, tr("Server key")) {
let key: String = key.trim().chars().filter(|c| !c.is_whitespace() && !c.is_control()).take(128).collect();
app.ui.live.share_key = if key.is_empty() { None } else { Some((server.trim().to_string(), key)) };
}
});
if widgets::button(ui, tr("Publish to another server…"), false, true).clicked() {
+17 -1
View File
@@ -237,7 +237,7 @@ fn the_server_key_shows_as_dots_while_shown_and_typed() {
let (mut h, _) = harness();
let shapes = open(&mut h);
click(&mut h, &shapes, "Advanced");
h.app.ui.live.share_key = "s3cret-key".into();
h.app.ui.live.share_key = Some(("wss://share.example/".to_string(), "s3cret-key".to_string()));
let shapes = h.frames(2);
let dots = "•".repeat(10);
assert!(has_text(&shapes, &dots) && !has_text(&shapes, "s3cret-key"));
@@ -505,6 +505,22 @@ fn the_server_preference_resets_and_turns_online_sharing_off() {
assert!(s.published.is_empty() && s.hosted.len() == 1, "shared on the local network");
}
#[test]
fn a_key_typed_for_one_server_is_not_sent_to_another() {
// The Server key field remembers the server it was typed for: it goes out only to that
// server. Changing the server in Preferences must not send the old server's key to the new
// one. (The key is set as the field editor would, bound to the server it was typed for.)
let (mut h, script) = harness();
h.app.ui.live.share_key = Some(("wss://share.example/".to_string(), "KeyKeyKeyKeyKeyKeyKey0".to_string()));
h.frames(2);
// Change the server in Preferences to another one, then share: the old key must not go to it.
h.app.session.execute("pref.publishServer", &json!({"url": "wss://other.example/"})).unwrap();
h.frames(2);
let shapes = open(&mut h);
click(&mut h, &shapes, "Start sharing");
assert_eq!(script.lock().unwrap().published.last().unwrap().key, None, "the old server's key is not sent to the new server");
}
#[test]
fn an_open_popover_asks_for_no_frames() {
let (mut h, script) = harness();
+11
View File
@@ -1019,6 +1019,17 @@ pub fn popups(app: &mut FigApp, ctx: &egui::Context) {
{
entries.insert(at + 1, (label, "component.combineVariants"));
}
// An instance's actions reach its main component: replace "Create component" with
// "Go to main component" (as in Figma's context menu).
if app
.session
.doc()
.ok()
.is_some_and(|doc| ids.iter().all(|i| doc.node(*i).is_some_and(|n| matches!(n.kind, figcraft_doc::NodeKind::Instance { .. }))))
&& let Some(at) = entries.iter().position(|e| e.1 == "component.create")
{
entries[at] = (tr("Go to main component"), "component.goToMain");
}
// A right-clicked ruler guide (selected, with no layers) can be removed.
if !has_sel && app.session.ds().is_ok_and(|d| d.selected_guide().is_some()) {
entries.splice(0..0, [(tr("Remove guide"), "guide.remove"), ("-", "")]);