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 cx.spawn(|this, mut cx| async move {
82 join.await?;
83 if let Some(project_id) = initial_project_id {
84 this.update(&mut cx, |this, cx| {
85 if let Some(app_state) = this.app_state.upgrade() {
86 workspace::join_remote_project(
87 project_id,
88 caller_user_id,
89 app_state,
90 cx,
91 )
92 .detach_and_log_err(cx);
93 }
94 })?;
95 }
96 anyhow::Ok(())
97 })
98 .detach_and_log_err(cx);
99 } else {
100 active_call.update(cx, |active_call, _| {
101 active_call.decline_incoming().log_err();
102 });
103 }
104 }
105
106 fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
107 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
108 let default_project = proto::ParticipantProject::default();
109 let initial_project = self
110 .call
111 .initial_project
112 .as_ref()
113 .unwrap_or(&default_project);
114 Flex::row()
115 .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
116 Image::from_data(avatar)
117 .with_style(theme.caller_avatar)
118 .aligned()
119 }))
120 .with_child(
121 Flex::column()
122 .with_child(
123 Label::new(
124 self.call.calling_user.github_login.clone(),
125 theme.caller_username.text.clone(),
126 )
127 .contained()
128 .with_style(theme.caller_username.container),
129 )
130 .with_child(
131 Label::new(
132 format!(
133 "is sharing a project in Zed{}",
134 if initial_project.worktree_root_names.is_empty() {
135 ""
136 } else {
137 ":"
138 }
139 ),
140 theme.caller_message.text.clone(),
141 )
142 .contained()
143 .with_style(theme.caller_message.container),
144 )
145 .with_children(if initial_project.worktree_root_names.is_empty() {
146 None
147 } else {
148 Some(
149 Label::new(
150 initial_project.worktree_root_names.join(", "),
151 theme.worktree_roots.text.clone(),
152 )
153 .contained()
154 .with_style(theme.worktree_roots.container),
155 )
156 })
157 .contained()
158 .with_style(theme.caller_metadata)
159 .aligned(),
160 )
161 .contained()
162 .with_style(theme.caller_container)
163 .flex(1., true)
164 .into_any()
165 }
166
167 fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
168 enum Accept {}
169 enum Decline {}
170
171 Flex::column()
172 .with_child(
173 MouseEventHandler::<Accept, Self>::new(0, cx, |_, cx| {
174 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
175 Label::new("Accept", theme.accept_button.text.clone())
176 .aligned()
177 .contained()
178 .with_style(theme.accept_button.container)
179 })
180 .with_cursor_style(CursorStyle::PointingHand)
181 .on_click(MouseButton::Left, |_, this, cx| {
182 this.respond(true, cx);
183 })
184 .flex(1., true),
185 )
186 .with_child(
187 MouseEventHandler::<Decline, Self>::new(0, cx, |_, cx| {
188 let theme = &cx.global::<Settings>().theme.incoming_call_notification;
189 Label::new("Decline", theme.decline_button.text.clone())
190 .aligned()
191 .contained()
192 .with_style(theme.decline_button.container)
193 })
194 .with_cursor_style(CursorStyle::PointingHand)
195 .on_click(MouseButton::Left, |_, this, cx| {
196 this.respond(false, cx);
197 })
198 .flex(1., true),
199 )
200 .constrained()
201 .with_width(
202 cx.global::<Settings>()
203 .theme
204 .incoming_call_notification
205 .button_width,
206 )
207 .into_any()
208 }
209}
210
211impl Entity for IncomingCallNotification {
212 type Event = ();
213}
214
215impl View for IncomingCallNotification {
216 fn ui_name() -> &'static str {
217 "IncomingCallNotification"
218 }
219
220 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
221 let background = cx
222 .global::<Settings>()
223 .theme
224 .incoming_call_notification
225 .background;
226
227 Flex::row()
228 .with_child(self.render_caller(cx))
229 .with_child(self.render_buttons(cx))
230 .contained()
231 .with_background_color(background)
232 .expanded()
233 .into_any()
234 }
235}