diff --git a/crates/pdfs-fuse/src/sharing.rs b/crates/pdfs-fuse/src/sharing.rs index c4875ca..4e429e1 100644 --- a/crates/pdfs-fuse/src/sharing.rs +++ b/crates/pdfs-fuse/src/sharing.rs @@ -660,6 +660,12 @@ impl Core { /// the shared uids; the per-node detail is then gathered best-effort — a single /// node racing with an unshare drops from the list rather than failing the whole /// request. + /// + /// A share with no member, no invitation and no link is left out: it is + /// shared with no one. Stop Sharing leaves exactly that behind, since the SDK + /// cannot delete a share, and Drive keeps listing it until its backend gets + /// round to removing it, minutes later. A share whose details could not all + /// be read stays listed, so a failed lookup never hides a live share. pub(crate) fn list_shared_by_me(&self) -> CoreResult> { let uids = self .rt @@ -678,24 +684,26 @@ impl Core { let members = self .rt .block_on(self.client.list_share_members(&uid)) - .map(|m| m.len()) - .unwrap_or(0); + .map(|m| m.len()); let proton_invites = self .rt .block_on(self.client.list_share_invitations(&uid)) - .map(|i| i.len()) - .unwrap_or(0); + .map(|i| i.len()); let external_invites = self .rt .block_on(self.client.list_external_invitations(&uid)) - .map(|i| i.len()) - .unwrap_or(0); - let link = self - .rt - .block_on(self.client.get_public_link(&uid)) - .ok() - .flatten() - .map(public_link_info); + .map(|i| i.len()); + let link = self.rt.block_on(self.client.get_public_link(&uid)); + if let (Ok(0), Ok(0), Ok(0), Ok(None)) = + (&members, &proton_invites, &external_invites, &link) + { + debug!(%uid, "shared by me: skipping an emptied share"); + continue; + } + let members = members.unwrap_or(0); + let proton_invites = proton_invites.unwrap_or(0); + let external_invites = external_invites.unwrap_or(0); + let link = link.ok().flatten().map(public_link_info); items.push(SharedItem { uid: uid.to_string(), is_dir: n.is_folder(), diff --git a/crates/pdfs-gui/src/app/pages/shared_by_me.rs b/crates/pdfs-gui/src/app/pages/shared_by_me.rs index 225d57c..c5e2eb6 100644 --- a/crates/pdfs-gui/src/app/pages/shared_by_me.rs +++ b/crates/pdfs-gui/src/app/pages/shared_by_me.rs @@ -565,6 +565,7 @@ pub(crate) fn prompt_stop_sharing(ui: &Rc, entries: &[DirEntry]) { let uids: Vec = entries.iter().map(|e| e.uid.clone()).collect(); dialog.connect_response(Some("stop"), move |_, _| { let done = done.clone(); + let stopped = uids.clone(); run_requests( &ui_c, uids.iter() @@ -572,6 +573,7 @@ pub(crate) fn prompt_stop_sharing(ui: &Rc, entries: &[DirEntry]) { .collect(), gettext_noop("Couldn't stop sharing"), move |ui| { + forget_shared_by_me(ui, &stopped); reload_listing(ui); toast(ui, &done); }, @@ -580,6 +582,18 @@ pub(crate) fn prompt_stop_sharing(ui: &Rc, entries: &[DirEntry]) { dialog.present(ui_window(ui).as_ref()); } +/// Take items whose sharing just stopped off the page now, rather than when +/// the reload that confirms it comes back: that reload asks after every share +/// in turn and takes a while with many of them. +fn forget_shared_by_me(ui: &Rc, uids: &[String]) { + let items = { + let mut items = ui.shared_by_me.items.borrow_mut(); + items.retain(|item| !uids.contains(&item.uid)); + items.clone() + }; + repaint_shared_by_me(ui, &items); +} + /// Open a shared item: a folder in My Files, a file the way My Files would. fn open_shared_by_me(ui: &Rc, entry: &DirEntry) { if entry.is_dir { diff --git a/docs/BUGS.md b/docs/BUGS.md index e3ced92..11c4899 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -81,6 +81,7 @@ original wording. | Entry | Title | What is left | |---|---|---| +| [B199](#b199--a-stopped-share-stays-on-the-shared-by-me-page-for-minutes) | A stopped share stays on the Shared by me page for minutes | The share itself is not deleted; the SDK has no call for it | | [B88](#b88--pdfs-pin-rename-move-delete-and-sharing-still-cannot-name-a-path-under-a-secondary-mount) | `pdfs pin`, `rename`, `move`, `delete` and sharing still cannot name a path under a secondary mount | `pin`, `unpin`, opening a file, thumbnails, uploads, sharing and versions | | [B72](#b72--pdfs-trash-never-returns) | `pdfs trash` never returns | The slowness of the first refresh is unmeasured | | [B71](#b71--conflict-sweep-is-not-production-ready-auto-trash-loop-crit-13) | Conflict sweep is not production-ready (auto-trash loop, CRIT-13) | Items 5 to 10 of the entry | @@ -301,6 +302,32 @@ a live account since. Newest first. +### B199 — A stopped share stays on the Shared by me page for minutes + +**Status:** Partly fixed. The share itself is still not deleted. +**Found:** 2026-10-09, reported from the app: after **Stop Sharing**, the row stayed, and an +older share showed with no people and no link. +**Where:** `crates/pdfs-fuse/src/sharing.rs`, `Core::stop_sharing_for_uid` and +`Core::list_shared_by_me`; `crates/pdfs-gui/src/app/pages/shared_by_me.rs`, `prompt_stop_sharing` + +**Repro:** Share a folder with a person, then choose **Stop Sharing…** on the Shared by me page. +The row stays, now with "—" for people and link, until a reload some minutes later. + +**Cause.** The Rust SDK has no call that deletes a share. Stop Sharing removes the public link, the +invitations and the members one by one, and leaves an empty share behind. Drive deletes an empty +share on its own, but only after a while, and until then `GET v2/volumes/{vid}/shares` still lists +it. The JS SDK ends `unshareNode` with `DELETE drive/shares/{shareId}?Force=1` for exactly this +reason. + +**Fix.** The daemon leaves a share with no member, no invitation and no link out of the Shared by +me listing, unless one of those lookups failed. The app also drops a stopped item from the page as +soon as the request succeeds, before the confirming reload. Left: a `delete_share` call in +`proton-drive-rs`, made at the end of `stop_sharing_for_uid`, so the share is really gone. + +**Test:** none automated; the listing needs a live account. + +--- + ### B198 — Switching to online-only deletes local files that never reached Drive **Status:** Fixed (unverified).