feat: browse your own shares as a peer receives them

The window could show any peer's share tree but never this account's, so
"am I actually sharing that?" had no answer short of asking a friend to
browse you. The tree is already there; what was missing was feeding it
from our own index.

B opens it — the same collapsible tree, filter, and navigation the peer
browse offers, built from `Shares::directories()`, which is the listing
GetShareFileList answers with. So what it shows is what the network sees,
not what the config meant to share. r re-scans the disk rather than
re-asking a peer, o steps across to the folders that feed it, and nothing
here queues a download from ourselves.

Attached to a daemon the index lives on the daemon's host, so it comes
back over a new `shares.files` method rather than being scanned locally;
`shares files [--filter]` prints the same listing headless, through the
`emit_listing` that `browse` now shares with it — the two are only
comparable while they are built the same way.

Two allocation fixes came out of the review, both on paths this reaches:
the daemon hands its listing to the DTO instead of deep-copying one it
already owns, and `Shares::directories()` allocates its map key once per
folder rather than once per file, which the peer-facing serve path has
been paying for all along.
This commit is contained in:
Michel de Graaf
2026-09-10 11:44:57 +02:00
parent d6d870b39f
commit 26b551d997
24 changed files with 606 additions and 57 deletions
+74 -1
View File
@@ -332,6 +332,9 @@ pub enum BrowseStatus {
/// UI state for browsing one user.
pub struct BrowseState {
pub username: String,
/// This session's own share index rather than a peer's: filled from the
/// local index, re-indexed rather than re-requested, and not downloadable.
pub own: bool,
pub status: BrowseStatus,
pub tree: Vec<BrowseNode>,
expanded: HashSet<String>,
@@ -356,6 +359,7 @@ impl BrowseState {
pub fn loading(username: String) -> Self {
Self {
username,
own: false,
status: BrowseStatus::Loading,
tree: Vec::new(),
expanded: HashSet::new(),
@@ -370,6 +374,15 @@ impl BrowseState {
}
}
/// An empty view of this session's own shares, waiting for the index.
#[must_use]
pub fn loading_own(username: String) -> Self {
Self {
own: true,
..Self::loading(username)
}
}
/// Populate from a received listing: build the tree, auto-expand the top
/// level so structure is visible, and set Loaded/Empty.
pub fn load(&mut self, directories: &[SharedDirectory]) {
@@ -528,7 +541,10 @@ impl BrowseTabs {
/// `true` if the caller should (re)issue a browse request — i.e. the tab is
/// new or was retried after a timeout.
pub fn open(&mut self, username: &str) -> bool {
if let Some(idx) = self.tabs.iter().position(|t| t.username == username)
if let Some(idx) = self
.tabs
.iter()
.position(|t| !t.own && t.username == username)
{
self.active = idx;
// Re-request only if the previous attempt gave up.
@@ -545,6 +561,40 @@ impl BrowseTabs {
}
}
/// Where this session's own share tab sits, if it is open.
#[must_use]
pub fn own_index(&self) -> Option<usize> {
self.tabs.iter().position(|tab| tab.own)
}
/// Focus the tab showing this session's own shares, opening it if it is
/// not there yet. Returns `true` when it still needs its listing.
pub fn open_own(&mut self, username: &str) -> bool {
if let Some(idx) = self.own_index() {
self.active = idx;
return false;
}
self.tabs
.push(BrowseState::loading_own(username.to_string()));
self.active = self.tabs.len() - 1;
true
}
/// Whether the focused tab is this session's own share index.
#[must_use]
pub fn active_is_own(&self) -> bool {
self.active_tab().is_some_and(|tab| tab.own)
}
/// Who the active tab's files would be downloaded from, if anyone. The
/// own tab names nobody: its files are already on this disk.
#[must_use]
pub fn download_target(&self) -> Option<&str> {
self.active_tab()
.filter(|tab| !tab.own)
.map(|tab| tab.username.as_str())
}
/// Retry the active tab if it timed out: reset it to loading and return the
/// username so the caller can re-issue the request. Returns `None` if the
/// active tab isn't in a retryable (timed-out) state.
@@ -740,6 +790,29 @@ mod tests {
assert!(tabs.is_empty());
}
#[test]
fn the_own_tab_is_opened_once_and_is_not_a_peer_of_the_same_name() {
let mut tabs = BrowseTabs::new();
// The first open needs a listing; focusing it again does not.
assert!(tabs.open_own("tester"));
assert!(!tabs.open_own("tester"));
assert_eq!(tabs.tabs.len(), 1);
assert_eq!(tabs.own_index(), Some(0));
assert!(tabs.active_tab().expect("tab").own);
// Browsing the account's own name over the network is a separate
// tab: the own tab holds the local index, not a peer's answer.
assert!(tabs.open("tester"));
assert_eq!(tabs.tabs.len(), 2);
assert!(!tabs.active_tab().expect("tab").own);
assert_eq!(tabs.own_index(), Some(0));
// The peer of that name is downloadable; we are not.
assert_eq!(tabs.download_target(), Some("tester"));
tabs.active = 0;
assert_eq!(tabs.download_target(), None);
}
#[test]
fn browse_tabs_reopen_after_timeout_requests_again() {
let mut tabs = BrowseTabs::new();