1use call::{ActiveCall, IncomingCall};
2use client::proto;
3use futures::StreamExt;
4use gpui::{
5 elements::*,
6 geometry::{rect::RectF, vector::vec2f},
7 impl_internal_actions,
8 platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
9 AppContext, Entity, RenderContext, View, ViewContext,
10};
11use settings::Settings;
12use util::ResultExt;
13use workspace::JoinProject;
14
15impl_internal_actions!(incoming_call_notification, [RespondToCall]);
16
17pub fn init(cx: &mut AppContext) {
18 cx.add_action(IncomingCallNotification::respond_to_call);
19
20 let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
21 cx.spawn(|mut cx| async move {
22 let mut notification_windows = Vec::new();
23 while let Some(incoming_call) = incoming_call.next().await {
24 for window_id in notification_windows.drain(..) {
25 cx.remove_window(window_id);
26 }
27
28 if let Some(incoming_call) = incoming_call {
29 const PADDING: f32 = 16.;
30 let window_size = cx.read(|cx| {
31 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
32 vec2f(theme.window_width, theme.window_height)
33 });
34
35 for screen in cx.platform().screens() {
36 let screen_bounds = screen.bounds();
37 let (window_id, _) = cx.add_window(
38 WindowOptions {
39 bounds: WindowBounds::Fixed(RectF::new(
40 screen_bounds.upper_right()
41 - vec2f(PADDING + window_size.x(), PADDING),
42 window_size,
43 )),
44 titlebar: None,
45 center: false,
46 focus: false,
47 kind: WindowKind::PopUp,
48 is_movable: false,
49 screen: Some(screen),
50 },
51 |_| IncomingCallNotification::new(incoming_call.clone()),
52 );
53
54 notification_windows.push(window_id);
55 }
56 }
57 }
58 })
59 .detach();
60}
61
62#[derive(Clone, PartialEq)]
63struct RespondToCall {
64 accept: bool,
65}
66
67pub struct IncomingCallNotification {
68 call: IncomingCall,
69}
70
71impl IncomingCallNotification {
72 pub fn new(call: IncomingCall) -> Self {
73 Self { call }
74 }
75
76 fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
77 let active_call = ActiveCall::global(cx);
78 if action.accept {
79 let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
80 let caller_user_id = self.call.calling_user.id;
81 let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
82 cx.spawn_weak(|_, mut cx| async move {
83 join.await?;
84 if let Some(project_id) = initial_project_id {
85 cx.update(|cx| {
86 cx.dispatch_global_action(JoinProject {
87 project_id,
88 follow_user_id: caller_user_id,
89 })
90 });
91 }
92 anyhow::Ok(())
93 })
94 .detach_and_log_err(cx);
95 } else {
96 active_call.update(cx, |active_call, _| {
97 active_call.decline_incoming().log_err();
98 });
99 }
100 }
101
102 fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
103 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
104 let default_project = proto::ParticipantProject::default();
105 let initial_project = self
106 .call
107 .initial_project
108 .as_ref()
109 .unwrap_or(&default_project);
110 Flex::row()
111 .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
112 Image::from_data(avatar)
113 .with_style(theme.caller_avatar)
114 .aligned()
115 .boxed()
116 }))
117 .with_child(
118 Flex::column()
119 .with_child(
120 Label::new(
121 self.call.calling_user.github_login.clone(),
122 theme.caller_username.text.clone(),
123 )
124 .contained()
125 .with_style(theme.caller_username.container)
126 .boxed(),
127 )
128 .with_child(
129 Label::new(
130 format!(
131 "is sharing a project in Zed{}",
132 if initial_project.worktree_root_names.is_empty() {
133 ""
134 } else {
135 ":"
136 }
137 ),
138 theme.caller_message.text.clone(),
139 )
140 .contained()
141 .with_style(theme.caller_message.container)
142 .boxed(),
143 )
144 .with_children(if initial_project.worktree_root_names.is_empty() {
145 None
146 } else {
147 Some(
148 Label::new(
149 initial_project.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.caller_metadata)
159 .aligned()
160 .boxed(),
161 )
162 .contained()
163 .with_style(theme.caller_container)
164 .flex(1., true)
165 .boxed()
166 }
167
168 fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
169 enum Accept {}
170 enum Decline {}
171
172 Flex::column()
173 .with_child(
174 MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
175 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
176 Label::new("Accept", theme.accept_button.text.clone())
177 .aligned()
178 .contained()
179 .with_style(theme.accept_button.container)
180 .boxed()
181 })
182 .with_cursor_style(CursorStyle::PointingHand)
183 .on_click(MouseButton::Left, |_, cx| {
184 cx.dispatch_action(RespondToCall { accept: true });
185 })
186 .flex(1., true)
187 .boxed(),
188 )
189 .with_child(
190 MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
191 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
192 Label::new("Decline", theme.decline_button.text.clone())
193 .aligned()
194 .contained()
195 .with_style(theme.decline_button.container)
196 .boxed()
197 })
198 .with_cursor_style(CursorStyle::PointingHand)
199 .on_click(MouseButton::Left, |_, cx| {
200 cx.dispatch_action(RespondToCall { accept: false });
201 })
202 .flex(1., true)
203 .boxed(),
204 )
205 .constrained()
206 .with_width(
207 cx.global::<Settings>()
208 .theme
209 .incoming_call_notification
210 .button_width,
211 )
212 .boxed()
213 }
214}
215
216impl Entity for IncomingCallNotification {
217 type Event = ();
218}
219
220impl View for IncomingCallNotification {
221 fn ui_name() -> &'static str {
222 "IncomingCallNotification"
223 }
224
225 fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
226 let background = cx
227 .global::<Settings>()
228 .theme
229 .incoming_call_notification
230 .background;
231
232 Flex::row()
233 .with_child(self.render_caller(cx))
234 .with_child(self.render_buttons(cx))
235 .contained()
236 .with_background_color(background)
237 .expanded()
238 .boxed()
239 }
240}