@@ -37,10 +37,8 @@ module App =
37 | NoIdentity
37 | NoIdentity
38 | Identified of identity: Identity * conn: ConnStatus
38 | Identified of identity: Identity * conn: ConnStatus
40− type IncomingMessage = { SenderPubkey: string; Message: ChatMessage }
42 type DecryptedEvent =
40 type DecryptedEvent =
43− | IncomingChat of IncomingMessage
41+ | IncomingChat of senderPubkey: string * message: ChatMessage
44 | IncomingDeliveryAck of senderPubkey: string * ackIds: string list
42 | IncomingDeliveryAck of senderPubkey: string * ackIds: string list
45 | IncomingProfile of senderPubkey: string * displayName: string * bio: string * photo: string option
43 | IncomingProfile of senderPubkey: string * displayName: string * bio: string * photo: string option
@@ -138,8 +136,8 @@ module App =
138 |> List.tryFind (fun c -> c.Pubkey = pk)
136 |> List.tryFind (fun c -> c.Pubkey = pk)
139 |> Option.map (fun c ->
137 |> Option.map (fun c ->
140 if c.Nickname <> fallback then c.Nickname
138 if c.Nickname <> fallback then c.Nickname
141− elif not (String.IsNullOrEmpty c.DisplayName) then c.DisplayName
139+ elif c.DisplayName <> "" then c.DisplayName
142− else c.Nickname)
140+ else fallback)
143 |> Option.defaultValue fallback
141 |> Option.defaultValue fallback
145 let private messagesFor (pk: string) (messages: Map<string, ChatMessage list>) =
143 let private messagesFor (pk: string) (messages: Map<string, ChatMessage list>) =
@@ -261,13 +259,12 @@ module App =
261 Page = Chat(pk, "")
259 Page = Chat(pk, "")
262 Messages = model.Messages |> appendMessage pk outMsg }
260 Messages = model.Messages |> appendMessage pk outMsg }
264− let cmds = [ CmdSend(session, pk, compose, id); saveCmdMsg model' ]
263+ [ CmdSend(session, pk, compose, id)
265− let cmds =
264+ saveCmdMsg model'
266− match model.Profile, isFirstInteraction with
265+ match model.Profile, isFirstInteraction with
267− | Some profile, true -> cmds @ [ CmdSendProfileToAll(session, profile, [ pk ]) ]
266+ | Some profile, true -> CmdSendProfileToAll(session, profile, [ pk ])
268− | _ -> cmds
267+ | _ -> () ]
271 | _ -> model, []
268 | _ -> model, []
273 | SendFailed err -> { model with Error = Some err }, []
270 | SendFailed err -> { model with Error = Some err }, []
@@ -279,10 +276,10 @@ module App =
279 |> List.fold
276 |> List.fold
280 (fun (m: Model) evt ->
277 (fun (m: Model) evt ->
281 match evt with
278 match evt with
282− | IncomingChat msg ->
279+ | IncomingChat(senderPubkey, message) ->
284− Contacts = m.Contacts |> addContactIfNew { Pubkey = msg.SenderPubkey; Nickname = truncKey msg.SenderPubkey; DisplayName = null; Bio = null; PhotoBase64 = null }
281+ Contacts = m.Contacts |> addContactIfNew { Pubkey = senderPubkey; Nickname = truncKey senderPubkey; DisplayName = ""; Bio = ""; PhotoBase64 = "" }
285− Messages = m.Messages |> appendMessage msg.SenderPubkey msg.Message }
282+ Messages = m.Messages |> appendMessage senderPubkey message }
286 | IncomingDeliveryAck(senderPubkey, ackIds) ->
283 | IncomingDeliveryAck(senderPubkey, ackIds) ->
287 { m with Messages = m.Messages |> markDelivered senderPubkey ackIds }
284 { m with Messages = m.Messages |> markDelivered senderPubkey ackIds }
288 | IncomingProfile(senderPubkey, displayName, bio, photo) ->
285 | IncomingProfile(senderPubkey, displayName, bio, photo) ->
@@ -291,7 +288,7 @@ module App =
291 m.Contacts
288 m.Contacts
292 |> List.map (fun c ->
289 |> List.map (fun c ->
293 if c.Pubkey = senderPubkey then
290 if c.Pubkey = senderPubkey then
294− { c with DisplayName = displayName; Bio = bio; PhotoBase64 = photo |> Option.defaultValue null }
291+ { c with DisplayName = displayName; Bio = bio; PhotoBase64 = photo |> Option.defaultValue "" }
295 else c) })
292 else c) })
296 { model with PollStatus = status; PollCursor = newCursor }
293 { model with PollStatus = status; PollCursor = newCursor }
@@ -311,7 +308,7 @@ module App =
311 let contact: Contact =
308 let contact: Contact =
312 match existing with
309 match existing with
313 | Some e -> { e with Nickname = cnn }
310 | Some e -> { e with Nickname = cnn }
314− | None -> { Pubkey = hex; Nickname = cnn; DisplayName = null; Bio = null; PhotoBase64 = null }
311+ | None -> { Pubkey = hex; Nickname = cnn; DisplayName = ""; Bio = ""; PhotoBase64 = "" }
316 let model' =
313 let model' =
317 { model with
314 { model with
@@ -350,13 +347,11 @@ module App =
350 match model.Page with
347 match model.Page with
351 | EditProfile(name, bio, photo) ->
348 | EditProfile(name, bio, photo) ->
352 let profile: Profile = { DisplayName = name; Bio = bio; PhotoBase64 = photo }
349 let profile: Profile = { DisplayName = name; Bio = bio; PhotoBase64 = photo }
350+ { model with Profile = Some profile; Page = Settings },
353− let cmds = [ CmdSaveProfile profile ]
351+ [ CmdSaveProfile profile
355− match trySession model with
352+ match trySession model with
357− cmds @ [ CmdSendProfileToAll(session, profile, model.Contacts |> List.map _.Pubkey) ]
353+ | Some session -> CmdSendProfileToAll(session, profile, model.Contacts |> List.map _.Pubkey)
358− | None -> cmds
354+ | None -> () ]
359− { model with Profile = Some profile; Page = Settings }, cmds
360 | _ -> model, []
355 | _ -> model, []
362 | StateLoaded(IdentityAndData(identity, data, profile)) ->
357 | StateLoaded(IdentityAndData(identity, data, profile)) ->
@@ -397,6 +392,14 @@ module App =
397 | ProfileMessage(id, displayName, bio, photo) ->
392 | ProfileMessage(id, displayName, bio, photo) ->
398 JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = displayName; bio = bio; photo = (photo |> Option.defaultValue null) |})
393 JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = displayName; bio = bio; photo = (photo |> Option.defaultValue null) |})
395+ let private sendEnvelope (session: Session) (recipientHex: string) (envelope: Envelope) =
397+ let payload = serializeEnvelope envelope
398+ let blob = Crypto.encrypt session.Identity.PrivKey (Crypto.fromHex recipientHex) payload
399+ let ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
400+ return! sendMessage session.Url session.Token recipientHex (Crypto.toHex blob) ts
400 let private parseEnvelope (json: string) =
403 let private parseEnvelope (json: string) =
401 use doc = JsonDocument.Parse(json)
404 use doc = JsonDocument.Parse(json)
402 let root = doc.RootElement
405 let root = doc.RootElement
@@ -427,13 +430,11 @@ module App =
427 | Some(plaintext, senderHex) ->
430 | Some(plaintext, senderHex) ->
428 match parseEnvelope plaintext with
431 match parseEnvelope plaintext with
429 | Some(TextMessage(id, body)) ->
432 | Some(TextMessage(id, body)) ->
430− Ok(IncomingChat
433+ Ok(IncomingChat(senderHex,
431− { SenderPubkey = senderHex
433− { Id = id; Body = body
434+ { Id = id; Body = body
434− Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
435+ Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
435− IsOutgoing = false
436+ IsOutgoing = false
436− Status = DeliveryStatus.Delivered } })
437+ Status = DeliveryStatus.Delivered }))
437 | Some(DeliveryAck(_, ackIds)) ->
438 | Some(DeliveryAck(_, ackIds)) ->
438 Ok(IncomingDeliveryAck(senderHex, ackIds))
439 Ok(IncomingDeliveryAck(senderHex, ackIds))
439 | Some(ProfileMessage(_, displayName, bio, photo)) ->
440 | Some(ProfileMessage(_, displayName, bio, photo)) ->
@@ -447,15 +448,12 @@ module App =
448 let chatsBySender =
449 let chatsBySender =
450− |> List.choose (function IncomingChat msg -> Some(msg.SenderPubkey, msg.Message.Id) | _ -> None)
451+ |> List.choose (function IncomingChat(senderPubkey, msg) -> Some(senderPubkey, msg.Id) | _ -> None)
451 |> List.groupBy fst
452 |> List.groupBy fst
452 |> List.map (fun (sender, pairs) -> sender, List.map snd pairs)
453 |> List.map (fun (sender, pairs) -> sender, List.map snd pairs)
454 for sender, msgIds in chatsBySender do
455 for sender, msgIds in chatsBySender do
456− let ackPayload = serializeEnvelope (DeliveryAck(Guid.NewGuid().ToString(), msgIds))
456+ try do! sendEnvelope session sender (DeliveryAck(Guid.NewGuid().ToString(), msgIds)) |> Async.Ignore
457− let blob = Crypto.encrypt session.Identity.PrivKey (Crypto.fromHex sender) ackPayload
458− do! sendMessage session.Url session.Token sender (Crypto.toHex blob) (DateTimeOffset.UtcNow.ToUnixTimeSeconds()) |> Async.Ignore
459 with _ -> ()
457 with _ -> ()
@@ -473,12 +471,7 @@ module App =
473 | CmdSend(session, recipientHex, text, messageId) ->
471 | CmdSend(session, recipientHex, text, messageId) ->
474 asyncCmd (async {
472 asyncCmd (async {
476− let recipPub = Crypto.fromHex recipientHex
477− let ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
478− let payload = serializeEnvelope (TextMessage(messageId, text))
474+ let! status = sendEnvelope session recipientHex (TextMessage(messageId, text))
479− let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
480− let! status = sendMessage session.Url session.Token recipientHex (Crypto.toHex blob) ts
482 match messageStatusToError status with
475 match messageStatusToError status with
483 | Some err -> return SendFailed err
476 | Some err -> return SendFailed err
484 | None -> return SendOk
477 | None -> return SendOk
@@ -567,10 +560,7 @@ module App =
567 Cmd.ofEffect (fun _ ->
560 Cmd.ofEffect (fun _ ->
569 for recipientHex in recipientHexes do
562 for recipientHex in recipientHexes do
571− let payload = serializeEnvelope (ProfileMessage(Guid.NewGuid().ToString(), profile.DisplayName, profile.Bio, profile.PhotoBase64))
563+ try do! sendEnvelope session recipientHex (ProfileMessage(Guid.NewGuid().ToString(), profile.DisplayName, profile.Bio, profile.PhotoBase64)) |> Async.Ignore
572− let blob = Crypto.encrypt session.Identity.PrivKey (Crypto.fromHex recipientHex) payload
573− do! sendMessage session.Url session.Token recipientHex (Crypto.toHex blob) (DateTimeOffset.UtcNow.ToUnixTimeSeconds()) |> Async.Ignore
574 with _ -> ()
564 with _ -> ()
575 } |> Async.Start)
565 } |> Async.Start)
@@ -630,11 +620,11 @@ module App =
631 Label("Profile:").font(size = 18.)
621 Label("Profile:").font(size = 18.)
632 match model.Profile with
622 match model.Profile with
633− | Some profile when not (String.IsNullOrEmpty profile.DisplayName) ->
623+ | Some profile when profile.DisplayName <> "" ->
634 Label($"Name: {profile.DisplayName}").font(size = 14.)
624 Label($"Name: {profile.DisplayName}").font(size = 14.)
635 | _ -> ()
625 | _ -> ()
636 match model.Profile with
626 match model.Profile with
637− | Some profile when not (String.IsNullOrEmpty profile.Bio) ->
627+ | Some profile when profile.Bio <> "" ->
638 Label($"Bio: {profile.Bio}").font(size = 14.).textColor(Colors.DimGray)
628 Label($"Bio: {profile.Bio}").font(size = 14.).textColor(Colors.DimGray)
639 | _ -> ()
629 | _ -> ()
640 let profilePage =
630 let profilePage =
@@ -773,7 +763,7 @@ module App =
773 viewErrorBanner model.Error
763 viewErrorBanner model.Error
775 match photo with
765 match photo with
776− | Some p when not (String.IsNullOrEmpty p) ->
766+ | Some p when p <> "" ->
777 Image(new MemoryStream(Convert.FromBase64String(p)) :> Stream)
767 Image(new MemoryStream(Convert.FromBase64String(p)) :> Stream)
778 .width(120.)
768 .width(120.)
779 .height(120.)
769 .height(120.)