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    pub social_connection: Option<SocialConnectionSnapshot>,
389    pub chat_id: String,
390    pub kind: ChatKind,
391    pub display_name: String,
392    pub nickname: Option<String>,
393    pub contact_note: Option<String>,
394    pub profile_name: Option<String>,
395    pub subtitle: Option<String>,
396    pub picture_url: Option<String>,
397    pub about: Option<String>,
398    pub member_count: u64,
399    pub last_message_preview: Option<String>,
400    pub last_message_at_secs: Option<u64>,
401    pub last_message_is_outgoing: Option<bool>,
402    #[uniffi(default = None)]
403    pub last_message_id: Option<String>,
404    #[uniffi(default = [])]
405    pub unread_mention_messages: Vec<MentionNotificationSnapshot>,
406    pub last_message_delivery: Option<DeliveryState>,
407    pub unread_count: u64,
408    pub is_typing: bool,
409    pub is_muted: bool,
410    pub is_pinned: bool,
411    /// Unsent composer text the user typed in this thread, persisted
412    /// across navigation / suspend / relaunch. Chat list rows render
413    /// "Draft: …" in place of the last message preview when this is
414    /// non-empty (Signal pattern).
415    pub draft: String,
416    /// True when this is a direct chat where the user has not yet
417    /// replied to a stranger — i.e. incoming messages exist but no
418    /// outgoing ones do. Shells render an Accept / Delete / Block
419    /// gate (Signal-style "message request"), suppress read /
420    /// delivery / typing receipts, and treat the conversation as
421    /// untrusted until the user accepts.
422    pub is_request: bool,
423}
424
425#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
426pub struct ChatParticipantSnapshot {
427    pub social_connection: Option<SocialConnectionSnapshot>,
428    pub owner_pubkey_hex: String,
429    pub display_name: String,
430    pub picture_url: Option<String>,
431    pub is_local_owner: bool,
432}
433
434/// Capability of the selected direct-chat peer. Profile search results are
435/// locators only; the composer unlocks after current owner-signed evidence is
436/// resolved. Group chats expose `None`.
437#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
438pub enum DirectChatCapabilityState {
439    Checking,
440    Available,
441    Unavailable,
442    CheckFailed,
443}
444
445#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
446pub struct ContactIdentitySnapshot {
447    pub is_following: bool,
448    pub can_follow: bool,
449    pub updating_follow: bool,
450    pub first_seen_name: Option<String>,
451    pub saved_name: Option<String>,
452    pub pending_name: Option<String>,
453    pub is_favorite: bool,
454}
455
456#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
457pub struct CurrentChatSnapshot {
458    pub contact_identity: Option<ContactIdentitySnapshot>,
459    pub social_connection: Option<SocialConnectionSnapshot>,
460    pub chat_id: String,
461    pub kind: ChatKind,
462    pub display_name: String,
463    pub nickname: Option<String>,
464    pub contact_note: Option<String>,
465    pub profile_name: Option<String>,
466    pub subtitle: Option<String>,
467    pub picture_url: Option<String>,
468    pub about: Option<String>,
469    pub group_id: Option<String>,
470    pub member_count: u64,
471    pub message_ttl_seconds: Option<u64>,
472    pub is_muted: bool,
473    pub participants: Vec<ChatParticipantSnapshot>,
474    pub messages: Vec<ChatMessageSnapshot>,
475    pub typing_indicators: Vec<TypingIndicatorSnapshot>,
476    /// Same persisted draft text exposed on `ChatThreadSnapshot`. The
477    /// chat screen pre-fills its composer with this on first appear.
478    pub draft: String,
479    /// Mirrors `ChatThreadSnapshot::is_request`. Chat screens replace
480    /// the composer with an Accept / Delete / Block gate when set.
481    pub is_request: bool,
482    pub direct_chat_capability: Option<DirectChatCapabilityState>,
483}
484
485#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
486pub struct GroupMemberSnapshot {
487    pub social_connection: Option<SocialConnectionSnapshot>,
488    pub owner_pubkey_hex: String,
489    pub display_name: String,
490    pub npub: String,
491    pub picture_url: Option<String>,
492    pub is_admin: bool,
493    pub is_creator: bool,
494    pub is_local_owner: bool,
495}
496
497#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
498pub struct GroupDetailsSnapshot {
499    pub group_id: String,
500    pub name: String,
501    pub picture_url: Option<String>,
502    /// Free-text group description (Signal calls this the group description).
503    /// `None` if unset. Admins can edit via `AppAction::UpdateGroupAbout`.
504    pub about: Option<String>,
505    pub created_by_display_name: String,
506    pub created_by_npub: String,
507    pub can_manage: bool,
508    pub is_muted: bool,
509    pub revision: u64,
510    pub members: Vec<GroupMemberSnapshot>,
511    pub invite_links: Vec<GroupInviteSnapshot>,
512}
513
514#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
515pub struct GroupInviteSnapshot {
516    pub id: String,
517    pub url: String,
518    pub title: String,
519    pub use_count: u32,
520    pub max_uses: Option<u32>,
521    pub expires_at: Option<u64>,
522}
523
524#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
525pub struct MutualGroupsSnapshot {
526    pub groups: Vec<ChatThreadSnapshot>,
527}
528
529#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
530pub struct RelayConnectionSnapshot {
531    pub url: String,
532    pub status: String,
533}
534
535#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
536pub struct NetworkStatusSnapshot {
537    pub relay_set_id: String,
538    pub relay_urls: Vec<String>,
539    pub relay_connections: Vec<RelayConnectionSnapshot>,
540    pub connected_relay_count: u64,
541    pub all_relays_offline_since_secs: Option<u64>,
542    pub syncing: bool,
543    pub pending_outbound_count: u64,
544    pub pending_group_control_count: u64,
545    pub recent_event_count: u64,
546    pub recent_log_count: u64,
547    pub last_debug_category: Option<String>,
548    pub last_debug_detail: Option<String>,
549}
550
551#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
552pub struct MobilePushSessionSnapshot {
553    pub recipient_pubkey_hex: String,
554    pub display_name: String,
555    pub state_json: String,
556    pub tracked_sender_pubkeys: Vec<String>,
557    pub has_receiving_capability: bool,
558}
559
560#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
561pub struct MobilePushDelayedAuthor {
562    pub author_pubkey: String,
563    pub since_secs: u64,
564}
565
566#[derive(uniffi::Record, Clone, Debug, Default, PartialEq, Eq)]
567pub struct MobilePushSyncSnapshot {
568    pub call_device_pubkey_hex: Option<String>,
569    pub owner_pubkey_hex: Option<String>,
570    pub message_author_pubkeys: Vec<String>,
571    pub delayed_message_authors: Vec<MobilePushDelayedAuthor>,
572    pub background_message_author_pubkeys: Vec<String>,
573    pub invite_response_pubkeys: Vec<String>,
574    pub sessions: Vec<MobilePushSessionSnapshot>,
575}
576
577#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
578pub struct PeerProfileDebugSnapshot {
579    pub owner_pubkey_hex: String,
580    pub owner_npub: String,
581    pub direct_send_readiness: String,
582    pub roster_device_count: u64,
583    pub known_device_count: u64,
584    pub active_session_count: u64,
585    pub session_count: u64,
586    pub receiving_session_count: u64,
587    pub tracked_sender_count: u64,
588    pub recent_handshake_device_count: u64,
589    pub last_handshake_at_secs: Option<u64>,
590    pub tracked_for_messages: bool,
591}
592
593#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
594pub struct MobilePushNotificationResolution {
595    pub should_show: bool,
596    pub title: String,
597    pub body: String,
598    pub payload_json: String,
599}
600
601#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
602pub struct MobilePushSubscriptionRequest {
603    pub method: String,
604    pub url: String,
605    pub authorization_header: String,
606    pub body_json: Option<String>,
607}
608
609/// One row inside the "Messages" section of a search result. Shells
610/// use the conversation identity for global results and the author identity
611/// for results within a chat. Both are resolved from persisted history so
612/// searching older messages does not depend on the loaded timeline.
613#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
614pub struct MessageSearchHit {
615    pub chat_id: String,
616    pub message_id: String,
617    pub chat_display_name: String,
618    pub chat_picture_url: Option<String>,
619    pub chat_kind: ChatKind,
620    pub author_pubkey: String,
621    pub author_display_name: String,
622    pub author_picture_url: Option<String>,
623    pub body: String,
624    pub is_outgoing: bool,
625    pub created_at_secs: u64,
626}
627
628/// A person discovered from local profiles, the account's social graph, or the
629/// global profile index. Chat capability is resolved only after selection;
630/// shells only render these fields.
631#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
632pub struct FollowedUserSearchResult {
633    pub social_connection: Option<SocialConnectionSnapshot>,
634    pub owner_pubkey_hex: String,
635    pub display_label: String,
636    pub profile_label: Option<String>,
637    pub picture_url: Option<String>,
638    pub about: Option<String>,
639    pub user_id: String,
640}
641
642#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
643pub struct SearchResultSnapshot {
644    pub query: String,
645    pub scope_chat_id: Option<String>,
646    pub people: Vec<FollowedUserSearchResult>,
647    pub contacts: Vec<ChatThreadSnapshot>,
648    pub groups: Vec<ChatThreadSnapshot>,
649    pub messages: Vec<MessageSearchHit>,
650    /// Inline shortcut row to show above the grouped sections when the
651    /// query is itself an npub / nprofile / hex pubkey / invite URL.
652    /// Shells render this as a single "Start chat with …" / "Accept
653    /// invite from …" row that dispatches the carried action on tap.
654    pub shortcut: Option<ChatInputShortcut>,
655}
656
657/// Classification of the free-text input typed into a chat-action
658/// field — the search bar, the "New chat" paste field, the share-link
659/// handler. Centralising the parsing here means the UI just looks at
660/// the enum and dispatches; no platform has its own ad-hoc
661/// `contains("://") && contains("#")` check.
662#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
663pub enum ChatInputShortcut {
664    /// Input is a recognized owner pubkey (npub / nprofile / hex).
665    /// `display` is the user-presentable short form (npub…).
666    DirectPeer {
667        peer_input: String,
668        display: String,
669        npub: String,
670        pubkey_hex: String,
671    },
672    /// Input is a paste of an invite URL. `display` is a short label
673    /// suitable for "Accept invite from …" rows.
674    Invite {
675        invite_input: String,
676        display: String,
677    },
678}
679
680impl SearchResultSnapshot {
681    pub fn empty(query: String, scope_chat_id: Option<String>) -> Self {
682        Self {
683            query,
684            scope_chat_id,
685            people: Vec::new(),
686            contacts: Vec::new(),
687            groups: Vec::new(),
688            messages: Vec::new(),
689            shortcut: None,
690        }
691    }
692
693    pub fn is_empty(&self) -> bool {
694        self.people.is_empty()
695            && self.contacts.is_empty()
696            && self.groups.is_empty()
697            && self.messages.is_empty()
698            && self.shortcut.is_none()
699    }
700}
701
702#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
703pub struct PublicInviteSnapshot {
704    pub url: String,
705    pub legacy_url: String,
706    pub title: String,
707    #[uniffi(default = true)]
708    pub open_chat_on_join: bool,
709}
710
711#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
712pub struct LinkDeviceSnapshot {
713    pub url: String,
714    pub device_input: String,
715}
716
717#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
718pub enum RemoteSignerPhase {
719    Connecting,
720    WaitingForSigner,
721    WaitingForApproval,
722    Finishing,
723}
724
725#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
726pub struct RemoteSignerLoginSnapshot {
727    pub connection_uri: Option<String>,
728    pub phase: RemoteSignerPhase,
729    pub auth_url: Option<String>,
730}
731
732#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
733pub struct CallSnapshot {
734    pub outgoing: bool,
735    pub target_bitrate_bps: u32,
736    pub key_frame_generation: u32,
737    pub media_connected: bool,
738    pub max_bitrate_bps: u32,
739    pub call_id: String,
740    pub chat_id: String,
741    pub peer_name: String,
742    pub phase: String,
743    pub video: bool,
744    pub video_capable: bool,
745    pub muted: bool,
746    pub remote_video: bool,
747    pub remote_muted: bool,
748    pub started_at_secs: u64,
749    pub connected_at_secs: Option<u64>,
750    pub end_reason: Option<String>,
751}
752
753#[derive(uniffi::Enum, Clone, Debug, PartialEq, Eq)]
754pub enum DeviceHistorySyncPhase {
755    Discovering,
756    Transferring,
757    Waiting,
758    Complete,
759}
760
761#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
762pub struct DeviceHistorySyncSnapshot {
763    pub phase: DeviceHistorySyncPhase,
764    pub imported_messages: u64,
765    pub total_messages: Option<u64>,
766}
767
768#[derive(uniffi::Record, Clone, Debug, PartialEq, Eq)]
769pub struct AppState {
770    pub rev: u64,
771    pub call: Option<CallSnapshot>,
772    pub router: Router,
773    pub account: Option<AccountSnapshot>,
774    pub device_roster: Option<DeviceRosterSnapshot>,
775    pub device_history_sync: Option<DeviceHistorySyncSnapshot>,
776    pub busy: BusyState,
777    pub chat_list: Vec<ChatThreadSnapshot>,
778    pub blocked_people: Vec<FollowedUserSearchResult>,
779    /// Invalidates native message pages after visibility policy changes.
780    pub message_visibility_revision: u64,
781    pub current_chat: Option<CurrentChatSnapshot>,
782    pub group_details: Option<GroupDetailsSnapshot>,
783    pub public_invite: Option<PublicInviteSnapshot>,
784    pub link_device: Option<LinkDeviceSnapshot>,
785    pub remote_signer_login: Option<RemoteSignerLoginSnapshot>,
786    pub network_status: Option<NetworkStatusSnapshot>,
787    pub mobile_push: MobilePushSyncSnapshot,
788    pub preferences: PreferencesSnapshot,
789    /// Invalidates native People-search caches when discovery inputs, block
790    /// exclusions, or discovery loading state change.
791    pub user_discovery_revision: u64,
792    pub user_discovery_syncing: bool,
793    pub toast: Option<String>,
794}
795
796impl AppState {
797    pub fn empty() -> Self {
798        Self {
799            rev: 0,
800            call: None,
801            router: Router {
802                default_screen: Screen::Welcome,
803                screen_stack: Vec::new(),
804            },
805            account: None,
806            device_roster: None,
807            device_history_sync: None,
808            busy: BusyState::default(),
809            chat_list: Vec::new(),
810            blocked_people: Vec::new(),
811            message_visibility_revision: 0,
812            current_chat: None,
813            group_details: None,
814            public_invite: None,
815            link_device: None,
816            remote_signer_login: None,
817            network_status: None,
818            mobile_push: MobilePushSyncSnapshot::default(),
819            preferences: PreferencesSnapshot::default(),
820            user_discovery_revision: 0,
821            user_discovery_syncing: false,
822            toast: None,
823        }
824    }
825}