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