Skip to main content

iris_chat_core/
state.rs

1use crate::DirectFileTransferSnapshot;
2use serde::{Deserialize, Serialize};
3
4#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
5pub enum Screen {
6    Welcome,
7    CreateAccount,
8    RestoreAccount,
9    RemoteSigner,
10    AddDevice,
11    ChatList,
12    NewChat,
13    NewGroup,
14    CreateInvite,
15    JoinInvite,
16    Settings,
17    Chat { chat_id: String },
18    DirectChatInfo { chat_id: String },
19    GroupDetails { group_id: String },
20    DeviceRoster,
21    DeviceRevoked,
22}
23
24#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
25pub struct Router {
26    pub default_screen: Screen,
27    pub screen_stack: Vec<Screen>,
28}
29
30#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
31pub struct BusyState {
32    pub creating_account: bool,
33    pub restoring_session: bool,
34    pub linking_device: bool,
35    pub creating_chat: bool,
36    pub creating_group: bool,
37    pub sending_message: bool,
38    pub updating_roster: bool,
39    pub updating_group: bool,
40    pub creating_invite: bool,
41    pub accepting_invite: bool,
42    pub syncing_network: bool,
43    pub uploading_attachment: bool,
44    pub upload_progress: Option<UploadProgress>,
45}
46
47#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
48pub struct UploadProgress {
49    pub bytes_uploaded: u64,
50    pub total_bytes: u64,
51}
52
53#[derive(
54    uniffi::Record, Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
55)]
56pub struct ChatMuteDeadline {
57    pub chat_id: String,
58    pub until_secs: u64,
59}
60
61#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
62pub struct PreferencesSnapshot {
63    pub voice_calls_enabled: bool,
64    pub video_calls_enabled: bool,
65    pub call_quality: String,
66    pub call_max_bitrate_bps: u32,
67    pub send_typing_indicators: bool,
68    pub send_read_receipts: bool,
69    #[uniffi(default = true)]
70    pub allow_message_deletion_by_others: bool,
71    /// Reversibly hide saved group messages written by blocked people.
72    #[uniffi(default = false)]
73    pub hide_blocked_group_messages: bool,
74    pub desktop_notifications_enabled: bool,
75    pub invite_acceptance_notifications_enabled: bool,
76    pub startup_at_login_enabled: bool,
77    pub nearby_enabled: bool,
78    pub nearby_bluetooth_enabled: bool,
79    pub nearby_lan_enabled: bool,
80    pub nearby_show_in_chat_list: bool,
81    /// Whether the on-device nearby mailbag actively reads and writes
82    /// store-and-forward records. When off, the local bag is left
83    /// alone (so contents survive a toggle off → on cycle) but no new
84    /// events are stored and the broadcast/replay path skips it.
85    pub nearby_mailbag_enabled: bool,
86    pub nostr_relay_urls: Vec<String>,
87    pub image_proxy_enabled: bool,
88    pub image_proxy_fallback_enabled: bool,
89    pub image_proxy_url: String,
90    pub image_proxy_key_hex: String,
91    pub image_proxy_salt_hex: String,
92    pub muted_chat_ids: Vec<String>,
93    pub timed_chat_mutes: Vec<ChatMuteDeadline>,
94    /// Muted groups where even identity-bearing @mentions stay silent.
95    #[uniffi(default = [])]
96    pub silent_mention_chat_ids: Vec<String>,
97    pub pinned_chat_ids: Vec<String>,
98    /// Owner pubkeys (hex) the local user has blocked. Blocking drops
99    /// the peer from the nostr relay subscription, the mobile push
100    /// subscription, and hides their thread from the chat list; any
101    /// further messages from them are also discarded at ingest.
102    pub blocked_owner_pubkeys: Vec<String>,
103    /// Owner pubkeys (hex) for which the user has accepted a message
104    /// request (Signal-style "whitelist"). A direct chat is treated as
105    /// a `is_request` thread until the peer is in this set; sending an
106    /// outgoing message implicitly adds them.
107    pub accepted_owner_pubkeys: Vec<String>,
108    pub debug_logging_enabled: bool,
109    pub accept_unknown_direct_messages: bool,
110    /// User-configurable notification server URL. Empty string means
111    /// "use the platform default" (notifications.iris.to in release,
112    /// notifications-sandbox.iris.to in debug). When non-empty, the
113    /// shells should pass this as the override to
114    /// `build_mobile_push_*_subscription_request`.
115    pub mobile_push_server_url: String,
116}
117
118impl Default for PreferencesSnapshot {
119    fn default() -> Self {
120        Self {
121            voice_calls_enabled: true,
122            video_calls_enabled: true,
123            call_quality: "auto".into(),
124            call_max_bitrate_bps: 2_000_000,
125            send_typing_indicators: true,
126            send_read_receipts: true,
127            allow_message_deletion_by_others: true,
128            hide_blocked_group_messages: false,
129            desktop_notifications_enabled: true,
130            invite_acceptance_notifications_enabled: true,
131            startup_at_login_enabled: true,
132            nearby_enabled: true,
133            nearby_bluetooth_enabled: false,
134            nearby_lan_enabled: false,
135            nearby_show_in_chat_list: true,
136            nearby_mailbag_enabled: true,
137            nostr_relay_urls: crate::core::configured_relays(),
138            image_proxy_enabled: true,
139            image_proxy_fallback_enabled: false,
140            image_proxy_url: crate::image_proxy::DEFAULT_IMAGE_PROXY_URL.to_string(),
141            image_proxy_key_hex: crate::image_proxy::DEFAULT_IMAGE_PROXY_KEY_HEX.to_string(),
142            image_proxy_salt_hex: crate::image_proxy::DEFAULT_IMAGE_PROXY_SALT_HEX.to_string(),
143            muted_chat_ids: Vec::new(),
144            timed_chat_mutes: Vec::new(),
145            silent_mention_chat_ids: Vec::new(),
146            pinned_chat_ids: Vec::new(),
147            blocked_owner_pubkeys: Vec::new(),
148            accepted_owner_pubkeys: Vec::new(),
149            debug_logging_enabled: false,
150            accept_unknown_direct_messages: true,
151            mobile_push_server_url: String::new(),
152        }
153    }
154}
155
156#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
157pub struct OutgoingAttachment {
158    pub file_path: String,
159    pub filename: String,
160}
161
162#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
163pub struct AttachmentDownloadResult {
164    pub data_base64: Option<String>,
165    pub error: Option<String>,
166}
167
168#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
169pub enum DeviceAuthorizationState {
170    Authorized,
171    AwaitingApproval,
172    Revoked,
173}
174
175#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
176pub struct AccountSnapshot {
177    pub public_key_hex: String,
178    pub npub: String,
179    pub display_name: String,
180    pub picture_url: Option<String>,
181    pub about: Option<String>,
182    pub device_public_key_hex: String,
183    pub device_npub: String,
184    pub has_owner_signing_authority: bool,
185    pub authorization_state: DeviceAuthorizationState,
186}
187
188#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
189pub struct DeviceEntrySnapshot {
190    pub display_name: String,
191    pub device_pubkey_hex: String,
192    pub device_npub: String,
193    pub is_current_device: bool,
194    pub is_connected: bool,
195    pub is_authorized: bool,
196    pub is_stale: bool,
197    pub added_at_secs: Option<u64>,
198    pub device_label: Option<String>,
199    pub client_label: Option<String>,
200}
201
202#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
203pub struct DeviceRosterSnapshot {
204    pub owner_public_key_hex: String,
205    pub owner_npub: String,
206    pub current_device_public_key_hex: String,
207    pub current_device_npub: String,
208    pub can_manage_devices: bool,
209    pub authorization_state: DeviceAuthorizationState,
210    pub devices: Vec<DeviceEntrySnapshot>,
211}
212
213#[derive(uniffi::Enum, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
214pub enum DeliveryState {
215    Queued,
216    Pending,
217    Sent,
218    Received,
219    Seen,
220    Failed,
221}
222
223#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
224pub enum ChatKind {
225    Direct,
226    Group,
227}
228
229#[derive(uniffi::Enum, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
230pub enum ChatMessageKind {
231    User,
232    System,
233}
234
235#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
236pub struct MessageAttachmentSnapshot {
237    /// Local historical receipts for complete encrypted attachment trees.
238    #[serde(default)]
239    #[uniffi(default = [])]
240    pub uploaded_urls: Vec<String>,
241    pub nhash: String,
242    pub filename: String,
243    pub filename_encoded: String,
244    pub htree_url: String,
245    pub is_image: bool,
246    pub is_video: bool,
247    pub is_audio: bool,
248}
249
250#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
251pub struct MessageReactionSnapshot {
252    pub emoji: String,
253    pub count: u64,
254    pub reacted_by_me: bool,
255}
256
257#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
258pub struct MessageReactor {
259    /// Hex-encoded pubkey of the user who reacted.
260    pub author: String,
261    /// Core-resolved display name for UI rows. Empty in persisted legacy data.
262    #[serde(default, skip_serializing_if = "String::is_empty")]
263    pub display_name: String,
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub picture_url: Option<String>,
266    /// Emoji content of their current (latest) reaction. Empty means unreacted.
267    pub emoji: String,
268}
269
270#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
271pub struct MessageRecipientDeliverySnapshot {
272    /// Hex-encoded owner/user pubkey.
273    pub owner_pubkey_hex: String,
274    /// Core-resolved display name for UI rows. Empty in persisted legacy data.
275    #[serde(default, skip_serializing_if = "String::is_empty")]
276    pub display_name: String,
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub picture_url: Option<String>,
279    pub delivery: DeliveryState,
280    pub updated_at_secs: u64,
281}
282
283#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
284#[serde(default)]
285pub struct MessageDeliveryTraceSnapshot {
286    pub outer_event_ids: Vec<String>,
287    pub pending_relay_event_ids: Vec<String>,
288    pub queued_protocol_targets: Vec<String>,
289    pub transport_channels: Vec<String>,
290    pub last_transport_error: Option<String>,
291}
292
293/// A local call summary. Signaling and media never enter message history.
294#[derive(uniffi::Record, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
295pub struct CallHistorySnapshot {
296    pub call_id: String,
297    pub direction: String,
298    pub outcome: String,
299    pub video: bool,
300    pub started_at_secs: u64,
301    pub answered_at_secs: Option<u64>,
302    pub ended_at_secs: u64,
303    pub duration_secs: u64,
304}
305
306#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
307pub struct MessageEditSnapshot {
308    pub id: String,
309    pub body: String,
310    pub created_at_secs: u64,
311}
312
313#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
314pub struct ChatMessageSnapshot {
315    #[uniffi(default = [])]
316    pub edit_history: Vec<MessageEditSnapshot>,
317    #[uniffi(default = false)]
318    pub deleted_for_everyone: bool,
319    /// Profile subject of a system notice, independent of the message author.
320    #[uniffi(default = None)]
321    pub system_notice_owner_pubkey_hex: Option<String>,
322    #[uniffi(default = None)]
323    pub direct_transfer: Option<DirectFileTransferSnapshot>,
324    pub id: String,
325    pub chat_id: String,
326    pub kind: ChatMessageKind,
327    pub author: String,
328    /// Hex-encoded owner/user pubkey for the author when known.
329    pub author_owner_pubkey_hex: Option<String>,
330    pub author_picture_url: Option<String>,
331    pub body: String,
332    pub attachments: Vec<MessageAttachmentSnapshot>,
333    pub reactions: Vec<MessageReactionSnapshot>,
334    pub reactors: Vec<MessageReactor>,
335    pub is_outgoing: bool,
336    pub created_at_secs: u64,
337    pub expires_at_secs: Option<u64>,
338    pub delivery: DeliveryState,
339    pub recipient_deliveries: Vec<MessageRecipientDeliverySnapshot>,
340    pub delivery_trace: MessageDeliveryTraceSnapshot,
341    /// Hex ID of the outer relay event that carried this rumor. The
342    /// notification extension joins on this to find a body the
343    /// foreground app already decrypted, so it can render a real
344    /// preview instead of "New activity". `None` for messages that
345    /// didn't come over the wire (system notices, locally-composed
346    /// outgoing rumors).
347    pub source_event_id: Option<String>,
348    #[uniffi(default = None)]
349    pub call: Option<CallHistorySnapshot>,
350}
351
352#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
353pub struct TypingIndicatorSnapshot {
354    pub chat_id: String,
355    pub display_name: String,
356    pub expires_at_secs: u64,
357}
358
359/// A social connection, not identity verification. Computed by nostr-social-graph.
360#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
361pub enum SocialBadge {
362    Warning,
363    Following,
364    Friend,
365    Trusted,
366    Muted,
367}
368
369#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
370pub struct SocialConnectionSnapshot {
371    pub badge: Option<SocialBadge>,
372    pub follow_distance: Option<u32>,
373    pub followed_by_friends: u32,
374    pub description: String,
375    /// Private account contact preference, independent of public follow relationships.
376    #[uniffi(default = false)]
377    pub is_favorite: bool,
378}
379
380#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
381pub struct MentionNotificationSnapshot {
382    pub message_id: String,
383    pub preview: String,
384}
385
386#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
387pub struct ChatThreadSnapshot {
388    /// A newly authenticated incoming chat in this runtime. Never inferred from
389    /// unread counts or restored history; replayed handshakes leave it unchanged.
390    #[uniffi(default = None)]
391    pub new_chat_notification_id: Option<String>,
392    pub social_connection: Option<SocialConnectionSnapshot>,
393    pub chat_id: String,
394    pub kind: ChatKind,
395    pub display_name: String,
396    pub nickname: Option<String>,
397    pub contact_note: Option<String>,
398    pub profile_name: Option<String>,
399    pub subtitle: Option<String>,
400    pub picture_url: Option<String>,
401    pub about: Option<String>,
402    pub member_count: u64,
403    pub last_message_preview: Option<String>,
404    pub last_message_at_secs: Option<u64>,
405    pub last_message_is_outgoing: Option<bool>,
406    #[uniffi(default = None)]
407    pub last_message_id: Option<String>,
408    #[uniffi(default = [])]
409    pub unread_mention_messages: Vec<MentionNotificationSnapshot>,
410    pub last_message_delivery: Option<DeliveryState>,
411    pub unread_count: u64,
412    pub is_typing: bool,
413    pub is_muted: bool,
414    pub is_pinned: bool,
415    /// Unsent composer text the user typed in this thread, persisted
416    /// across navigation / suspend / relaunch. Chat list rows render
417    /// "Draft: …" in place of the last message preview when this is
418    /// non-empty (Signal pattern).
419    pub draft: String,
420    /// True when this is a direct chat where the user has not yet
421    /// replied to a stranger — i.e. incoming messages exist but no
422    /// outgoing ones do. Shells render an Accept / Delete / Block
423    /// gate (Signal-style "message request"), suppress read /
424    /// delivery / typing receipts, and treat the conversation as
425    /// untrusted until the user accepts.
426    pub is_request: bool,
427}
428
429#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
430pub struct ChatParticipantSnapshot {
431    pub social_connection: Option<SocialConnectionSnapshot>,
432    pub owner_pubkey_hex: String,
433    pub display_name: String,
434    pub picture_url: Option<String>,
435    pub is_local_owner: bool,
436}
437
438/// Capability of the selected direct-chat peer. Profile search results are
439/// locators only; the composer unlocks after current owner-signed evidence is
440/// resolved. Group chats expose `None`.
441#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
442pub enum DirectChatCapabilityState {
443    Checking,
444    Available,
445    Unavailable,
446    CheckFailed,
447}
448
449#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
450pub struct ContactIdentitySnapshot {
451    pub is_following: bool,
452    pub can_follow: bool,
453    pub updating_follow: bool,
454    pub first_seen_name: Option<String>,
455    pub saved_name: Option<String>,
456    pub pending_name: Option<String>,
457    pub is_favorite: bool,
458}
459
460#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
461pub struct CurrentChatSnapshot {
462    pub contact_identity: Option<ContactIdentitySnapshot>,
463    pub social_connection: Option<SocialConnectionSnapshot>,
464    pub chat_id: String,
465    pub kind: ChatKind,
466    pub display_name: String,
467    pub nickname: Option<String>,
468    pub contact_note: Option<String>,
469    pub profile_name: Option<String>,
470    pub subtitle: Option<String>,
471    pub picture_url: Option<String>,
472    pub about: Option<String>,
473    pub group_id: Option<String>,
474    pub member_count: u64,
475    pub message_ttl_seconds: Option<u64>,
476    pub is_muted: bool,
477    pub participants: Vec<ChatParticipantSnapshot>,
478    pub messages: Vec<ChatMessageSnapshot>,
479    pub typing_indicators: Vec<TypingIndicatorSnapshot>,
480    /// Same persisted draft text exposed on `ChatThreadSnapshot`. The
481    /// chat screen pre-fills its composer with this on first appear.
482    pub draft: String,
483    /// Mirrors `ChatThreadSnapshot::is_request`. Chat screens replace
484    /// the composer with an Accept / Delete / Block gate when set.
485    pub is_request: bool,
486    pub direct_chat_capability: Option<DirectChatCapabilityState>,
487}
488
489#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
490pub struct GroupMemberSnapshot {
491    pub social_connection: Option<SocialConnectionSnapshot>,
492    pub owner_pubkey_hex: String,
493    pub display_name: String,
494    pub npub: String,
495    pub picture_url: Option<String>,
496    pub is_admin: bool,
497    pub is_creator: bool,
498    pub is_local_owner: bool,
499}
500
501#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
502pub struct GroupDetailsSnapshot {
503    pub group_id: String,
504    pub name: String,
505    pub picture_url: Option<String>,
506    /// Free-text group description (Signal calls this the group description).
507    /// `None` if unset. Admins can edit via `AppAction::UpdateGroupAbout`.
508    pub about: Option<String>,
509    pub created_by_display_name: String,
510    pub created_by_npub: String,
511    pub can_manage: bool,
512    pub is_muted: bool,
513    pub revision: u64,
514    pub members: Vec<GroupMemberSnapshot>,
515    pub invite_links: Vec<GroupInviteSnapshot>,
516}
517
518#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
519pub struct GroupInviteSnapshot {
520    pub id: String,
521    pub url: String,
522    pub title: String,
523    pub use_count: u32,
524    pub max_uses: Option<u32>,
525    pub expires_at: Option<u64>,
526}
527
528#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
529pub struct MutualGroupsSnapshot {
530    pub groups: Vec<ChatThreadSnapshot>,
531}
532
533#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
534pub struct RelayConnectionSnapshot {
535    pub url: String,
536    pub status: String,
537}
538
539#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
540pub struct NetworkStatusSnapshot {
541    pub relay_set_id: String,
542    pub relay_urls: Vec<String>,
543    pub relay_connections: Vec<RelayConnectionSnapshot>,
544    pub connected_relay_count: u64,
545    pub all_relays_offline_since_secs: Option<u64>,
546    pub syncing: bool,
547    pub pending_outbound_count: u64,
548    pub pending_group_control_count: u64,
549    pub recent_event_count: u64,
550    pub recent_log_count: u64,
551    pub last_debug_category: Option<String>,
552    pub last_debug_detail: Option<String>,
553}
554
555#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
556pub struct MobilePushSessionSnapshot {
557    pub recipient_pubkey_hex: String,
558    pub display_name: String,
559    pub state_json: String,
560    pub tracked_sender_pubkeys: Vec<String>,
561    pub has_receiving_capability: bool,
562}
563
564#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
565pub struct MobilePushDelayedAuthor {
566    pub author_pubkey: String,
567    pub since_secs: u64,
568}
569
570#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
571pub struct MobilePushSyncSnapshot {
572    pub call_device_pubkey_hex: Option<String>,
573    pub owner_pubkey_hex: Option<String>,
574    pub message_author_pubkeys: Vec<String>,
575    pub delayed_message_authors: Vec<MobilePushDelayedAuthor>,
576    pub background_message_author_pubkeys: Vec<String>,
577    pub invite_response_pubkeys: Vec<String>,
578    pub sessions: Vec<MobilePushSessionSnapshot>,
579}
580
581#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
582pub struct PeerProfileDebugSnapshot {
583    pub owner_pubkey_hex: String,
584    pub owner_npub: String,
585    pub direct_send_readiness: String,
586    pub roster_device_count: u64,
587    pub known_device_count: u64,
588    pub active_session_count: u64,
589    pub session_count: u64,
590    pub receiving_session_count: u64,
591    pub tracked_sender_count: u64,
592    pub recent_handshake_device_count: u64,
593    pub last_handshake_at_secs: Option<u64>,
594    pub tracked_for_messages: bool,
595}
596
597#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
598pub struct MobilePushNotificationResolution {
599    pub should_show: bool,
600    pub title: String,
601    pub body: String,
602    pub payload_json: String,
603}
604
605#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
606pub struct MobilePushSubscriptionRequest {
607    pub method: String,
608    pub url: String,
609    pub authorization_header: String,
610    pub body_json: Option<String>,
611}
612
613/// One row inside the "Messages" section of a search result. Shells
614/// use the conversation identity for global results and the author identity
615/// for results within a chat. Both are resolved from persisted history so
616/// searching older messages does not depend on the loaded timeline.
617#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
618pub struct MessageSearchHit {
619    pub chat_id: String,
620    pub message_id: String,
621    pub chat_display_name: String,
622    pub chat_picture_url: Option<String>,
623    pub chat_kind: ChatKind,
624    pub author_pubkey: String,
625    pub author_display_name: String,
626    pub author_picture_url: Option<String>,
627    pub body: String,
628    pub is_outgoing: bool,
629    pub created_at_secs: u64,
630}
631
632/// A person discovered from local profiles, the account's social graph, or the
633/// global profile index. Chat capability is resolved only after selection;
634/// shells only render these fields.
635#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
636pub struct FollowedUserSearchResult {
637    pub social_connection: Option<SocialConnectionSnapshot>,
638    pub owner_pubkey_hex: String,
639    pub display_label: String,
640    pub profile_label: Option<String>,
641    pub picture_url: Option<String>,
642    pub about: Option<String>,
643    pub user_id: String,
644}
645
646#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
647pub struct SearchResultSnapshot {
648    pub query: String,
649    pub scope_chat_id: Option<String>,
650    pub people: Vec<FollowedUserSearchResult>,
651    pub contacts: Vec<ChatThreadSnapshot>,
652    pub groups: Vec<ChatThreadSnapshot>,
653    pub messages: Vec<MessageSearchHit>,
654    /// Inline shortcut row to show above the grouped sections when the
655    /// query is itself an npub / nprofile / hex pubkey / invite URL.
656    /// Shells render this as a single "Start chat with …" / "Accept
657    /// invite from …" row that dispatches the carried action on tap.
658    pub shortcut: Option<ChatInputShortcut>,
659}
660
661/// Classification of the free-text input typed into a chat-action
662/// field — the search bar, the "New chat" paste field, the share-link
663/// handler. Centralising the parsing here means the UI just looks at
664/// the enum and dispatches; no platform has its own ad-hoc
665/// `contains("://") && contains("#")` check.
666#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
667pub enum ChatInputShortcut {
668    /// Input is a recognized owner pubkey (npub / nprofile / hex).
669    /// `display` is the user-presentable short form (npub…).
670    DirectPeer {
671        peer_input: String,
672        display: String,
673        npub: String,
674        pubkey_hex: String,
675    },
676    /// Input is a paste of an invite URL. `display` is a short label
677    /// suitable for "Accept invite from …" rows.
678    Invite {
679        invite_input: String,
680        display: String,
681    },
682}
683
684impl SearchResultSnapshot {
685    pub fn empty(query: String, scope_chat_id: Option<String>) -> Self {
686        Self {
687            query,
688            scope_chat_id,
689            people: Vec::new(),
690            contacts: Vec::new(),
691            groups: Vec::new(),
692            messages: Vec::new(),
693            shortcut: None,
694        }
695    }
696
697    pub fn is_empty(&self) -> bool {
698        self.people.is_empty()
699            && self.contacts.is_empty()
700            && self.groups.is_empty()
701            && self.messages.is_empty()
702            && self.shortcut.is_none()
703    }
704}
705
706#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
707pub struct PublicInviteSnapshot {
708    pub url: String,
709    pub legacy_url: String,
710    pub title: String,
711    #[uniffi(default = true)]
712    pub open_chat_on_join: bool,
713}
714
715#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
716pub struct LinkDeviceSnapshot {
717    pub url: String,
718    pub device_input: String,
719}
720
721#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
722pub enum RemoteSignerPhase {
723    Connecting,
724    WaitingForSigner,
725    WaitingForApproval,
726    Finishing,
727}
728
729#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
730pub struct RemoteSignerLoginSnapshot {
731    pub connection_uri: Option<String>,
732    pub phase: RemoteSignerPhase,
733    pub auth_url: Option<String>,
734}
735
736#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
737pub struct CallSnapshot {
738    pub outgoing: bool,
739    pub target_bitrate_bps: u32,
740    pub key_frame_generation: u32,
741    pub media_connected: bool,
742    pub max_bitrate_bps: u32,
743    pub call_id: String,
744    pub chat_id: String,
745    pub peer_name: String,
746    pub phase: String,
747    pub video: bool,
748    pub video_capable: bool,
749    pub muted: bool,
750    pub remote_video: bool,
751    pub remote_muted: bool,
752    pub started_at_secs: u64,
753    pub connected_at_secs: Option<u64>,
754    pub end_reason: Option<String>,
755}
756
757#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
758pub enum DeviceHistorySyncPhase {
759    Discovering,
760    Transferring,
761    Waiting,
762    Complete,
763}
764
765#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
766pub struct DeviceHistorySyncSnapshot {
767    pub phase: DeviceHistorySyncPhase,
768    pub imported_messages: u64,
769    pub total_messages: Option<u64>,
770}
771
772#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
773pub struct AppState {
774    pub rev: u64,
775    pub call: Option<CallSnapshot>,
776    pub router: Router,
777    pub account: Option<AccountSnapshot>,
778    pub device_roster: Option<DeviceRosterSnapshot>,
779    pub device_history_sync: Option<DeviceHistorySyncSnapshot>,
780    pub busy: BusyState,
781    pub chat_list: Vec<ChatThreadSnapshot>,
782    pub blocked_people: Vec<FollowedUserSearchResult>,
783    /// Invalidates native message pages after visibility policy changes.
784    pub message_visibility_revision: u64,
785    pub current_chat: Option<CurrentChatSnapshot>,
786    pub group_details: Option<GroupDetailsSnapshot>,
787    pub public_invite: Option<PublicInviteSnapshot>,
788    pub link_device: Option<LinkDeviceSnapshot>,
789    pub remote_signer_login: Option<RemoteSignerLoginSnapshot>,
790    pub network_status: Option<NetworkStatusSnapshot>,
791    pub mobile_push: MobilePushSyncSnapshot,
792    pub preferences: PreferencesSnapshot,
793    /// Invalidates native People-search caches when discovery inputs, block
794    /// exclusions, or discovery loading state change.
795    pub user_discovery_revision: u64,
796    pub user_discovery_syncing: bool,
797    pub toast: Option<String>,
798}
799
800impl AppState {
801    pub fn empty() -> Self {
802        Self {
803            rev: 0,
804            call: None,
805            router: Router {
806                default_screen: Screen::Welcome,
807                screen_stack: Vec::new(),
808            },
809            account: None,
810            device_roster: None,
811            device_history_sync: None,
812            busy: BusyState::default(),
813            chat_list: Vec::new(),
814            blocked_people: Vec::new(),
815            message_visibility_revision: 0,
816            current_chat: None,
817            group_details: None,
818            public_invite: None,
819            link_device: None,
820            remote_signer_login: None,
821            network_status: None,
822            mobile_push: MobilePushSyncSnapshot::default(),
823            preferences: PreferencesSnapshot::default(),
824            user_discovery_revision: 0,
825            user_discovery_syncing: false,
826            toast: None,
827        }
828    }
829}