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                            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}