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