Skip to main content

iris_chat_core/
state.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
4pub enum Screen {
5    Welcome,
6    CreateAccount,
7    RestoreAccount,
8    AddDevice,
9    ChatList,
10    NewChat,
11    NewGroup,
12    CreateInvite,
13    JoinInvite,
14    Settings,
15    Chat { chat_id: String },
16    GroupDetails { group_id: String },
17    DeviceRoster,
18    AwaitingDeviceApproval,
19    DeviceRevoked,
20}
21
22#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
23pub struct Router {
24    pub default_screen: Screen,
25    pub screen_stack: Vec<Screen>,
26}
27
28#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
29pub struct BusyState {
30    pub creating_account: bool,
31    pub restoring_session: bool,
32    pub linking_device: bool,
33    pub creating_chat: bool,
34    pub creating_group: bool,
35    pub sending_message: bool,
36    pub updating_roster: bool,
37    pub updating_group: bool,
38    pub creating_invite: bool,
39    pub accepting_invite: bool,
40    pub syncing_network: bool,
41    pub uploading_attachment: bool,
42}
43
44#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
45pub struct PreferencesSnapshot {
46    pub send_typing_indicators: bool,
47    pub send_read_receipts: bool,
48    pub desktop_notifications_enabled: bool,
49    pub invite_acceptance_notifications_enabled: bool,
50    pub startup_at_login_enabled: bool,
51    pub nearby_bluetooth_enabled: bool,
52    pub nearby_lan_enabled: bool,
53    pub nostr_relay_urls: Vec<String>,
54    pub image_proxy_enabled: bool,
55    pub image_proxy_url: String,
56    pub image_proxy_key_hex: String,
57    pub image_proxy_salt_hex: String,
58    pub muted_chat_ids: Vec<String>,
59    pub pinned_chat_ids: Vec<String>,
60    pub debug_logging_enabled: bool,
61    pub accept_unknown_direct_messages: bool,
62    /// User-configurable notification server URL. Empty string means
63    /// "use the platform default" (notifications.iris.to in release,
64    /// notifications-sandbox.iris.to in debug). When non-empty, the
65    /// shells should pass this as the override to
66    /// `build_mobile_push_*_subscription_request`.
67    pub mobile_push_server_url: String,
68}
69
70impl Default for PreferencesSnapshot {
71    fn default() -> Self {
72        Self {
73            send_typing_indicators: false,
74            send_read_receipts: true,
75            desktop_notifications_enabled: true,
76            invite_acceptance_notifications_enabled: true,
77            startup_at_login_enabled: true,
78            nearby_bluetooth_enabled: false,
79            nearby_lan_enabled: false,
80            nostr_relay_urls: crate::core::configured_relays(),
81            image_proxy_enabled: true,
82            image_proxy_url: crate::image_proxy::DEFAULT_IMAGE_PROXY_URL.to_string(),
83            image_proxy_key_hex: crate::image_proxy::DEFAULT_IMAGE_PROXY_KEY_HEX.to_string(),
84            image_proxy_salt_hex: crate::image_proxy::DEFAULT_IMAGE_PROXY_SALT_HEX.to_string(),
85            muted_chat_ids: Vec::new(),
86            pinned_chat_ids: Vec::new(),
87            debug_logging_enabled: false,
88            accept_unknown_direct_messages: true,
89            mobile_push_server_url: String::new(),
90        }
91    }
92}
93
94#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
95pub struct OutgoingAttachment {
96    pub file_path: String,
97    pub filename: String,
98}
99
100#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
101pub struct AttachmentDownloadResult {
102    pub data_base64: Option<String>,
103    pub error: Option<String>,
104}
105
106#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
107pub enum DeviceAuthorizationState {
108    Authorized,
109    AwaitingApproval,
110    Revoked,
111}
112
113#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
114pub struct AccountSnapshot {
115    pub public_key_hex: String,
116    pub npub: String,
117    pub display_name: String,
118    pub picture_url: Option<String>,
119    pub device_public_key_hex: String,
120    pub device_npub: String,
121    pub has_owner_signing_authority: bool,
122    pub authorization_state: DeviceAuthorizationState,
123}
124
125#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
126pub struct DeviceEntrySnapshot {
127    pub device_pubkey_hex: String,
128    pub device_npub: String,
129    pub is_current_device: bool,
130    pub is_authorized: bool,
131    pub is_stale: bool,
132    pub added_at_secs: Option<u64>,
133}
134
135#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
136pub struct DeviceRosterSnapshot {
137    pub owner_public_key_hex: String,
138    pub owner_npub: String,
139    pub current_device_public_key_hex: String,
140    pub current_device_npub: String,
141    pub can_manage_devices: bool,
142    pub authorization_state: DeviceAuthorizationState,
143    pub devices: Vec<DeviceEntrySnapshot>,
144}
145
146#[derive(uniffi::Enum, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
147pub enum DeliveryState {
148    Queued,
149    Pending,
150    Sent,
151    Received,
152    Seen,
153    Failed,
154}
155
156#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
157pub enum ChatKind {
158    Direct,
159    Group,
160}
161
162#[derive(uniffi::Enum, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
163pub enum ChatMessageKind {
164    User,
165    System,
166}
167
168#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
169pub struct MessageAttachmentSnapshot {
170    pub nhash: String,
171    pub filename: String,
172    pub filename_encoded: String,
173    pub htree_url: String,
174    pub is_image: bool,
175    pub is_video: bool,
176    pub is_audio: bool,
177}
178
179#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
180pub struct MessageReactionSnapshot {
181    pub emoji: String,
182    pub count: u64,
183    pub reacted_by_me: bool,
184}
185
186#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
187pub struct MessageReactor {
188    /// Hex-encoded pubkey of the user who reacted.
189    pub author: String,
190    /// Emoji content of their current (latest) reaction. Empty means unreacted.
191    pub emoji: String,
192}
193
194#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
195pub struct MessageRecipientDeliverySnapshot {
196    /// Hex-encoded owner/user pubkey.
197    pub owner_pubkey_hex: String,
198    pub delivery: DeliveryState,
199    pub updated_at_secs: u64,
200}
201
202#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
203#[serde(default)]
204pub struct MessageDeliveryTraceSnapshot {
205    pub outer_event_ids: Vec<String>,
206    pub pending_relay_event_ids: Vec<String>,
207    pub queued_protocol_targets: Vec<String>,
208    pub target_device_ids: Vec<String>,
209    pub transport_channels: Vec<String>,
210    pub last_transport_error: Option<String>,
211}
212
213#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
214pub struct ChatMessageSnapshot {
215    pub id: String,
216    pub chat_id: String,
217    pub kind: ChatMessageKind,
218    pub author: String,
219    pub body: String,
220    pub attachments: Vec<MessageAttachmentSnapshot>,
221    pub reactions: Vec<MessageReactionSnapshot>,
222    pub reactors: Vec<MessageReactor>,
223    pub is_outgoing: bool,
224    pub created_at_secs: u64,
225    pub expires_at_secs: Option<u64>,
226    pub delivery: DeliveryState,
227    pub recipient_deliveries: Vec<MessageRecipientDeliverySnapshot>,
228    pub delivery_trace: MessageDeliveryTraceSnapshot,
229    /// Hex ID of the outer relay event that carried this rumor. The
230    /// notification extension joins on this to find a body the
231    /// foreground app already decrypted, so it can render a real
232    /// preview instead of "New activity". `None` for messages that
233    /// didn't come over the wire (system notices, locally-composed
234    /// outgoing rumors).
235    pub source_event_id: Option<String>,
236}
237
238#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
239pub struct TypingIndicatorSnapshot {
240    pub chat_id: String,
241    pub display_name: String,
242    pub expires_at_secs: u64,
243}
244
245#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
246pub struct ChatThreadSnapshot {
247    pub chat_id: String,
248    pub kind: ChatKind,
249    pub display_name: String,
250    pub subtitle: Option<String>,
251    pub picture_url: Option<String>,
252    pub member_count: u64,
253    pub last_message_preview: Option<String>,
254    pub last_message_at_secs: Option<u64>,
255    pub last_message_is_outgoing: Option<bool>,
256    pub last_message_delivery: Option<DeliveryState>,
257    pub unread_count: u64,
258    pub is_typing: bool,
259    pub is_muted: bool,
260    pub is_pinned: bool,
261    /// Unsent composer text the user typed in this thread, persisted
262    /// across navigation / suspend / relaunch. Chat list rows render
263    /// "Draft: …" in place of the last message preview when this is
264    /// non-empty (Signal pattern).
265    pub draft: String,
266}
267
268#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
269pub struct CurrentChatSnapshot {
270    pub chat_id: String,
271    pub kind: ChatKind,
272    pub display_name: String,
273    pub subtitle: Option<String>,
274    pub picture_url: Option<String>,
275    pub group_id: Option<String>,
276    pub member_count: u64,
277    pub message_ttl_seconds: Option<u64>,
278    pub is_muted: bool,
279    pub messages: Vec<ChatMessageSnapshot>,
280    pub typing_indicators: Vec<TypingIndicatorSnapshot>,
281    /// Same persisted draft text exposed on `ChatThreadSnapshot`. The
282    /// chat screen pre-fills its composer with this on first appear.
283    pub draft: String,
284}
285
286#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
287pub struct GroupMemberSnapshot {
288    pub owner_pubkey_hex: String,
289    pub display_name: String,
290    pub npub: String,
291    pub is_admin: bool,
292    pub is_creator: bool,
293    pub is_local_owner: bool,
294}
295
296#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
297pub struct GroupDetailsSnapshot {
298    pub group_id: String,
299    pub name: String,
300    pub picture_url: Option<String>,
301    pub created_by_display_name: String,
302    pub created_by_npub: String,
303    pub can_manage: bool,
304    pub is_muted: bool,
305    pub revision: u64,
306    pub members: Vec<GroupMemberSnapshot>,
307}
308
309#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
310pub struct RelayConnectionSnapshot {
311    pub url: String,
312    pub status: String,
313}
314
315#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
316pub struct NetworkStatusSnapshot {
317    pub relay_set_id: String,
318    pub relay_urls: Vec<String>,
319    pub relay_connections: Vec<RelayConnectionSnapshot>,
320    pub connected_relay_count: u64,
321    pub all_relays_offline_since_secs: Option<u64>,
322    pub syncing: bool,
323    pub pending_outbound_count: u64,
324    pub pending_group_control_count: u64,
325    pub recent_event_count: u64,
326    pub recent_log_count: u64,
327    pub last_debug_category: Option<String>,
328    pub last_debug_detail: Option<String>,
329}
330
331#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
332pub struct MobilePushSessionSnapshot {
333    pub recipient_pubkey_hex: String,
334    pub display_name: String,
335    pub state_json: String,
336    pub tracked_sender_pubkeys: Vec<String>,
337    pub has_receiving_capability: bool,
338}
339
340#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
341pub struct MobilePushSyncSnapshot {
342    pub owner_pubkey_hex: Option<String>,
343    pub message_author_pubkeys: Vec<String>,
344    pub invite_response_pubkeys: Vec<String>,
345    pub sessions: Vec<MobilePushSessionSnapshot>,
346}
347
348#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
349pub struct PeerProfileDebugSnapshot {
350    pub owner_pubkey_hex: String,
351    pub owner_npub: String,
352    pub roster_device_count: u64,
353    pub known_device_count: u64,
354    pub active_session_count: u64,
355    pub session_count: u64,
356    pub receiving_session_count: u64,
357    pub tracked_sender_count: u64,
358    pub recent_handshake_device_count: u64,
359    pub last_handshake_at_secs: Option<u64>,
360    pub tracked_for_messages: bool,
361}
362
363#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
364pub struct MobilePushNotificationResolution {
365    pub should_show: bool,
366    pub title: String,
367    pub body: String,
368    pub payload_json: String,
369}
370
371#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
372pub struct MobilePushSubscriptionRequest {
373    pub method: String,
374    pub url: String,
375    pub authorization_header: String,
376    pub body_json: Option<String>,
377}
378
379/// One row inside the "Messages" section of a search result. Shells
380/// render this as a single conversation list row whose subtitle is the
381/// matched body and whose title is the chat's display name (resolved
382/// here so the UI doesn't have to look up the parent thread).
383#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
384pub struct MessageSearchHit {
385    pub chat_id: String,
386    pub message_id: String,
387    pub chat_display_name: String,
388    pub chat_picture_url: Option<String>,
389    pub chat_kind: ChatKind,
390    pub author_pubkey: String,
391    pub body: String,
392    pub is_outgoing: bool,
393    pub created_at_secs: u64,
394}
395
396#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
397pub struct SearchResultSnapshot {
398    pub query: String,
399    pub scope_chat_id: Option<String>,
400    pub contacts: Vec<ChatThreadSnapshot>,
401    pub groups: Vec<ChatThreadSnapshot>,
402    pub messages: Vec<MessageSearchHit>,
403    /// Inline shortcut row to show above the grouped sections when the
404    /// query is itself an npub / nprofile / hex pubkey / invite URL.
405    /// Shells render this as a single "Start chat with …" / "Accept
406    /// invite from …" row that dispatches the carried action on tap.
407    pub shortcut: Option<ChatInputShortcut>,
408}
409
410/// Classification of the free-text input typed into a chat-action
411/// field — the search bar, the "New chat" paste field, the share-link
412/// handler. Centralising the parsing here means the UI just looks at
413/// the enum and dispatches; no platform has its own ad-hoc
414/// `contains("://") && contains("#")` check.
415#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
416pub enum ChatInputShortcut {
417    /// Input is a recognized owner pubkey (npub / nprofile / hex).
418    /// `display` is the user-presentable short form (npub…).
419    DirectPeer {
420        peer_input: String,
421        display: String,
422        npub: String,
423        pubkey_hex: String,
424    },
425    /// Input is a paste of an invite URL. `display` is a short label
426    /// suitable for "Accept invite from …" rows.
427    Invite {
428        invite_input: String,
429        display: String,
430    },
431}
432
433impl SearchResultSnapshot {
434    pub fn empty(query: String, scope_chat_id: Option<String>) -> Self {
435        Self {
436            query,
437            scope_chat_id,
438            contacts: Vec::new(),
439            groups: Vec::new(),
440            messages: Vec::new(),
441            shortcut: None,
442        }
443    }
444
445    pub fn is_empty(&self) -> bool {
446        self.contacts.is_empty()
447            && self.groups.is_empty()
448            && self.messages.is_empty()
449            && self.shortcut.is_none()
450    }
451}
452
453#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
454pub struct PublicInviteSnapshot {
455    pub url: String,
456}
457
458#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
459pub struct LinkDeviceSnapshot {
460    pub url: String,
461    pub device_input: String,
462}
463
464#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
465pub struct AppState {
466    pub rev: u64,
467    pub router: Router,
468    pub account: Option<AccountSnapshot>,
469    pub device_roster: Option<DeviceRosterSnapshot>,
470    pub busy: BusyState,
471    pub chat_list: Vec<ChatThreadSnapshot>,
472    pub current_chat: Option<CurrentChatSnapshot>,
473    pub group_details: Option<GroupDetailsSnapshot>,
474    pub public_invite: Option<PublicInviteSnapshot>,
475    pub link_device: Option<LinkDeviceSnapshot>,
476    pub network_status: Option<NetworkStatusSnapshot>,
477    pub mobile_push: MobilePushSyncSnapshot,
478    pub preferences: PreferencesSnapshot,
479    pub toast: Option<String>,
480}
481
482impl AppState {
483    pub fn empty() -> Self {
484        Self {
485            rev: 0,
486            router: Router {
487                default_screen: Screen::Welcome,
488                screen_stack: Vec::new(),
489            },
490            account: None,
491            device_roster: None,
492            busy: BusyState::default(),
493            chat_list: Vec::new(),
494            current_chat: None,
495            group_details: None,
496            public_invite: None,
497            link_device: None,
498            network_status: None,
499            mobile_push: MobilePushSyncSnapshot::default(),
500            preferences: PreferencesSnapshot::default(),
501            toast: None,
502        }
503    }
504}