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