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_size = screen.size();
35 let (window_id, _) = cx.add_window(
36 WindowOptions {
37 bounds: WindowBounds::Fixed(RectF::new(
38 vec2f(screen_size.x() - window_size.x() - PADDING, 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 accepts_first_mouse: true,
48 },
49 |_| {
50 ProjectSharedNotification::new(
51 owner.clone(),
52 *project_id,
53 worktree_root_names.clone(),
54 )
55 },
56 );
57 notification_windows
58 .entry(*project_id)
59 .or_insert(Vec::new())
60 .push(window_id);
61 }
62 }
63 room::Event::RemoteProjectUnshared { project_id } => {
64 if let Some(window_ids) = notification_windows.remove(&project_id) {
65 for window_id in window_ids {
66 cx.remove_window(window_id);
67 }
68 }
69 }
70 room::Event::Left => {
71 for (_, window_ids) in notification_windows.drain() {
72 for window_id in window_ids {
73 cx.remove_window(window_id);
74 }
75 }
76 }
77 _ => {}
78 })
79 .detach();
80}
81
82pub struct ProjectSharedNotification {
83 project_id: u64,
84 worktree_root_names: Vec<String>,
85 owner: Arc<User>,
86}
87
88impl ProjectSharedNotification {
89 fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
90 Self {
91 project_id,
92 worktree_root_names,
93 owner,
94 }
95 }
96
97 fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
98 let window_id = cx.window_id();
99 cx.remove_window(window_id);
100 cx.propagate_action();
101 }
102
103 fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
104 let window_id = cx.window_id();
105 cx.remove_window(window_id);
106 }
107
108 fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
109 let theme = &cx.global::<Settings>().theme.project_shared_notification;
110 Flex::row()
111 .with_children(self.owner.avatar.clone().map(|avatar| {
112 Image::new(avatar)
113 .with_style(theme.owner_avatar)
114 .aligned()
115 .boxed()
116 }))
117 .with_child(
118 Flex::column()
119 .with_child(
120 Label::new(
121 self.owner.github_login.clone(),
122 theme.owner_username.text.clone(),
123 )
124 .contained()
125 .with_style(theme.owner_username.container)
126 .boxed(),
127 )
128 .with_child(
129 Label::new(
130 format!(
131 "is sharing a project in Zed{}",
132 if self.worktree_root_names.is_empty() {
133 ""
134 } else {
135 ":"
136 }
137 ),
138 theme.message.text.clone(),
139 )
140 .contained()
141 .with_style(theme.message.container)
142 .boxed(),
143 )
144 .with_children(if self.worktree_root_names.is_empty() {
145 None
146 } else {
147 Some(
148 Label::new(
149 self.worktree_root_names.join(", "),
150 theme.worktree_roots.text.clone(),
151 )
152 .contained()
153 .with_style(theme.worktree_roots.container)
154 .boxed(),
155 )
156 })
157 .contained()
158 .with_style(theme.owner_metadata)
159 .aligned()
160 .boxed(),
161 )
162 .contained()
163 .with_style(theme.owner_container)
164 .flex(1., true)
165 .boxed()
166 }
167
168 fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
169 enum Open {}
170 enum Dismiss {}
171
172 let project_id = self.project_id;
173 let owner_user_id = self.owner.id;
174
175 Flex::column()
176 .with_child(
177 MouseEventHandler::<Open>::new(0, cx, |_, cx| {
178 let theme = &cx.global::<Settings>().theme.project_shared_notification;
179 Label::new("Open".to_string(), theme.open_button.text.clone())
180 .aligned()
181 .contained()
182 .with_style(theme.open_button.container)
183 .boxed()
184 })
185 .with_cursor_style(CursorStyle::PointingHand)
186 .on_click(MouseButton::Left, move |_, cx| {
187 cx.dispatch_action(JoinProject {
188 project_id,
189 follow_user_id: owner_user_id,
190 });
191 })
192 .flex(1., true)
193 .boxed(),
194 )
195 .with_child(
196 MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
197 let theme = &cx.global::<Settings>().theme.project_shared_notification;
198 Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
199 .aligned()
200 .contained()
201 .with_style(theme.dismiss_button.container)
202 .boxed()
203 })
204 .with_cursor_style(CursorStyle::PointingHand)
205 .on_click(MouseButton::Left, |_, cx| {
206 cx.dispatch_action(DismissProject);
207 })
208 .flex(1., true)
209 .boxed(),
210 )
211 .constrained()
212 .with_width(
213 cx.global::<Settings>()
214 .theme
215 .project_shared_notification
216 .button_width,
217 )
218 .boxed()
219 }
220}
221
222impl Entity for ProjectSharedNotification {
223 type Event = ();
224}
225
226impl View for ProjectSharedNotification {
227 fn ui_name() -> &'static str {
228 "ProjectSharedNotification"
229 }
230
231 fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
232 let background = cx
233 .global::<Settings>()
234 .theme
235 .project_shared_notification
236 .background;
237 Flex::row()
238 .with_child(self.render_owner(cx))
239 .with_child(self.render_buttons(cx))
240 .contained()
241 .with_background_color(background)
242 .expanded()
243 .boxed()
244 }
245}