Files
Ely-GPUI-Components/examples/gallery/pages/collab/sharing.rs
T
ZacharyZhang-NY 11f3e08294 T22: Collaboration
- presence: avatars ringed in each peer's color with where they are, press to
  follow; a breathing live dot; follow mode frames the view and stops on Escape
- remote cursors and selections painted over a field from its own layout
- comment threads with replies, reactions and resolve; a marker that opens a
  thread in a bubble; a sidebar of open and resolved threads
- annotations washed in their authors' colors, notes on hover
- suggestion mode: edits become suggestions word by word; track changes shows
  them struck and underlined, to accept or reject
- sharing: invite by email with checked chips, roles, the access list and who
  the link lets in, in one dialog
- primitives::hold_focus gives a mode its own focus, shared with ZenMode
- AvatarGroup's tuck and count shared; Avatar::ring_color; TagInput::check;
  TextInput::bounds_for_range; Popover::size
- icons: smile-plus, user-plus, message-square-diff
- gallery: the Collaboration page with scripted states
2026-09-26 05:10:37 -04:00

169 lines
5.7 KiB
Rust

use ely_gpui_component::{
buttons::{Button, ButtonVariant},
collab::{AccessList, InviteInput, LinkAccess, Member, Role, ShareDialog},
data_display::{Activity, ActivityFeed},
forms::is_email,
primitives::IconName,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use jiff::{Timestamp, ToSpan};
use crate::{
probe::probe,
ui::{keep, section, set},
};
fn member(name: &str, email: &str, role: Role) -> Member {
Member {
name: name.to_string().into(),
email: email.to_string().into(),
picture: None,
role,
owner: false,
}
}
fn members() -> Vec<Member> {
vec![
Member {
owner: true,
..member("You", "you@example.com", Role::Edit)
},
member("Ada Park", "ada@example.com", Role::Edit),
member("Grace Lin", "grace@example.com", Role::Comment),
member("Alan Reyes", "alan@example.com", Role::View),
]
}
pub fn sharing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let open = keep("collab-share-open", || false, window, cx);
let emails = keep(
"collab-emails",
|| {
vec![
SharedString::from("mae@example.com"),
SharedString::from("tim@example"),
]
},
window,
cx,
);
let role = keep("collab-invite-role", || Role::Edit, window, cx);
let people = keep("collab-members", members, window, cx);
let link = keep("collab-link", || None::<Role>, window, cx);
let dialog = open.read(cx).then(|| {
let (now_emails, now_role, now_people, now_link) = (
emails.read(cx).clone(),
*role.read(cx),
people.read(cx).clone(),
*link.read(cx),
);
let (close, typed, invited, cleared, picked) = (
open.clone(),
emails.clone(),
people.clone(),
emails.clone(),
role.clone(),
);
let (roled, removed) = (people.clone(), people.clone());
ShareDialog::new("collab-share", "Lift", move |_, cx| set(&close, false, cx))
.invite(
InviteInput::new(
"collab-invite",
now_emails,
move |next, _, cx| set(&typed, next.to_vec(), cx),
move |_, cx| {
let (added, role) = (cleared.read(cx).clone(), *picked.read(cx));
invited.update(cx, |people, cx| {
people.extend(added.iter().filter(|email| is_email(email)).map(
|email| {
let (name, _) =
email.split_once('@').expect("an address holds @");
member(name, email, role)
},
));
cx.notify();
});
set(&cleared, Vec::new(), cx)
},
)
.role(now_role, move |next, _, cx| set(&role, next, cx)),
)
.access(
AccessList::new("collab-access", now_people)
.on_role(move |ix, next, _, cx| {
roled.update(cx, |people, cx| {
people[ix].role = next;
cx.notify();
})
})
.on_remove(move |ix, _, cx| {
removed.update(cx, |people, cx| {
people.remove(ix);
cx.notify();
})
}),
)
.link(LinkAccess::new(
"collab-link",
"https://example.com/d/lift",
now_link,
move |next, _, cx| set(&link, next, cx),
))
});
section(
"ShareDialog / InviteInput / PermissionSelect / AccessList",
"Sharing in one place: add people by email, each chip marked until it is an address; choose what they may do; change or take away anyone's access; and choose who the link lets in.",
cx,
)
.child(probe(
"collab-share",
div().flex().child(
Button::new("collab-share-open", "Share")
.variant(ButtonVariant::Primary)
.icon(IconName::UserPlus)
.on_click(move |_, _, cx| set(&open, true, cx)),
),
))
.children(dialog)
}
pub fn activity(cx: &mut App) -> impl IntoElement + use<> {
let now = Timestamp::now();
let entry = |key: &str, actor: &str, verb: &str, object: &str, minutes: i64| {
Activity::new(
key.to_string(),
actor.to_string(),
verb.to_string(),
now - minutes.minutes(),
)
.object(object.to_string())
};
section(
"ActivityLog → data_display::ActivityFeed",
"What people did in the document, newest first.",
cx,
)
.child(
div().w(px(560.)).child(
ActivityFeed::new("collab-activity")
.entry(entry(
"reply",
"Alan Reyes",
"replied on",
"blends in gamma space",
4,
))
.entry(entry(
"suggest",
"Grace Lin",
"suggested a change to",
"Lift",
35,
))
.entry(entry("resolve", "Ada Park", "resolved", "On Lift", 180))
.entry(entry("share", "You", "shared", "Lift", 1440)),
),
)
}