incoming_call_notification.rs

  1use call::{ActiveCall, IncomingCall};
  2use client::proto;
  3use futures::StreamExt;
  4use gpui::{
  5    elements::*,
  6    geometry::{rect::RectF, vector::vec2f},
  7    impl_internal_actions,
  8    platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
  9    AppContext, Entity, RenderContext, View, ViewContext,
 10};
 11use settings::Settings;
 12use util::ResultExt;
 13use workspace::JoinProject;
 14
 15impl_internal_actions!(incoming_call_notification, [RespondToCall]);
 16
 17pub fn init(cx: &mut AppContext) {
 18    cx.add_action(IncomingCallNotification::respond_to_call);
 19
 20    let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
 21    cx.spawn(|mut cx| async move {
 22        let mut notification_windows = Vec::new();
 23        while let Some(incoming_call) = incoming_call.next().await {
 24            for window_id in notification_windows.drain(..) {
 25                cx.remove_window(window_id);
 26            }
 27
 28            if let Some(incoming_call) = incoming_call {
 29                const PADDING: f32 = 16.;
 30                let window_size = cx.read(|cx| {
 31                    let theme = &cx.global::<Settings>().theme.incoming_call_notification;
 32                    vec2f(theme.window_width, theme.window_height)
 33                });
 34
 35                for screen in cx.platform().screens() {
 36                    let screen_bounds = screen.bounds();
 37                    let (window_id, _) = cx.add_window(
 38                        WindowOptions {
 39                            bounds: WindowBounds::Fixed(RectF::new(
 40                                screen_bounds.upper_right()
 41                                    - vec2f(PADDING + window_size.x(), PADDING),
 42                                window_size,
 43                            )),
 44                            titlebar: None,
 45                            center: false,
 46                            focus: false,
 47                            kind: WindowKind::PopUp,
 48                            is_movable: false,
 49                            screen: Some(screen),
 50                        },
 51                        |_| IncomingCallNotification::new(incoming_call.clone()),
 52                    );
 53
 54                    notification_windows.push(window_id);
 55                }
 56            }
 57        }
 58    })
 59    .detach();
 60}
 61
 62#[derive(Clone, PartialEq)]
 63struct RespondToCall {
 64    accept: bool,
 65}
 66
 67pub struct IncomingCallNotification {
 68    call: IncomingCall,
 69}
 70
 71impl IncomingCallNotification {
 72    pub fn new(call: IncomingCall) -> Self {
 73        Self { call }
 74    }
 75
 76    fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
 77        let active_call = ActiveCall::global(cx);
 78        if action.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            cx.spawn_weak(|_, mut cx| async move {
 83                join.await?;
 84                if let Some(project_id) = initial_project_id {
 85                    cx.update(|cx| {
 86                        cx.dispatch_global_action(JoinProject {
 87                            project_id,
 88                            follow_user_id: caller_user_id,
 89                        })
 90                    });
 91                }
 92                anyhow::Ok(())
 93            })
 94            .detach_and_log_err(cx);
 95        } else {
 96            active_call.update(cx, |active_call, _| {
 97                active_call.decline_incoming().log_err();
 98            });
 99        }
100    }
101
102    fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
103        let theme = &cx.global::<Settings>().theme.incoming_call_notification;
104        let default_project = proto::ParticipantProject::default();
105        let initial_project = self
106            .call
107            .initial_project
108            .as_ref()
109            .unwrap_or(&default_project);
110        Flex::row()
111            .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
112                Image::from_data(avatar)
113                    .with_style(theme.caller_avatar)
114                    .aligned()
115                    .boxed()
116            }))
117            .with_child(
118                Flex::column()
119                    .with_child(
120                        Label::new(
121                            self.call.calling_user.github_login.clone(),
122                            theme.caller_username.text.clone(),
123                        )
124                        .contained()
125                        .with_style(theme.caller_username.container)
126                        .boxed(),
127                    )
128                    .with_child(
129                        Label::new(
130                            format!(
131                                "is sharing a project in Zed{}",
132                                if initial_project.worktree_root_names.is_empty() {
133                                    ""
134                                } else {
135                                    ":"
136                                }
137                            ),
138                            theme.caller_message.text.clone(),
139                        )
140                        .contained()
141                        .with_style(theme.caller_message.container)
142                        .boxed(),
143                    )
144                    .with_children(if initial_project.worktree_root_names.is_empty() {
145                        None
146                    } else {
147                        Some(
148                            Label::new(
149                                initial_project.worktree_root_names.join(", "),
150                                theme.worktree_roots.text.clone(),
151                            )
152                            .contained()
153                            .with_style(theme.worktree_roots.container)
154                            .boxed(),
155                        )
156                    })
157                    .contained()
158                    .with_style(theme.caller_metadata)
159                    .aligned()
160                    .boxed(),
161            )
162            .contained()
163            .with_style(theme.caller_container)
164            .flex(1., true)
165            .boxed()
166    }
167
168    fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
169        enum Accept {}
170        enum Decline {}
171
172        Flex::column()
173            .with_child(
174                MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
175                    let theme = &cx.global::<Settings>().theme.incoming_call_notification;
176                    Label::new("Accept", theme.accept_button.text.clone())
177                        .aligned()
178                        .contained()
179                        .with_style(theme.accept_button.container)
180                        .boxed()
181                })
182                .with_cursor_style(CursorStyle::PointingHand)
183                .on_click(MouseButton::Left, |_, cx| {
184                    cx.dispatch_action(RespondToCall { accept: true });
185                })
186                .flex(1., true)
187                .boxed(),
188            )
189            .with_child(
190                MouseEventHandler::<Decline>::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                        .boxed()
197                })
198                .with_cursor_style(CursorStyle::PointingHand)
199                .on_click(MouseButton::Left, |_, cx| {
200                    cx.dispatch_action(RespondToCall { accept: false });
201                })
202                .flex(1., true)
203                .boxed(),
204            )
205            .constrained()
206            .with_width(
207                cx.global::<Settings>()
208                    .theme
209                    .incoming_call_notification
210                    .button_width,
211            )
212            .boxed()
213    }
214}
215
216impl Entity for IncomingCallNotification {
217    type Event = ();
218}
219
220impl View for IncomingCallNotification {
221    fn ui_name() -> &'static str {
222        "IncomingCallNotification"
223    }
224
225    fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
226        let background = cx
227            .global::<Settings>()
228            .theme
229            .incoming_call_notification
230            .background;
231
232        Flex::row()
233            .with_child(self.render_caller(cx))
234            .with_child(self.render_buttons(cx))
235            .contained()
236            .with_background_color(background)
237            .expanded()
238            .boxed()
239    }
240}