share_page.rs
⎇
Raw
1//! Public share page: renders a shared item without login.
2
3use crate::i18n;
4use api_types::FileKind;
5use leptos::prelude::*;
6use wasm_bindgen_futures::spawn_local;
7
8use crate::api::{self, Me, Mode, RootInfo, ShareInfo, UserInfo};
9use crate::components::toast::provide_toast;
10use crate::editor::EditTarget;
11use crate::preview::{PreviewTarget, preview_kind};
12use crate::router::Location;
13use crate::views::browser::Browser;
14use crate::views::file_view::{FileView, UnsupportedTarget};
15use crate::views::workspace::FileWorkspace;
16
17#[derive(Clone, PartialEq)]
18enum SharePageState {
19 Loading,
20 NotFound,
21 Expired,
22 Active,
23}
24
25/// The full-page view for a shared *file*: the share page *is* the file.
26/// The file sits at the (empty) path of its synthetic root.
27fn file_view_for_share(i: &ShareInfo) -> Option<FileView> {
28 let kind = i.kind?;
29 let root_id = i.root_id;
30 let name = i.name.clone();
31 let path = String::new();
32 match kind {
33 FileKind::Text => Some(FileView::Editor(EditTarget {
34 root_id,
35 path,
36 name,
37 readonly: !i.writable,
38 })),
39 k if preview_kind(k).is_some() => Some(FileView::Preview(
40 PreviewTarget {
41 root_id,
42 path,
43 name,
44 },
45 preview_kind(k).unwrap(),
46 )),
47 k => Some(FileView::Unsupported(UnsupportedTarget {
48 root_id,
49 path,
50 name,
51 kind: k,
52 })),
53 }
54}
55
56#[component]
57pub fn ShareView(token: String, loc: ReadSignal<Location>) -> impl IntoView {
58 let _ = provide_toast();
59 let (me, set_me) = signal(Option::<Me>::None);
60 let (info, set_info) = signal(Option::<ShareInfo>::None);
61 let (state, set_state) = signal(SharePageState::Loading);
62
63 // Resolve the share once per token and scope all file API calls to it.
64 {
65 let tok = token.clone();
66 let set_me2 = set_me;
67 let set_info2 = set_info;
68 let set_state2 = set_state;
69 spawn_local(async move {
70 api::set_share_token(Some(&tok));
71 match api::resolve_share(&tok).await {
72 Ok(i) => {
73 let mode = if i.writable { Mode::Rw } else { Mode::Ro };
74 let me_val = Me {
75 first_boot: false,
76 user: Some(UserInfo {
77 id: i.id,
78 name: "shared".to_string(),
79 is_admin: false,
80 single_click_open: false,
81 language: None,
82 }),
83 roots: vec![RootInfo {
84 id: i.root_id,
85 name: i.name.clone(),
86 path: "/".to_string(),
87 mode,
88 }],
89 allow_writable_shares: false,
90 };
91 set_me2.set(Some(me_val));
92 set_info2.set(Some(i));
93 set_state2.set(SharePageState::Active);
94 }
95 Err(e) => {
96 let expired = e.status() == Some(410);
97 set_state2.set(if expired {
98 SharePageState::Expired
99 } else {
100 SharePageState::NotFound
101 });
102 }
103 }
104 });
105 on_cleanup(move || api::set_share_token(None));
106 }
107
108 // The shared file-view plumbing (topbar, close/save/dirty, selection,
109 // dialogs, Escape). Closing a folder share returns to the listing;
110 // closing a file share re-opens the file itself (there is nothing to
111 // close back to, so the close button and Escape stay disabled as
112 // well).
113 let ws = FileWorkspace::new(
114 me,
115 loc,
116 move || info.get().and_then(|i| file_view_for_share(&i)),
117 move || state.get() == SharePageState::Active,
118 );
119 let file_view = ws.file_view;
120 let open_file = ws.open_file;
121 let selected = ws.selected;
122 let set_selected = ws.set_selected;
123 let dialog = ws.dialog;
124 let set_dialog = ws.set_dialog;
125 let set_browser_refresh = ws.set_browser_refresh;
126 let topbar_ref = ws.topbar_ref;
127 let topbar_left = ws.topbar_left();
128 let topbar_right = ws.topbar_right();
129 let file_content = ws.file_content();
130 let overlays = ws.overlays();
131
132 // A file share opens straight into its viewer.
133 {
134 let info2 = info;
135 let set_fv = open_file;
136 Effect::new(move |_| {
137 if let Some(i) = info2.get()
138 && i.is_file
139 {
140 set_fv.set(file_view_for_share(&i));
141 }
142 });
143 }
144
145 view! {
146 <div
147 class=move || {
148 if file_view.get().is_some() {
149 "layout share-layout file-mode".to_string()
150 } else {
151 "layout share-layout".to_string()
152 }
153 }
154 >
155 <header class="topbar" node_ref=topbar_ref>
156 <div class="topbar-left">
157 {topbar_left}
158 </div>
159 <div class="topbar-right">
160 {topbar_right}
161 </div>
162 </header>
163 <main class="content" class:file-open=move || file_view.get().is_some()>
164 {move || match state.get() {
165 SharePageState::Loading => view! {
166 <div class="center-screen">
167 <p class="muted">{i18n::tr(i18n::k::LOADING_SHARE)}</p>
168 </div>
169 }
170 .into_view()
171 .into_any(),
172 SharePageState::NotFound => view! {
173 <div class="center-screen">
174 <h2>{i18n::tr(i18n::k::SHARE_NOT_FOUND)}</h2>
175 <p class="muted">{i18n::tr(i18n::k::SHARE_NOT_FOUND_MSG)}</p>
176 </div>
177 }
178 .into_view()
179 .into_any(),
180 SharePageState::Expired => view! {
181 <div class="center-screen">
182 <h2>{i18n::tr(i18n::k::SHARE_EXPIRED)}</h2>
183 <p class="muted">{i18n::tr(i18n::k::SHARE_EXPIRED_MSG)}</p>
184 </div>
185 }
186 .into_view()
187 .into_any(),
188 SharePageState::Active => {
189 let Some(i) = info.get() else {
190 return {
191 view! {}.into_any()
192 };
193 };
194 // File shares render through the full-page file view
195 // (opened by the effect above); only folder shares
196 // get a browser.
197 if i.is_file {
198 view! {}
199 .into_view()
200 .into_any()
201 } else {
202 if me.get().is_none() {
203 return view! {
204 <div class="center-screen">
205 <p class="muted">{i18n::tr(i18n::k::LOADING)}</p>
206 </div>
207 }
208 .into_view()
209 .into_any();
210 }
211 view! {
212 <Browser
213 me=me
214 set_me=set_me
215 loc=loc
216 open_file=open_file
217 set_browser_refresh=set_browser_refresh
218 selected=selected
219 set_selected=set_selected
220 set_dialog=set_dialog
221 dialog=dialog
222 file_view=file_view
223 />
224 }
225 .into_view()
226 .into_any()
227 }
228 }
229 }}
230 {file_content}
231 </main>
232 {overlays}
233 </div>
234 }
235}
236