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