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