1use crate::notification_window_options;
2use call::{ActiveCall, IncomingCall};
3use client::proto;
4use futures::StreamExt;
5use gpui::{
6 elements::*,
7 geometry::vector::vec2f,
8 platform::{CursorStyle, MouseButton},
9 AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
10};
11use std::sync::{Arc, Weak};
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<WindowHandle<IncomingCallNotification>> = Vec::new();
20 while let Some(incoming_call) = incoming_call.next().await {
21 for window in notification_windows.drain(..) {
22 window.remove(&mut cx);
23 }
24
25 if let Some(incoming_call) = incoming_call {
26 let window_size = cx.read(|cx| {
27 let theme = &theme::current(cx).incoming_call_notification;
28 vec2f(theme.window_width, theme.window_height)
29 });
30
31 for screen in cx.platform().screens() {
32 let window = cx
33 .add_window(notification_window_options(screen, window_size), |_| {
34 IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
35 });
36
37 notification_windows.push(window);
38 }
39 }
40 }
41 })
42 .detach();
43}
44
45#[derive(Clone, PartialEq)]
46struct RespondToCall {
47 accept: bool,
48}
49
50pub struct IncomingCallNotification {
51 call: IncomingCall,
52 app_state: Weak<AppState>,
53}
54
55impl IncomingCallNotification {
56 pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
57 Self { call, app_state }
58 }
59
60 fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
61 let active_call = ActiveCall::global(cx);
62 if accept {
63 let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
64 let caller_user_id = self.call.calling_user.id;
65 let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
66 let app_state = self.app_state.clone();
67 cx.app_context()
68 .spawn(|mut cx| async move {
69 join.await?;
70 if let Some(project_id) = initial_project_id {
71 cx.update(|cx| {
72 if let Some(app_state) = app_state.upgrade() {
73 workspace::join_remote_project(
74 project_id,
75 caller_user_id,
76 app_state,
77 cx,
78 )
79 .detach_and_log_err(cx);
80 }
81 });
82 }
83 anyhow::Ok(())
84 })
85 .detach_and_log_err(cx);
86 } else {
87 active_call.update(cx, |active_call, cx| {
88 active_call.decline_incoming(cx).log_err();
89 });
90 }
91 }
92
93 fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
94 let theme = &theme::current(cx).incoming_call_notification;
95 let default_project = proto::ParticipantProject::default();
96 let initial_project = self
97 .call
98 .initial_project
99 .as_ref()
100 .unwrap_or(&default_project);
101 Flex::row()
102 .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
103 Image::from_data(avatar)
104 .with_style(theme.caller_avatar)
105 .aligned()
106 }))
107 .with_child(
108 Flex::column()
109 .with_child(
110 Label::new(
111 self.call.calling_user.github_login.clone(),
112 theme.caller_username.text.clone(),
113 )
114 .contained()
115 .with_style(theme.caller_username.container),
116 )
117 .with_child(
118 Label::new(
119 format!(
120 "is sharing a project in Zed{}",
121 if initial_project.worktree_root_names.is_empty() {
122 ""
123 } else {
124 ":"
125 }
126 ),
127 theme.caller_message.text.clone(),
128 )
129 .contained()
130 .with_style(theme.caller_message.container),
131 )
132 .with_children(if initial_project.worktree_root_names.is_empty() {
133 None
134 } else {
135 Some(
136 Label::new(
137 initial_project.worktree_root_names.join(", "),
138 theme.worktree_roots.text.clone(),
139 )
140 .contained()
141 .with_style(theme.worktree_roots.container),
142 )
143 })
144 .contained()
145 .with_style(theme.caller_metadata)
146 .aligned(),
147 )
148 .contained()
149 .with_style(theme.caller_container)
150 .flex(1., true)
151 .into_any()
152 }
153
154 fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
155 enum Accept {}
156 enum Decline {}
157
158 let theme = theme::current(cx);
159 Flex::column()
160 .with_child(
161 MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
162 let theme = &theme.incoming_call_notification;
163 Label::new("Accept", theme.accept_button.text.clone())
164 .aligned()
165 .contained()
166 .with_style(theme.accept_button.container)
167 })
168 .with_cursor_style(CursorStyle::PointingHand)
169 .on_click(MouseButton::Left, |_, this, cx| {
170 this.respond(true, cx);
171 })
172 .flex(1., true),
173 )
174 .with_child(
175 MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
176 let theme = &theme.incoming_call_notification;
177 Label::new("Decline", theme.decline_button.text.clone())
178 .aligned()
179 .contained()
180 .with_style(theme.decline_button.container)
181 })
182 .with_cursor_style(CursorStyle::PointingHand)
183 .on_click(MouseButton::Left, |_, this, cx| {
184 this.respond(false, cx);
185 })
186 .flex(1., true),
187 )
188 .constrained()
189 .with_width(theme.incoming_call_notification.button_width)
190 .into_any()
191 }
192}
193
194impl Entity for IncomingCallNotification {
195 type Event = ();
196}
197
198impl View for IncomingCallNotification {
199 fn ui_name() -> &'static str {
200 "IncomingCallNotification"
201 }
202
203 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
204 let background = theme::current(cx).incoming_call_notification.background;
205 Flex::row()
206 .with_child(self.render_caller(cx))
207 .with_child(self.render_buttons(cx))
208 .contained()
209 .with_background_color(background)
210 .expanded()
211 .into_any()
212 }
213}