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