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