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 util::ResultExt;
13use workspace::AppState;
14
15pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
16 let app_state = Arc::downgrade(app_state);
17 let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
18 cx.spawn(|mut cx| async move {
19 let mut notification_windows = Vec::new();
20 while let Some(incoming_call) = incoming_call.next().await {
21 for window_id in notification_windows.drain(..) {
22 cx.remove_window(window_id);
23 }
24
25 if let Some(incoming_call) = incoming_call {
26 const PADDING: f32 = 16.;
27 let window_size = cx.read(|cx| {
28 let theme = &theme::current(cx).incoming_call_notification;
29 vec2f(theme.window_width, theme.window_height)
30 });
31
32 for screen in cx.platform().screens() {
33 let screen_bounds = screen.bounds();
34 let (window_id, _) = cx.add_window(
35 WindowOptions {
36 bounds: WindowBounds::Fixed(RectF::new(
37 screen_bounds.upper_right()
38 - vec2f(PADDING + window_size.x(), PADDING),
39 window_size,
40 )),
41 titlebar: None,
42 center: false,
43 focus: false,
44 kind: WindowKind::PopUp,
45 is_movable: false,
46 screen: Some(screen),
47 },
48 |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
49 );
50
51 notification_windows.push(window_id);
52 }
53 }
54 }
55 })
56 .detach();
57}
58
59#[derive(Clone, PartialEq)]
60struct RespondToCall {
61 accept: bool,
62}
63
64pub struct IncomingCallNotification {
65 call: IncomingCall,
66 app_state: Weak<AppState>,
67}
68
69impl IncomingCallNotification {
70 pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
71 Self { call, app_state }
72 }
73
74 fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
75 let active_call = ActiveCall::global(cx);
76 if accept {
77 let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
78 let caller_user_id = self.call.calling_user.id;
79 let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
80 let app_state = self.app_state.clone();
81 cx.app_context()
82 .spawn(|mut cx| async move {
83 join.await?;
84 if let Some(project_id) = initial_project_id {
85 cx.update(|cx| {
86 if let Some(app_state) = app_state.upgrade() {
87 workspace::join_remote_project(
88 project_id,
89 caller_user_id,
90 app_state,
91 cx,
92 )
93 .detach_and_log_err(cx);
94 }
95 });
96 }
97 anyhow::Ok(())
98 })
99 .detach_and_log_err(cx);
100 } else {
101 active_call.update(cx, |active_call, _| {
102 active_call.decline_incoming().log_err();
103 });
104 }
105 }
106
107 fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
108 let theme = &theme::current(cx).incoming_call_notification;
109 let default_project = proto::ParticipantProject::default();
110 let initial_project = self
111 .call
112 .initial_project
113 .as_ref()
114 .unwrap_or(&default_project);
115 Flex::row()
116 .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
117 Image::from_data(avatar)
118 .with_style(theme.caller_avatar)
119 .aligned()
120 }))
121 .with_child(
122 Flex::column()
123 .with_child(
124 Label::new(
125 self.call.calling_user.github_login.clone(),
126 theme.caller_username.text.clone(),
127 )
128 .contained()
129 .with_style(theme.caller_username.container),
130 )
131 .with_child(
132 Label::new(
133 format!(
134 "is sharing a project in Zed{}",
135 if initial_project.worktree_root_names.is_empty() {
136 ""
137 } else {
138 ":"
139 }
140 ),
141 theme.caller_message.text.clone(),
142 )
143 .contained()
144 .with_style(theme.caller_message.container),
145 )
146 .with_children(if initial_project.worktree_root_names.is_empty() {
147 None
148 } else {
149 Some(
150 Label::new(
151 initial_project.worktree_root_names.join(", "),
152 theme.worktree_roots.text.clone(),
153 )
154 .contained()
155 .with_style(theme.worktree_roots.container),
156 )
157 })
158 .contained()
159 .with_style(theme.caller_metadata)
160 .aligned(),
161 )
162 .contained()
163 .with_style(theme.caller_container)
164 .flex(1., true)
165 .into_any()
166 }
167
168 fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
169 enum Accept {}
170 enum Decline {}
171
172 let theme = theme::current(cx);
173 Flex::column()
174 .with_child(
175 MouseEventHandler::<Accept, Self>::new(0, cx, |_, _| {
176 let theme = &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, |_, _| {
190 let theme = &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(theme.incoming_call_notification.button_width)
204 .into_any()
205 }
206}
207
208impl Entity for IncomingCallNotification {
209 type Event = ();
210}
211
212impl View for IncomingCallNotification {
213 fn ui_name() -> &'static str {
214 "IncomingCallNotification"
215 }
216
217 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
218 let background = theme::current(cx).incoming_call_notification.background;
219 Flex::row()
220 .with_child(self.render_caller(cx))
221 .with_child(self.render_buttons(cx))
222 .contained()
223 .with_background_color(background)
224 .expanded()
225 .into_any()
226 }
227}