1use std::sync::{Arc, Weak};
2
3use call::{ActiveCall, IncomingCall};
4use client::proto;
5use futures::StreamExt;
6use gpui::{
7 elements::*,
8 geometry::{rect::RectF, vector::vec2f},
9 platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
10 AnyElement, AppContext, Entity, View, ViewContext,
11};
12use settings::Settings;
13use util::ResultExt;
14use workspace::AppState;
15
16pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
17 let app_state = Arc::downgrade(app_state);
18 let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
19 cx.spawn(|mut cx| async move {
20 let mut notification_windows = Vec::new();
21 while let Some(incoming_call) = incoming_call.next().await {
22 for window_id in notification_windows.drain(..) {
23 cx.remove_window(window_id);
24 }
25
26 if let Some(incoming_call) = incoming_call {
27 const PADDING: f32 = 16.;
28 let window_size = cx.read(|cx| {
29 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
30 vec2f(theme.window_width, theme.window_height)
31 });
32
33 for screen in cx.platform().screens() {
34 let screen_bounds = screen.bounds();
35 let (window_id, _) = cx.add_window(
36 WindowOptions {
37 bounds: WindowBounds::Fixed(RectF::new(
38 screen_bounds.upper_right()
39 - vec2f(PADDING + window_size.x(), PADDING),
40 window_size,
41 )),
42 titlebar: None,
43 center: false,
44 focus: false,
45 show: true,
46 kind: WindowKind::PopUp,
47 is_movable: false,
48 screen: Some(screen),
49 },
50 |_| IncomingCallNotification::new(incoming_call.clone(), app_state.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 app_state: Weak<AppState>,
69}
70
71impl IncomingCallNotification {
72 pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
73 Self { call, app_state }
74 }
75
76 fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
77 let active_call = ActiveCall::global(cx);
78 if 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 let app_state = self.app_state.clone();
83 cx.app_context()
84 .spawn(|mut cx| async move {
85 join.await?;
86 if let Some(project_id) = initial_project_id {
87 cx.update(|cx| {
88 if let Some(app_state) = app_state.upgrade() {
89 workspace::join_remote_project(
90 project_id,
91 caller_user_id,
92 app_state,
93 cx,
94 )
95 .detach_and_log_err(cx);
96 }
97 });
98 }
99 anyhow::Ok(())
100 })
101 .detach_and_log_err(cx);
102 } else {
103 active_call.update(cx, |active_call, _| {
104 active_call.decline_incoming().log_err();
105 });
106 }
107 }
108
109 fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
110 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
111 let default_project = proto::ParticipantProject::default();
112 let initial_project = self
113 .call
114 .initial_project
115 .as_ref()
116 .unwrap_or(&default_project);
117 Flex::row()
118 .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
119 Image::from_data(avatar)
120 .with_style(theme.caller_avatar)
121 .aligned()
122 }))
123 .with_child(
124 Flex::column()
125 .with_child(
126 Label::new(
127 self.call.calling_user.github_login.clone(),
128 theme.caller_username.text.clone(),
129 )
130 .contained()
131 .with_style(theme.caller_username.container),
132 )
133 .with_child(
134 Label::new(
135 format!(
136 "is sharing a project in Zed{}",
137 if initial_project.worktree_root_names.is_empty() {
138 ""
139 } else {
140 ":"
141 }
142 ),
143 theme.caller_message.text.clone(),
144 )
145 .contained()
146 .with_style(theme.caller_message.container),
147 )
148 .with_children(if initial_project.worktree_root_names.is_empty() {
149 None
150 } else {
151 Some(
152 Label::new(
153 initial_project.worktree_root_names.join(", "),
154 theme.worktree_roots.text.clone(),
155 )
156 .contained()
157 .with_style(theme.worktree_roots.container),
158 )
159 })
160 .contained()
161 .with_style(theme.caller_metadata)
162 .aligned(),
163 )
164 .contained()
165 .with_style(theme.caller_container)
166 .flex(1., true)
167 .into_any()
168 }
169
170 fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
171 enum Accept {}
172 enum Decline {}
173
174 Flex::column()
175 .with_child(
176 MouseEventHandler::<Accept, Self>::new(0, cx, |_, cx| {
177 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
178 Label::new("Accept", theme.accept_button.text.clone())
179 .aligned()
180 .contained()
181 .with_style(theme.accept_button.container)
182 })
183 .with_cursor_style(CursorStyle::PointingHand)
184 .on_click(MouseButton::Left, |_, this, cx| {
185 this.respond(true, cx);
186 })
187 .flex(1., true),
188 )
189 .with_child(
190 MouseEventHandler::<Decline, Self>::new(0, cx, |_, cx| {
191 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
192 Label::new("Decline", theme.decline_button.text.clone())
193 .aligned()
194 .contained()
195 .with_style(theme.decline_button.container)
196 })
197 .with_cursor_style(CursorStyle::PointingHand)
198 .on_click(MouseButton::Left, |_, this, cx| {
199 this.respond(false, cx);
200 })
201 .flex(1., true),
202 )
203 .constrained()
204 .with_width(
205 cx.global::<Settings>()
206 .theme
207 .incoming_call_notification
208 .button_width,
209 )
210 .into_any()
211 }
212}
213
214impl Entity for IncomingCallNotification {
215 type Event = ();
216}
217
218impl View for IncomingCallNotification {
219 fn ui_name() -> &'static str {
220 "IncomingCallNotification"
221 }
222
223 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
224 let background = cx
225 .global::<Settings>()
226 .theme
227 .incoming_call_notification
228 .background;
229
230 Flex::row()
231 .with_child(self.render_caller(cx))
232 .with_child(self.render_buttons(cx))
233 .contained()
234 .with_background_color(background)
235 .expanded()
236 .into_any()
237 }
238}