@@ -43,6 +43,15 @@ module App =
4343 | Connecting
4444 | Online of Session
4545
46+ type PollResultData =
47+ { Messages: IncomingMessage list
48+ Status: string
49+ Cursor: uint64 }
50+
51+ type LoadedState =
52+ { Identity: Identity option
53+ Data: Store.DataDto option }
54+
4655 // ── Model ──
4756
4857 type Model =
@@ -78,11 +87,11 @@ module App =
7887 | SetNewNickname of string
7988 | DoSaveContact
8089 | DoDeleteContact of string
81− | PollResult of messages: IncomingMessage list * status: string * cursor: uint64
90+ | PollResult of PollResultData
8291 | DoPoll
8392 | CopyPubKey
8493 | DismissError
85− | StateLoaded of Identity option * Store.DataDto option
94+ | StateLoaded of LoadedState
8695
8796 // ── CmdMsg ──
8897
@@ -110,6 +119,9 @@ module App =
110119 let private messagesFor (pk: string) (messages: Map<string, ChatMessage list>) =
111120 messages |> Map.tryFind pk |> Option.defaultValue []
112121
122+ let private appendMessage (pk: string) (msg: ChatMessage) (messages: Map<string, ChatMessage list>) =
123+ messages |> Map.add pk (messagesFor pk messages @ [ msg ])
124+
113125 let private addContactIfNew (contact: Contact) (contacts: Contact list) =
114126 if contacts |> List.exists (fun c -> c.Pubkey = contact.Pubkey) then contacts
115127 else contacts @ [ contact ]
@@ -194,26 +206,23 @@ module App =
194206 Body = text
195207 Timestamp = DateTimeOffset.UtcNow
196208 IsOutgoing = true }
197− let msgs = messagesFor pk model.Messages
198209
199210 let model' =
200211 { model with
201212 ComposeText = ""
202− Messages = model.Messages |> Map.add pk (msgs @ [ outMsg ]) }
213+ Messages = model.Messages |> appendMessage pk outMsg }
203214
204215 model', CmdSend(session, pk, text, id) :: saveCmd model'
205216 | _ -> model, []
206217
207218 | Sent status -> { model with LastSendStatus = status }, []
208219
209− | PollResult(incoming, status, newCursor) ->
220+ | PollResult { Messages = incoming; Status = status; Cursor = newCursor } ->
210221 let model' =
211222 incoming
212223 |> List.fold
213224 (fun m msg ->
214− let msgs = messagesFor msg.FromPubkey m.Messages
215−
216− if msgs |> List.exists (fun x -> x.Id = msg.MessageId) then
225+ if messagesFor msg.FromPubkey m.Messages |> List.exists (fun x -> x.Id = msg.MessageId) then
217226 m
218227 else
219228 let chatMsg =
@@ -224,7 +233,7 @@ module App =
224233
225234 { m with
226235 Contacts = m.Contacts |> addContactIfNew { Pubkey = msg.FromPubkey; Nickname = truncKey msg.FromPubkey }
227− Messages = m.Messages |> Map.add msg.FromPubkey (msgs @ [ chatMsg ]) })
236+ Messages = m.Messages |> appendMessage msg.FromPubkey chatMsg })
228237 { model with PollStatus = status; PollCursor = newCursor }
229238
230239 let cmds =
@@ -273,10 +282,12 @@ module App =
273282
274283 | DismissError -> { model with Error = None }, []
275284
276− | StateLoaded(identityOpt, dataOpt) ->
285+ | StateLoaded { Identity = identityOpt; Data = dataOpt } ->
277286 match identityOpt with
278287 | None -> model, []
279288 | Some identity ->
289+ let model' = { model with Identity = Some identity; Conn = Connecting }
290+
280291 let model' =
281292 match dataOpt with
282293 | Some data ->
@@ -302,22 +313,14 @@ module App =
302313 acc |> Map.add pk msgs)
303314 Map.empty
304315
305− { model with
306− Identity = Some identity
316+ { model' with
307317 Contacts = contacts
308318 Messages = messages
309− ServerUrl =
310− if data.ServerUrl <> "" then data.ServerUrl
311− else model.ServerUrl
319+ ServerUrl = if data.ServerUrl <> "" then data.ServerUrl else model.ServerUrl
312320 PollCursor = data.PollCursor
313− Page = Conversations
314− Conn = Connecting }
321+ Page = Conversations }
315322
316− | None ->
317− { model with
318− Identity = Some identity
319− Page = Settings
320− Conn = Connecting }
323+ | None -> { model' with Page = Settings }
321324
322325 model', [ CmdConnect(model'.ServerUrl, identity) ]
323326
@@ -366,12 +369,12 @@ module App =
366369 try
367370 let! challenge = ApiClient.requestChallenge url identity.PubKeyHex
368371 let sigHex = Crypto.signChallenge identity.PrivKey challenge
369− let! (token, _) = ApiClient.verify url identity.PubKeyHex challenge sigHex
372+ let! result = ApiClient.verify url identity.PubKeyHex challenge sigHex
370373
371− if String.IsNullOrEmpty(token) then
374+ if String.IsNullOrEmpty(result.Token) then
372375 return AuthErr "Authentication rejected by server"
373376 else
374− return AuthOk token
377+ return AuthOk result.Token
375378 with ex ->
376379 return AuthErr ex.Message
377380 })
@@ -379,17 +382,16 @@ module App =
379382 | CmdSend(session, recipientHex, text, messageId) ->
380383 asyncCmd (async {
381384 try
382− let pubKey = Crypto.fromHex session.Identity.PubKeyHex
383385 let recipPub = Crypto.fromHex recipientHex
384386 let ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
385387
386388 let payload =
387389 $"""{{ "type": "text", "id": "{messageId}", "timestamp": {ts}, "body": {JsonSerializer.Serialize(text)} }}"""
388390
389− let blob = Crypto.encrypt session.Identity.PrivKey pubKey recipPub payload
391+ let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
390392 let blobHex = Crypto.toHex blob
391− let! (status, msgId) = ApiClient.sendMessage session.Url session.Token recipientHex blobHex ts
392− return Sent $"{status}:{msgId}"
393+ let! result = ApiClient.sendMessage session.Url session.Token recipientHex blobHex ts
394+ return Sent $"{result.Status}:{result.MessageId}"
393395 with ex ->
394396 return DismissError // TODO: show send errors properly
395397 })
@@ -397,15 +399,13 @@ module App =
397399 | CmdPoll(session, cursor) ->
398400 asyncCmd (async {
399401 try
400− let! (events, newCursor) = ApiClient.poll session.Url session.Token cursor
402+ let! response = ApiClient.poll session.Url session.Token cursor
401403 let messages, ackIds, errors =
402− events
404+ response.Events
403405 |> List.fold (fun (msgs, acks, errs) evt ->
404− if evt.EventType <> "message" then (msgs, acks, errs)
405− else
406− match decryptEvent session.Identity.PrivKey evt.Payload with
407− | Ok msg -> (msg :: msgs, evt.Id :: acks, errs)
408− | Error err -> (msgs, acks, err :: errs))
406+ match decryptEvent session.Identity.PrivKey evt.Payload with
407+ | Ok msg -> (msg :: msgs, evt.Id :: acks, errs)
408+ | Error err -> (msgs, acks, err :: errs))
409409 ([], [], [])
410410
411411 if not ackIds.IsEmpty then
@@ -414,17 +414,17 @@ module App =
414414 let now = DateTimeOffset.UtcNow.ToString("HH:mm:ss")
415415
416416 let status =
417− $"[{now}] evts:{events.Length} msgs:{messages.Length} errs:{errors.Length}"
417+ $"[{now}] evts:{response.Events.Length} msgs:{messages.Length} errs:{errors.Length}"
418418 + (match errors with err :: _ -> $" [{err}]" | [] -> "")
419419
420− return PollResult(List.rev messages, status, newCursor)
420+ return PollResult { Messages = List.rev messages; Status = status; Cursor = response.Cursor }
421421 with
422422 | :? TimeoutException ->
423− return PollResult([], "polling...", cursor)
423+ return PollResult { Messages = []; Status = "polling..."; Cursor = cursor }
424424 | ex ->
425425 let now = DateTimeOffset.UtcNow.ToString("HH:mm:ss")
426426 do! Async.Sleep 3000
427− return PollResult([], $"[{now}] poll error: {ex.Message}", cursor)
427+ return PollResult { Messages = []; Status = $"[{now}] poll error: {ex.Message}"; Cursor = cursor }
428428 })
429429
430430 | CmdCopyToClipboard text ->
@@ -436,7 +436,7 @@ module App =
436436 asyncCmd (async {
437437 let! identity = Store.loadIdentity ()
438438 let data = Store.loadData ()
439− return StateLoaded(identity, data)
439+ return StateLoaded { Identity = identity; Data = data }
440440 })
441441
442442 | CmdSaveIdentity identity ->