Skip to main content

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 SAS
132    /// as its word list (`emoji_labels`) + `decimal` and the
133    /// Match/Cancel buttons. huddle 0.9: the glyph form was dropped from
134    /// the UI (emoji-free), so it's no longer carried here.
135    SasCodeReady {
136        room_id: String,
137        partner_fingerprint: String,
138        tx_id: String,
139        emoji_labels: String,
140        decimal: String,
141    },
142    /// Phase G: SAS completed — both sides confirmed the match. The
143    /// partner's fingerprint is now verified (per-room + global).
144    SasVerified {
145        room_id: String,
146        partner_fingerprint: String,
147    },
148    /// Phase F follow-up: 30 seconds passed since we broadcast a
149    /// `CodeJoinRequest` and no `CodeJoinResponse` ever came back. The
150    /// owner either ignored us (bad/expired code), wasn't online, or
151    /// the network dropped our packet. Fired by the timeout task
152    /// spawned in `join_room_with_code` once it confirms our pending
153    /// secret is still sitting in the map.
154    CodeJoinTimedOut { room_id: String, reason: String },
155    /// Phase C follow-up: we dialed a peer via an invite link, the
156    /// peer identified, and the fingerprint they cryptographically
157    /// asserted doesn't match the one the invite claimed. The
158    /// connection has already been dropped. The TUI shows an error
159    /// modal so the user knows the link is forged or stale.
160    InviteFingerprintMismatch {
161        address: String,
162        claimed: String,
163        actual: String,
164    },
165    /// Phase D follow-up: aggregated NAT reachability state derived
166    /// from the AutoNAT probe stream. The app layer maintains a small
167    /// "do any probes say reachable?" tally; this event fires when
168    /// that aggregate changes. The TUI renders it as a badge in the
169    /// lobby header ('reachable' / 'private' / 'detecting').
170    NatStatusChanged { label: String, reachable: bool },
171    /// Phase D follow-up: a successful DCUtR upgrade — a relay-hopped
172    /// connection became direct. The TUI shows a transient status
173    /// line ("direct connection to <peer>"). Fires only on success;
174    /// failures stay in the debug log.
175    DcutrSucceeded { peer_label: String },
176    /// huddle 0.5: a peer announced or cleared their self-declared
177    /// username via a signed `ProfileUpdate`. `username = None` means
178    /// the peer is now `[anonymous]`. TUI consumers redraw the chat
179    /// + member list so the new label flows through.
180    PeerProfileUpdated {
181        fingerprint: String,
182        username: Option<String>,
183    },
184    /// huddle 0.5: the local user's `go_dark` call succeeded — every
185    /// joined room got a best-effort `MemberLeave`, the network task
186    /// shut down, and the data dir was wiped. TUI shows a final
187    /// "Goodbye" modal and exits the process.
188    WentDark,
189    /// huddle 0.7.7: a user-initiated dial (`d` / `a` / paste-invite /
190    /// People-pane reconnect) connected, Identify completed, and we've
191    /// idempotently opened a DM with the freshly-identified peer. The
192    /// TUI listens for this and switches its pane to `Dm(room_id)` so
193    /// the user lands in a chat surface instead of having to hunt for
194    /// a way to message the peer they just dialed.
195    ///
196    /// Auto-reconnects on startup do NOT fire this — we consume the
197    /// address from `pending_auto_dm_addrs` and only the user-initiated
198    /// paths register there in the first place.
199    AutoOpenDm {
200        room_id: String,
201        fingerprint: String,
202    },
203    /// huddle 1.0: a signed `ContactRequest` arrived on our relay inbox
204    /// from a peer we haven't added yet (the "add by HD-ID over the
205    /// internet" path). The TUI surfaces it in the Contacts pane's Requests
206    /// section to accept (opens a DM) or decline. Mutual requests — where we
207    /// already added the sender — auto-connect without firing this.
208    ContactRequestReceived {
209        fingerprint: String,
210        display_name: Option<String>,
211        note: Option<String>,
212    },
213}