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