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