@@ -81,7 +81,7 @@ module App =
81 | SendFailed of string
81 | SendFailed of string
83 | DoSaveContact
83 | DoSaveContact
84− | PollResult of events: IncomingEvent list * status: PollStatus * cursor: int64
84+ | PollResult of events: IncomingEvent list * ackIds: string list * status: PollStatus * cursor: int64
85 | CopyPubKey
85 | CopyPubKey
86 | DismissError
86 | DismissError
87 | StateLoaded of StateLoadResult
87 | StateLoaded of StateLoadResult
@@ -95,6 +95,7 @@ module App =
95 | CmdConnect of url: string * identity: Identity
95 | CmdConnect of url: string * identity: Identity
96 | CmdSend of session: Session * recipientHex: string * text: string * messageId: string
96 | CmdSend of session: Session * recipientHex: string * text: string * messageId: string
97 | CmdPoll of session: Session * cursor: int64
97 | CmdPoll of session: Session * cursor: int64
98+ | CmdAckAndDeliver of session: Session * ackIds: string list * events: IncomingEvent list
98 | CmdCopyToClipboard of string
99 | CmdCopyToClipboard of string
99 | CmdLoadState
100 | CmdLoadState
100 | CmdSaveIdentity of Identity
101 | CmdSaveIdentity of Identity
@@ -154,7 +155,7 @@ module App =
154 match evt.Envelope with
155 match evt.Envelope with
155 | Envelope.TextMessage(id, body) ->
156 | Envelope.TextMessage(id, body) ->
156 let message =
157 let message =
157− { Id = id; Body = body; Timestamp = DateTimeOffset.FromUnixTimeSeconds(evt.Timestamp)
158+ { Id = id; Body = body; Timestamp = DateTimeOffset.FromUnixTimeMilliseconds(evt.Timestamp)
158 IsOutgoing = false; Status = DeliveryStatus.Delivered }
159 IsOutgoing = false; Status = DeliveryStatus.Delivered }
160 Contacts =
161 Contacts =
@@ -221,7 +222,7 @@ module App =
221 match model.Auth with
222 match model.Auth with
222 | Identified(id, Connecting) ->
223 | Identified(id, Connecting) ->
223 let session = { Url = model.ServerUrl; Token = token; Identity = id }
224 let session = { Url = model.ServerUrl; Token = token; Identity = id }
224− { model with Auth = Identified(id, Online token); Page = Conversations; Error = None },
225+ { model with Auth = Identified(id, Online token); Page = Conversations; Error = None; PollStatus = Polling },
225 [ CmdPoll(session, model.PollCursor) ]
226 [ CmdPoll(session, model.PollCursor) ]
226 | _ -> model, []
227 | _ -> model, []
@@ -258,12 +259,15 @@ module App =
258 | SendFailed err -> { model with Error = Some err }, []
259 | SendFailed err -> { model with Error = Some err }, []
259 | SendOk -> model, []
260 | SendOk -> model, []
261− | PollResult(events, status, newCursor) ->
262+ | PollResult(events, ackIds, status, newCursor) ->
262 let model' = events |> List.fold applyEvent { model with PollStatus = status; PollCursor = newCursor }
263 let model' = events |> List.fold applyEvent { model with PollStatus = status; PollCursor = newCursor }
265 [ match trySession model' with
266 [ match trySession model' with
268+ if not ackIds.IsEmpty || not events.IsEmpty then
269+ CmdAckAndDeliver(session, ackIds, events)
266− | Some session -> CmdPoll(session, model'.PollCursor)
270+ CmdPoll(session, model'.PollCursor)
267 | None -> ()
271 | None -> ()
268 if not events.IsEmpty then saveCmdMsg model' ]
272 if not events.IsEmpty then saveCmdMsg model' ]
@@ -329,6 +333,8 @@ module App =
330 // ── mapCmd ──
334 // ── mapCmd ──
336+ let private log msg = eprintfn $"[skrepka] {msg}"
332 let private asyncCmd (op: Async<Msg>) : Cmd<Msg> =
338 let private asyncCmd (op: Async<Msg>) : Cmd<Msg> =
333 Cmd.ofEffect (fun dispatch ->
339 Cmd.ofEffect (fun dispatch ->
@@ -343,13 +349,13 @@ module App =
343 | Envelope.DeliveryAck ackIds ->
349 | Envelope.DeliveryAck ackIds ->
344 JsonSerializer.Serialize({| ``type`` = "delivery.ack"; id = Guid.NewGuid().ToString(); ack_ids = ackIds |})
350 JsonSerializer.Serialize({| ``type`` = "delivery.ack"; id = Guid.NewGuid().ToString(); ack_ids = ackIds |})
345 | Envelope.ProfileMessage profile ->
351 | Envelope.ProfileMessage profile ->
346− JsonSerializer.Serialize({| ``type`` = "profile"; id = Guid.NewGuid().ToString(); timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = profile.DisplayName; bio = profile.Bio; photo = profile.PhotoBase64 |})
352+ JsonSerializer.Serialize({| ``type`` = "profile"; id = Guid.NewGuid().ToString(); timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); display_name = profile.DisplayName; bio = profile.Bio; photo = profile.PhotoBase64 |})
348 let private sendEnvelope (session: Session) (recipientHex: string) (envelope: Envelope) =
354 let private sendEnvelope (session: Session) (recipientHex: string) (envelope: Envelope) =
350 let payload = serializeEnvelope envelope
356 let payload = serializeEnvelope envelope
351 let blob = Crypto.encrypt session.Identity.PrivKey (Crypto.fromHex recipientHex) payload
357 let blob = Crypto.encrypt session.Identity.PrivKey (Crypto.fromHex recipientHex) payload
352− let ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
358+ let ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
353 do! sendMessage session.Url session.Token recipientHex (Crypto.toHex blob) ts
359 do! sendMessage session.Url session.Token recipientHex (Crypto.toHex blob) ts
@@ -421,8 +427,11 @@ module App =
422 | CmdPoll(session, cursor) ->
428 | CmdPoll(session, cursor) ->
423 asyncCmd (async {
429 asyncCmd (async {
430+ log $"poll start cursor={cursor}"
425 let! response = poll session.Url session.Token cursor
432 let! response = poll session.Url session.Token cursor
433+ log $"poll response: {response.Events.Length} events, cursor={response.Cursor}"
426 let ackIds = response.Events |> Array.map _.Id |> Array.toList
435 let ackIds = response.Events |> Array.map _.Id |> Array.toList
428 let results =
437 let results =
@@ -430,30 +439,40 @@ module App =
430 |> Array.choose (fun evt ->
439 |> Array.choose (fun evt ->
431 match evt.EventType with
440 match evt.EventType with
432 | Message -> Some(decryptEvent session.Identity.PrivKey evt.Payload)
441 | Message -> Some(decryptEvent session.Identity.PrivKey evt.Payload)
433− | UnknownEvent _ -> None)
442+ | UnknownEvent s -> log $"unknown event type: {s}"; None)
435 let events = results |> Array.choose Result.toOption |> Array.toList
444 let events = results |> Array.choose Result.toOption |> Array.toList
436 let errors = results |> Array.choose (function Error e -> Some e | _ -> None) |> Array.toList
445 let errors = results |> Array.choose (function Error e -> Some e | _ -> None) |> Array.toList
438− if not ackIds.IsEmpty then
447+ for e in errors do log $"decrypt error: {e}"
439− try do! ackMessages session.Url session.Token ackIds
442− do! sendDeliveryAcks session events
449+ if response.Events.Length = 0 then
450+ do! Async.Sleep 5000
444 let chatCount = events |> List.sumBy (fun e -> match e.Envelope with Envelope.TextMessage _ -> 1 | _ -> 0)
452 let chatCount = events |> List.sumBy (fun e -> match e.Envelope with Envelope.TextMessage _ -> 1 | _ -> 0)
445 let status =
453 let status =
446 $"evts:{response.Events.Length} msgs:{chatCount} errs:{errors.Length}"
454 $"evts:{response.Events.Length} msgs:{chatCount} errs:{errors.Length}"
447 + (match List.tryHead errors with Some e -> $" [{e}]" | None -> "")
455 + (match List.tryHead errors with Some e -> $" [{e}]" | None -> "")
448− return PollResult(events, Received status, response.Cursor)
456+ return PollResult(events, ackIds, Received status, response.Cursor)
450 | :? TimeoutException ->
458 | :? TimeoutException ->
451− return PollResult([], Polling, cursor)
460+ return PollResult([], [], Polling, cursor)
462+ log $"poll error: {ex.Message}"
453 do! Async.Sleep 3000
463 do! Async.Sleep 3000
454− return PollResult([], PollError ex.Message, cursor)
464+ return PollResult([], [], PollError ex.Message, cursor)
467+ | CmdAckAndDeliver(session, ackIds, events) ->
468+ Cmd.ofEffect (fun _ ->
470+ if not ackIds.IsEmpty then
471+ try do! ackMessages session.Url session.Token ackIds
473+ do! sendDeliveryAcks session events
457 | CmdCopyToClipboard text ->
476 | CmdCopyToClipboard text ->
458 Cmd.ofEffect (fun _ ->
477 Cmd.ofEffect (fun _ ->
459 Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.Default.SetTextAsync(text)
478 Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.Default.SetTextAsync(text)
@@ -594,7 +613,8 @@ module App =
594 Button("Settings", SetPage Settings)
613 Button("Settings", SetPage Settings)
616+ let pubPrefix = match model.Auth with Identified(id, _) -> id.PubKeyHex.[..7] | _ -> "?"
597− Label($"{connStatusLabel model.Auth} | {formatPollStatus model.PollStatus}")
617+ Label($"{connStatusLabel model.Auth} [{pubPrefix}] | {formatPollStatus model.PollStatus}")
598 .font(size = 10.)
618 .font(size = 10.)
599 .textColor(Colors.DimGray)
619 .textColor(Colors.DimGray)