feat(friends): persist per-friend share preference across launches
The host's "auto-share my code with" picker was session-scoped — an in-memory exclusion set that reset to share-with-all on every launch. Move the preference onto the friend itself: a `share: bool` on `Friend` in friends.toml, `#[serde(default = true)]` so new friends are included and an older file without the field loads as share-with-all. The picker now toggles the stored flag and persists immediately (like the other settings), and `selected_share_targets` filters on it. This drops the parallel `share_excluded` state and is self-cleaning: removing a friend takes their preference with them, no stale ids linger. `upsert`'s update path leaves `share` untouched, so a name/presence refresh can't reset the user's choice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+22
-31
@@ -66,7 +66,7 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy}
|
||||
use winit::raw_window_handle::HasWindowHandle as _;
|
||||
use winit::window::{Window, WindowAttributes, WindowId};
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use self::child::{ChildEvent, ChildProc};
|
||||
use self::presence::{PresenceEvent, PresenceHandle};
|
||||
@@ -645,7 +645,6 @@ pub fn run(relay: Option<String>) -> anyhow::Result<()> {
|
||||
friends,
|
||||
display_name: gui_settings.display_name,
|
||||
met: Vec::new(),
|
||||
share_excluded: BTreeSet::new(),
|
||||
share_status: BTreeMap::new(),
|
||||
notices: Vec::new(),
|
||||
show_notices: false,
|
||||
@@ -713,17 +712,15 @@ fn control_hello(name: &str) -> ControlMsg {
|
||||
}
|
||||
}
|
||||
|
||||
/// The accepted friends a host's share push targets: every accepted friend the
|
||||
/// user hasn't excluded on the host form. A free function (over the store + the
|
||||
/// exclusion set) so the selection rule is unit-testable without a live UI.
|
||||
fn selected_share_targets(
|
||||
friends: &FriendStore,
|
||||
excluded: &BTreeSet<iroh::EndpointId>,
|
||||
) -> Vec<iroh::EndpointId> {
|
||||
/// The accepted friends a host's share push targets: every accepted friend
|
||||
/// whose per-friend `share` flag is on (toggled on the host form, persisted in
|
||||
/// `friends.toml`). A free function over the store so the rule is unit-testable
|
||||
/// without a live UI.
|
||||
fn selected_share_targets(friends: &FriendStore) -> Vec<iroh::EndpointId> {
|
||||
friends
|
||||
.friends
|
||||
.iter()
|
||||
.filter(|f| f.state == FriendState::Accepted && !excluded.contains(&f.id))
|
||||
.filter(|f| f.state == FriendState::Accepted && f.share)
|
||||
.map(|f| f.id)
|
||||
.collect()
|
||||
}
|
||||
@@ -956,11 +953,6 @@ struct PixelPassApp {
|
||||
/// Peers met this session (over a connection) who aren't yet in the friends
|
||||
/// list — drives the "add friend" offer. Session-scoped, not persisted.
|
||||
met: Vec<MetPeer>,
|
||||
/// Accepted friends *excluded* from the host's share push, toggled on the
|
||||
/// host form. Stored as the exclusion (not the inclusion) so the default —
|
||||
/// an empty set — means "share with everyone," and a friend added mid-session
|
||||
/// is included automatically. Session-scoped.
|
||||
share_excluded: BTreeSet<iroh::EndpointId>,
|
||||
/// Delivery state for the current host session's share campaign: a friend is
|
||||
/// present once targeted, `true` once their ACK arrives. Drives the live
|
||||
/// "delivered / retrying" list on the running host screen. Cleared on stop.
|
||||
@@ -1225,7 +1217,7 @@ impl PixelPassApp {
|
||||
let Some(code) = self.host.share_code.clone() else {
|
||||
return;
|
||||
};
|
||||
let targets = selected_share_targets(&self.friends, &self.share_excluded);
|
||||
let targets = selected_share_targets(&self.friends);
|
||||
if targets.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -1984,12 +1976,12 @@ impl PixelPassApp {
|
||||
if self.presence.is_none() {
|
||||
return;
|
||||
}
|
||||
let accepted: Vec<(iroh::EndpointId, String)> = self
|
||||
let accepted: Vec<(iroh::EndpointId, String, bool)> = self
|
||||
.friends
|
||||
.friends
|
||||
.iter()
|
||||
.filter(|f| f.state == FriendState::Accepted)
|
||||
.map(|f| (f.id, f.name.clone()))
|
||||
.map(|f| (f.id, f.name.clone(), f.share))
|
||||
.collect();
|
||||
if accepted.is_empty() {
|
||||
return;
|
||||
@@ -1997,14 +1989,13 @@ impl PixelPassApp {
|
||||
ui.add_space(12.0);
|
||||
ui.separator();
|
||||
ui.label("Auto-share my code with:");
|
||||
for (id, name) in &accepted {
|
||||
let mut on = !self.share_excluded.contains(id);
|
||||
for (id, name, share) in &accepted {
|
||||
let mut on = *share;
|
||||
if ui.checkbox(&mut on, name.as_str()).changed() {
|
||||
if on {
|
||||
self.share_excluded.remove(id);
|
||||
} else {
|
||||
self.share_excluded.insert(*id);
|
||||
if let Some(f) = self.friends.find_mut(id) {
|
||||
f.share = on;
|
||||
}
|
||||
self.save_friends();
|
||||
}
|
||||
}
|
||||
ui.label(
|
||||
@@ -2714,7 +2705,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn share_targets_are_accepted_friends_minus_excluded() {
|
||||
fn share_targets_are_accepted_friends_with_share_on() {
|
||||
let mut friends = FriendStore::default();
|
||||
let alice = id();
|
||||
let bob = id();
|
||||
@@ -2723,15 +2714,15 @@ mod tests {
|
||||
friends.upsert(bob, "Bob".into(), FriendState::Accepted);
|
||||
friends.upsert(pending, "Pat".into(), FriendState::PendingIncoming);
|
||||
|
||||
// No exclusions → every accepted friend, and never a pending one.
|
||||
let all = selected_share_targets(&friends, &BTreeSet::new());
|
||||
// Default (share on for all) → every accepted friend, never a pending one.
|
||||
let all = selected_share_targets(&friends);
|
||||
assert_eq!(all.len(), 2);
|
||||
assert!(all.contains(&alice) && all.contains(&bob));
|
||||
assert!(!all.contains(&pending));
|
||||
|
||||
// Excluding Bob drops only Bob.
|
||||
let excluded: BTreeSet<_> = [bob].into_iter().collect();
|
||||
let some = selected_share_targets(&friends, &excluded);
|
||||
// Turning Bob's share off drops only Bob.
|
||||
friends.find_mut(&bob).unwrap().share = false;
|
||||
let some = selected_share_targets(&friends);
|
||||
assert_eq!(some, vec![alice]);
|
||||
}
|
||||
|
||||
@@ -2739,6 +2730,6 @@ mod tests {
|
||||
fn share_targets_empty_without_accepted_friends() {
|
||||
let mut friends = FriendStore::default();
|
||||
friends.upsert(id(), "Out".into(), FriendState::PendingOutgoing);
|
||||
assert!(selected_share_targets(&friends, &BTreeSet::new()).is_empty());
|
||||
assert!(selected_share_targets(&friends).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user