1use call::{room, ActiveCall};
2use client::User;
3use collections::HashMap;
4use gpui::{
5 actions,
6 elements::*,
7 geometry::{rect::RectF, vector::vec2f},
8 CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
9 WindowBounds, WindowKind, WindowOptions,
10};
11use settings::Settings;
12use std::sync::Arc;
13use workspace::JoinProject;
14
15actions!(project_shared_notification, [DismissProject]);
16
17pub fn init(cx: &mut MutableAppContext) {
18 cx.add_action(ProjectSharedNotification::join);
19 cx.add_action(ProjectSharedNotification::dismiss);
20
21 let active_call = ActiveCall::global(cx);
22 let mut notification_windows = HashMap::default();
23 cx.subscribe(&active_call, move |_, event, cx| match event {
24 room::Event::RemoteProjectShared {
25 owner,
26 project_id,
27 worktree_root_names,
28 } => {
29 const PADDING: f32 = 16.;
30 let theme = &cx.global::<Settings>().theme.project_shared_notification;
31 let window_size = vec2f(theme.window_width, theme.window_height);
32
33 for screen in cx.platform().screens() {
34 let screen_bounds = screen.bounds();
35 let (window_id, _) = cx.add_window(
36 WindowOptions {
37 bounds: WindowBounds::Fixed(RectF::new(
38 screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING),
39 window_size,
40 )),
41 titlebar: None,
42 center: false,
43 focus: false,
44 kind: WindowKind::PopUp,
45 is_movable: false,
46 screen: Some(screen),
47 },
48 |_| {
49 ProjectSharedNotification::new(
50 owner.clone(),
51 *project_id,
52 worktree_root_names.clone(),
53 )
54 },
55 );
56 notification_windows
57 .entry(*project_id)
58 .or_insert(Vec::new())
59 .push(window_id);
60 }
61 }
62 room::Event::RemoteProjectUnshared { project_id } => {
63 if let Some(window_ids) = notification_windows.remove(&project_id) {
64 for window_id in window_ids {
65 cx.remove_window(window_id);
66 }
67 }
68 }
69 room::Event::Left => {
70 for (_, window_ids) in notification_windows.drain() {
71 for window_id in window_ids {
72 cx.remove_window(window_id);
73 }
74 }
75 }
76 _ => {}
77 })
78 .detach();
79}
80
81pub struct ProjectSharedNotification {
82 project_id: u64,
83 worktree_root_names: Vec<String>,
84 owner: Arc<User>,
85}
86
87impl ProjectSharedNotification {
88 fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
89 Self {
90 project_id,
91 worktree_root_names,
92 owner,
93 }
94 }
95
96 fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
97 let window_id = cx.window_id();
98 cx.remove_window(window_id);
99 cx.propagate_action();
100 }
101
102 fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
103 let window_id = cx.window_id();
104 cx.remove_window(window_id);
105 }
106
107 fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
108 let theme = &cx.global::<Settings>().theme.project_shared_notification;
109 Flex::row()
110 .with_children(self.owner.avatar.clone().map(|avatar| {
111 Image::new(avatar)
112 .with_style(theme.owner_avatar)
113 .aligned()
114 .boxed()
115 }))
116 .with_child(
117 Flex::column()
118 .with_child(
119 Label::new(
120 self.owner.github_login.clone(),
121 theme.owner_username.text.clone(),
122 )
123 .contained()
124 .with_style(theme.owner_username.container)
125 .boxed(),
126 )
127 .with_child(
128 Label::new(
129 format!(
130 "is sharing a project in Zed{}",
131 if self.worktree_root_names.is_empty() {
132 ""
133 } else {
134 ":"
135 }
136 ),
137 theme.message.text.clone(),
138 )
139 .contained()
140 .with_style(theme.message.container)
141 .boxed(),
142 )
143 .with_children(if self.worktree_root_names.is_empty() {
144 None
145 } else {
146 Some(
147 Label::new(
148 self.worktree_root_names.join(", "),
149 theme.worktree_roots.text.clone(),
150 )
151 .contained()
152 .with_style(theme.worktree_roots.container)
153 .boxed(),
154 )
155 })
156 .contained()
157 .with_style(theme.owner_metadata)
158 .aligned()
159 .boxed(),
160 )
161 .contained()
162 .with_style(theme.owner_container)
163 .flex(1., true)
164 .boxed()
165 }
166
167 fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
168 enum Open {}
169 enum Dismiss {}
170
171 let project_id = self.project_id;
172 let owner_user_id = self.owner.id;
173
174 Flex::column()
175 .with_child(
176 MouseEventHandler::<Open>::new(0, cx, |_, cx| {
177 let theme = &cx.global::<Settings>().theme.project_shared_notification;
178 Label::new("Open".to_string(), theme.open_button.text.clone())
179 .aligned()
180 .contained()
181 .with_style(theme.open_button.container)
182 .boxed()
183 })
184 .with_cursor_style(CursorStyle::PointingHand)
185 .on_click(MouseButton::Left, move |_, cx| {
186 cx.dispatch_action(JoinProject {
187 project_id,
188 follow_user_id: owner_user_id,
189 });
190 })
191 .flex(1., true)
192 .boxed(),
193 )
194 .with_child(
195 MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
196 let theme = &cx.global::<Settings>().theme.project_shared_notification;
197 Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
198 .aligned()
199 .contained()
200 .with_style(theme.dismiss_button.container)
201 .boxed()
202 })
203 .with_cursor_style(CursorStyle::PointingHand)
204 .on_click(MouseButton::Left, |_, cx| {
205 cx.dispatch_action(DismissProject);
206 })
207 .flex(1., true)
208 .boxed(),
209 )
210 .constrained()
211 .with_width(
212 cx.global::<Settings>()
213 .theme
214 .project_shared_notification
215 .button_width,
216 )
217 .boxed()
218 }
219}
220
221impl Entity for ProjectSharedNotification {
222 type Event = ();
223}
224
225impl View for ProjectSharedNotification {
226 fn ui_name() -> &'static str {
227 "ProjectSharedNotification"
228 }
229
230 fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
231 let background = cx
232 .global::<Settings>()
233 .theme
234 .project_shared_notification
235 .background;
236 Flex::row()
237 .with_child(self.render_owner(cx))
238 .with_child(self.render_buttons(cx))
239 .contained()
240 .with_background_color(background)
241 .expanded()
242 .boxed()
243 }
244}