incoming_call_notification.rs

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