1//! Domain types: persisted state (stored in the kv capability as JSON) and the
2//! `ViewModel` types the Swift shell renders.
7use serde::{Deserialize, Serialize};
11// ---------------------------------------------------------------------------
12// Persisted state (serialized to the kv store as JSON)
13// ---------------------------------------------------------------------------
15pub const DEFAULT_SERVER_URL: &str = "http://localhost:8080";
17/// Cap on locally retained messages per conversation (PROTOCOL.md §9: clients
18/// SHOULD age out local message history). Both the in-memory `Vec` and the
19/// `messages:<peer>` kv blob are trimmed to this many most-recent messages.
20pub const MAX_MESSAGES_PER_PEER: usize = 1000;
22/// Every persisted struct is `#[serde(default)]`.
24/// A kv blob that fails to deserialize is indistinguishable from an absent key
25/// (see `app::parse_kv`), and the next mutation overwrites it — so a field added
26/// in a later release would silently wipe the user's contacts, outbox, or
27/// history on first launch. Defaulting the missing fields keeps old blobs
28/// loadable across schema evolution instead.
29#[derive(Serialize, Deserialize, Clone, Debug)]
32 pub server_url: String,
35impl Default for Settings {
36 fn default() -> Self {
38 server_url: DEFAULT_SERVER_URL.to_string(),
43#[derive(Serialize, Deserialize, Clone, Debug, Default)]
45pub struct OwnProfile {
46 pub display_name: String,
48 /// base64-encoded image, or `None`.
49 pub photo: Option<String>,
52#[derive(Serialize, Deserialize, Clone, Debug, Default)]
57 pub display_name: String,
59 pub photo: Option<String>,
61 /// `ts` of the last applied profile message (drop older replays).
62 pub last_profile_ts: i64,
63 /// `ts` of the last applied delivery.ack (drop older replays).
69 pub fn new(pubkey: String, nickname: String, added_at: i64) -> Self {
73 display_name: String::new(),
83 /// Best human label: nickname, else shared display name, else truncated @p.
84 pub fn label(&self) -> String {
85 if !self.nickname.is_empty() {
87 } else if !self.display_name.is_empty() {
88 self.display_name.clone()
90 trunc_ob(&self.pubkey)
95#[derive(Serialize, Deserialize, Clone, Debug, Default)]
97pub struct StoredMessage {
102 /// Only meaningful for outgoing messages.
106/// Transient send failures one outbox item may absorb before it is given up on.
108/// Without a ceiling the head of the outbox is retried forever, and because the
109/// outbox is a strict FIFO an item that can never be delivered blocks every
110/// message queued behind it — permanently.
111pub const MAX_OUTBOX_RETRIES: u32 = 10;
113/// ...and a wall-clock bound on the same thing, for the case where the retries
114/// come in slowly enough that the counter alone would take days to run out. 24h.
115pub const OUTBOX_TTL_MS: i64 = 86_400_000;
117/// A queued outbound payload awaiting encryption + send.
118#[derive(Serialize, Deserialize, Clone, Debug, Default)]
120pub struct OutboxItem {
121 pub recipient: String,
122 /// The serialized plaintext payload JSON (encrypted only at send time).
123 pub envelope_json: Arc<String>,
124 /// `Some("profile")` for a profile broadcast, `None` for everything else.
126 /// A profile is *state*, not an event: only the latest one matters, so
127 /// `SaveProfile` overwrites the pending item for a recipient rather than
128 /// queueing another. Without the marker there is nothing to overwrite by, and
129 /// five quick profile edits mean five payloads (photo and all) per contact.
130 pub kind: Option<String>,
131 /// Transient send failures this item has absorbed so far.
133 /// `now_ms()` when this item was first put on the wire, or `0` if it never
134 /// has been. The TTL runs from here, so an item queued during a week offline
135 /// still gets its full retry budget once the network comes back.
136 pub first_attempt: i64,
140 pub fn new(recipient: String, envelope_json: Arc<String>) -> Self {
150 /// A profile broadcast — the one payload kind that supersedes rather than
151 /// accumulates (see `kind`).
152 pub fn profile(recipient: String, envelope_json: Arc<String>) -> Self {
154 kind: Some("profile".to_string()),
155 ..OutboxItem::new(recipient, envelope_json)
159 pub fn is_profile(&self) -> bool {
160 self.kind.as_deref() == Some("profile")
163 /// Has this item exhausted its retry budget or outlived the TTL?
165 /// `first_attempt == 0` means it has never been sent (or was queued by a build
166 /// that predates the field), so the TTL has nothing to measure from and only
167 /// the counter applies.
168 pub fn is_expired(&self, now: i64) -> bool {
169 self.retries >= MAX_OUTBOX_RETRIES
170 || (self.first_attempt > 0 && now.saturating_sub(self.first_attempt) > OUTBOX_TTL_MS)
174// ---------------------------------------------------------------------------
176// ---------------------------------------------------------------------------
178/// Full @p rendering of a hex pubkey. Falls back to the input for anything that
179/// isn't an even-length hex string — the @p encoding is only defined on byte
180/// *pairs*, so an odd-length input has no syllable spelling at all.
181pub fn hex_to_ob(hex_key: &str) -> String {
184 .and_then(|bytes| phonemic::to_ob(&bytes))
185 .unwrap_or_else(|| hex_key.to_string())
188/// Truncated @p: first two and last two syllables (`a-b-…-y-z`).
189pub fn trunc_ob(hex_key: &str) -> String {
190 let ob = hex_to_ob(hex_key);
191 let parts: Vec<&str> = ob.split('-').collect();
197 parts[parts.len() - 2],
198 parts[parts.len() - 1]
205// ---------------------------------------------------------------------------
206// ViewModel (rendered by the shell)
207// ---------------------------------------------------------------------------
209#[derive(Facet, Serialize, Deserialize, Clone, Default, Debug)]
210pub struct ProfileVM {
211 pub display_name: String,
213 /// base64 image or empty.
217#[derive(Facet, Serialize, Deserialize, Clone, Default, Debug)]
218pub struct ContactVM {
224 pub last_message: String,
228#[derive(Facet, Serialize, Deserialize, Clone, Default, Debug)]
229pub struct MessageVM {
237#[derive(Facet, Serialize, Deserialize, Clone, Default, Debug)]
238pub struct ViewModel {
239 /// "conversations" | "chat" | "add_contact" | "settings" | "edit_profile"
241 pub has_identity: bool,
242 pub my_pubkey_hex: String,
243 pub my_pubkey_ob: String,
244 /// "offline" | "connecting" | "online"
245 pub conn_status: String,
246 pub server_url: String,
247 pub profile: ProfileVM,
248 pub contacts: Vec<ContactVM>,
249 pub active_peer: String,
250 pub active_peer_name: String,
251 pub active_peer_ob: String,
252 pub active_peer_blocked: bool,
253 pub messages: Vec<MessageVM>,
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list