collab: Improve project and call notification UI (#47964)

This PR moves the `CollabNotification` component from the collab_ui
crate to the UI crate, so that we can add a component preview to it. It
also fixes problems with text truncation and other little details.

<img width="700" height="1674" alt="Screenshot 2026-01-29 at 4  07@2x"
src="https://github.com/user-attachments/assets/6aca6cff-564a-4729-b505-c02d8c3af97f"
/>

Release Notes:

- Collab: Fixed overflowing text in project sharing and call
notifications.
This commit is contained in:
Danilo Leal authored and GitHub committed 2026-01-29 16:33:38 -03:00
1 parent ad69357968
commit 22e2c7ac8a
12 files changed
+151 -135

No files matched your search

Generated
-2
View File
@@ -3448,7 +3448,6 @@ dependencies = [
"serde_json",
"settings",
"smallvec",
"story",
"telemetry",
"theme",
"time",
@@ -15917,7 +15916,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"collab_ui",
"ctrlc",
"dialoguer",
"editor",
-2
View File
@@ -14,7 +14,6 @@ doctest = false
[features]
default = []
stories = ["dep:story"]
test-support = [
"call/test-support",
"client/test-support",
@@ -52,7 +51,6 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
story = { workspace = true, optional = true }
telemetry.workspace = true
theme.workspace = true
time.workspace = true
-7
View File
@@ -1,17 +1,10 @@
mod collab_notification;
pub mod incoming_call_notification;
pub mod project_shared_notification;
#[cfg(feature = "stories")]
mod stories;
use gpui::App;
use std::sync::Arc;
use workspace::AppState;
#[cfg(feature = "stories")]
pub use stories::*;
pub fn init(app_state: &Arc<AppState>, cx: &mut App) {
incoming_call_notification::init(app_state, cx);
project_shared_notification::init(app_state, cx);
@@ -1,52 +0,0 @@
use gpui::{AnyElement, SharedUri, img, prelude::*};
use smallvec::SmallVec;
use ui::prelude::*;
#[derive(IntoElement)]
pub struct CollabNotification {
avatar_uri: SharedUri,
accept_button: Button,
dismiss_button: Button,
children: SmallVec<[AnyElement; 2]>,
}
impl CollabNotification {
pub fn new(
avatar_uri: impl Into<SharedUri>,
accept_button: Button,
dismiss_button: Button,
) -> Self {
Self {
avatar_uri: avatar_uri.into(),
accept_button,
dismiss_button,
children: SmallVec::new(),
}
}
}
impl ParentElement for CollabNotification {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl RenderOnce for CollabNotification {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.text_ui(cx)
.justify_between()
.size_full()
.overflow_hidden()
.elevation_3(cx)
.p_2()
.gap_2()
.child(img(self.avatar_uri).w_12().h_12().rounded_full())
.child(v_flex().overflow_hidden().children(self.children))
.child(
v_flex()
.child(self.accept_button)
.child(self.dismiss_button),
)
}
}
@@ -1,11 +1,10 @@
use crate::notification_window_options;
use crate::notifications::collab_notification::CollabNotification;
use call::{ActiveCall, IncomingCall};
use futures::StreamExt;
use gpui::{App, WindowHandle, prelude::*};
use std::sync::{Arc, Weak};
use ui::{Button, Label, prelude::*};
use ui::{CollabNotification, prelude::*};
use util::ResultExt;
use workspace::AppState;
@@ -118,10 +117,10 @@ impl Render for IncomingCallNotification {
move |_, _, cx| state.respond(false, cx)
}),
)
.child(v_flex().overflow_hidden().child(Label::new(format!(
.child(Label::new(format!(
"{} is sharing a project in Zed",
self.state.call.calling_user.github_login
)))),
))),
)
}
}
@@ -1,12 +1,11 @@
use crate::notification_window_options;
use crate::notifications::collab_notification::CollabNotification;
use call::{ActiveCall, room};
use client::User;
use collections::HashMap;
use gpui::{App, Size};
use std::sync::{Arc, Weak};
use ui::{Button, Label, prelude::*};
use ui::{CollabNotification, prelude::*};
use util::ResultExt;
use workspace::AppState;
@@ -122,6 +121,14 @@ impl ProjectSharedNotification {
impl Render for ProjectSharedNotification {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font = theme::setup_ui_font(window, cx);
let no_worktree_root_names = self.worktree_root_names.is_empty();
let punctuation = if no_worktree_root_names { "" } else { ":" };
let main_label = format!(
"{} is sharing a project with you{}",
self.owner.github_login.clone(),
punctuation
);
div().size_full().font(ui_font).child(
CollabNotification::new(
@@ -135,19 +142,9 @@ impl Render for ProjectSharedNotification {
},
)),
)
.child(Label::new(self.owner.github_login.clone()))
.child(Label::new(format!(
"is sharing a project in Zed{}",
if self.worktree_root_names.is_empty() {
""
} else {
":"
}
)))
.children(if self.worktree_root_names.is_empty() {
None
} else {
Some(Label::new(self.worktree_root_names.join(", ")))
.child(Label::new(main_label))
.when(!no_worktree_root_names, |this| {
this.child(Label::new(self.worktree_root_names.join(", ")).color(Color::Muted))
}),
)
}
@@ -1,48 +0,0 @@
use gpui::prelude::*;
use story::{Story, StoryItem, StorySection};
use ui::prelude::*;
use crate::notifications::collab_notification::CollabNotification;
pub struct CollabNotificationStory;
impl Render for CollabNotificationStory {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_container = |width, height| div().w(px(width)).h(px(height));
Story::container(cx)
.child(Story::title_for::<CollabNotification>(cx))
.child(
StorySection::new().child(StoryItem::new(
"Incoming Call Notification",
window_container(400., 72.).child(
CollabNotification::new(
"https://avatars.githubusercontent.com/u/1486634?v=4",
Button::new("accept", "Accept"),
Button::new("decline", "Decline"),
)
.child(
v_flex()
.overflow_hidden()
.child(Label::new("maxdeviant is sharing a project in Zed")),
),
),
)),
)
.child(
StorySection::new().child(StoryItem::new(
"Project Shared Notification",
window_container(400., 72.).child(
CollabNotification::new(
"https://avatars.githubusercontent.com/u/1714999?v=4",
Button::new("open", "Open"),
Button::new("dismiss", "Dismiss"),
)
.child(Label::new("iamnbutler"))
.child(Label::new("is sharing a project in Zed:"))
.child(Label::new("zed")),
),
)),
)
}
}
-1
View File
@@ -15,7 +15,6 @@ path = "src/storybook.rs"
[dependencies]
anyhow.workspace = true
clap = { workspace = true, features = ["derive", "string"] }
collab_ui = { workspace = true, features = ["stories"] }
ctrlc = "3.4"
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
editor.workspace = true
-4
View File
@@ -13,7 +13,6 @@ use ui::prelude::*;
pub enum ComponentStory {
ApplicationMenu,
AutoHeightEditor,
CollabNotification,
ContextMenu,
Cursor,
Focus,
@@ -33,9 +32,6 @@ impl ComponentStory {
.new(|cx| title_bar::ApplicationMenuStory::new(window, cx))
.into(),
Self::AutoHeightEditor => AutoHeightEditorStory::new(window, cx).into(),
Self::CollabNotification => cx
.new(|_| collab_ui::notifications::CollabNotificationStory)
.into(),
Self::ContextMenu => cx.new(|_| ui::ContextMenuStory).into(),
Self::Cursor => cx.new(|_| crate::stories::CursorStory).into(),
Self::Focus => FocusStory::model(window, cx).into(),
+2
View File
@@ -4,6 +4,7 @@ mod banner;
mod button;
mod callout;
mod chip;
mod collab;
mod content_group;
mod context_menu;
mod data_table;
@@ -50,6 +51,7 @@ pub use banner::*;
pub use button::*;
pub use callout::*;
pub use chip::*;
pub use collab::*;
pub use content_group::*;
pub use context_menu::*;
pub use data_table::*;
@@ -0,0 +1,134 @@
use gpui::{AnyElement, SharedUri, prelude::*};
use smallvec::SmallVec;
use crate::{Avatar, prelude::*};
#[derive(IntoElement, RegisterComponent)]
pub struct CollabNotification {
avatar_uri: SharedUri,
accept_button: Button,
dismiss_button: Button,
children: SmallVec<[AnyElement; 2]>,
}
impl CollabNotification {
pub fn new(
avatar_uri: impl Into<SharedUri>,
accept_button: Button,
dismiss_button: Button,
) -> Self {
Self {
avatar_uri: avatar_uri.into(),
accept_button,
dismiss_button,
children: SmallVec::new(),
}
}
}
impl ParentElement for CollabNotification {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl RenderOnce for CollabNotification {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.p_2()
.size_full()
.text_ui(cx)
.justify_between()
.overflow_hidden()
.elevation_3(cx)
.gap_1()
.child(
h_flex()
.min_w_0()
.gap_4()
.child(Avatar::new(self.avatar_uri).size(px(40.)))
.child(v_flex().truncate().children(self.children)),
)
.child(
v_flex()
.items_center()
.child(self.accept_button)
.child(self.dismiss_button),
)
}
}
impl Component for CollabNotification {
fn scope() -> ComponentScope {
ComponentScope::Collaboration
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let avatar = "https://avatars.githubusercontent.com/u/67129314?v=4";
let container = || div().h(px(72.)).w(px(400.)); // Size of the actual notification window
let examples = vec![
single_example(
"Incoming Call",
container()
.child(
CollabNotification::new(
avatar,
Button::new("accept", "Accept"),
Button::new("decline", "Decline"),
)
.child(Label::new("the user is inviting you to a call")),
)
.into_any_element(),
),
single_example(
"Screen Share Request",
container()
.child(
CollabNotification::new(
avatar,
Button::new("accept", "View"),
Button::new("decline", "Ignore"),
)
.child(Label::new("the user is sharing their screen")),
)
.into_any_element(),
),
single_example(
"Project Shared",
container()
.child(
CollabNotification::new(
avatar,
Button::new("accept", "Open"),
Button::new("decline", "Dismiss"),
)
.child(Label::new("the user is sharing a project"))
.child(Label::new("zed").color(Color::Muted)),
)
.into_any_element(),
),
single_example(
"Overflowing Content",
container()
.child(
CollabNotification::new(
avatar,
Button::new("accept", "Accept"),
Button::new("decline", "Decline"),
)
.child(Label::new(
"a_very_long_username_that_might_overflow is sharing a project in Zed:",
))
.child(
Label::new("zed-cloud, zed, edit-prediction-bench, zed.dev")
.color(Color::Muted),
),
)
.into_any_element(),
),
];
Some(example_group(examples).vertical().into_any_element())
}
}