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