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_size = screen.size();
 36                    let (window_id, _) = cx.add_window(
 37                        WindowOptions {
 38                            bounds: WindowBounds::Fixed(RectF::new(
 39                                vec2f(screen_size.x() - window_size.x() - PADDING, 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()),
 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}
 68
 69impl IncomingCallNotification {
 70    pub fn new(call: IncomingCall) -> Self {
 71        Self { call }
 72    }
 73
 74    fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
 75        let active_call = ActiveCall::global(cx);
 76        if action.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            cx.spawn_weak(|_, mut cx| async move {
 81                join.await?;
 82                if let Some(project_id) = initial_project_id {
 83                    cx.update(|cx| {
 84                        cx.dispatch_global_action(JoinProject {
 85                            project_id,
 86                            follow_user_id: caller_user_id,
 87                        })
 88                    });
 89                }
 90                anyhow::Ok(())
 91            })
 92            .detach_and_log_err(cx);
 93        } else {
 94            active_call.update(cx, |active_call, _| {
 95                active_call.decline_incoming().log_err();
 96            });
 97        }
 98    }
 99
100    fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
101        let theme = &cx.global::<Settings>().theme.incoming_call_notification;
102        let default_project = proto::ParticipantProject::default();
103        let initial_project = self
104            .call
105            .initial_project
106            .as_ref()
107            .unwrap_or(&default_project);
108        Flex::row()
109            .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
110                Image::new(avatar)
111                    .with_style(theme.caller_avatar)
112                    .aligned()
113                    .boxed()
114            }))
115            .with_child(
116                Flex::column()
117                    .with_child(
118                        Label::new(
119                            self.call.calling_user.github_login.clone(),
120                            theme.caller_username.text.clone(),
121                        )
122                        .contained()
123                        .with_style(theme.caller_username.container)
124                        .boxed(),
125                    )
126                    .with_child(
127                        Label::new(
128                            format!(
129                                "is sharing a project in Zed{}",
130                                if initial_project.worktree_root_names.is_empty() {
131                                    ""
132                                } else {
133                                    ":"
134                                }
135                            ),
136                            theme.caller_message.text.clone(),
137                        )
138                        .contained()
139                        .with_style(theme.caller_message.container)
140                        .boxed(),
141                    )
142                    .with_children(if initial_project.worktree_root_names.is_empty() {
143                        None
144                    } else {
145                        Some(
146                            Label::new(
147                                initial_project.worktree_root_names.join(", "),
148                                theme.worktree_roots.text.clone(),
149                            )
150                            .contained()
151                            .with_style(theme.worktree_roots.container)
152                            .boxed(),
153                        )
154                    })
155                    .contained()
156                    .with_style(theme.caller_metadata)
157                    .aligned()
158                    .boxed(),
159            )
160            .contained()
161            .with_style(theme.caller_container)
162            .flex(1., true)
163            .boxed()
164    }
165
166    fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
167        enum Accept {}
168        enum Decline {}
169
170        Flex::column()
171            .with_child(
172                MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
173                    let theme = &cx.global::<Settings>().theme.incoming_call_notification;
174                    Label::new("Accept".to_string(), theme.accept_button.text.clone())
175                        .aligned()
176                        .contained()
177                        .with_style(theme.accept_button.container)
178                        .boxed()
179                })
180                .with_cursor_style(CursorStyle::PointingHand)
181                .on_click(MouseButton::Left, |_, cx| {
182                    cx.dispatch_action(RespondToCall { accept: true });
183                })
184                .flex(1., true)
185                .boxed(),
186            )
187            .with_child(
188                MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
189                    let theme = &cx.global::<Settings>().theme.incoming_call_notification;
190                    Label::new("Decline".to_string(), theme.decline_button.text.clone())
191                        .aligned()
192                        .contained()
193                        .with_style(theme.decline_button.container)
194                        .boxed()
195                })
196                .with_cursor_style(CursorStyle::PointingHand)
197                .on_click(MouseButton::Left, |_, cx| {
198                    cx.dispatch_action(RespondToCall { accept: false });
199                })
200                .flex(1., true)
201                .boxed(),
202            )
203            .constrained()
204            .with_width(
205                cx.global::<Settings>()
206                    .theme
207                    .incoming_call_notification
208                    .button_width,
209            )
210            .boxed()
211    }
212}
213
214impl Entity for IncomingCallNotification {
215    type Event = ();
216}
217
218impl View for IncomingCallNotification {
219    fn ui_name() -> &'static str {
220        "IncomingCallNotification"
221    }
222
223    fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
224        let background = cx
225            .global::<Settings>()
226            .theme
227            .incoming_call_notification
228            .background;
229
230        Flex::row()
231            .with_child(self.render_caller(cx))
232            .with_child(self.render_buttons(cx))
233            .contained()
234            .with_background_color(background)
235            .expanded()
236            .boxed()
237    }
238}