use crate::actions::AppAction;
use crate::state::{
AccountSnapshot, AppState, ChatKind, ChatMessageKind, ChatMessageSnapshot, ChatMuteDeadline,
ChatParticipantSnapshot, ChatThreadSnapshot, CurrentChatSnapshot, DeliveryState,
DeviceAuthorizationState, DeviceEntrySnapshot, DeviceRosterSnapshot, DirectChatCapabilityState,
GroupDetailsSnapshot, GroupMemberSnapshot, LinkDeviceSnapshot, MessageAttachmentSnapshot,
MessageDeliveryTraceSnapshot, MessageReactionSnapshot, MessageReactor,
MessageRecipientDeliverySnapshot, MobilePushDelayedAuthor, MobilePushNotificationResolution,
MobilePushSubscriptionRequest, MobilePushSyncSnapshot, NetworkStatusSnapshot,
OutgoingAttachment, PeerProfileDebugSnapshot, PreferencesSnapshot, PublicInviteSnapshot,
RelayConnectionSnapshot, Router, Screen, TypingIndicatorSnapshot,
};
use crate::updates::{AppUpdate, CoreMsg, InternalEvent, RelayPublishDrainResult};
use flume::Sender;
use iris_chat_protocol::*;
use nostr::{Alphabet, EventBuilder, SingleLetterTag, UnsignedEvent};
use nostr_double_ratchet::parse_group_sender_key_message_event_unchecked;
use nostr_double_ratchet::{
apply_app_keys_snapshot_with_required_device, build_app_keys_device_authorization_filter,
build_protocol_discovery_filters, deterministic_link_invite_for_device, is_app_keys_event,
resolve_app_keys_owner_for_device, AppKeys, DeviceEntry, APP_KEYS_EVENT_KIND,
CHAT_MESSAGE_KIND, CHAT_SETTINGS_KIND, INVITE_EVENT_KIND, INVITE_LIST_LABEL,
INVITE_RESPONSE_KIND, MESSAGE_EVENT_KIND, REACTION_KIND, RECEIPT_KIND, TYPING_KIND,
};
use nostr_double_ratchet::{
GroupIncomingEvent, GroupSnapshot, Invite, OwnerPubkey as NdrOwnerPubkey,
SessionManagerSnapshot, SessionState, UnixSeconds as NdrUnixSeconds,
};
use nostr_double_ratchet_pairwise_codec as pairwise_codec;
use nostr_identity::{
approve_nostr_identity_device_approval_bootstrap,
build_nostr_identity_device_approval_receipt_event,
build_nostr_identity_roster_op_event_with_client_nonce,
create_nostr_identity_device_approval_request, encode_nostr_identity_device_approval_bootstrap,
nostr_identity_device_approval_bootstrap, parse_nostr_identity_device_approval_bootstrap,
ApproveNostrIdentityDeviceApprovalBootstrapOptions,
CreateNostrIdentityDeviceApprovalRequestOptions, NostrIdentityDeviceApprovalBootstrap,
NostrIdentityDeviceApprovalReceipt, NostrIdentityId, NostrIdentityRosterOp, PrivateEvent,
NOSTR_IDENTITY_DEVICE_APPROVAL_LABEL_MAX_BYTES, NOSTR_IDENTITY_DEVICE_APPROVAL_RECEIPT_SCHEMA,
};
use nostr_sdk::prelude::{
Client, Event, Filter, Keys, Kind, PublicKey, RelayNotification, RelayPoolNotification,
RelayStatus, RelayUrl, SubscribeOptions, SubscriptionId, Timestamp, ToBech32,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashSet, VecDeque};
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::time::{sleep, sleep_until, Duration, Instant};
#[cfg(test)]
use nostr_double_ratchet::{
invite_response_event, message_event, parse_group_sender_key_message_event, parse_invite_event,
parse_message_event, GroupEventManager, GROUP_ROSTER_FACT_KIND, GROUP_SENDER_KEY_MESSAGE_KIND,
};
#[cfg(test)]
use nostr_double_ratchet::{
AuthorizedDevice, DevicePubkey as NdrDevicePubkey, DeviceRoster, GroupManagerSnapshot,
ProtocolContext, SessionManager,
};
#[cfg(test)]
use rand::rngs::OsRng;
mod account;
mod account_app_keys;
mod account_authorization;
mod account_logout;
mod account_pending_link;
mod account_signer;
mod account_signer_relay;
mod attachment_upload;
mod ble_background;
pub(crate) mod device_link_signer;
mod device_link_signer_transport;
mod registration_recovery;
mod remote_signer;
mod remote_signer_rpc;
mod remote_signer_transport;
mod remote_signer_uri;
pub use attachment_upload::{
download_hashtree_attachment, download_hashtree_attachment_with_limit,
};
mod attachments;
mod calls;
pub(crate) mod direct_file_tcp;
pub(crate) mod direct_files;
pub(crate) use calls::push::{build_call_push_subscription_request, resolve_call_push_invite};
mod block_sync;
mod chat_deletions;
mod chat_mute_sync;
mod chat_pin_sync;
mod chat_reactions;
mod chat_read_state;
mod chat_receipts;
mod chat_settings;
mod contact_name_sync;
use chat_mute_sync::ChatMuteState;
use chat_pin_sync::ChatPinState;
mod chat_typing;
mod chats;
mod config;
mod contact_details;
mod contact_identity;
mod device_approval;
mod device_sync;
mod device_sync_tcp;
mod direct_chat_capability;
mod fips_nearby;
mod group_membership;
mod groups;
mod identity;
mod invites;
mod issued_invites;
mod lifecycle;
mod mesh_pubsub;
mod message_expiry;
mod message_mutations;
pub(crate) mod startup_restore;
use message_mutations::{MessageMutation, MESSAGE_DELETE_KIND, MESSAGE_EDIT_KIND};
pub(crate) mod desktop_read_notifications;
mod group_metadata_recovery;
mod invite_notifications;
mod mobile_push;
mod model;
pub(crate) mod notifications;
mod payloads;
mod persistence;
mod private_contacts;
mod private_device_labels;
mod private_group_wake;
mod profile;
mod profile_directory;
pub(crate) use profile::{fallback_profile_name_for_identity, profile_name_for_identity};
mod app_core_state;
mod message_requests;
mod profile_helpers;
mod profile_search;
mod profile_search_capability;
mod profile_search_remote;
mod projection;
mod protocol;
mod protocol_filters;
mod public_follow;
mod publish_helpers;
mod publishing;
mod publishing_fact_events;
mod publishing_identity;
mod relay;
use app_core_state::{BatchState, DebugSnapshotState, RelayStatusState, TimerTokens};
pub(crate) mod routing;
mod social_connection;
mod storage;
mod support;
#[cfg(test)]
mod tests;
mod user_discovery;
mod user_discovery_graph;
mod user_discovery_social;
pub(super) const APPCORE_PROTOCOL_LABEL: &str = "appcore-protocol";
pub(super) const LOCAL_INVITE_PUBLISH_LABEL: &str = "invite";
pub(super) const PENDING_RELAY_CONTROL_PUBLISH_MAX_ROWS: usize = 2_048;
type OwnerPubkey = PublicKey;
type DevicePubkey = PublicKey;
use account_app_keys::next_app_keys_created_at;
use account_app_keys::{
canonical_known_app_keys_snapshot, known_app_keys_from_ndr, known_app_keys_to_ndr,
preserve_known_app_key_labels,
};
use attachment_upload::upload_profile_picture_to_hashtree;
pub(crate) use attachments::strip_reply_quote;
use attachments::*;
use config::*;
pub(crate) use config::{
app_version_string, build_summary, configured_relays, relay_set_id, trusted_test_build_flag,
};
use device_sync::*;
use device_sync_tcp::*;
pub(crate) use fips_nearby::{HostBleAttachment, HostBleOwnership};
use identity::*;
pub(crate) use identity::{normalize_peer_input_for_display, parse_peer_input};
pub(crate) use mobile_push::{
build_mobile_push_create_subscription_request, build_mobile_push_delete_subscription_request,
build_mobile_push_list_subscriptions_request, build_mobile_push_update_subscription_request,
decrypt_mobile_push_notification, mobile_push_stored_subscription_id_key,
read_mobile_push_notification_indexes, resolve_mobile_push_notification,
resolve_mobile_push_server_url,
};
pub(crate) use model::ProfileSearchFetchResult;
pub(crate) use model::ProtocolSubscriptionPlan;
pub(crate) use model::UserDiscoveryFetchResult;
use model::*;
use payloads::*;
use profile_helpers::*;
pub(crate) use profile_search::prewarm_default_social_graph;
pub(crate) use profile_search::search_people;
use protocol_filters::*;
use publish_helpers::*;
pub use storage::validate_account_storage;
use storage::{open_database, AppStore, DataDirLock, SqliteStorageAdapter};
pub(crate) use storage::{search_messages_fts, PersistedMessageSearchHit, SharedConnection};
pub(crate) fn chat_read_state(
state: &AppState,
chat_id: &str,
latest_limit: Option<usize>,
) -> AppState {
let chat_id = chat_id.trim();
AppState {
account: state.account.clone(),
preferences: state.preferences.clone(),
chat_list: state
.chat_list
.iter()
.find(|chat| chat.chat_id == chat_id)
.cloned()
.into_iter()
.collect(),
current_chat: state
.current_chat
.as_ref()
.filter(|chat| chat.chat_id == chat_id)
.map(|chat| {
let start = latest_limit
.map(|limit| chat.messages.len().saturating_sub(limit.max(1)))
.unwrap_or(chat.messages.len());
clone_chat_with_messages(chat, chat.messages.get(start..).unwrap_or_default())
}),
..AppState::empty()
}
}
fn clone_chat_with_messages(
chat: &CurrentChatSnapshot,
messages: &[ChatMessageSnapshot],
) -> CurrentChatSnapshot {
CurrentChatSnapshot {
contact_identity: chat.contact_identity.clone(),
social_connection: chat.social_connection.clone(),
chat_id: chat.chat_id.clone(),
kind: chat.kind.clone(),
display_name: chat.display_name.clone(),
nickname: chat.nickname.clone(),
contact_note: chat.contact_note.clone(),
profile_name: chat.profile_name.clone(),
subtitle: chat.subtitle.clone(),
picture_url: chat.picture_url.clone(),
about: chat.about.clone(),
group_id: chat.group_id.clone(),
member_count: chat.member_count,
message_ttl_seconds: chat.message_ttl_seconds,
is_muted: chat.is_muted,
participants: chat.participants.clone(),
messages: messages.to_vec(),
typing_indicators: chat.typing_indicators.clone(),
draft: chat.draft.clone(),
is_request: chat.is_request,
direct_chat_capability: chat.direct_chat_capability.clone(),
}
}
pub(crate) fn chat_snapshot_from_state_and_db(
state: &AppState,
shared_db: Option<&SharedConnection>,
chat_id: &str,
limit: usize,
) -> Option<CurrentChatSnapshot> {
let chat_id = chat_id.trim();
if chat_id.is_empty() || state.account.is_none() {
return None;
}
if let Some(current) = state
.current_chat
.as_ref()
.filter(|chat| chat.chat_id == chat_id)
{
let start = current.messages.len().saturating_sub(limit.max(1));
return Some(clone_chat_with_messages(
current,
current.messages.get(start..).unwrap_or_default(),
));
}
build_chat_snapshot_with_messages(
state,
shared_db,
chat_id,
ChatPageRequest::Latest {
limit: limit.max(1),
},
)
}
pub(crate) fn chat_snapshot_before_from_state_and_db(
state: &AppState,
shared_db: Option<&SharedConnection>,
chat_id: &str,
before_message_id: &str,
limit: usize,
) -> Option<CurrentChatSnapshot> {
build_chat_snapshot_with_messages(
state,
shared_db,
chat_id,
ChatPageRequest::Before {
before_message_id,
limit: limit.max(1),
},
)
}
pub(crate) fn chat_snapshot_around_message_from_state_and_db(
state: &AppState,
shared_db: Option<&SharedConnection>,
chat_id: &str,
message_id: &str,
before_limit: usize,
after_limit: usize,
) -> Option<CurrentChatSnapshot> {
build_chat_snapshot_with_messages(
state,
shared_db,
chat_id,
ChatPageRequest::Around {
message_id,
before_limit,
after_limit,
},
)
}
enum ChatPageRequest<'a> {
Latest {
limit: usize,
},
Before {
before_message_id: &'a str,
limit: usize,
},
Around {
message_id: &'a str,
before_limit: usize,
after_limit: usize,
},
}
fn build_chat_snapshot_with_messages(
state: &AppState,
shared_db: Option<&SharedConnection>,
chat_id: &str,
request: ChatPageRequest<'_>,
) -> Option<CurrentChatSnapshot> {
let chat_id = chat_id.trim();
if chat_id.is_empty() || state.account.is_none() {
return None;
}
let thread = state.chat_list.iter().find(|chat| chat.chat_id == chat_id);
let mut messages = load_chat_messages(shared_db, chat_id, request, state)?;
let group_id = group_id_from_chat_id(chat_id);
let kind = thread
.map(|thread| thread.kind.clone())
.unwrap_or_else(|| chat_kind_for_id(chat_id));
let participants = state
.current_chat
.as_ref()
.filter(|chat| chat.chat_id == chat_id)
.map(|chat| chat.participants.clone())
.unwrap_or_else(|| fallback_chat_participants(state, thread, chat_id, &kind));
messages = messages
.into_iter()
.map(|mut message| {
if let Some(db) = shared_db {
direct_files::decorate(&mut message, state.account.as_ref(), db);
}
decorate_chat_page_message(
message,
chat_id,
&kind,
state.account.as_ref(),
&participants,
)
})
.collect();
let direct_chat_capability = state
.current_chat
.as_ref()
.filter(|chat| chat.chat_id == chat_id)
.and_then(|chat| chat.direct_chat_capability.clone());
Some(CurrentChatSnapshot {
contact_identity: state
.current_chat
.as_ref()
.filter(|chat| chat.chat_id == chat_id)
.and_then(|chat| chat.contact_identity.clone())
.or_else(|| contact_identity::contact_identity_from_db(state, shared_db, thread)),
social_connection: thread.and_then(|thread| thread.social_connection.clone()),
chat_id: chat_id.to_string(),
kind,
display_name: thread
.map(|thread| thread.display_name.clone())
.or_else(|| {
state
.account
.as_ref()
.filter(|account| account.public_key_hex == chat_id)
.map(|_| crate::search::NOTE_TO_SELF.to_string())
})
.unwrap_or_else(|| fallback_chat_title(chat_id)),
nickname: thread.and_then(|thread| thread.nickname.clone()),
contact_note: thread.and_then(|thread| thread.contact_note.clone()),
profile_name: thread.and_then(|thread| thread.profile_name.clone()),
subtitle: thread
.and_then(|thread| thread.subtitle.clone())
.or_else(|| group_id.as_ref().map(|_| "Group".to_string())),
picture_url: thread.and_then(|thread| thread.picture_url.clone()),
about: thread.and_then(|thread| thread.about.clone()),
group_id,
member_count: thread.map(|thread| thread.member_count).unwrap_or(0),
message_ttl_seconds: None,
is_muted: thread.map(|thread| thread.is_muted).unwrap_or(false),
participants,
messages,
typing_indicators: Vec::new(),
draft: thread
.map(|thread| thread.draft.clone())
.unwrap_or_default(),
is_request: thread.map(|thread| thread.is_request).unwrap_or(false),
direct_chat_capability,
})
}
fn load_chat_messages(
shared_db: Option<&SharedConnection>,
chat_id: &str,
request: ChatPageRequest<'_>,
state: &AppState,
) -> Option<Vec<ChatMessageSnapshot>> {
let Some(shared) = shared_db else {
return match request {
ChatPageRequest::Latest { .. } => Some(Vec::new()),
ChatPageRequest::Before { .. } | ChatPageRequest::Around { .. } => None,
};
};
let conn = match request {
ChatPageRequest::Around { .. } => shared.lock().ok()?,
_ => shared.try_lock().ok()?,
};
let preferences = &state.preferences;
let intervals_json = if is_group_chat_id(chat_id) {
state
.account
.as_ref()
.map(|account| storage::blocked_message_intervals(&conn, &account.public_key_hex))
.transpose()
.ok()?
} else {
None
};
let intervals = intervals_json.as_deref();
let hidden_authors_json = (preferences.hide_blocked_group_messages
&& is_group_chat_id(chat_id))
.then(|| serde_json::to_string(&preferences.blocked_owner_pubkeys).ok())
.flatten();
let hidden = hidden_authors_json.as_deref();
let result = match request {
ChatPageRequest::Latest { limit } => {
storage::load_recent_messages_with_visibility(&conn, chat_id, limit, hidden, intervals)
}
ChatPageRequest::Before {
before_message_id,
limit,
} => storage::load_messages_before_with_visibility(
&conn,
chat_id,
before_message_id,
limit,
hidden,
intervals,
),
ChatPageRequest::Around {
message_id,
before_limit,
after_limit,
} => storage::load_messages_around_with_visibility(
&conn,
chat_id,
message_id,
before_limit,
after_limit,
hidden,
intervals,
),
};
let messages = result
.map_err(|error| crate::perflog!("chat.page.read.failed error={error}"))
.ok()?;
Some(
messages
.iter()
.map(chats::chat_message_from_persisted)
.collect(),
)
}
fn fallback_chat_participants(
state: &AppState,
thread: Option<&ChatThreadSnapshot>,
chat_id: &str,
kind: &ChatKind,
) -> Vec<ChatParticipantSnapshot> {
let mut participants = Vec::new();
if let Some(account) = state.account.as_ref() {
participants.push(ChatParticipantSnapshot {
social_connection: None,
owner_pubkey_hex: account.public_key_hex.clone(),
display_name: account.display_name.trim().to_string(),
picture_url: account.picture_url.clone(),
is_local_owner: true,
});
}
if matches!(kind, ChatKind::Direct)
&& state
.account
.as_ref()
.is_none_or(|account| account.public_key_hex != chat_id)
{
participants.push(ChatParticipantSnapshot {
social_connection: thread.and_then(|thread| thread.social_connection.clone()),
owner_pubkey_hex: chat_id.to_string(),
display_name: thread
.map(|thread| thread.display_name.trim().to_string())
.filter(|name| !name.is_empty())
.unwrap_or_else(|| profile::fallback_profile_name_for_identity(chat_id)),
picture_url: thread.and_then(|thread| thread.picture_url.clone()),
is_local_owner: false,
});
}
participants
}
fn decorate_chat_page_message(
mut message: ChatMessageSnapshot,
chat_id: &str,
kind: &ChatKind,
account: Option<&AccountSnapshot>,
participants: &[ChatParticipantSnapshot],
) -> ChatMessageSnapshot {
let author_owner = message.author_owner_pubkey_hex.clone().or_else(|| {
if matches!(message.kind, ChatMessageKind::System) {
None
} else if message.is_outgoing {
account.map(|account| account.public_key_hex.clone())
} else if matches!(kind, ChatKind::Direct) {
Some(chat_id.to_string())
} else {
None
}
});
if let Some(owner) = author_owner {
if let Some(participant) = participant_for_owner(participants, &owner) {
message.author = participant.display_name.clone();
message.author_picture_url = participant.picture_url.clone();
}
message.author_owner_pubkey_hex = Some(owner);
}
message.recipient_deliveries = message
.recipient_deliveries
.into_iter()
.map(|delivery| decorate_chat_page_delivery(delivery, participants))
.collect();
message.reactors = message
.reactors
.into_iter()
.map(|reactor| decorate_chat_page_reactor(reactor, participants))
.collect();
message
}
fn decorate_chat_page_delivery(
mut delivery: MessageRecipientDeliverySnapshot,
participants: &[ChatParticipantSnapshot],
) -> MessageRecipientDeliverySnapshot {
if let Some(participant) = participant_for_owner(participants, &delivery.owner_pubkey_hex) {
delivery.display_name = participant.display_name.clone();
delivery.picture_url = participant.picture_url.clone();
} else if delivery.display_name.trim().is_empty() {
delivery.display_name =
profile::fallback_profile_name_for_identity(&delivery.owner_pubkey_hex);
}
delivery
}
fn decorate_chat_page_reactor(
mut reactor: MessageReactor,
participants: &[ChatParticipantSnapshot],
) -> MessageReactor {
if let Some(participant) = participant_for_owner(participants, &reactor.author) {
reactor.display_name = participant.display_name.clone();
reactor.picture_url = participant.picture_url.clone();
} else if reactor.display_name.trim().is_empty() {
reactor.display_name = profile::fallback_profile_name_for_identity(&reactor.author);
}
reactor
}
fn participant_for_owner<'a>(
participants: &'a [ChatParticipantSnapshot],
owner_pubkey_hex: &str,
) -> Option<&'a ChatParticipantSnapshot> {
participants
.iter()
.find(|participant| participant.owner_pubkey_hex == owner_pubkey_hex)
}
fn group_id_from_chat_id(chat_id: &str) -> Option<String> {
chat_id
.strip_prefix("group:")
.filter(|group_id| !group_id.trim().is_empty())
.map(ToString::to_string)
}
fn fallback_chat_title(chat_id: &str) -> String {
if is_group_chat_id(chat_id) {
return "Group".to_string();
}
let trimmed = chat_id.trim();
let boundary = trimmed
.char_indices()
.map(|(index, _)| index)
.nth(12)
.unwrap_or(trimmed.len());
if boundary < trimmed.len() {
format!("{}...", &trimmed[..boundary])
} else {
trimmed.to_string()
}
}
pub struct AppCore {
update_tx: Sender<AppUpdate>,
core_sender: Sender<CoreMsg>,
priority_sender: Sender<CoreMsg>,
shared_state: Arc<RwLock<AppState>>,
runtime: tokio::runtime::Runtime,
data_dir: PathBuf,
attachment_service: Option<attachment_upload::AttachmentService>,
state: AppState,
logged_in: Option<LoggedInState>,
private_contacts: private_contacts::PrivateContactRuntime,
protocol_engine: Option<ProtocolEngine>,
pub(crate) startup_restore: Arc<startup_restore::StartupRestoreControl>,
deferred_startup_restore: Option<AppAction>,
pending_linked_device: Option<PendingLinkedDeviceState>,
pending_signer_login: Option<account_signer::PendingSignerLogin>,
pending_remote_signer: Option<remote_signer::PendingRemoteSigner>,
pending_device_link_signer: Option<device_link_signer::PendingDeviceLinkSigner>,
device_approval_relay_urls: Vec<RelayUrl>,
private_chat_invites: BTreeMap<String, Invite>,
issued_invites: BTreeMap<String, issued_invites::IssuedInvite>,
pending_invite_upload: Option<(String, String)>,
open_invite_chat_on_join: bool,
pending_private_invite_responses: BTreeMap<String, invites::PendingPrivateInviteResponseV1>,
pending_private_invite_cleanup_retry: bool,
new_chat_notifications: BTreeMap<String, String>,
pending_private_invite_lookup: Option<(String, device_sync::InviteAcceptanceIntent)>,
pending_outgoing_invite_acceptance: Option<invites::PendingOutgoingInviteAcceptance>,
threads: BTreeMap<String, ThreadRecord>,
chat_deletions: BTreeMap<String, u64>,
chat_read_states: BTreeMap<String, ChatReadState>,
chat_mute_states: BTreeMap<String, ChatMuteState>,
chat_pin_states: BTreeMap<String, ChatPinState>,
private_blocks: BTreeMap<String, block_sync::PrivateBlockPolicy>,
private_block_revision: u64,
name_approvals: BTreeMap<String, contact_name_sync::NameApprovalPolicy>,
active_chat_id: Option<String>,
screen_stack: Vec<Screen>,
next_message_id: u64,
owner_profiles: BTreeMap<String, OwnerProfileRecord>,
profile_directory: profile_directory::ProfileDirectory,
profile_metadata_fetch_inflight: HashSet<String>,
app_keys: BTreeMap<String, KnownAppKeys>,
direct_chat_capability_runtime: DirectChatCapabilityRuntime,
pending_follow: Option<(String, String, String, bool)>,
social_graph: Option<nostr_social_graph::SocialGraph>,
user_discovery: UserDiscoveryCache,
user_discovery_runtime: UserDiscoveryRuntime,
profile_search_runtime: ProfileSearchRuntime,
user_discovery_revision: u64,
user_discovery_syncing: bool,
groups: BTreeMap<String, GroupSnapshot>,
group_pictures: BTreeMap<String, String>,
typing_indicators: BTreeMap<String, TypingIndicatorRecord>,
typing_floor_secs: BTreeMap<String, u64>,
chat_message_ttl_seconds: BTreeMap<String, u64>,
preferences: PreferencesSnapshot,
recent_handshake_peers: BTreeMap<String, RecentHandshakePeer>,
seen_event_ids: HashSet<String>,
seen_event_order: VecDeque<String>,
event_validation: relay::EventValidationCache,
timers: TimerTokens,
defer_owner_app_keys_publish: bool,
owner_registration_lookup_generation: Option<u64>,
current_device_labels: Option<CurrentDeviceLabels>,
discovery_relays: Arc<protocol::public_discovery::DiscoveryRelays>,
protocol_subscription_runtime: ProtocolSubscriptionRuntime,
relay_transport_runtime: RelayTransportRuntime,
relay_status: RelayStatusState,
relay_connected_count: u64,
all_relays_offline_since_secs: Option<u64>,
device_sync: Option<DeviceSyncRuntime>,
update_sources: UpdateSources,
fips_traffic_history: Arc<tokio::sync::Mutex<support::TrafficHistory>>,
calls: calls::CallRuntime,
pending_host_ble: Option<HostBleAttachment>,
host_ble_ownership: Option<HostBleOwnership>,
host_ble_attached: bool,
fips_nearby_links: Vec<crate::updates::FipsNearbyLinkSnapshot>,
fips_nearby_bootstrap: std::cell::RefCell<Option<fips_nearby::FipsNearbyBootstrapCache>>,
fips_connection_generation: u64,
#[cfg(test)]
test_fips_rendezvous_addr: Option<std::net::SocketAddrV4>,
#[cfg(test)]
test_fips_udp: Option<(std::net::SocketAddr, std::net::SocketAddr, String)>,
pending_relay_publishes: BTreeMap<String, PendingRelayPublish>,
publication_task_cancellations:
std::cell::RefCell<BTreeMap<String, std::sync::Weak<std::sync::atomic::AtomicBool>>>,
pending_relay_publish_inflight: HashSet<String>,
pending_decrypted_delivery_acks: HashSet<String>,
event_transport_channels: BTreeMap<String, String>,
pending_mobile_push_events: VecDeque<Event>,
debug_log: VecDeque<DebugLogEntry>,
fips_nearby_receive_diagnostics: fips_nearby::ReceiveDiagnostics,
debug_event_counters: DebugEventCounters,
debug_snapshot: DebugSnapshotState,
batch: BatchState,
pending_outgoing_receipts: BTreeMap<(String, String), Vec<String>>,
pending_delivered_receipts: BTreeMap<(String, String), Instant>,
pending_delivered_receipt_flush_due_at: Option<Instant>,
pending_delivered_receipt_token: u64,
last_emitted_state: Option<AppState>,
app_store: AppStore,
_data_dir_lock: DataDirLock,
cached_mobile_push: MobilePushSyncSnapshot,
mobile_push_dirty: bool,
suspended: bool,
ble_background: ble_background::BleBackground,
app_in_foreground: bool,
}
impl AppCore {
#[inline]
fn rebuild_persist_and_emit_state(&mut self) {
self.rebuild_state();
self.persist_best_effort();
self.emit_state();
}
}
async fn connect_client_with_timeout(client: &Client, timeout: Duration) {
client.connect().await;
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if connected_relay_count_for_client(client).await > 0 {
return;
}
sleep(Duration::from_millis(250)).await;
}
}
async fn connected_relay_count_for_client(client: &Client) -> usize {
client
.relays()
.await
.values()
.filter(|relay| relay.status() == RelayStatus::Connected)
.count()
}