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