@@ -63,7 +63,8 @@ module App =
63 Messages: Map<string, ChatMessage list>
63 Messages: Map<string, ChatMessage list>
64 Error: string option
64 Error: string option
65 PollCursor: int64
65 PollCursor: int64
66− Profile: Profile option }
66+ Profile: Profile option
67+ FlushingOutbox: bool }
68 // ── Msg ──
69 // ── Msg ──
@@ -76,7 +77,6 @@ module App =
76 | AuthErr of string
77 | AuthErr of string
77 | DoDisconnect
78 | DoDisconnect
79− | SendFailed of string
80 | DoSaveContact
80 | DoSaveContact
81 | PollResult of events: IncomingEvent list * status: string * cursor: int64
81 | PollResult of events: IncomingEvent list * status: string * cursor: int64
82 | CopyPubKey
82 | CopyPubKey
@@ -85,12 +85,15 @@ module App =
85 | DoPickPhoto
85 | DoPickPhoto
86 | PhotoPicked of byte[] option
86 | PhotoPicked of byte[] option
87 | DoSaveProfile
87 | DoSaveProfile
89+ | FlushResult of sent: bool
89 // ── CmdMsg ──
91 // ── CmdMsg ──
91 type CmdMsg =
93 type CmdMsg =
92 | CmdConnect of url: string * identity: Identity
94 | CmdConnect of url: string * identity: Identity
93− | CmdSend of session: Session * recipientHex: string * envelope: Envelope
95+ | CmdEnqueue of recipientHex: string * envelope: Envelope
96+ | CmdFlushOutbox of session: Session
94 | CmdPoll of session: Session * cursor: int64
97 | CmdPoll of session: Session * cursor: int64
95 | CmdCopyToClipboard of string
98 | CmdCopyToClipboard of string
96 | CmdLoadState
99 | CmdLoadState
@@ -228,7 +231,8 @@ module App =
228 Messages = Map.empty
231 Messages = Map.empty
229 Error = None
232 Error = None
230 PollCursor = 0L
233 PollCursor = 0L
231− Profile = None },
234+ Profile = None
235+ FlushingOutbox = false },
232 [ CmdLoadState ]
236 [ CmdLoadState ]
234 // ── Update ──
238 // ── Update ──
@@ -267,23 +271,27 @@ module App =
267 Auth = Identified(id, Online token)
271 Auth = Identified(id, Online token)
268 Page = Conversations
272 Page = Conversations
269 Error = None
273 Error = None
270− PollStatus = "polling..." },
274+ PollStatus = "polling..."
275+ FlushingOutbox = true },
271− [ CmdPoll(session, model.PollCursor) ]
276+ [ CmdPoll(session, model.PollCursor)
277+ CmdFlushOutbox session ]
272 | _ -> model, []
278 | _ -> model, []
274 | AuthErr err ->
280 | AuthErr err ->
275 { setConn Offline model with
281 { setConn Offline model with
276− Error = Some err },
282+ Error = Some err
283+ FlushingOutbox = false },
279 | DoDisconnect ->
286 | DoDisconnect ->
280 { setConn Offline model with
287 { setConn Offline model with
281− PollStatus = "" },
288+ PollStatus = ""
289+ FlushingOutbox = false },
284 | DoSend ->
292 | DoSend ->
285− match model.Page, trySession model with
293+ match model.Page with
286− | Chat(pk, compose), Some session when compose <> "" ->
294+ | Chat(pk, compose) when compose <> "" ->
287 let id = Guid.NewGuid().ToString()
295 let id = Guid.NewGuid().ToString()
289 let outMsg =
297 let outMsg =
@@ -300,14 +308,30 @@ module App =
300 Messages = model.Messages |> appendMessage pk outMsg }
308 Messages = model.Messages |> appendMessage pk outMsg }
303− [ CmdSend(session, pk, Envelope.TextMessage(id, compose))
311+ [ CmdEnqueue(pk, Envelope.TextMessage(id, compose))
305 match model.Profile with
312 match model.Profile with
306− | Some profile when isFirstInteraction -> CmdSend(session, pk, Envelope.ProfileMessage profile)
313+ | Some profile when isFirstInteraction -> CmdEnqueue(pk, Envelope.ProfileMessage profile)
307− | _ -> () ]
314+ | _ -> ()
308 | _ -> model, []
316 | _ -> model, []
310− | SendFailed err -> { model with Error = Some err }, []
318+ | StartFlush ->
319+ if model.FlushingOutbox then
322+ match trySession model with
324+ { model with FlushingOutbox = true },
325+ [ CmdFlushOutbox session ]
328+ | FlushResult sent ->
330+ match trySession model with
331+ | Some session -> model, [ CmdFlushOutbox session ]
332+ | None -> { model with FlushingOutbox = false }, []
334+ { model with FlushingOutbox = false }, []
312 | PollResult(events, status, newCursor) ->
336 | PollResult(events, status, newCursor) ->
313 let model' =
337 let model' =
@@ -379,11 +403,8 @@ module App =
379 Profile = Some profile
403 Profile = Some profile
380 Page = Settings },
404 Page = Settings },
381 [ CmdSaveProfile profile
405 [ CmdSaveProfile profile
382− match trySession model with
384− for pk in model.Contacts.Keys do
406+ for pk in model.Contacts.Keys do
385− CmdSend(session, pk, Envelope.ProfileMessage profile)
407+ CmdEnqueue(pk, Envelope.ProfileMessage profile) ]
387 | _ -> model, []
408 | _ -> model, []
389 | StateLoaded(Some identity, Some data, profile) ->
410 | StateLoaded(Some identity, Some data, profile) ->
@@ -591,9 +612,33 @@ module App =
591 Cmd.OfAsync.either (fun (url, id) -> authenticate url id) (url, identity) AuthOk (fun ex ->
612 Cmd.OfAsync.either (fun (url, id) -> authenticate url id) (url, identity) AuthOk (fun ex ->
592 AuthErr ex.Message)
613 AuthErr ex.Message)
594− | CmdSend(session, recipientHex, envelope) ->
615+ | CmdEnqueue(recipientHex, envelope) ->
595− Cmd.OfAsync.attempt (fun (s, r, e) -> sendEnvelope s r e) (session, recipientHex, envelope) (fun ex ->
616+ Cmd.ofEffect (fun dispatch ->
596− SendFailed ex.Message)
617+ let json = serializeEnvelope envelope
618+ Store.enqueueOutbox recipientHex json
619+ dispatch StartFlush)
621+ | CmdFlushOutbox session ->
622+ Cmd.ofEffect (fun dispatch ->
624+ match Store.peekOutbox () with
628+ Crypto.encrypt session.Identity.PrivKey (Crypto.fromHex item.RecipientHex) item.EnvelopeJson
630+ let ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
631+ do! sendMessage session.Url session.Token item.RecipientHex (Crypto.toHex blob) ts
632+ Store.dequeueOutbox item.Id
633+ dispatch (FlushResult true)
635+ log $"outbox send error: {ex.Message}"
636+ do! Async.Sleep 3000
637+ dispatch (FlushResult true)
639+ dispatch (FlushResult false)
598 | CmdPoll(session, cursor) ->
643 | CmdPoll(session, cursor) ->
599 Cmd.ofEffect (fun dispatch ->
644 Cmd.ofEffect (fun dispatch ->