@@ -70,7 +70,7 @@ module App =
7070 | DoSend
7171 | SendFailed of string
7272 | DoSaveContact
73− | PollResult of events: IncomingEvent list * ackIds: string list * status: string * cursor: int64
73+ | PollResult of events: IncomingEvent list * status: string * cursor: int64
7474 | CopyPubKey
7575 | DismissError
7676 | StateLoaded of Identity option * Data option * Profile option
@@ -84,14 +84,12 @@ module App =
8484 | CmdConnect of url: string * identity: Identity
8585 | CmdSend of session: Session * recipientHex: string * envelope: Envelope
8686 | CmdPoll of session: Session * cursor: int64
87− | CmdAckAndDeliver of session: Session * ackIds: string list * events: IncomingEvent list
8887 | CmdCopyToClipboard of string
8988 | CmdLoadState
9089 | CmdSaveIdentity of Identity
9190 | CmdSaveData of Data
9291 | CmdPickPhoto
9392 | CmdSaveProfile of Profile
94− | CmdSendProfileToAll of session: Session * Profile * recipientHexes: string list
9593
9694 // ── Helpers ──
9795
@@ -127,7 +125,9 @@ module App =
127125 let private markDelivered (pk: string) (ackIds: string list) (messages: Map<string, ChatMessage list>) =
128126 let ackSet = Set.ofList ackIds
129127 messages |> Map.change pk (Option.map (List.map (fun m ->
130− if m.IsOutgoing && Set.contains m.Id ackSet then { m with Status = DeliveryStatus.Delivered } else m)))
128+ match m.Direction with
129+ | Outgoing _ when Set.contains m.Id ackSet -> { m with Direction = Outgoing DeliveryStatus.Delivered }
130+ | _ -> m)))
131131
132132 let private newContact pubkey nickname : Contact =
133133 { Pubkey = pubkey; Nickname = nickname; DisplayName = ""; Bio = ""; PhotoBase64 = "" }
@@ -139,8 +139,7 @@ module App =
139139 match evt.Envelope with
140140 | Envelope.TextMessage(id, body) ->
141141 let message =
142− { Id = id; Body = body; Timestamp = evt.Timestamp
143− IsOutgoing = false; Status = DeliveryStatus.Delivered }
142+ { Id = id; Body = body; Timestamp = evt.Timestamp; Direction = Incoming }
144143 { m with
145144 Contacts =
146145 if Map.containsKey evt.Sender m.Contacts then m.Contacts
@@ -222,8 +221,7 @@ module App =
222221 { Id = id
223222 Body = compose
224223 Timestamp = DateTimeOffset.UtcNow
225− IsOutgoing = true
226− Status = DeliveryStatus.Sent }
224+ Direction = Outgoing DeliveryStatus.Sent }
227225
228226 let isFirstInteraction = model.Messages |> messagesFor pk |> List.isEmpty
229227
@@ -235,22 +233,19 @@ module App =
235233 model',
236234 [ CmdSend(session, pk, Envelope.TextMessage(id, compose))
237235 saveCmdMsg model'
238− match model.Profile, isFirstInteraction with
239− | Some profile, true -> CmdSendProfileToAll(session, profile, [ pk ])
236+ match model.Profile with
237+ | Some profile when isFirstInteraction -> CmdSend(session, pk, Envelope.ProfileMessage profile)
240238 | _ -> () ]
241239 | _ -> model, []
242240
243241 | SendFailed err -> { model with Error = Some err }, []
244242
245− | PollResult(events, ackIds, status, newCursor) ->
243+ | PollResult(events, status, newCursor) ->
246244 let model' = events |> List.fold applyEvent { model with PollStatus = status; PollCursor = newCursor }
247245
248246 model',
249247 [ match trySession model' with
250− | Some session ->
251− if not ackIds.IsEmpty || not events.IsEmpty then
252− CmdAckAndDeliver(session, ackIds, events)
253− CmdPoll(session, model'.PollCursor)
248+ | Some session -> CmdPoll(session, model'.PollCursor)
254249 | None -> ()
255250 if not events.IsEmpty then saveCmdMsg model' ]
256251
@@ -295,7 +290,9 @@ module App =
295290 { model with Profile = Some profile; Page = Settings },
296291 [ CmdSaveProfile profile
297292 match trySession model with
298− | Some session -> CmdSendProfileToAll(session, profile, model.Contacts.Keys |> Seq.toList)
293+ | Some session ->
294+ for pk in model.Contacts.Keys do
295+ CmdSend(session, pk, Envelope.ProfileMessage profile)
299296 | None -> () ]
300297 | _ -> model, []
301298
@@ -375,19 +372,6 @@ module App =
375372 with ex ->
376373 Error $"parse: {ex.Message}"
377374
378− let private sendDeliveryAcks (session: Session) (events: IncomingEvent list) =
379− async {
380− let chatsBySender =
381− events
382− |> List.choose (fun evt -> match evt.Envelope with Envelope.TextMessage(id, _) -> Some(evt.Sender, id) | _ -> None)
383− |> List.groupBy fst
384− |> List.map (fun (sender, pairs) -> sender, List.map snd pairs)
385−
386− for sender, msgIds in chatsBySender do
387− try do! sendEnvelope session sender (Envelope.DeliveryAck msgIds)
388− with _ -> ()
389− }
390−
391375 let mapCmd cmdMsg =
392376 match cmdMsg with
393377 | CmdConnect(url, identity) ->
@@ -427,6 +411,22 @@ module App =
427411
428412 for e in errors do log $"decrypt error: {e}"
429413
414+ // Fire-and-forget: ack server + send delivery acks
415+ if not ackIds.IsEmpty || not events.IsEmpty then
416+ async {
417+ if not ackIds.IsEmpty then
418+ try do! ackMessages session.Url session.Token ackIds
419+ with _ -> ()
420+ let acksBySender =
421+ events
422+ |> List.choose (fun evt -> match evt.Envelope with Envelope.TextMessage(id, _) -> Some(evt.Sender, id) | _ -> None)
423+ |> List.groupBy fst
424+ |> List.map (fun (sender, pairs) -> sender, List.map snd pairs)
425+ for sender, msgIds in acksBySender do
426+ try do! sendEnvelope session sender (Envelope.DeliveryAck msgIds)
427+ with _ -> ()
428+ } |> Async.Start
429+
430430 if response.Events.Length = 0 then
431431 do! Async.Sleep 5000
432432
@@ -434,26 +434,17 @@ module App =
434434 let status =
435435 $"evts:{response.Events.Length} msgs:{chatCount} errs:{errors.Length}"
436436 + (match List.tryHead errors with Some e -> $" [{e}]" | None -> "")
437− return PollResult(events, ackIds, status, response.Cursor)
437+ return PollResult(events, status, response.Cursor)
438438 with
439439 | :? TimeoutException ->
440440 log "poll timeout"
441− return PollResult([], [], "polling...", cursor)
441+ return PollResult([], "polling...", cursor)
442442 | ex ->
443443 log $"poll error: {ex.Message}"
444444 do! Async.Sleep 3000
445− return PollResult([], [], $"poll error: {ex.Message}", cursor)
445+ return PollResult([], $"poll error: {ex.Message}", cursor)
446446 })
447447
448− | CmdAckAndDeliver(session, ackIds, events) ->
449− Cmd.ofEffect (fun _ ->
450− async {
451− if not ackIds.IsEmpty then
452− try do! ackMessages session.Url session.Token ackIds
453− with _ -> ()
454− do! sendDeliveryAcks session events
455− } |> Async.Start)
456−
457448 | CmdCopyToClipboard text ->
458449 Cmd.ofEffect (fun _ ->
459450 Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.Default.SetTextAsync(text)
@@ -492,14 +483,6 @@ module App =
492483 | CmdSaveProfile profile ->
493484 Cmd.ofEffect (fun _ -> Store.saveProfile profile)
494485
495− | CmdSendProfileToAll(session, profile, recipientHexes) ->
496− Cmd.ofEffect (fun _ ->
497− async {
498− for recipientHex in recipientHexes do
499− try do! sendEnvelope session recipientHex (Envelope.ProfileMessage profile)
500− with _ -> ()
501− } |> Async.Start)
502−
503486 // ── View ──
504487
505488 let private viewErrorBanner (error: string option) =
@@ -514,6 +497,8 @@ module App =
514497 let private viewSetup () =
515498 ContentPage(
516499 (VStack(spacing = 24.) {
500+ Image("logo.png")
501+ .margin(8, 0)
517502 Label("Skrepka")
518503 .font(size = 32.)
519504 .centerTextHorizontal()
@@ -637,8 +622,12 @@ module App =
637622 .font(size = 14.)
638623 else
639624 for m in msgs do
640− let tick = if m.IsOutgoing && m.Status = DeliveryStatus.Delivered then " \u2713" else ""
641− Label((if m.IsOutgoing then $"You: {m.Body}" else m.Body) + tick)
625+ let prefix, tick =
626+ match m.Direction with
627+ | Outgoing DeliveryStatus.Delivered -> "You: ", " \u2713"
628+ | Outgoing _ -> "You: ", ""
629+ | Incoming -> "", ""
630+ Label($"{prefix}{m.Body}{tick}")
642631 .padding(8.)
643632 .font(size = 14.)
644633