feat(friends): friend store, mutual handshake, and Friends UI (phase 3)
Build the friends feature on top of the phase-2 control plane: you can now befriend someone you've connected with and manage a contacts list. - common/friends.rs: a persisted FriendStore in its own friends.toml (kept out of config.toml so a headless --reconfigure can't clobber it, same as identity.key). Friends are keyed by stable control EndpointId; state is PendingOutgoing / PendingIncoming / Accepted. The handshake transitions (on_friend_request → mutual-match detection, on_friend_ accept) are pure and unit-tested. - gui/code.rs: the bootstrap. The GUI host wraps its share code as `pixelpassF1:<control-id>.<ticket>` so a viewer learns the host's stable id; unwrap is lenient, so a bare/CLI ticket still works (no friend offer). The video/streaming path is untouched. - presence service gains an outbound path (unbounded channel → per-msg send tasks) and exposes our control id for wrapping codes. - gui wiring: on connect, the viewer announces itself to the host with a Hello (carrying our display name); the host replies once, so both ends learn each other and an "Add friend" offer appears on the running host/view screens. Incoming requests/accepts/declines fold into the store with desktop notifications. New Friends screen (accept/decline/ remove, edit your display name, see your id) reachable from the menu, which shows a pending-request count. New [gui] display_name setting, seeded from $USER. Verified: friends store + handshake transitions covered by unit tests (7); code wrap/unwrap round-trips (4); the control loopback still passes; the live GUI starts clean with the presence endpoint online. fmt + clippy clean on both features; 41 gui + 8 headless tests pass. The full two-party UX (connect → mutual add → persisted) wants a cross-machine manual check, as usual. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,10 @@ pub struct GuiSettings {
|
||||
/// `~/.config/pixelpass/themes/`). Defaults to the built-in Default Dark.
|
||||
#[serde(default = "default_theme")]
|
||||
pub theme: String,
|
||||
/// The display name shown to friends (in requests and shared codes).
|
||||
/// Seeded from the login name; editable in Settings.
|
||||
#[serde(default = "default_display_name")]
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
impl Default for GuiSettings {
|
||||
@@ -44,6 +48,7 @@ impl Default for GuiSettings {
|
||||
close_to_tray: false,
|
||||
show_qr: true,
|
||||
theme: default_theme(),
|
||||
display_name: default_display_name(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +61,15 @@ fn default_theme() -> String {
|
||||
"Default Dark".to_string()
|
||||
}
|
||||
|
||||
/// Seed the friends display name from the login name, falling back to a
|
||||
/// generic label when `$USER` isn't set.
|
||||
fn default_display_name() -> String {
|
||||
std::env::var("USER")
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| "PixelPass user".to_string())
|
||||
}
|
||||
|
||||
/// Result of the first-run upstream measurement.
|
||||
///
|
||||
/// `status = "unmeasured"` means we've never asked the user — show the
|
||||
|
||||
Reference in New Issue
Block a user