mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-10 20:29:33 -06:00
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:
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"), ("-", "")]);
|
||||
|
||||
Reference in New Issue
Block a user