huddle_core/app/events.rs
1use libp2p::PeerId;
2
3#[derive(Debug, Clone)]
4pub struct DiscoveredRoom {
5 pub room_id: String,
6 pub name: String,
7 pub encrypted: bool,
8 pub member_count: u32,
9 pub creator_fingerprint: String,
10 pub last_seen: i64,
11 /// True for rooms loaded from local storage that we haven't rejoined
12 /// yet this session (encrypted rooms whose passphrase key isn't in
13 /// memory). The lobby renders these with a "saved" hint; pressing
14 /// Enter goes through the join flow with passphrase prompt.
15 pub restorable: bool,
16 /// huddle 0.5.1: cached host multiaddrs from the announcing peer's
17 /// `RoomAnnouncement.host_addrs`. Used by `dial_by_id_or_username`
18 /// to resolve an HD- ID or username back to a dialable address
19 /// when the target is on our gossipsub mesh.
20 pub host_addrs: Vec<String>,
21}
22
23#[derive(Debug, Clone)]
24pub enum AppEvent {
25 /// A room was discovered (announced on the global topic).
26 RoomDiscovered(DiscoveredRoom),
27 /// A previously-discovered room hasn't been re-announced — TTL expired.
28 RoomLost { room_id: String },
29 /// We successfully joined a room (subscribed to its topic).
30 RoomJoined { room_id: String },
31 /// We left a room.
32 RoomLeft { room_id: String },
33 /// A new member appeared in a room we're in.
34 MemberJoined {
35 room_id: String,
36 fingerprint: String,
37 },
38 /// A member left a room we're in.
39 MemberLeft {
40 room_id: String,
41 fingerprint: String,
42 },
43 /// A message arrived in a room.
44 MessageReceived {
45 room_id: String,
46 sender_fingerprint: String,
47 body: String,
48 sent_at: i64,
49 },
50 /// Our own message was sent successfully.
51 MessageSent {
52 room_id: String,
53 body: String,
54 message_id: i64,
55 },
56 /// Listening on a network address.
57 ListeningOn { address: String },
58 /// A peer was discovered on the LAN.
59 PeerDiscovered { peer_id: PeerId },
60 /// A peer's mDNS presence expired — they left the LAN or stopped
61 /// announcing. The lobby refreshes its online/offline indicators.
62 PeerExpired { peer_id: PeerId },
63 /// We've fired a dial command — useful for the UI to show "dialing...".
64 Dialing { address: String },
65 /// A user-initiated dial completed successfully.
66 DialSucceeded { address: String, peer_id: PeerId },
67 /// A user-initiated dial failed.
68 DialFailed { address: String, error: String },
69 /// Non-fatal error.
70 Error { description: String },
71 /// Someone (us or a peer) offered a file in a room.
72 FileOffered {
73 room_id: String,
74 file_id: String,
75 name: String,
76 size_bytes: u64,
77 sender_fingerprint: String,
78 },
79 /// A chunk of an incoming transfer arrived. `total_bytes` is the
80 /// announced size from the offer.
81 FileProgress {
82 file_id: String,
83 bytes_received: u64,
84 total_bytes: u64,
85 },
86 /// All chunks of a transfer received and SHA-256 verified.
87 FileReady { file_id: String },
88 /// User saved a ready file to Downloads.
89 FileSaved { file_id: String, path: String },
90 /// A transfer failed (hash mismatch, decrypt error, IO error).
91 FileFailed { file_id: String, reason: String },
92 /// A peer initiated a key rotation in a room we're in. The UI
93 /// surfaces a modal asking the user to enter the new passphrase.
94 RotationRequested {
95 room_id: String,
96 rotator_fingerprint: String,
97 new_salt: Vec<u8>,
98 },
99 /// Someone in a room started typing. The UI re-reads typing peers
100 /// from `AppHandle::typers_in_room` on each render; the event is
101 /// just a nudge.
102 TypingChanged { room_id: String },
103 /// A received message included our fingerprint (full or short
104 /// form). The TUI uses this to ring the terminal bell, even in
105 /// muted rooms.
106 MentionReceived { room_id: String, body: String },
107 /// Phase A: an unknown peer has dialed us and Identify has
108 /// completed. The TUI shows an accept/reject/trust modal with the
109 /// peer's short fingerprint. Routed through `replace_modal_if_idle`
110 /// so it doesn't clobber whatever the user is typing.
111 InboundDial {
112 peer_id: PeerId,
113 /// 24-char fingerprint, freshly derived from the peer's Ed25519
114 /// pubkey via Identify — proves they hold the matching key.
115 fingerprint: String,
116 /// String form of the listener-side multiaddr (the address as
117 /// seen from our side of the connection). Mostly informational
118 /// for the user; we persist it on accept so the lobby online
119 /// dot tracks the peer.
120 address: String,
121 },
122 /// Phase G: SAS code is ready on both sides — both ephemeral
123 /// X25519 pubkeys exchanged + ECDH derived. The TUI shows the
124 /// `code` (emoji + decimal) and the Match/Cancel buttons.
125 SasCodeReady {
126 room_id: String,
127 partner_fingerprint: String,
128 tx_id: String,
129 emoji_string: String,
130 emoji_labels: String,
131 decimal: String,
132 },
133 /// Phase G: SAS completed — both sides confirmed the match. The
134 /// partner's fingerprint is now verified (per-room + global).
135 SasVerified {
136 room_id: String,
137 partner_fingerprint: String,
138 },
139 /// Phase F follow-up: 30 seconds passed since we broadcast a
140 /// `CodeJoinRequest` and no `CodeJoinResponse` ever came back. The
141 /// owner either ignored us (bad/expired code), wasn't online, or
142 /// the network dropped our packet. Fired by the timeout task
143 /// spawned in `join_room_with_code` once it confirms our pending
144 /// secret is still sitting in the map.
145 CodeJoinTimedOut { room_id: String, reason: String },
146 /// Phase C follow-up: we dialed a peer via an invite link, the
147 /// peer identified, and the fingerprint they cryptographically
148 /// asserted doesn't match the one the invite claimed. The
149 /// connection has already been dropped. The TUI shows an error
150 /// modal so the user knows the link is forged or stale.
151 InviteFingerprintMismatch {
152 address: String,
153 claimed: String,
154 actual: String,
155 },
156 /// Phase D follow-up: aggregated NAT reachability state derived
157 /// from the AutoNAT probe stream. The app layer maintains a small
158 /// "do any probes say reachable?" tally; this event fires when
159 /// that aggregate changes. The TUI renders it as a badge in the
160 /// lobby header ('reachable' / 'private' / 'detecting').
161 NatStatusChanged { label: String, reachable: bool },
162 /// Phase D follow-up: a successful DCUtR upgrade — a relay-hopped
163 /// connection became direct. The TUI shows a transient status
164 /// line ("direct connection to <peer>"). Fires only on success;
165 /// failures stay in the debug log.
166 DcutrSucceeded { peer_label: String },
167 /// huddle 0.5: a peer announced or cleared their self-declared
168 /// username via a signed `ProfileUpdate`. `username = None` means
169 /// the peer is now `[anonymous]`. TUI consumers redraw the chat
170 /// + member list so the new label flows through.
171 PeerProfileUpdated {
172 fingerprint: String,
173 username: Option<String>,
174 },
175 /// huddle 0.5: the local user's `go_dark` call succeeded — every
176 /// joined room got a best-effort `MemberLeave`, the network task
177 /// shut down, and the data dir was wiped. TUI shows a final
178 /// "Goodbye" modal and exits the process.
179 WentDark,
180}