mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
comment.list takes mentions (a name, any case): only threads where a message, the first or a reply, @mentions it, by the same parser the thread view highlights with and each message's mentions reports; combined with the other filters.
This commit is contained in:
+2
-1
File diff suppressed because one or more lines are too long
@@ -332,13 +332,37 @@ fn mentionable<'a>(people: &'a [String], user: &'a str) -> Vec<&'a str> {
|
||||
people.iter().map(String::as_str).chain([user]).collect()
|
||||
}
|
||||
|
||||
/// The names `text` @mentions by the rule the thread view highlights them with
|
||||
/// ([`figcraft_doc::mentions`]), and whether each is one of `people` (else it is the letters and
|
||||
/// digits after the `@`).
|
||||
fn mentioned<'a>(text: &'a str, people: &[&'a str]) -> impl Iterator<Item = (&'a str, bool)> {
|
||||
mentions(text, people).into_iter().map(move |(r, who)| (who.or_else(|| text.get(r.start + 1..r.end)).unwrap_or_default(), who.is_some()))
|
||||
}
|
||||
|
||||
/// Whether a message of `c` @mentions `name` (any case), as `comment.list` reports mentions;
|
||||
/// `name` resolves like one of `people`, so a name nobody in the file has can be several words.
|
||||
fn thread_mentions(c: &Comment, people: &[&str], name: &str) -> bool {
|
||||
let people: Vec<&str> = people.iter().copied().chain([name]).collect();
|
||||
c.messages().any(|m| mentioned(&m.text, &people).any(|(n, _)| n.eq_ignore_ascii_case(name)))
|
||||
}
|
||||
|
||||
/// `comment.list`'s `mentions` filter: a name (a leading `@` is fine), or none.
|
||||
fn mentions_param(p: &Value) -> crate::Result<Option<String>> {
|
||||
match get(p, "mentions") {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::String(s)) if !s.trim().trim_start_matches('@').trim().is_empty() => {
|
||||
Ok(Some(s.trim().trim_start_matches('@').trim().to_string()))
|
||||
}
|
||||
Some(_) => Err(bad("comment.list", "`mentions` must be a name")),
|
||||
}
|
||||
}
|
||||
|
||||
/// A message as JSON, with the names it @mentions: `{name, known, you}` each, resolved against
|
||||
/// `people` (`mentionable`); `known: false` is a name nobody in the file has.
|
||||
fn message_json(m: &Message, people: &[&str], user: &str) -> Value {
|
||||
let mut named: Vec<Value> = Vec::new();
|
||||
for (r, who) in mentions(&m.text, people) {
|
||||
let name = who.or_else(|| m.text.get(r.start + 1..r.end)).unwrap_or_default();
|
||||
let v = json!({"name": name, "known": who.is_some(), "you": who.is_some_and(|w| w.eq_ignore_ascii_case(user))});
|
||||
for (name, known) in mentioned(&m.text, people) {
|
||||
let v = json!({"name": name, "known": known, "you": known && name.eq_ignore_ascii_case(user)});
|
||||
if !named.contains(&v) {
|
||||
named.push(v);
|
||||
}
|
||||
@@ -807,16 +831,21 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"comment.list",
|
||||
"List comments",
|
||||
"{page? (default: current; \"all\" for every page), filter? (all|open|resolved|mine|unread; default all), sort? (date|page; default date), query? (search text), frame? (a top-level frame id), resolved? (false hides resolved ones), author? (whose threads and read state)}",
|
||||
"{page? (default: current; \"all\" for every page), filter? (all|open|resolved|mine|unread; default all), sort? (date|page; default date), query? (search text), frame? (a top-level frame id), resolved? (false hides resolved ones), author? (whose threads and read state), mentions? (a name, any case: only threads where a message @mentions it, as each message's `mentions` reports)}",
|
||||
false,
|
||||
|s, p| {
|
||||
let doc = s.doc()?;
|
||||
let (page, filter, sort, text) = list_params("comment.list", s, p)?;
|
||||
let want = mentions_param(p)?;
|
||||
let who = author(s, p);
|
||||
let q = Query { page, frame: id(p, "frame"), filter, sort, text: &text };
|
||||
let people = doc.comment_people();
|
||||
let people = mentionable(&people, &who);
|
||||
let list: Vec<Value> = query(doc, &q, &who).into_iter().map(|c| comment_json(doc, c, &who, &people)).collect();
|
||||
let list: Vec<Value> = query(doc, &q, &who)
|
||||
.into_iter()
|
||||
.filter(|c| want.as_deref().is_none_or(|w| thread_mentions(c, &people, w)))
|
||||
.map(|c| comment_json(doc, c, &who, &people))
|
||||
.collect();
|
||||
Ok(json!({"comments": list}))
|
||||
}
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! `comment.list` reports the names each message @mentions, by the rule the thread view highlights
|
||||
//! them with: resolved against the file's people where it can be, and whether the name is you.
|
||||
//! Its `mentions` param lists only the threads where a message mentions a name.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -42,3 +43,37 @@ fn the_mention_rule_matches_the_highlighting_rule() {
|
||||
assert_eq!(figcraft_doc::mention_ranges("@Sam Lee, @sam, x@Sam, @Zoe!", &people), vec![0..8, 10..14]);
|
||||
assert!(figcraft_doc::mentions("@ and a@b.com", &people).is_empty());
|
||||
}
|
||||
|
||||
/// The ids of the threads `comment.list` returns for `p`.
|
||||
fn listed(s: &mut Session, p: Value) -> Vec<u64> {
|
||||
let l = s.execute("comment.list", &p).unwrap();
|
||||
l["comments"].as_array().unwrap().iter().map(|c| c["id"].as_u64().unwrap()).collect()
|
||||
}
|
||||
|
||||
fn add(s: &mut Session, text: &str, author: &str) -> u64 {
|
||||
s.execute("comment.add", &json!({"x": 0, "y": 0, "text": text, "author": author})).unwrap()["id"].as_u64().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mentions_param_lists_only_the_threads_that_mention_a_name() {
|
||||
let mut s = Session::new();
|
||||
let claude = add(&mut s, "@Claude make this bigger", "Sam");
|
||||
let in_reply = add(&mut s, "Spacing looks off", "Sam");
|
||||
s.execute("comment.reply", &json!({"id": in_reply, "text": "agreed, @claude: tighten it", "author": "Ada Lovelace"})).unwrap();
|
||||
let other = add(&mut s, "@Ada Lovelace and @Zoe Smith, mail claude@example.com", "Sam");
|
||||
add(&mut s, "No mentions here", "Sam");
|
||||
let mut both = listed(&mut s, json!({"mentions": "CLAUDE"}));
|
||||
both.sort_unstable();
|
||||
assert_eq!(both, vec![claude, in_reply], "any case, in a reply too, never an email address");
|
||||
assert_eq!(listed(&mut s, json!({"mentions": " @Claude "})), listed(&mut s, json!({"mentions": "claude"})), "a leading @ is fine");
|
||||
assert_eq!(listed(&mut s, json!({"mentions": "ada lovelace"})), vec![other]);
|
||||
assert!(listed(&mut s, json!({"mentions": "Ada"})).is_empty(), "@Ada Lovelace names Ada Lovelace, not Ada");
|
||||
assert_eq!(listed(&mut s, json!({"mentions": "Zoe Smith"})), vec![other], "a name nobody in the file has, several words");
|
||||
assert_eq!(listed(&mut s, json!({"mentions": null})).len(), 4);
|
||||
// Combined with the other filters: resolved threads drop out.
|
||||
s.execute("comment.resolve", &json!({"id": claude})).unwrap();
|
||||
assert_eq!(listed(&mut s, json!({"mentions": "Claude", "filter": "open"})), vec![in_reply]);
|
||||
for junk in [json!(""), json!("@"), json!(5), json!(["Claude"])] {
|
||||
assert!(s.execute("comment.list", &json!({"mentions": junk})).is_err(), "{junk}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Collaboration parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); an AI agent attached over MCP shows where it works (#1416); 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); 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); comment.list reports each message's @mentions (#1415); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); 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); a refused batch's fixes take time linear in the batch (#1063); image entries are checked as they apply; branches in live sessions and branch history entries; server message limits follow `max_file_mb`; a damaged log generation field keeps flushed edits; the web build joins from a `#join=` link; removing someone from a live session; live-session revert guard; live sessions per document; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; cursor chat and spotlight in live sessions; branch version history and finer list merges; comments and version history behaviour suite and fixes; hosting the server: `wss://` in the desktop app, server releases, deployment guide; team libraries on the server and swap library; branches: create, update from main, review and merge; wording: the standalone server with share links to edit or view; numbers unchanged) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (`comment.list {mentions}` lists only the threads that @mention a name (#1415); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); an AI agent attached over MCP shows where it works (#1416); 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); 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); comment.list reports each message's @mentions (#1415); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); 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); a refused batch's fixes take time linear in the batch (#1063); image entries are checked as they apply; branches in live sessions and branch history entries; server message limits follow `max_file_mb`; a damaged log generation field keeps flushed edits; the web build joins from a `#join=` link; removing someone from a live session; live-session revert guard; live sessions per document; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; cursor chat and spotlight in live sessions; branch version history and finer list merges; comments and version history behaviour suite and fixes; hosting the server: `wss://` in the desktop app, server releases, deployment guide; team libraries on the server and swap library; branches: create, update from main, review and merge; wording: the standalone server with share links to edit or view; numbers unchanged) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Figma is multiplayer first; FigCraft is local first. **Ready ~13% (estimated):** comments, version
|
||||
history and branches (review and merge) work inside one file; the real-time collaboration core landed in #103 (sync model,
|
||||
@@ -29,6 +29,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | M9, part of #1415: `comment.list {mentions}` lists only the threads where a message @mentions a name, combined with the other filters, so an agent tagged in comments reads just its threads. Numbers unchanged. |
|
||||
| 2026-10-30 | minor | M9, #917: the browser build publishes to a `figcraft-server`. Share ▸ Online (the server in Preferences) and Share ▸ Publish to server… upload the open file over the page's own WebSocket, make the links and join codes others join with, and keep the tab in sync with the new file, as the desktop app does; asking a server about its hosting, stopping a share and sharing again go out the same way. The Share popover offers only online sharing there (a page can't listen, so the local network stays the desktop app's); without a server in Preferences it says so and offers joining. A failed publish says why and leaves the file as it was. The browser keeps only your name across reloads, so the owner token of a file it put on an open server lasts as long as the page (`crates/collab-net/src/exchange.rs`, run by the browser client's driver; the requests and what their answers mean are shared with the desktop transport in `requests.rs`). Tests: `exchange_tests` (scripted server: with and without the key, sharing or not, refusals, an old server without join codes, time-outs, leaving while publishing, the background requests), `live_join_tests`; checked in a browser against a local server. Percentages and hours unchanged |
|
||||
| 2026-10-30 | minor | Where an AI agent works (M8, #1416): an agent attached over MCP (`figcraft-cli mcp --attach`) changed the file with no sign of where or whether it was still at work. Now, while it runs commands, the people row at the top of the right panel shows its avatar, ringed in its own colour and named as its comments are signed ("Claude (agent)"), and the canvas outlines the layers its last edit changed (the innermost ones), or that it last selected, in that colour with its name. Both go 8 seconds after its last command, and only the file it works in shows them. Display only: no undo step, never saved, never sent to a live session's other people, and the user's own commands never make one (`agent_presence_tests` in the engine, `live::agent` tests in the UI). Ready unchanged |
|
||||
| 2026-10-29 | minor | M9, #1380: your name is one setting, shown where you join. Preferences calls it "Your name" (it signs your comments and is what others see in live sessions). The Join dialog shows it, editable, beside the code, as the Share popover does, and the field is empty while it is the default. A new name is kept at once, and the browser build keeps it in the page's local storage, so a reload joins as you (the other preferences still start afresh there). Changing it while in live sessions renames you in each of them: a `name` signal, which the server cleans and answers with your `PeerJoined` to everyone, and newcomers' welcomes list the new name. A rename made while joining, or before a reconnect, goes out once the host reads signals. An older server ignores it, and clearing the name keeps the session's name until you join again. Tests: `collab/src/rename_tests.rs`, `collab-net/src/rename_tests.rs` (two apps over loopback), `ui-egui/src/display_name_tests.rs`, and `figcraft-web`'s `kept` round-trip. Numbers unchanged |
|
||||
|
||||
+7
-2
@@ -1186,13 +1186,18 @@ someone (`comment.people` lists the names used in the file and, as `you`, who is
|
||||
Read state is kept per name: a new message is unread for everyone but its author;
|
||||
`comment.markRead {id? | link?, read?}` marks one thread (or, without `id`, all of them) read or
|
||||
unread (not an undo step). `comment.list {page? | "all", filter? (all|open|resolved|mine|unread),
|
||||
sort? (date|page), query?, frame?}` returns the threads with their position, `frame`/`frameName`,
|
||||
sort? (date|page), query?, frame?, mentions?}` returns the threads with their position, `frame`/`frameName`,
|
||||
`pageName`, `unread`, `link` and replies (with `edited` times and `reactions`). Each message, the
|
||||
first and every reply, has `mentions`: the names it @mentions by the rule the app highlights them
|
||||
with, `{name, known, you}` each. `known` means the file's people (`comment.people`, or you) have
|
||||
the name, spelt as they spell it; `you` means it names you (or `author`). `@Name` counts at the
|
||||
start of the text or after a character that isn't a letter or digit, so `a@b.com` mentions nobody;
|
||||
an unknown name is the letters and digits after the `@`. Links are
|
||||
an unknown name is the letters and digits after the `@`. `mentions: "<name>"` (any case; a leading
|
||||
`@` is fine) lists only the threads where a message, the first or a reply, mentions that name as
|
||||
those `mentions` report it, combined with the other filters: `{"mentions": "Claude", "filter":
|
||||
"unread"}` is the unread threads that tag Claude. `@Ada Lovelace` mentions "Ada Lovelace", not
|
||||
"Ada", when the file knows Ada Lovelace; a name nobody in the file has can still be several words
|
||||
(`"Zoe Smith"` finds `@Zoe Smith`). Links are
|
||||
`figcraft://comment/<id>`: `comment.link {id}` returns one, `comment.open {id | link, view?}` goes
|
||||
to its page, zooms to its frame (`fit`, the default; `center` keeps the zoom; `keep` doesn't move)
|
||||
and opens it in the app, and `comment.next {id?, dir?, …list params}` steps to the next or previous
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user