//! Public share page: renders a shared item without login. use crate::i18n; use api_types::FileKind; use leptos::prelude::*; use wasm_bindgen_futures::spawn_local; use crate::api::{self, Me, Mode, RootInfo, ShareInfo, UserInfo}; use crate::components::toast::provide_toast; use crate::editor::EditTarget; use crate::preview::{PreviewTarget, preview_kind}; use crate::router::Location; use crate::views::browser::Browser; use crate::views::file_view::{FileView, UnsupportedTarget}; use crate::views::workspace::FileWorkspace; #[derive(Clone, PartialEq)] enum SharePageState { Loading, NotFound, Expired, /// The share asks for a password. The server withholds even its name /// until the password is entered, so nothing about it is known yet. Locked, Active, } /// The full-page view for a shared *file*: the share page *is* the file. /// The file sits at the (empty) path of its synthetic root. fn file_view_for_share(i: &ShareInfo) -> Option { let kind = i.kind?; let root_id = i.root_id; let name = i.name.clone(); let path = String::new(); match kind { FileKind::Text => Some(FileView::Editor(EditTarget { root_id, path, name, readonly: !i.writable, })), k if preview_kind(k).is_some() => Some(FileView::Preview( PreviewTarget { root_id, path, name, }, preview_kind(k).unwrap(), )), k => Some(FileView::Unsupported(UnsupportedTarget { root_id, path, name, kind: k, })), } } #[component] pub fn ShareView(token: String, loc: ReadSignal) -> impl IntoView { let _ = provide_toast(); let (me, set_me) = signal(Option::::None); let (info, set_info) = signal(Option::::None); let (state, set_state) = signal(SharePageState::Loading); // Apply a resolved share: it becomes the synthetic "me" every file call // is scoped to. Shared by the initial resolve and by the unlock form. let accept = Callback::new(move |i: ShareInfo| { let mode = if i.writable { Mode::Rw } else { Mode::Ro }; set_me.set(Some(Me { first_boot: false, user: Some(UserInfo { id: i.id, name: "shared".to_string(), is_admin: false, single_click_open: false, thumbnails: true, language: None, default_root_id: None, }), roots: vec![RootInfo { id: i.root_id, name: i.name.clone(), path: "/".to_string(), mode, }], allow_writable_shares: false, // A share page cannot read the server config, so it asks and lets // the 404 decide. The tile falls back to its icon either way. thumbnails_available: true, public_url: None, })); set_info.set(Some(i)); set_state.set(SharePageState::Active); }); // Resolve the share once per token and scope all file API calls to it. { let tok = token.clone(); let set_state2 = set_state; spawn_local(async move { api::set_share_token(Some(&tok)); match api::resolve_share(&tok).await { Ok(i) => accept.run(i), // 401 means password protected, not missing or gone. Err(e) => set_state2.set(match e.status() { Some(410) => SharePageState::Expired, Some(401) => SharePageState::Locked, _ => SharePageState::NotFound, }), } }); on_cleanup(move || api::set_share_token(None)); } // The shared file-view plumbing (topbar, close/save/dirty, selection, // dialogs, Escape). Closing a folder share returns to the listing; // closing a file share re-opens the file itself (there is nothing to // close back to, so the close button and Escape stay disabled as // well). let token_for_form = token.clone(); let ws = FileWorkspace::new( me, loc, move || info.get().and_then(|i| file_view_for_share(&i)), move || state.get() == SharePageState::Active, ); let file_view = ws.file_view; let open_file = ws.open_file; let selected = ws.selected; let set_selected = ws.set_selected; let dialog = ws.dialog; let set_dialog = ws.set_dialog; let set_browser_refresh = ws.set_browser_refresh; let topbar_ref = ws.topbar_ref; let topbar_left = ws.topbar_left(); let topbar_right = ws.topbar_right(); let file_content = ws.file_content(); let overlays = ws.overlays(); // A file share opens straight into its viewer. { let info2 = info; let set_fv = open_file; Effect::new(move |_| { if let Some(i) = info2.get() && i.is_file { set_fv.set(file_view_for_share(&i)); } }); } view! {
{topbar_left}
{topbar_right}
{move || match state.get() { SharePageState::Loading => view! {

{i18n::tr(i18n::k::LOADING_SHARE)}

} .into_view() .into_any(), SharePageState::NotFound => view! {

{i18n::tr(i18n::k::SHARE_NOT_FOUND)}

{i18n::tr(i18n::k::SHARE_NOT_FOUND_MSG)}

} .into_view() .into_any(), SharePageState::Expired => view! {

{i18n::tr(i18n::k::SHARE_EXPIRED)}

{i18n::tr(i18n::k::SHARE_EXPIRED_MSG)}

} .into_view() .into_any(), SharePageState::Locked => view! { } .into_view() .into_any(), SharePageState::Active => { let Some(i) = info.get() else { return { view! {}.into_any() }; }; // File shares render through the full-page file view // (opened by the effect above); only folder shares // get a browser. if i.is_file { view! {} .into_view() .into_any() } else { if me.get().is_none() { return view! {

{i18n::tr(i18n::k::LOADING)}

} .into_view() .into_any(); } view! { } .into_view() .into_any() } } }} {file_content}
{overlays}
} } /// The password gate of a protected share. The page knows nothing about the /// share yet, so it shows the prompt alone. #[component] fn UnlockForm(token: String, accept: Callback) -> impl IntoView { let (password, set_password) = signal(String::new()); let (error, set_error) = signal(Option::::None); let (busy, set_busy) = signal(false); // A Callback, not a plain closure: both the button and Enter need it, // and a closure would be moved into whichever handler came first. let submit = Callback::new(move |_: ()| { let pw = password.get(); if pw.is_empty() || busy.get() { return; } set_busy.set(true); set_error.set(None); let tok = token.clone(); spawn_local(async move { match api::unlock_share(&tok, &pw).await { Ok(i) => accept.run(i), Err(e) => { set_error.set(Some(e.to_string())); set_busy.set(false); } } }); }); view! {

{i18n::tr(i18n::k::SHARE_LOCKED_TITLE)}

{i18n::tr(i18n::k::ERR_SHARE_LOCKED)}

().ok()) { set_password.set(t.value()); set_error.set(None); } } on:keydown=move |ev: web_sys::KeyboardEvent| { if ev.key() == "Enter" { ev.prevent_default(); submit.run(()); } } /> {move || { let Some(e) = error.get() else { return view! {}.into_any(); }; view! {

{e}

}.into_view().into_any() }}
} }