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