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 show: true,
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, cx| {
103 active_call.decline_incoming(cx).log_err();
104 });
105 }
106 }
107
108 fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
109 let theme = &theme::current(cx).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 let theme = theme::current(cx);
174 Flex::column()
175 .with_child(
176 MouseEventHandler::<Accept, Self>::new(0, cx, |_, _| {
177 let theme = &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, |_, _| {
191 let theme = &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(theme.incoming_call_notification.button_width)
205 .into_any()
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 ViewContext<Self>) -> AnyElement<Self> {
219 let background = theme::current(cx).incoming_call_notification.background;
220 Flex::row()
221 .with_child(self.render_caller(cx))
222 .with_child(self.render_buttons(cx))
223 .contained()
224 .with_background_color(background)
225 .expanded()
226 .into_any()
227 }
228}