@@ -24,7 +24,7 @@ module App =
2424 | Chat of pubkey: string * compose: string
2525 | AddContact of pubkey: string * nickname: string
2626 | Settings
27− | EditProfile of displayName: string * bio: string * photo: string option
27+ | EditProfile of displayName: string * bio: string * photo: string
2828
2929 type Session = { Url: string; Token: string; Identity: Identity }
3030
@@ -40,7 +40,7 @@ module App =
4040 type DecryptedEvent =
4141 | IncomingChat of senderPubkey: string * message: ChatMessage
4242 | IncomingDeliveryAck of senderPubkey: string * ackIds: string list
43− | IncomingProfile of senderPubkey: string * displayName: string * bio: string * photo: string option
43+ | IncomingProfile of senderPubkey: string * displayName: string * bio: string * photo: string
4444
4545 type PollReceived = { Events: int; Messages: int; Errors: int; FirstError: string option }
4646
@@ -56,7 +56,7 @@ module App =
5656 { Page: Page
5757 Auth: AuthState
5858 ServerUrl: string
59− Contacts: Contact list
59+ Contacts: Map<string, Contact>
6060 PollStatus: PollStatus
6161 Messages: Map<string, ChatMessage list>
6262 Error: string option
@@ -92,7 +92,7 @@ module App =
9292 | SetProfileDisplayName of string
9393 | SetProfileBio of string
9494 | DoPickPhoto
95− | PhotoPicked of string option
95+ | PhotoPicked of string
9696 | DoSaveProfile
9797
9898 // ── CmdMsg ──
@@ -130,10 +130,10 @@ module App =
130130 |> Option.orElseWith (fun () -> Crypto.tryFromHex input)
131131 |> Option.filter (fun bytes -> bytes.Length = 32)
132132
133− let private contactName (contacts: Contact list) (pk: string) =
133+ let private contactName (contacts: Map<string, Contact>) (pk: string) =
134134 let fallback = truncKey pk
135135 contacts
136− |> List.tryFind (fun c -> c.Pubkey = pk)
136+ |> Map.tryFind pk
137137 |> Option.map (fun c ->
138138 if c.Nickname <> fallback then c.Nickname
139139 elif c.DisplayName <> "" then c.DisplayName
@@ -156,15 +156,6 @@ module App =
156156 |> fun updated -> Map.add pk updated messages
157157 | None -> messages
158158
159− let private addContactIfNew (contact: Contact) (contacts: Contact list) =
160− if contacts |> List.exists (fun c -> c.Pubkey = contact.Pubkey) then contacts
161− else contacts @ [ contact ]
162−
163− let private upsertContact (contact: Contact) (contacts: Contact list) =
164− match contacts |> List.tryFindIndex (fun c -> c.Pubkey = contact.Pubkey) with
165− | Some i -> contacts |> List.updateAt i contact
166− | None -> contacts @ [ contact ]
167−
168159 let private newContact pubkey nickname : Contact =
169160 { Pubkey = pubkey; Nickname = nickname; DisplayName = ""; Bio = ""; PhotoBase64 = "" }
170161
@@ -193,7 +184,7 @@ module App =
193184 Auth = NoIdentity
194185 ServerUrl = "http://localhost:8080"
195186 PollStatus = Idle
196− Contacts = []
187+ Contacts = Map.empty
197188 Messages = Map.empty
198189 Error = None
199190 PollCursor = 0L
@@ -281,18 +272,17 @@ module App =
281272 match evt with
282273 | IncomingChat(senderPubkey, message) ->
283274 { m with
284− Contacts = m.Contacts |> addContactIfNew (newContact senderPubkey (truncKey senderPubkey))
275+ Contacts =
276+ if Map.containsKey senderPubkey m.Contacts then m.Contacts
277+ else Map.add senderPubkey (newContact senderPubkey (truncKey senderPubkey)) m.Contacts
285278 Messages = m.Messages |> appendMessage senderPubkey message }
286279 | IncomingDeliveryAck(senderPubkey, ackIds) ->
287280 { m with Messages = m.Messages |> markDelivered senderPubkey ackIds }
288281 | IncomingProfile(senderPubkey, displayName, bio, photo) ->
289282 { m with
290283 Contacts =
291− m.Contacts
292− |> List.map (fun c ->
293− if c.Pubkey = senderPubkey then
294− { c with DisplayName = displayName; Bio = bio; PhotoBase64 = photo |> Option.defaultValue "" }
295− else c) })
284+ m.Contacts |> Map.change senderPubkey (Option.map (fun c ->
285+ { c with DisplayName = displayName; Bio = bio; PhotoBase64 = photo })) })
296286 { model with PollStatus = status; PollCursor = newCursor }
297287
298288 model',
@@ -307,15 +297,14 @@ module App =
307297 match tryParsePubkey cpk with
308298 | Some bytes ->
309299 let hex = Crypto.toHex bytes
310− let existing = model.Contacts |> List.tryFind (fun c -> c.Pubkey = hex)
311− let contact: Contact =
312− match existing with
300+ let contact =
301+ match Map.tryFind hex model.Contacts with
313302 | Some e -> { e with Nickname = cnn }
314303 | None -> newContact hex cnn
315304
316305 let model' =
317306 { model with
318− Contacts = model.Contacts |> upsertContact contact
307+ Contacts = Map.add hex contact model.Contacts
319308 Page = Conversations }
320309
321310 model', [ saveCmdMsg model' ]
@@ -353,7 +342,7 @@ module App =
353342 { model with Profile = Some profile; Page = Settings },
354343 [ CmdSaveProfile profile
355344 match trySession model with
356− | Some session -> CmdSendProfileToAll(session, profile, model.Contacts |> List.map _.Pubkey)
345+ | Some session -> CmdSendProfileToAll(session, profile, model.Contacts.Keys |> Seq.toList)
357346 | None -> () ]
358347 | _ -> model, []
359348
@@ -385,7 +374,7 @@ module App =
385374 type private Envelope =
386375 | TextMessage of id: string * body: string
387376 | DeliveryAck of id: string * ackIds: string list
388− | ProfileMessage of id: string * displayName: string * bio: string * photo: string option
377+ | ProfileMessage of id: string * displayName: string * bio: string * photo: string
389378
390379 let private serializeEnvelope = function
391380 | TextMessage(id, body) ->
@@ -393,7 +382,7 @@ module App =
393382 | DeliveryAck(id, ackIds) ->
394383 JsonSerializer.Serialize({| ``type`` = "delivery.ack"; id = id; ack_ids = ackIds |})
395384 | ProfileMessage(id, displayName, bio, photo) ->
396− JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = displayName; bio = bio; photo = (photo |> Option.defaultValue null) |})
385+ JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = displayName; bio = bio; photo = photo |})
397386
398387 let private sendEnvelope (session: Session) (recipientHex: string) (envelope: Envelope) =
399388 async {
@@ -420,7 +409,7 @@ module App =
420409 | "profile" ->
421410 let displayName = root.GetProperty("display_name").GetString()
422411 let bio = tryGetJsonString root "bio" |> Option.defaultValue ""
423− let photo = tryGetJsonString root "photo"
412+ let photo = tryGetJsonString root "photo" |> Option.defaultValue ""
424413 Some(ProfileMessage(root.GetProperty("id").GetString(), displayName, bio, photo))
425414 | _ -> None
426415
@@ -487,17 +476,15 @@ module App =
487476 let! response = poll session.Url session.Token cursor
488477 let ackIds = response.Events |> Array.map _.Id |> Array.toList
489478
490− let events, errors =
491− Array.foldBack
492− (fun (evt: PollEvent) (evts, errs) ->
493− match evt.EventType with
494− | Message ->
495− match decryptEvent session.Identity.PrivKey evt.Payload with
496− | Ok event -> (event :: evts, errs)
497− | Error err -> (evts, err :: errs)
498− | UnknownEvent _ -> (evts, errs))
499− response.Events
500− ([], [])
479+ let results =
480+ response.Events
481+ |> Array.choose (fun evt ->
482+ match evt.EventType with
483+ | Message -> Some(decryptEvent session.Identity.PrivKey evt.Payload)
484+ | UnknownEvent _ -> None)
485+
486+ let events = results |> Array.choose (function Ok e -> Some e | _ -> None) |> Array.toList
487+ let errors = results |> Array.choose (function Error e -> Some e | _ -> None) |> Array.toList
501488
502489 if not ackIds.IsEmpty then
503490 try do! ackMessages session.Url session.Token ackIds
@@ -544,14 +531,14 @@ module App =
544531 try
545532 let! results = Microsoft.Maui.Media.MediaPicker.Default.PickPhotosAsync() |> Async.AwaitTask
546533 match results |> Seq.tryHead with
547− | None -> return PhotoPicked None
534+ | None -> return PhotoPicked ""
548535 | Some file ->
549536 use! stream = file.OpenReadAsync() |> Async.AwaitTask
550537 use ms = new MemoryStream()
551538 do! stream.CopyToAsync(ms) |> Async.AwaitTask
552− return PhotoPicked(Some(Convert.ToBase64String(ms.ToArray())))
539+ return PhotoPicked(Convert.ToBase64String(ms.ToArray()))
553540 with _ ->
554− return PhotoPicked None
541+ return PhotoPicked ""
555542 })
556543
557544 | CmdSaveProfile profile ->
@@ -623,7 +610,7 @@ module App =
623610 let displayName, bio, photo =
624611 match model.Profile with
625612 | Some p -> p.DisplayName, p.Bio, p.PhotoBase64
626− | None -> "", "", None
613+ | None -> "", "", ""
627614 if displayName <> "" then
628615 Label($"Name: {displayName}").font(size = 14.)
629616 if bio <> "" then
@@ -673,7 +660,7 @@ module App =
673660 .centerTextHorizontal()
674661 .textColor(Colors.Gray)
675662
676− for c in model.Contacts do
663+ for c in model.Contacts.Values do
677664 let preview =
678665 messagesFor c.Pubkey model.Messages
679666 |> List.tryLast
@@ -759,13 +746,12 @@ module App =
759746
760747 viewErrorBanner model.Error
761748
762− match photo with
763− | Some p when p <> "" ->
764− Image(new MemoryStream(Convert.FromBase64String(p)) :> Stream)
749+ if photo <> "" then
750+ Image(new MemoryStream(Convert.FromBase64String(photo)) :> Stream)
765751 .width(120.)
766752 .height(120.)
767753 .centerHorizontal()
768− | _ ->
754+ else
769755 Label("No Photo")
770756 .font(size = 16.)
771757 .centerTextHorizontal()