1use crate::{sidebar::SidebarSide, AppState, ToggleFollow, Workspace};
2use anyhow::Result;
3use client::{proto, Client, Contact};
4use gpui::{
5 elements::*, ElementBox, Entity, ImageData, MutableAppContext, RenderContext, Task, View,
6 ViewContext,
7};
8use project::Project;
9use settings::Settings;
10use std::sync::Arc;
11use util::ResultExt;
12
13pub struct WaitingRoom {
14 project_id: u64,
15 avatar: Option<Arc<ImageData>>,
16 message: String,
17 waiting: bool,
18 client: Arc<Client>,
19 _join_task: Task<Result<()>>,
20}
21
22impl Entity for WaitingRoom {
23 type Event = ();
24
25 fn release(&mut self, _: &mut MutableAppContext) {
26 if self.waiting {
27 self.client
28 .send(proto::LeaveProject {
29 project_id: self.project_id,
30 })
31 .log_err();
32 }
33 }
34}
35
36impl View for WaitingRoom {
37 fn ui_name() -> &'static str {
38 "WaitingRoom"
39 }
40
41 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
42 let theme = &cx.global::<Settings>().theme.workspace;
43
44 Flex::column()
45 .with_children(self.avatar.clone().map(|avatar| {
46 Image::new(avatar)
47 .with_style(theme.joining_project_avatar)
48 .aligned()
49 .boxed()
50 }))
51 .with_child(
52 Text::new(
53 self.message.clone(),
54 theme.joining_project_message.text.clone(),
55 )
56 .contained()
57 .with_style(theme.joining_project_message.container)
58 .aligned()
59 .boxed(),
60 )
61 .aligned()
62 .contained()
63 .with_background_color(theme.background)
64 .boxed()
65 }
66}
67
68impl WaitingRoom {
69 pub fn new(
70 contact: Arc<Contact>,
71 project_index: usize,
72 app_state: Arc<AppState>,
73 cx: &mut ViewContext<Self>,
74 ) -> Self {
75 let project_id = contact.projects[project_index].id;
76 let client = app_state.client.clone();
77 let _join_task = cx.spawn_weak({
78 let contact = contact.clone();
79 |this, mut cx| async move {
80 let project = Project::remote(
81 project_id,
82 app_state.client.clone(),
83 app_state.user_store.clone(),
84 app_state.project_store.clone(),
85 app_state.languages.clone(),
86 app_state.fs.clone(),
87 cx.clone(),
88 )
89 .await;
90
91 if let Some(this) = this.upgrade(&cx) {
92 this.update(&mut cx, |this, cx| {
93 this.waiting = false;
94 match project {
95 Ok(project) => {
96 cx.replace_root_view(|cx| {
97 let mut workspace =
98 Workspace::new(project, app_state.default_item_factory, cx);
99 (app_state.initialize_workspace)(
100 &mut workspace,
101 &app_state,
102 cx,
103 );
104 workspace.toggle_sidebar(SidebarSide::Left, cx);
105 if let Some((host_peer_id, _)) = workspace
106 .project
107 .read(cx)
108 .collaborators()
109 .iter()
110 .find(|(_, collaborator)| collaborator.replica_id == 0)
111 {
112 if let Some(follow) = workspace
113 .toggle_follow(&ToggleFollow(*host_peer_id), cx)
114 {
115 follow.detach_and_log_err(cx);
116 }
117 }
118 workspace
119 });
120 }
121 Err(error) => {
122 let login = &contact.user.github_login;
123 let message = match error {
124 project::JoinProjectError::HostDeclined => {
125 format!("@{} declined your request.", login)
126 }
127 project::JoinProjectError::HostClosedProject => {
128 format!(
129 "@{} closed their copy of {}.",
130 login,
131 humanize_list(
132 &contact.projects[project_index]
133 .visible_worktree_root_names
134 )
135 )
136 }
137 project::JoinProjectError::HostWentOffline => {
138 format!("@{} went offline.", login)
139 }
140 project::JoinProjectError::Other(error) => {
141 log::error!("error joining project: {}", error);
142 "An error occurred.".to_string()
143 }
144 };
145 this.message = message;
146 cx.notify();
147 }
148 }
149 })
150 }
151
152 Ok(())
153 }
154 });
155
156 Self {
157 project_id,
158 avatar: contact.user.avatar.clone(),
159 message: format!(
160 "Asking to join @{}'s copy of {}...",
161 contact.user.github_login,
162 humanize_list(&contact.projects[project_index].visible_worktree_root_names)
163 ),
164 waiting: true,
165 client,
166 _join_task,
167 }
168 }
169}
170
171fn humanize_list<'a>(items: impl IntoIterator<Item = &'a String>) -> String {
172 let mut list = String::new();
173 let mut items = items.into_iter().enumerate().peekable();
174 while let Some((ix, item)) = items.next() {
175 if ix > 0 {
176 list.push_str(", ");
177 if items.peek().is_none() {
178 list.push_str("and ");
179 }
180 }
181
182 list.push_str(item);
183 }
184 list
185}