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