project_shared_notification.rs

  1use call::{room, ActiveCall};
  2use client::User;
  3use collections::HashMap;
  4use gpui::{
  5    actions,
  6    elements::*,
  7    geometry::{rect::RectF, vector::vec2f},
  8    platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
  9    AppContext, Entity, RenderContext, View, ViewContext,
 10};
 11use settings::Settings;
 12use std::sync::Arc;
 13use workspace::JoinProject;
 14
 15actions!(project_shared_notification, [DismissProject]);
 16
 17pub fn init(cx: &mut AppContext) {
 18    cx.add_action(ProjectSharedNotification::join);
 19    cx.add_action(ProjectSharedNotification::dismiss);
 20
 21    let active_call = ActiveCall::global(cx);
 22    let mut notification_windows = HashMap::default();
 23    cx.subscribe(&active_call, move |_, event, cx| match event {
 24        room::Event::RemoteProjectShared {
 25            owner,
 26            project_id,
 27            worktree_root_names,
 28        } => {
 29            const PADDING: f32 = 16.;
 30            let theme = &cx.global::<Settings>().theme.project_shared_notification;
 31            let window_size = vec2f(theme.window_width, theme.window_height);
 32
 33            for screen in cx.platform().screens() {
 34                let screen_bounds = screen.bounds();
 35                let (window_id, _) = cx.add_window(
 36                    WindowOptions {
 37                        bounds: WindowBounds::Fixed(RectF::new(
 38                            screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING),
 39                            window_size,
 40                        )),
 41                        titlebar: None,
 42                        center: false,
 43                        focus: false,
 44                        kind: WindowKind::PopUp,
 45                        is_movable: false,
 46                        screen: Some(screen),
 47                    },
 48                    |_| {
 49                        ProjectSharedNotification::new(
 50                            owner.clone(),
 51                            *project_id,
 52                            worktree_root_names.clone(),
 53                        )
 54                    },
 55                );
 56                notification_windows
 57                    .entry(*project_id)
 58                    .or_insert(Vec::new())
 59                    .push(window_id);
 60            }
 61        }
 62        room::Event::RemoteProjectUnshared { project_id } => {
 63            if let Some(window_ids) = notification_windows.remove(&project_id) {
 64                for window_id in window_ids {
 65                    cx.remove_window(window_id);
 66                }
 67            }
 68        }
 69        room::Event::Left => {
 70            for (_, window_ids) in notification_windows.drain() {
 71                for window_id in window_ids {
 72                    cx.remove_window(window_id);
 73                }
 74            }
 75        }
 76        _ => {}
 77    })
 78    .detach();
 79}
 80
 81pub struct ProjectSharedNotification {
 82    project_id: u64,
 83    worktree_root_names: Vec<String>,
 84    owner: Arc<User>,
 85}
 86
 87impl ProjectSharedNotification {
 88    fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
 89        Self {
 90            project_id,
 91            worktree_root_names,
 92            owner,
 93        }
 94    }
 95
 96    fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
 97        let window_id = cx.window_id();
 98        cx.remove_window(window_id);
 99        cx.propagate_action();
100    }
101
102    fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
103        let window_id = cx.window_id();
104        cx.remove_window(window_id);
105    }
106
107    fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
108        let theme = &cx.global::<Settings>().theme.project_shared_notification;
109        Flex::row()
110            .with_children(self.owner.avatar.clone().map(|avatar| {
111                Image::from_data(avatar)
112                    .with_style(theme.owner_avatar)
113                    .aligned()
114                    .boxed()
115            }))
116            .with_child(
117                Flex::column()
118                    .with_child(
119                        Label::new(
120                            self.owner.github_login.clone(),
121                            theme.owner_username.text.clone(),
122                        )
123                        .contained()
124                        .with_style(theme.owner_username.container)
125                        .boxed(),
126                    )
127                    .with_child(
128                        Label::new(
129                            format!(
130                                "is sharing a project in Zed{}",
131                                if self.worktree_root_names.is_empty() {
132                                    ""
133                                } else {
134                                    ":"
135                                }
136                            ),
137                            theme.message.text.clone(),
138                        )
139                        .contained()
140                        .with_style(theme.message.container)
141                        .boxed(),
142                    )
143                    .with_children(if self.worktree_root_names.is_empty() {
144                        None
145                    } else {
146                        Some(
147                            Label::new(
148                                self.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.owner_metadata)
158                    .aligned()
159                    .boxed(),
160            )
161            .contained()
162            .with_style(theme.owner_container)
163            .flex(1., true)
164            .boxed()
165    }
166
167    fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
168        enum Open {}
169        enum Dismiss {}
170
171        let project_id = self.project_id;
172        let owner_user_id = self.owner.id;
173
174        Flex::column()
175            .with_child(
176                MouseEventHandler::<Open>::new(0, cx, |_, cx| {
177                    let theme = &cx.global::<Settings>().theme.project_shared_notification;
178                    Label::new("Open", theme.open_button.text.clone())
179                        .aligned()
180                        .contained()
181                        .with_style(theme.open_button.container)
182                        .boxed()
183                })
184                .with_cursor_style(CursorStyle::PointingHand)
185                .on_click(MouseButton::Left, move |_, cx| {
186                    cx.dispatch_action(JoinProject {
187                        project_id,
188                        follow_user_id: owner_user_id,
189                    });
190                })
191                .flex(1., true)
192                .boxed(),
193            )
194            .with_child(
195                MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
196                    let theme = &cx.global::<Settings>().theme.project_shared_notification;
197                    Label::new("Dismiss", theme.dismiss_button.text.clone())
198                        .aligned()
199                        .contained()
200                        .with_style(theme.dismiss_button.container)
201                        .boxed()
202                })
203                .with_cursor_style(CursorStyle::PointingHand)
204                .on_click(MouseButton::Left, |_, cx| {
205                    cx.dispatch_action(DismissProject);
206                })
207                .flex(1., true)
208                .boxed(),
209            )
210            .constrained()
211            .with_width(
212                cx.global::<Settings>()
213                    .theme
214                    .project_shared_notification
215                    .button_width,
216            )
217            .boxed()
218    }
219}
220
221impl Entity for ProjectSharedNotification {
222    type Event = ();
223}
224
225impl View for ProjectSharedNotification {
226    fn ui_name() -> &'static str {
227        "ProjectSharedNotification"
228    }
229
230    fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
231        let background = cx
232            .global::<Settings>()
233            .theme
234            .project_shared_notification
235            .background;
236        Flex::row()
237            .with_child(self.render_owner(cx))
238            .with_child(self.render_buttons(cx))
239            .contained()
240            .with_background_color(background)
241            .expanded()
242            .boxed()
243    }
244}