From 7b886404c38eaa1c24f6cb4570ff85e16ebd9ef0 Mon Sep 17 00:00:00 2001 From: BFlat Date: Fri, 30 Oct 2026 13:03:31 +0700 Subject: [PATCH] M8: Opening a file contacts no team library server it names until you check it (#1621) * M8: opening a file contacts no team library server it names until you check it (#1097) * M8: translate the team library check strings (#1097) * M8: the Assets panel library test checks what allows a library, without its own fake link (Sonar duplication and coverage) --- ROADMAP.md | 3 +- crates/engine/src/assets/library_server.rs | 31 +++++- crates/engine/src/cmd/library.rs | 32 ++++++ crates/engine/src/extensions/plugins.rs | 2 + crates/engine/src/lib.rs | 2 + crates/engine/src/library_beacon_tests.rs | 117 +++++++++++++++++++++ crates/engine/src/prefs.rs | 7 ++ crates/engine/src/sharing/live.rs | 4 +- crates/ui-egui/locales/ar.toml | 3 + crates/ui-egui/locales/bn.toml | 3 + crates/ui-egui/locales/cs.toml | 3 + crates/ui-egui/locales/de.toml | 3 + crates/ui-egui/locales/en.toml | 3 + crates/ui-egui/locales/es.toml | 3 + crates/ui-egui/locales/fi.toml | 3 + crates/ui-egui/locales/fr.toml | 3 + crates/ui-egui/locales/id.toml | 3 + crates/ui-egui/locales/it.toml | 3 + crates/ui-egui/locales/ja.toml | 3 + crates/ui-egui/locales/ka.toml | 3 + crates/ui-egui/locales/ko.toml | 3 + crates/ui-egui/locales/nl.toml | 3 + crates/ui-egui/locales/pl.toml | 3 + crates/ui-egui/locales/pt-BR.toml | 3 + crates/ui-egui/locales/ru.toml | 3 + crates/ui-egui/locales/sv.toml | 3 + crates/ui-egui/locales/th.toml | 3 + crates/ui-egui/locales/tr.toml | 3 + crates/ui-egui/locales/vi.toml | 3 + crates/ui-egui/locales/zh-Hans.toml | 3 + crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/src/library_beacon_tests.rs | 32 ++++++ crates/ui-egui/src/panels/assets_panel.rs | 21 +++- docs/components-parity.md | 3 +- docs/mcp.md | 4 +- docs/roadmap.md | 3 +- docs/server.md | 6 ++ 37 files changed, 325 insertions(+), 10 deletions(-) create mode 100644 crates/engine/src/library_beacon_tests.rs create mode 100644 crates/ui-egui/src/library_beacon_tests.rs diff --git a/ROADMAP.md b/ROADMAP.md index 4c7d4dced..1c42de0d7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); components can't contain their own instances, directly or through other components; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); offline Penpot 2.x import/export (#1143); optional batch continue-on-error with per-step outcomes (#1386); the Design panel's sections on one grid (#858); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); images placed at their own size, 2x screenshots in points (#1360); the toolbar on one 32 pt grid, without its Resources button (#1210); M4: zero-length round-capped dashes draw dots; dash expansion uses the existing safety budget; extracted from #1143; Arabic draft catalog; scaled instances keep their scale when their main is rebuilt (#922); .fig embedded images are copied once per blob, not once per hash (#1059); page ruler guides survive save and reopen (#1476); colour picker LCH and OKLCH models; non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); components can't contain their own instances, directly or through other components; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); offline Penpot 2.x import/export (#1143); optional batch continue-on-error with per-step outcomes (#1386); the Design panel's sections on one grid (#858); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); images placed at their own size, 2x screenshots in points (#1360); the toolbar on one 32 pt grid, without its Resources button (#1210); M4: zero-length round-capped dashes draw dots; dash expansion uses the existing safety budget; extracted from #1143; Arabic draft catalog; scaled instances keep their scale when their main is rebuilt (#922); .fig embedded images are copied once per blob, not once per hash (#1059); page ruler guides survive save and reopen (#1476); colour picker LCH and OKLCH models; non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -355,6 +355,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M8, #1097: while the Assets panel showed a file, the app asked the server of every team library the file named for updates, with the link's token and a usage report, so a file someone sent could make the app contact any server. Now only team libraries you added (`library.add`) or checked on this computer are checked by themselves (kept with the preferences as hashes of their links); another one waits in **Assets ▸ Libraries** as "Not checked yet" until you click **Check for updates** (`library.check`, which plugins can't call). Placing or applying something from it is your own action and asks its server as before. Numbers unchanged. Tests: `library_beacon_tests` (engine and UI). | | 2026-10-30 | minor | M8, #1121: client-side maps were unbounded. A hostile host or server could mint people without end: the presence waiting for the UI, the people a connection had seen and the session's list of who is here each grew with every new id; and a session server's set of deleted layer ids (kept so undo may create them again) only grew. Now a client keeps track of at most `MAX_PEERS` (4,096) others: past it, someone new is left out with a logged warning (in a welcome too), someone already here can still be renamed, and someone leaving makes room; a server's `max_people` above 4,096 is lowered to it with a startup warning (the server still starts). A session remembers at most 262,144 deleted ids, forgetting the lowest first (on restore too). Threat model in `collab-protocol.md`. Tests: `client_cap_tests.rs` (collab-net), `live_peer_cap_tests.rs` (engine), `deleted_cap_tests.rs` (collab), the server's `max_people` clamp. | | 2026-10-30 | minor | M8, #1117: `doc.list` returned the path on disk of every open file to any caller, plugins and attached AI agents included. It now gives an agent's calls (MCP, or a control call naming an `agent`) and plugins each file's `id`, name and state without `path`; the user's own scripts (the CLI, control calls without an agent) still get paths, and the app's UI reads its tabs directly. Numbers unchanged. Tests: `doc_list_paths_tests`. | | 2026-10-30 | minor | M8, #1571 (part): a large selection redid the same work on every frame. A pointer move draws two to five egui frames, and each one worked out every selected layer's outline, the selection box and size, the slots shown, and the Design panel's instance checks and position values (X, Y, rotation, constraints, alignment) again, though only the first frame after a move or an edit can differ. Now they are kept with the document snapshot, the selection and, for the overlay, the view's mapping to the screen and the overlay green, and made again when any of those changes (`selection_cache`); the hover and drop-target highlights are drawn every frame as before. Nothing looks different. Dragging 648 layers of a synthetic file of about 1,900 (release build, control-driven, two frames per move): the canvas overlays 1.99 to 0.86 ms a frame, the Design panel 2.54 to 1.40 ms. Not yet: the frame of each move still works everything out (the selection spacing, the export section, the outlines), the canvas texture is uploaded whole after each render. Tests: `canvas/selection_cache_tests.rs`. | diff --git a/crates/engine/src/assets/library_server.rs b/crates/engine/src/assets/library_server.rs index b9dbce478..5950b681b 100644 --- a/crates/engine/src/assets/library_server.rs +++ b/crates/engine/src/assets/library_server.rs @@ -26,6 +26,15 @@ pub const NOT_FETCHED: &str = "the library hasn't been fetched from its server y pub const NO_LIBRARY: &str = "that file has no library on the server yet: publish it there first (Libraries ▸ Publish to server)"; /// Most components one usage report lists. pub const MAX_USAGE: usize = 10_000; +/// Most team libraries remembered as yours ([`Prefs::team_libraries`](crate::Prefs)): adding or +/// checking another forgets the one used longest ago. +pub const MAX_TEAM_LIBRARIES: usize = 500; + +/// How [`Prefs::team_libraries`](crate::Prefs) keeps a link: its SHA-256, so the preferences hold +/// no token. +fn link_key(link: &str) -> String { + figcraft_format::image_hash(link.trim().as_bytes()) +} /// Whether a library's path is a share link of a file on a server (else it is a library file). pub fn is_server_link(path: &str) -> bool { @@ -156,15 +165,35 @@ impl Session { self.take_library(path, reply).map_err(EngineError::Other) } + /// Whether team library `link` is one you added or checked here ([`Prefs::team_libraries`](crate::Prefs)): + /// only those are checked for updates by themselves. + pub fn library_allowed(&self, link: &str) -> bool { + self.prefs.team_libraries.contains(&link_key(link)) + } + + /// Remember team library `link` as one you use (the app saves the preferences at once). + pub(crate) fn allow_library(&mut self, link: &str) { + let key = link_key(link); + let list = &mut self.prefs.team_libraries; + if list.first() != Some(&key) { + list.retain(|k| *k != key); + list.insert(0, key); + list.truncate(MAX_TEAM_LIBRARIES); + self.live.unsaved = true; + } + } + /// Ask the servers of the active document's team libraries for newer versions, in the /// background, reporting how the document uses each (the Assets panel calls it now and then). + /// Only libraries you added or checked here are asked: opening a file that names another + /// server contacts nothing until you check it (`library.check`, #1097). pub fn refresh_server_libraries(&mut self) { let Ok(doc) = self.doc() else { return }; let jobs: Vec<(String, LibraryReq)> = doc .libraries .enabled .iter() - .filter(|l| is_server_link(&l.path)) + .filter(|l| is_server_link(&l.path) && self.library_allowed(&l.path)) .map(|l| { let have = match self.libraries.cached(&l.path) { Some(Ok(lib)) => Some(lib.manifest.version), diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index 0c7edbb2b..f4f70ce44 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -85,6 +85,13 @@ pub fn specs() -> Vec { false, updates ), + spec!( + "library.check", + "Check team libraries for updates", + "{library? (an enabled team library: id, name or link; all of this file's when omitted)} asks their servers for the latest version in the background, and from then on checks them by themselves: opening a file doesn't contact the server of a team library you haven't added or checked → {checking}", + false, + check + ), spec!( "library.acceptUpdates", "Accept library updates", @@ -524,6 +531,10 @@ fn add(s: &mut Session, p: &Value) -> Result { let raw = req_string(cmd, p, "path")?; let path = if is_server_link(&raw) { raw.trim().to_string() } else { absolute_path(raw.trim()) }; let lib = s.load_library(&path)?; + if is_server_link(&path) { + // Added by you: its server may be checked for updates from now on (#1097). + s.allow_library(&path); + } let m = &lib.manifest; let doc = s.doc()?; if doc.published.as_ref().is_some_and(|me| me.manifest.id == m.id) { @@ -549,6 +560,27 @@ fn add(s: &mut Session, p: &Value) -> Result { Ok(out) } +/// `library.check`: the user lets this file's team libraries (or one) be checked; they are asked now. +fn check(s: &mut Session, p: &Value) -> Result { + let cmd = "library.check"; + let doc = s.doc()?; + let links: Vec = match string(p, "library") { + Some(key) => { + let l = find_link(doc, &key).ok_or_else(|| bad(cmd, format!("no enabled library `{key}` (library.list shows them)")))?; + if !is_server_link(&l.path) { + return Err(bad(cmd, format!("`{key}` is a library file, not a team library on a server"))); + } + vec![l.path.clone()] + } + None => doc.libraries.enabled.iter().filter(|l| is_server_link(&l.path)).map(|l| l.path.clone()).collect(), + }; + for l in &links { + s.allow_library(l); + } + s.refresh_server_libraries(); + Ok(json!({"checking": links.len()})) +} + fn remove(s: &mut Session, p: &Value) -> Result { let cmd = "library.remove"; let key = req_string(cmd, p, "id")?; diff --git a/crates/engine/src/extensions/plugins.rs b/crates/engine/src/extensions/plugins.rs index 443864b92..cd9d35ec6 100644 --- a/crates/engine/src/extensions/plugins.rs +++ b/crates/engine/src/extensions/plugins.rs @@ -107,6 +107,8 @@ const DENIED: &[&str] = &[ "plugins.", "libraries.", "library.add", + // Contacts the servers a file names, and remembers them as yours (#1097). + "library.check", // Uploads the open document to a server. "library.publishToServer", "edit.undo", diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 35875518f..e0ef1ff72 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -361,6 +361,8 @@ mod jam_open_tests; #[cfg(test)] mod keymap_tests; #[cfg(test)] +mod library_beacon_tests; +#[cfg(test)] mod library_path_tests; #[cfg(test)] mod library_range_styles_tests; diff --git a/crates/engine/src/library_beacon_tests.rs b/crates/engine/src/library_beacon_tests.rs new file mode 100644 index 000000000..187ea5264 --- /dev/null +++ b/crates/engine/src/library_beacon_tests.rs @@ -0,0 +1,117 @@ +//! Opening a file that names a team library on some server doesn't contact that server (#1097): +//! only team libraries you added or checked are asked for updates by themselves. + +use std::sync::{Arc, Mutex}; + +use figcraft_collab::message::{ClientMsg, LibraryReply, LibraryReq}; +use figcraft_doc::{Document, LibraryLink}; +use serde_json::json; + +use crate::live::{Event, HostOptions, LiveLink, NetRequest, Presence, SessionInfo}; +use crate::{Prefs, Session}; + +const LINK: &str = "ws://tracker.example:9/?file=f&token=per-file-secret"; + +/// Every request the app would send anywhere. +#[derive(Default)] +struct Wire { + sent: Vec, +} + +struct Link(Arc>); + +impl Link { + fn note(&self, what: &str) { + self.0.lock().unwrap().sent.push(what.into()); + } +} + +impl LiveLink for Link { + fn host(&mut self, _: u64, _: HostOptions) -> Result { + self.note("host"); + Err("no".into()) + } + fn join(&mut self, _: u64, _: &str, _: &str) -> Result<(), String> { + self.note("join"); + Err("no".into()) + } + fn leave(&mut self, _: u64) {} + fn presence(&mut self, _: u64, _: &Presence) {} + fn send(&mut self, _: u64, _: ClientMsg) { + self.note("send"); + } + fn poll(&mut self) -> Vec<(u64, Event)> { + Vec::new() + } + fn library(&mut self, link: &str, _: LibraryReq) -> Result { + self.note(link); + Ok(LibraryReply::Empty) + } + fn library_background(&mut self, jobs: Vec<(String, LibraryReq)>) { + for (link, _) in jobs { + self.note(&link); + } + } + fn background(&mut self, _: u64, _: NetRequest) -> bool { + self.note("background"); + false + } +} + +/// A session with `prefs` that opened a file (from disk) whose libraries include [`LINK`]. +fn opened(prefs: Prefs, tag: &str) -> (Session, Arc>, std::path::PathBuf) { + let mut d = Document::new("Someone else's file"); + d.libraries_mut().enabled.push(LibraryLink { id: "lib".into(), name: "Kit".into(), path: LINK.into(), version: 3 }); + let path = std::env::temp_dir().join(format!("figcraft-beacon-{}-{tag}.figcraft", std::process::id())); + std::fs::write(&path, figcraft_format::save_bytes(&d, None).unwrap()).unwrap(); + let mut s = Session::new(); + s.prefs = prefs; + let wire = Arc::new(Mutex::new(Wire::default())); + s.live.install(Box::new(Link(wire.clone()))); + s.execute("file.open", &json!({"path": path.to_string_lossy()})).unwrap(); + assert_eq!(s.doc().unwrap().libraries.enabled.len(), 1); + (s, wire, path) +} + +fn sent(w: &Arc>) -> Vec { + w.lock().unwrap().sent.clone() +} + +#[test] +fn opening_a_file_naming_an_unknown_team_library_contacts_nothing() { + let (mut s, wire, path) = opened(Prefs::default(), "unknown"); + // What the Assets panel does now and then while the file is shown. + for _ in 0..3 { + s.refresh_server_libraries(); + } + assert!(sent(&wire).is_empty(), "no connection attempt: {:?}", sent(&wire)); + assert!(!s.library_allowed(LINK)); + // You check it: now it is asked, and from then on by itself. + assert_eq!(s.execute("library.check", &json!({})).unwrap()["checking"], 1); + assert_eq!(sent(&wire), vec![LINK.to_string()]); + assert!(s.library_allowed(LINK) && s.live.unsaved, "kept with the preferences"); + s.refresh_server_libraries(); + assert_eq!(sent(&wire).len(), 2); + // Kept as a hash: the preferences hold no token. + assert!(!serde_json::to_string(&s.prefs).unwrap().contains("per-file-secret")); + let _ = std::fs::remove_file(path); +} + +#[test] +fn a_team_library_you_checked_before_is_checked_when_a_file_names_it() { + let mut mine = Session::new(); + mine.allow_library(LINK); + let (mut s, wire, path) = opened(mine.prefs.clone(), "checked"); + s.refresh_server_libraries(); + assert_eq!(sent(&wire), vec![LINK.to_string()]); + let _ = std::fs::remove_file(path); +} + +#[test] +fn checking_one_team_library_by_name() { + let (mut s, wire, path) = opened(Prefs::default(), "named"); + assert!(s.execute("library.check", &json!({"library": "Nope"})).is_err()); + assert_eq!(s.execute("library.check", &json!({"library": "Kit"})).unwrap()["checking"], 1); + assert_eq!(sent(&wire), vec![LINK.to_string()]); + let _ = std::fs::remove_file(path); +} diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index 3c241ef6b..709482218 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -110,6 +110,12 @@ pub struct Prefs { /// `widget:`. #[serde(skip_serializing_if = "Vec::is_empty")] pub recent_actions: Vec, + /// The team libraries you added (`library.add`) or checked (`library.check`), as hashes of + /// their links (which carry tokens), newest first, at most + /// [`MAX_TEAM_LIBRARIES`](crate::library_server::MAX_TEAM_LIBRARIES): only these are checked + /// for updates by themselves. A file naming another one doesn't contact its server (#1097). + #[serde(skip_serializing_if = "Vec::is_empty")] + pub team_libraries: Vec, } /// Recently run actions kept for the Actions palette (newest first). @@ -150,6 +156,7 @@ impl Default for Prefs { view: BTreeMap::new(), board_grid: Default::default(), recent_actions: Vec::new(), + team_libraries: Vec::new(), } } } diff --git a/crates/engine/src/sharing/live.rs b/crates/engine/src/sharing/live.rs index 4065f817b..ec3c094e1 100644 --- a/crates/engine/src/sharing/live.rs +++ b/crates/engine/src/sharing/live.rs @@ -196,8 +196,8 @@ pub struct Lives { /// Registers the app as the opener of `figcraft://` invite links (the desktop app on Windows /// installs one; see [`crate::invite_links`]). pub links: Option>, - /// The recorded online files changed since the app last saved the preferences (it saves them - /// at once: a crash must not lose an owner token). + /// The recorded online files or your team libraries changed since the app last saved the + /// preferences (it saves them at once: a crash must not lose an owner token). pub unsaved: bool, /// Servers that couldn't say what became of your files at the last `collab.files {refresh}`. pub servers: crate::online::Problems, diff --git a/crates/ui-egui/locales/ar.toml b/crates/ui-egui/locales/ar.toml index 0b41c0f5b..4a988de6a 100644 --- a/crates/ui-egui/locales/ar.toml +++ b/crates/ui-egui/locales/ar.toml @@ -946,6 +946,9 @@ status = "draft — needs native review" "Not in this font" = "غير متاح في هذا الخط" "Not properties, kept as written: {names}" = "ليست خصائص، أُبقيت كما كُتبت: {names}" "Not read yet" = "لم تُقرأ بعد" +"Check for updates" = "التحقق من التحديثات" +"Not checked yet" = "لم يُتحقق منها بعد" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "يستخدم هذا الملف مكتبات فريق على خوادم لم تستخدمها هنا. لا يُتصل بها حتى تتحقق منها." "Not shared" = "غير مشارك" "Note {n}" = "الملاحظة {n}" "Nothing changed since this version." = "لم يتغير شيء منذ هذا الإصدار." diff --git a/crates/ui-egui/locales/bn.toml b/crates/ui-egui/locales/bn.toml index f7e0cf4dd..6e0e9fc73 100644 --- a/crates/ui-egui/locales/bn.toml +++ b/crates/ui-egui/locales/bn.toml @@ -914,6 +914,9 @@ status = "draft — needs native review" "Not in this font" = "এই ফন্টে নেই" "Not properties, kept as written: {names}" = "প্রপার্টি নয়, যেমনটি লেখা হয়েছে তেমনি রাখা হয়েছে: {names}" "Not read yet" = "এখনো পড়া হয়নি" +"Check for updates" = "আপডেট দেখুন" +"Not checked yet" = "এখনো যাচাই করা হয়নি" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "এই ফাইলটি এমন সার্ভারের টিম লাইব্রেরি ব্যবহার করে যা আপনি এখানে ব্যবহার করেননি। আপনি যাচাই না করা পর্যন্ত সেগুলোর সাথে যোগাযোগ করা হয় না।" "Not shared" = "শেয়ার করা হয়নি" "Note {n}" = "নোট {n}" "Nothing changed since this was marked ready for dev." = "এটি ডেভের জন্য প্রস্তুত হিসেবে চিহ্নিত করার পর থেকে কিছু পরিবর্তিত হয়নি।" diff --git a/crates/ui-egui/locales/cs.toml b/crates/ui-egui/locales/cs.toml index 06a9fc4bf..adfb45b7e 100644 --- a/crates/ui-egui/locales/cs.toml +++ b/crates/ui-egui/locales/cs.toml @@ -896,6 +896,9 @@ status = "draft — needs native review" "Not in this font" = "Není v tomto písmu" "Not properties, kept as written: {names}" = "Nejde o vlastnosti, ponecháno beze změny: {names}" "Not read yet" = "Dosud nepřečteno" +"Check for updates" = "Zkontrolovat aktualizace" +"Not checked yet" = "Dosud nezkontrolováno" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Tento soubor používá týmové knihovny na serverech, které jste tu dosud nepoužili. Nekontaktují se, dokud je nezkontrolujete." "Not shared" = "Nesdíleno" "Note {n}" = "Poznámka {n}" "Nothing changed since this was marked ready for dev." = "Od označení jako připravené pro vývoj se nic nezměnilo." diff --git a/crates/ui-egui/locales/de.toml b/crates/ui-egui/locales/de.toml index 33c7e3960..aa9d040b1 100644 --- a/crates/ui-egui/locales/de.toml +++ b/crates/ui-egui/locales/de.toml @@ -870,6 +870,9 @@ status = "draft — needs native review" "Not in this font" = "Nicht in dieser Schrift" "Not properties, kept as written: {names}" = "Keine Eigenschaften, unverändert übernommen: {names}" "Not read yet" = "Noch nicht gelesen" +"Check for updates" = "Nach Updates suchen" +"Not checked yet" = "Noch nicht geprüft" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Diese Datei nutzt Teambibliotheken auf Servern, die Sie hier noch nicht verwendet haben. Diese Server werden erst kontaktiert, wenn Sie die Bibliotheken prüfen." "Not shared" = "Nicht geteilt" "Note {n}" = "Notiz {n}" "Nothing changed since this version." = "Seit dieser Version hat sich nichts geändert." diff --git a/crates/ui-egui/locales/en.toml b/crates/ui-egui/locales/en.toml index e76aa0e13..7529d93b8 100644 --- a/crates/ui-egui/locales/en.toml +++ b/crates/ui-egui/locales/en.toml @@ -221,6 +221,7 @@ name = "English" "Can comment" = "Can comment" "Check" = "Check" "Check for Updates" = "Check for Updates" +"Check for updates" = "Check for updates" "Check for updates…" = "Check for updates…" "Checking for updates…" = "Checking for updates…" "Checking the fonts installed on this computer…" = "Checking the fonts installed on this computer…" @@ -953,6 +954,7 @@ name = "English" "Not in this font" = "Not in this font" "Not now" = "Not now" "Not properties, kept as written: {names}" = "Not properties, kept as written: {names}" +"Not checked yet" = "Not checked yet" "Not read yet" = "Not read yet" "Not shared" = "Not shared" "Note {n}" = "Note {n}" @@ -1502,6 +1504,7 @@ name = "English" "This branch was merged or archived." = "This branch was merged or archived." "This computer only" = "This computer only" "This computer remembers the owner token, so the file can be managed from the home screen's Online list. Copy it to manage the file from somewhere else: it is a password for this file and won't be shown again." = "This computer remembers the owner token, so the file can be managed from the home screen's Online list. Copy it to manage the file from somewhere else: it is a password for this file and won't be shown again." +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." "This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." = "This page keeps the owner token only while it is open. Copy it to manage the file later: it is a password for this file and won't be shown again." "This condition is edited as an expression" = "This condition is edited as an expression" "This file" = "This file" diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml index f95ecb03e..95743f10a 100644 --- a/crates/ui-egui/locales/es.toml +++ b/crates/ui-egui/locales/es.toml @@ -940,6 +940,9 @@ status = "draft — needs native review" "Not in this font" = "No está en esta fuente" "Not properties, kept as written: {names}" = "No son propiedades, se quedan tal cual: {names}" "Not read yet" = "Aún no leída" +"Check for updates" = "Buscar actualizaciones" +"Not checked yet" = "Aún sin comprobar" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Este archivo usa bibliotecas de equipo en servidores que no has usado aquí. No se contactan hasta que las compruebes." "Not shared" = "Sin compartir" "Note {n}" = "Nota {n}" "Nothing changed since this version." = "Nada cambió desde esta versión." diff --git a/crates/ui-egui/locales/fi.toml b/crates/ui-egui/locales/fi.toml index 1a9af63e9..851583efc 100644 --- a/crates/ui-egui/locales/fi.toml +++ b/crates/ui-egui/locales/fi.toml @@ -916,6 +916,9 @@ status = "draft — needs native review" "Not in this font" = "Ei tässä fontissa" "Not properties, kept as written: {names}" = "Eivät ole ominaisuuksia, säilytetään sellaisinaan: {names}" "Not read yet" = "Ei vielä luettu" +"Check for updates" = "Tarkista päivitykset" +"Not checked yet" = "Ei vielä tarkistettu" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Tämä tiedosto käyttää tiimikirjastoja palvelimilla, joita et ole käyttänyt täällä. Niihin ei oteta yhteyttä ennen kuin tarkistat ne." "Not shared" = "Ei jaettu" "Note {n}" = "Huomautus {n}" "Nothing changed since this was marked ready for dev." = "Mikään ei ole muuttunut kehitysvalmiiksi merkitsemisen jälkeen." diff --git a/crates/ui-egui/locales/fr.toml b/crates/ui-egui/locales/fr.toml index 932c1d726..894f4eeac 100644 --- a/crates/ui-egui/locales/fr.toml +++ b/crates/ui-egui/locales/fr.toml @@ -914,6 +914,9 @@ status = "draft — needs native review" "Not in this font" = "Absent de cette police" "Not properties, kept as written: {names}" = "Pas des propriétés, conservés tels quels : {names}" "Not read yet" = "Pas encore lu" +"Check for updates" = "Rechercher des mises à jour" +"Not checked yet" = "Pas encore vérifiée" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Ce fichier utilise des bibliothèques d’équipe sur des serveurs que vous n’avez pas utilisés ici. Ils ne sont contactés que lorsque vous les vérifiez." "Not shared" = "Non partagé" "Note {n}" = "Note {n}" "Nothing changed since this version." = "Rien n’a changé depuis cette version." diff --git a/crates/ui-egui/locales/id.toml b/crates/ui-egui/locales/id.toml index b3bd1bc9e..56d09e3a2 100644 --- a/crates/ui-egui/locales/id.toml +++ b/crates/ui-egui/locales/id.toml @@ -903,6 +903,9 @@ status = "draft — needs native review" "Not in this font" = "Tidak ada di font ini" "Not properties, kept as written: {names}" = "Bukan properti, dibiarkan apa adanya: {names}" "Not read yet" = "Belum dibaca" +"Check for updates" = "Periksa pembaruan" +"Not checked yet" = "Belum diperiksa" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "File ini memakai pustaka tim di server yang belum pernah Anda gunakan di sini. Server itu tidak dihubungi sampai Anda memeriksanya." "Not shared" = "Tidak dibagikan" "Note {n}" = "Catatan {n}" "Nothing changed since this was marked ready for dev." = "Tidak ada perubahan sejak ditandai siap untuk dev." diff --git a/crates/ui-egui/locales/it.toml b/crates/ui-egui/locales/it.toml index 299d322d5..84eccc637 100644 --- a/crates/ui-egui/locales/it.toml +++ b/crates/ui-egui/locales/it.toml @@ -884,6 +884,9 @@ status = "draft — needs native review" "Not in this font" = "Non presente in questo font" "Not properties, kept as written: {names}" = "Non sono proprietà, mantenute come scritte: {names}" "Not read yet" = "Non ancora letto" +"Check for updates" = "Verifica aggiornamenti" +"Not checked yet" = "Non ancora verificata" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Questo file usa librerie del team su server che non hai usato qui. Non vengono contattati finché non le verifichi." "Not shared" = "Non condiviso" "Note {n}" = "Nota {n}" "Nothing changed since this was marked ready for dev." = "Nulla è cambiato da quando è stato segnato come pronto per lo sviluppo." diff --git a/crates/ui-egui/locales/ja.toml b/crates/ui-egui/locales/ja.toml index 298722c4c..6cc8ad2f7 100644 --- a/crates/ui-egui/locales/ja.toml +++ b/crates/ui-egui/locales/ja.toml @@ -907,6 +907,9 @@ status = "draft — needs native review" "Not in this font" = "このフォントにはない" "Not properties, kept as written: {names}" = "プロパティではないため記述どおりに保持:{names}" "Not read yet" = "未読" +"Check for updates" = "アップデートを確認" +"Not checked yet" = "未確認" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "このファイルは、ここで使ったことのないサーバー上のチームライブラリを使う。確認するまでそれらのサーバーには接続しない。" "Not shared" = "共有していない" "Note {n}" = "メモ {n}" "Nothing changed since this version." = "このバージョンから変更なし。" diff --git a/crates/ui-egui/locales/ka.toml b/crates/ui-egui/locales/ka.toml index 35f8825c5..4014462b1 100644 --- a/crates/ui-egui/locales/ka.toml +++ b/crates/ui-egui/locales/ka.toml @@ -923,6 +923,9 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv "Not in this font" = "ამ შრიფტში არ არის" "Not properties, kept as written: {names}" = "არ არის თვისებები, დარჩა დაწერილი სახით: {names}" "Not read yet" = "ჯერ არ არის წაკითხული" +"Check for updates" = "განახლებების შემოწმება" +"Not checked yet" = "ჯერ არ არის შემოწმებული" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "ეს ფაილი იყენებს გუნდის ბიბლიოთეკებს სერვერებზე, რომლებიც აქ არ გამოგიყენებიათ. მათ არ დავუკავშირდებით, სანამ არ შეამოწმებთ." "Not shared" = "არ არის გაზიარებული" "Note {n}" = "შენიშვნა {n}" "Nothing changed since this version." = "ამ ვერსიის შემდეგ არაფერი შეცვლილა." diff --git a/crates/ui-egui/locales/ko.toml b/crates/ui-egui/locales/ko.toml index 5524afc68..681eb524b 100644 --- a/crates/ui-egui/locales/ko.toml +++ b/crates/ui-egui/locales/ko.toml @@ -866,6 +866,9 @@ status = "draft — needs native review" "Not in this font" = "이 글꼴에 없음" "Not properties, kept as written: {names}" = "속성이 아니므로 작성된 대로 유지: {names}" "Not read yet" = "아직 읽지 않음" +"Check for updates" = "업데이트 확인" +"Not checked yet" = "아직 확인 안 함" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "이 파일은 여기서 사용한 적 없는 서버의 팀 라이브러리를 사용합니다. 확인하기 전까지는 해당 서버에 연결하지 않습니다." "Not shared" = "공유 안 함" "Note {n}" = "메모 {n}" "Nothing changed since this version." = "이 버전 이후 변경된 내용 없음." diff --git a/crates/ui-egui/locales/nl.toml b/crates/ui-egui/locales/nl.toml index 4f9b27329..89a993346 100644 --- a/crates/ui-egui/locales/nl.toml +++ b/crates/ui-egui/locales/nl.toml @@ -916,6 +916,9 @@ status = "draft — needs native review" "Not in this font" = "Niet in dit lettertype" "Not properties, kept as written: {names}" = "Geen eigenschappen, ongewijzigd overgenomen: {names}" "Not read yet" = "Nog niet gelezen" +"Check for updates" = "Controleren op updates" +"Not checked yet" = "Nog niet gecontroleerd" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Dit bestand gebruikt teambibliotheken op servers die je hier nog niet hebt gebruikt. Er wordt pas contact mee gemaakt als je ze controleert." "Not shared" = "Niet gedeeld" "Note {n}" = "Notitie {n}" "Nothing changed since this was marked ready for dev." = "Er is niets gewijzigd sinds dit was gemarkeerd als ready for dev." diff --git a/crates/ui-egui/locales/pl.toml b/crates/ui-egui/locales/pl.toml index 7b60c4c92..2081b1cd5 100644 --- a/crates/ui-egui/locales/pl.toml +++ b/crates/ui-egui/locales/pl.toml @@ -918,6 +918,9 @@ name = "Polski" "Not in this font" = "Brak w tej czcionce" "Not properties, kept as written: {names}" = "Nie są to właściwości, zachowano bez zmian: {names}" "Not read yet" = "Jeszcze nieprzeczytane" +"Check for updates" = "Sprawdź aktualizacje" +"Not checked yet" = "Jeszcze nie sprawdzono" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Ten plik korzysta z bibliotek zespołu na serwerach, których tu jeszcze nie używano. Nie są kontaktowane, dopóki ich nie sprawdzisz." "Not shared" = "Nieudostępnione" "Note {n}" = "Notatka {n}" "Nothing changed since this was marked ready for dev." = "Nic się nie zmieniło od czasu oznaczenia jako gotowe do wdrożenia." diff --git a/crates/ui-egui/locales/pt-BR.toml b/crates/ui-egui/locales/pt-BR.toml index 64d3dc131..4033ecc8e 100644 --- a/crates/ui-egui/locales/pt-BR.toml +++ b/crates/ui-egui/locales/pt-BR.toml @@ -865,6 +865,9 @@ status = "draft — needs native review" "Not in this font" = "Não existe nesta fonte" "Not properties, kept as written: {names}" = "Não são propriedades, mantidos como escritos: {names}" "Not read yet" = "Ainda não lido" +"Check for updates" = "Verificar atualizações" +"Not checked yet" = "Ainda não verificada" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Este arquivo usa bibliotecas de equipe em servidores que você não usou aqui. Eles só são contatados quando você os verifica." "Not shared" = "Não compartilhado" "Note {n}" = "Nota {n}" "Nothing changed since this version." = "Nada mudou desde esta versão." diff --git a/crates/ui-egui/locales/ru.toml b/crates/ui-egui/locales/ru.toml index 9cb885913..01c2813a5 100644 --- a/crates/ui-egui/locales/ru.toml +++ b/crates/ui-egui/locales/ru.toml @@ -918,6 +918,9 @@ name = "Русский" "Not in this font" = "Нет в этом шрифте" "Not properties, kept as written: {names}" = "Не свойства, оставлены как есть: {names}" "Not read yet" = "Ещё не прочитано" +"Check for updates" = "Проверить обновления" +"Not checked yet" = "Ещё не проверено" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Этот файл использует командные библиотеки на серверах, которые вы здесь не использовали. К ним не обращаются, пока вы их не проверите." "Not shared" = "Не опубликовано" "Note {n}" = "Заметка {n}" "Nothing changed since this was marked ready for dev." = "С момента отметки «Готово к разработке» изменений нет." diff --git a/crates/ui-egui/locales/sv.toml b/crates/ui-egui/locales/sv.toml index 7832111bf..549b7ff82 100644 --- a/crates/ui-egui/locales/sv.toml +++ b/crates/ui-egui/locales/sv.toml @@ -940,6 +940,9 @@ name = "Svenska" "Not in this font" = "Finns inte i det här typsnittet" "Not properties, kept as written: {names}" = "Inte egenskaper, behölls som de skrevs: {names}" "Not read yet" = "Inte läst än" +"Check for updates" = "Sök efter uppdateringar" +"Not checked yet" = "Inte kontrollerat än" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Den här filen använder teambibliotek på servrar som du inte har använt här. De kontaktas inte förrän du kontrollerar dem." "Not shared" = "Inte delad" "Note {n}" = "Anteckning {n}" "Nothing changed since this version." = "Ingenting har ändrats sedan den här versionen." diff --git a/crates/ui-egui/locales/th.toml b/crates/ui-egui/locales/th.toml index f17434ff6..4af404982 100644 --- a/crates/ui-egui/locales/th.toml +++ b/crates/ui-egui/locales/th.toml @@ -910,6 +910,9 @@ status = "draft — needs native review" "Not in this font" = "ไม่มีในฟอนต์นี้" "Not properties, kept as written: {names}" = "ไม่ใช่พร็อพเพอร์ตี คงไว้ตามที่เขียน: {names}" "Not read yet" = "ยังไม่ได้อ่าน" +"Check for updates" = "ตรวจหาอัปเดต" +"Not checked yet" = "ยังไม่ได้ตรวจ" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "ไฟล์นี้ใช้ไลบรารีทีมบนเซิร์ฟเวอร์ที่คุณยังไม่เคยใช้ที่นี่ จะไม่มีการติดต่อเซิร์ฟเวอร์เหล่านั้นจนกว่าคุณจะตรวจ" "Not shared" = "ไม่ได้แชร์" "Note {n}" = "โน้ต {n}" "Nothing changed since this version." = "ไม่มีอะไรเปลี่ยนตั้งแต่เวอร์ชันนี้" diff --git a/crates/ui-egui/locales/tr.toml b/crates/ui-egui/locales/tr.toml index ef525a8b7..6f84aa376 100644 --- a/crates/ui-egui/locales/tr.toml +++ b/crates/ui-egui/locales/tr.toml @@ -900,6 +900,9 @@ name = "Türkçe" "Not in this font" = "Bu yazı tipinde yok" "Not properties, kept as written: {names}" = "Özellik değil, yazıldığı gibi korundu: {names}" "Not read yet" = "Henüz okunmadı" +"Check for updates" = "Güncellemeleri denetle" +"Not checked yet" = "Henüz denetlenmedi" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Bu dosya, burada kullanmadığınız sunuculardaki ekip kitaplıklarını kullanıyor. Siz denetleyene kadar bu sunuculara bağlanılmaz." "Not shared" = "Paylaşılmadı" "Note {n}" = "Not {n}" "Nothing changed since this was marked ready for dev." = "Geliştirmeye hazır olarak işaretlendiğinden beri hiçbir şey değişmedi." diff --git a/crates/ui-egui/locales/vi.toml b/crates/ui-egui/locales/vi.toml index 1f3ec5e96..e729d8cc0 100644 --- a/crates/ui-egui/locales/vi.toml +++ b/crates/ui-egui/locales/vi.toml @@ -885,6 +885,9 @@ status = "draft — needs native review" "Not in this font" = "Không có trong phông chữ này" "Not properties, kept as written: {names}" = "Không phải thuộc tính, được giữ nguyên như đã viết: {names}" "Not read yet" = "Chưa đọc" +"Check for updates" = "Kiểm tra cập nhật" +"Not checked yet" = "Chưa kiểm tra" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "Tệp này dùng thư viện nhóm trên các máy chủ bạn chưa dùng ở đây. Các máy chủ đó không được liên hệ cho đến khi bạn kiểm tra." "Not shared" = "Chưa chia sẻ" "Note {n}" = "Ghi chú {n}" "Nothing changed since this was marked ready for dev." = "Chưa có gì thay đổi kể từ khi được đánh dấu sẵn sàng cho dev." diff --git a/crates/ui-egui/locales/zh-Hans.toml b/crates/ui-egui/locales/zh-Hans.toml index d5934657d..ae545b2f7 100644 --- a/crates/ui-egui/locales/zh-Hans.toml +++ b/crates/ui-egui/locales/zh-Hans.toml @@ -871,6 +871,9 @@ status = "draft — needs native review" "Not in this font" = "此字体不支持" "Not properties, kept as written: {names}" = "不是属性,按原样保留:{names}" "Not read yet" = "尚未读取" +"Check for updates" = "检查更新" +"Not checked yet" = "尚未检查" +"This file uses team libraries on servers you haven't used here. They aren't contacted until you check them." = "此文件使用了你在此处未用过的服务器上的团队库。在你检查之前,不会连接这些服务器。" "Not shared" = "未分享" "Note {n}" = "备注 {n}" "Nothing changed since this version." = "自此版本以来没有任何更改。" diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index daee33db6..6061b91af 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -130,6 +130,8 @@ mod layers_keys_tests; #[cfg(test)] mod layers_rename_tests; #[cfg(test)] +mod library_beacon_tests; +#[cfg(test)] mod live_chat_tests; #[cfg(test)] mod live_docs_tests; diff --git a/crates/ui-egui/src/library_beacon_tests.rs b/crates/ui-egui/src/library_beacon_tests.rs new file mode 100644 index 000000000..e3b001627 --- /dev/null +++ b/crates/ui-egui/src/library_beacon_tests.rs @@ -0,0 +1,32 @@ +//! The Assets panel showing a file that names a team library on an unknown server doesn't +//! contact it (#1097): the library waits until the user clicks Check for updates. That a library +//! not allowed is never asked is the engine's `library_beacon_tests`; here, what allows it. + +use figcraft_doc::LibraryLink; +use figcraft_engine::Session; + +use crate::layers_drag_tests::Ui; +use crate::live_tests::{flat, has_text, text_center}; + +const LINK: &str = "ws://tracker.example:9/?file=f&token=per-file-secret"; + +#[test] +fn the_assets_panel_checks_an_unknown_library_server_only_when_asked() { + let mut s = Session::new(); + s.doc_mut().unwrap().libraries_mut().enabled.push(LibraryLink { id: "lib".into(), name: "Kit".into(), path: LINK.into(), version: 1 }); + let mut u = Ui::new(s); + u.app.ui.left_tab = crate::LeftTab::Assets; + u.app.ui.libraries = true; + for _ in 0..4 { + u.frame(vec![]); + u.advance_clock(60.0); + } + assert!(!u.app.session.library_allowed(LINK), "showing the panel allows nothing"); + let shapes = flat(&u.frame_output(vec![]).shapes); + assert!(has_text(&shapes, "Not checked yet")); + let at = text_center(&shapes, "Check for updates"); + u.button(at, true); + u.button(at, false); + u.frame(vec![]); + assert!(u.app.session.library_allowed(LINK), "allowed once the user checked"); +} diff --git a/crates/ui-egui/src/panels/assets_panel.rs b/crates/ui-egui/src/panels/assets_panel.rs index 4fe5566df..7592b2574 100644 --- a/crates/ui-egui/src/panels/assets_panel.rs +++ b/crates/ui-egui/src/panels/assets_panel.rs @@ -50,6 +50,7 @@ const PAD: f32 = 16.0; const TO_SERVER: &str = "library.publishToServer"; const USAGE: &str = "library.usage"; const ADD: &str = "library.add"; +const CHECK: &str = "library.check"; /// Where an asset comes from: this file or an enabled library (its id). #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -288,8 +289,10 @@ fn refresh_libraries(app: &mut FigApp, ui: &Ui) { // Team libraries: in the background, at once for one not fetched yet. let sid = Id::unique("asset_server_libraries_checked"); let fetched: Option = ui.data(|d| d.get_temp(sid)); - let unread = paths.iter().any(|p| is_server_link(p) && app.session.libraries.cached(p).is_none()); - if paths.iter().any(|p| is_server_link(p)) && (unread || fetched.is_none_or(|l| now - l >= SERVER_REFRESH)) { + // Only the ones you added or checked: a file can't make the app contact a server (#1097). + let team: Vec<&String> = paths.iter().filter(|p| is_server_link(p) && app.session.library_allowed(p)).collect(); + let unread = team.iter().any(|p| app.session.libraries.cached(p).is_none()); + if !team.is_empty() && (unread || fetched.is_none_or(|l| now - l >= SERVER_REFRESH)) { app.session.refresh_server_libraries(); ui.data_mut(|d| d.insert_temp(sid, now)); } @@ -1054,7 +1057,8 @@ fn enabled_libraries(app: &mut FigApp, ui: &mut Ui, w: f32, doc: &Document) { } Some(Ok(_)) => (tr("The file now holds another library").into(), t.danger), Some(Err(e)) => (e.to_string(), t.danger), - None if is_server_link(&l.path) => (tr("Fetching from the server…").into(), t.text_secondary), + None if is_server_link(&l.path) && app.session.library_allowed(&l.path) => (tr("Fetching from the server…").into(), t.text_secondary), + None if is_server_link(&l.path) => (tr("Not checked yet").into(), t.text_secondary), None => (tr("Not read yet").into(), t.text_secondary), }; let text_w = w - PAD - if swappable { 72.0 } else { 44.0 }; @@ -1092,6 +1096,17 @@ fn enabled_libraries(app: &mut FigApp, ui: &mut Ui, w: f32, doc: &Document) { }); } } + // A file names its team libraries' servers: they are contacted only once you say so (#1097). + let unchecked = doc.libraries.enabled.iter().any(|l| is_server_link(&l.path) && !app.session.library_allowed(&l.path)); + if unchecked { + hint(ui, tr("This file uses team libraries on servers you haven't used here. They aren't contacted until you check them.")); + ui.horizontal(|ui| { + ui.add_space(PAD); + if crate::widgets::button(ui, tr("Check for updates"), false, true).clicked() && !crate::menus::dev_mode_refuses(app, CHECK) { + app.run(CHECK, json!({})); + } + }); + } ui.add_space(4.0); let mut add = false; ui.horizontal(|ui| { diff --git a/docs/components-parity.md b/docs/components-parity.md index 4cf946178..41d0b23ed 100644 --- a/docs/components-parity.md +++ b/docs/components-parity.md @@ -1,6 +1,6 @@ # Components, variants, styles and variables parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (opening a file contacts no team library server it names until you check it (#1097); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7) Design systems are why teams adopt Figma. **Components: breadth ~96%, ready ~72%. Styles and variables: breadth ~95%, ready ~76% (estimated).** QA'd with a synthetic design system of ~300 @@ -78,6 +78,7 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`. | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M8, #1097: library updates from a team library a file names are offered, not fetched from its server by themselves: only team libraries you added or checked are checked in the background (**Check for updates** in Assets ▸ Libraries). Numbers unchanged. | | 2026-10-30 | minor | M8, #1114: variables import from an explicit file path (`variable.import`, the `variables` of `file.open` and `file.import`) takes regular files of at most 256 MiB, the limit the sibling `variables.json` already had; anything else is refused before the document changes. Grade unchanged. Tests: `import_export_tests::an_explicit_variables_path_is_capped_like_the_sibling_file`, `variables_io_tests::variables_import_caps_the_file_it_reads` | | 2026-10-30 | minor | M3, #1536 and #1537: component properties are listed in the order they were made in, and renaming one keeps its place (they were sorted by name, so a rename moved the property); that holds for every kind, on every variant of a set and on instances, in `component.properties`, the Design panel and `.fig` export. Files from before keep their properties by name until one is added or renamed. Make slot (the Slot menu of a frame inside a component) asks for the slot's name first: a field under the Slot row with the frame's name selected, Enter or Make slot to make it, Escape to cancel; a name taken keeps the field open. Renaming stays in the component's Properties list. Tests: `engine/src/prop_order_tests.rs`, `ui-egui/src/make_slot_naming_tests.rs`. Left: dragging the shared properties to reorder them, and renaming a slot with only its frame selected. Numbers unchanged | | 2026-10-29 | minor | M3, #1338: the instance-swap property's menu (on an instance, its main component's default and the component playground) and the instance's swap menu open with a search field at the top that has the keyboard; typing filters the components by name (any case, headings without matches drop out), an empty result reads "No matching components", Up and Down step through the matches and Enter picks one (the first without a step), Escape clears the search and then closes the menu. Only the list scrolls; the menu grows back when the search is cleared. Tests: `component_swap_search_tests`. Numbers unchanged | diff --git a/docs/mcp.md b/docs/mcp.md index 3d270306f..a25ae8fad 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -892,7 +892,9 @@ most when it can't be reached). - `library.add {path: }` enables it (a view link is enough); then `library.place`, `library.applyStyle`, `library.bindVariable`… work as for library files. `library.list` and `library.updates` ask the server for a newer version first (`"server": true` in the listing); - `library.acceptUpdates` takes it, overrides kept. + `library.acceptUpdates` takes it, overrides kept. The app's own background checks ask only about + team libraries added (`library.add`) or checked (`library.check {library?}`) on this computer, + so opening a file that names another one contacts nothing (#1097). - `library.usage {library?}` → `{id, name, instances, files: [{name, updated, instances}], components: [{id, name, instances, files}]}`: what the files using it reported (each fetch reports the asking document's name and its instances per component). `library` is an enabled diff --git a/docs/roadmap.md b/docs/roadmap.md index b8c2a92f4..9aa8bc884 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # FigCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); M8: mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); M2: components can't contain their own instances, directly or through others; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); M0.9: optional batch continue-on-error with per-step outcomes (#1386); Alt pressed during a drag duplicates (#1385); variables in paint props (M3, #1000); finding layers without selecting them (M0.9, part of #996); the attached MCP server reads the app's replies with a 512 MiB bound (#1113); a failed batch keeps the undo steps it undid (#1410); Arabic draft catalog; Android Vector Drawable export; a selected unfilled shape drags over a filled layer below it (#1370); text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); M8: mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); M2: components can't contain their own instances, directly or through others; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); M0.9: optional batch continue-on-error with per-step outcomes (#1386); Alt pressed during a drag duplicates (#1385); variables in paint props (M3, #1000); finding layers without selecting them (M0.9, part of #996); the attached MCP server reads the app's replies with a 512 MiB bound (#1113); a failed batch keeps the undo steps it undid (#1410); Arabic draft catalog; Android Vector Drawable export; a selected unfilled shape drags over a filled layer below it (#1370); text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and [`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours @@ -75,6 +75,7 @@ blockers. | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M8, #1097: team libraries are checked in the background only when you added or checked them here (`library.check`); opening a file that names another server contacts nothing. Tests: `library_beacon_tests` | | 2026-10-30 | minor | M8, #1121: client-side maps were unbounded. A hostile host or server could mint people without end: the presence waiting for the UI, the people a connection had seen and the session's list of who is here each grew with every new id; and a session server's set of deleted layer ids (kept so undo may create them again) only grew. Now a client keeps track of at most `MAX_PEERS` (4,096) others: past it, someone new is left out with a logged warning (in a welcome too), someone already here can still be renamed, and someone leaving makes room; a server's `max_people` above 4,096 is lowered to it with a startup warning (the server still starts). A session remembers at most 262,144 deleted ids, forgetting the lowest first (on restore too). Threat model in `collab-protocol.md`. Tests: `client_cap_tests.rs` (collab-net), `live_peer_cap_tests.rs` (engine), `deleted_cap_tests.rs` (collab), the server's `max_people` clamp. | | 2026-10-30 | minor | M8, #1117: `doc.list` leaves out file paths for AI agents and plugins (ids and names stay). Tests: `doc_list_paths_tests` | | 2026-10-30 | minor | M8, #1571 (part): a large selection redid the same work on every frame. A pointer move draws two to five egui frames, and each one worked out every selected layer's outline, the selection box and size, the slots shown, and the Design panel's instance checks and position values (X, Y, rotation, constraints, alignment) again, though only the first frame after a move or an edit can differ. Now they are kept with the document snapshot, the selection and, for the overlay, the view's mapping to the screen and the overlay green, and made again when any of those changes (`selection_cache`); the hover and drop-target highlights are drawn every frame as before. Nothing looks different. Dragging 648 layers of a synthetic file of about 1,900 (release build, control-driven, two frames per move): the canvas overlays 1.99 to 0.86 ms a frame, the Design panel 2.54 to 1.40 ms. Not yet: the frame of each move still works everything out (the selection spacing, the export section, the outlines), the canvas texture is uploaded whole after each render. Tests: `canvas/selection_cache_tests.rs`. | diff --git a/docs/server.md b/docs/server.md index 2aa8cf733..c2af5b5a9 100644 --- a/docs/server.md +++ b/docs/server.md @@ -209,6 +209,12 @@ which other files use without anyone copying the library file around. **Library updates available**; accepting keeps instance overrides. While the server can't be reached, the last version fetched stays in use. The file keeps the link, token included: sharing that file shares read access to the library. +- **Only libraries you chose are checked by themselves.** The app asks only about team libraries + you added or checked on this computer (kept as hashes of their links in the preferences). A file + someone sent you that names another one contacts nothing when you open it: **Assets ▸ Libraries** + says it isn't checked yet, and **Check for updates** (`library.check`) asks its server and + checks it by itself from then on (#1097). Placing one of its components or applying one of its + styles asks the server too, since you did that yourself. - **Usage.** Each time a file's app asks for the library it reports which of its components the file uses and how often. **Show usage** in the library file (`library.usage`) lists the files and the instances of each component. Files are told apart by their names; a file's report changes