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