feat(avatars): 6 selectable preset avatars over presence — W4 Phase 2

Adds bundled preset avatars on top of the monogram foundation.

- New `Avatar` enum (Monogram | Preset(u8)) in the avatar module, serde-encoded;
  6 preset PNGs embedded via include_bytes! (placeholder art in assets/avatars/
  — swap for real designs later). +3 unit tests.
- `PeerState` gains `avatar` (rides the gossip presence plane like `sharing`);
  `AppConfig` gains `avatar` (persisted). Avatar choice flows app → core (Join +
  new SetAvatar command) → every self-state announce, so it reaches the room incl.
  late joiners, and changing it mid-call re-announces live.
- Settings "Avatar" section: monogram + 6 preset tiles, applied live + persisted.
- Rendering: `avatar_view` draws the chosen preset image (iced `image` feature,
  now enabled) else the monogram, in the self card, peer rows, and chat (chat
  looks up the sender's avatar from presence by id).

⚠️ BREAKING gossip wire change: presence Announce is signed (S2) and the
signature is recomputed by re-serializing the parsed struct, so a new PeerState
field means old and new builds can't verify each other's presence — ALL peers
must run a build >= this one (same as the S2 change). Redeploy dopedart before
2-machine testing.

Build + clippy clean, 222 lib + integration tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 16:43:24 -04:00
co-authored by Claude Opus 4.8
parent 19194f0cee
commit 3007002b1b
16 changed files with 881 additions and 27 deletions
+6
View File
@@ -34,6 +34,11 @@ pub struct PeerState {
/// older configs / peers that predate the field still deserialize.
#[serde(default)]
pub sharing: Option<String>,
/// This peer's chosen avatar (W4). Rides presence so it reaches everyone
/// (incl. late joiners) with no server, like `sharing`. Defaulted so older
/// peers/configs that predate the field still deserialize (→ monogram).
#[serde(default)]
pub avatar: crate::avatar::Avatar,
}
#[derive(Debug, Clone)]
@@ -180,6 +185,7 @@ mod tests {
is_muted: false,
addr,
sharing: None,
avatar: crate::avatar::Avatar::default(),
}
}