@@ -26,15 +26,15 @@ module App =
26 type Page =
26 type Page =
28 | Conversations
28 | Conversations
29− | Chat of pubkey: string
29+ | Chat of pubkey: string * compose: string
30− | AddContact
30+ | AddContact of pubkey: string * nickname: string
31 | Settings
31 | Settings
33 type Session = { Url: string; Token: string; Identity: Identity }
33 type Session = { Url: string; Token: string; Identity: Identity }
35 type ConnState =
35 type ConnState =
37− | Connecting of Identity
37+ | Connecting
38 | Online of Session
38 | Online of Session
40 // ── Model ──
40 // ── Model ──
@@ -47,12 +47,8 @@ module App =
47 Contacts: Contact list
47 Contacts: Contact list
48 PollStatus: string
48 PollStatus: string
49 Messages: Map<string, ChatMessage list>
49 Messages: Map<string, ChatMessage list>
53 Error: string option
50 Error: string option
54− PollCursor: uint64
51+ PollCursor: int64 }
55− LastSendStatus: string }
57 // ── Msg ──
53 // ── Msg ──
@@ -67,11 +63,11 @@ module App =
67 | DoDisconnect
63 | DoDisconnect
68 | SetCompose of string
64 | SetCompose of string
70− | Sent of ApiClient.SendResult
66+ | Sent of Result<ApiClient.SendResult, string>
71 | SetNewPubkey of string
67 | SetNewPubkey of string
72 | SetNewNickname of string
68 | SetNewNickname of string
73 | DoSaveContact
69 | DoSaveContact
74− | PollResult of messages: (string * ChatMessage) list * status: string * cursor: uint64
70+ | PollResult of messages: (string * ChatMessage) list * status: string * cursor: int64
75 | CopyPubKey
71 | CopyPubKey
76 | DismissError
72 | DismissError
77 | StateLoaded of Identity * Data option
73 | StateLoaded of Identity * Data option
@@ -82,7 +78,7 @@ module App =
82 | CmdGenIdentity
78 | CmdGenIdentity
83 | CmdConnect of url: string * identity: Identity
79 | CmdConnect of url: string * identity: Identity
84 | CmdSend of session: Session * recipientHex: string * text: string * messageId: string
80 | CmdSend of session: Session * recipientHex: string * text: string * messageId: string
85− | CmdPoll of session: Session * cursor: uint64
81+ | CmdPoll of session: Session * cursor: int64
86 | CmdCopyToClipboard of string
82 | CmdCopyToClipboard of string
87 | CmdLoadState
83 | CmdLoadState
88 | CmdSaveIdentity of Identity
84 | CmdSaveIdentity of Identity
@@ -136,12 +132,8 @@ module App =
136 PollStatus = ""
132 PollStatus = ""
137 Contacts = []
133 Contacts = []
138 Messages = Map.empty
134 Messages = Map.empty
142 Error = None
135 Error = None
143− PollCursor = 0UL
136+ PollCursor = 0L },
144− LastSendStatus = "" },
145 [ CmdLoadState ]
137 [ CmdLoadState ]
147 // ── Update ──
139 // ── Update ──
@@ -161,12 +153,12 @@ module App =
161 | DoConnect ->
153 | DoConnect ->
162 match model.Identity with
154 match model.Identity with
163 | Some id ->
155 | Some id ->
164− { model with Conn = Connecting id }, [ CmdConnect(model.ServerUrl, id) ]
156+ { model with Conn = Connecting }, [ CmdConnect(model.ServerUrl, id) ]
165 | None -> { model with Error = Some "No identity generated" }, []
157 | None -> { model with Error = Some "No identity generated" }, []
167 | AuthOk token ->
159 | AuthOk token ->
168− match model.Conn with
160+ match model.Identity, model.Conn with
169− | Connecting id ->
161+ | Some id, Connecting ->
170 let session = { Url = model.ServerUrl; Token = token; Identity = id }
162 let session = { Url = model.ServerUrl; Token = token; Identity = id }
171 { model with Conn = Online session; Page = Conversations; Error = None },
163 { model with Conn = Online session; Page = Conversations; Error = None },
172 [ CmdPoll(session, model.PollCursor) ]
164 [ CmdPoll(session, model.PollCursor) ]
@@ -176,28 +168,31 @@ module App =
177 | DoDisconnect -> { model with Conn = Offline }, []
169 | DoDisconnect -> { model with Conn = Offline }, []
179− | SetCompose text -> { model with ComposeText = text }, []
171+ | SetCompose text ->
172+ match model.Page with
173+ | Chat(pk, _) -> { model with Page = Chat(pk, text) }, []
181 | DoSend ->
176 | DoSend ->
182 match model.Page, trySession model with
177 match model.Page, trySession model with
183− | Chat pk, Some session when model.ComposeText <> "" ->
178+ | Chat(pk, compose), Some session when compose <> "" ->
184− let text = model.ComposeText
185 let id = Guid.NewGuid().ToString()
179 let id = Guid.NewGuid().ToString()
186 let outMsg =
180 let outMsg =
187 { Id = id
181 { Id = id
188− Body = text
182+ Body = compose
189 Timestamp = DateTimeOffset.UtcNow
183 Timestamp = DateTimeOffset.UtcNow
190 IsOutgoing = true }
184 IsOutgoing = true }
192 let model' =
186 let model' =
193 { model with
187 { model with
194− ComposeText = ""
188+ Page = Chat(pk, "")
195 Messages = model.Messages |> appendMessage pk outMsg }
189 Messages = model.Messages |> appendMessage pk outMsg }
197− model', CmdSend(session, pk, text, id) :: saveCmd model'
191+ model', CmdSend(session, pk, compose, id) :: saveCmd model'
198 | _ -> model, []
192 | _ -> model, []
200− | Sent result -> { model with LastSendStatus = $"{result.Status}:{result.MessageId}" }, []
194+ | Sent(Ok _) -> model, []
195+ | Sent(Error err) -> { model with Error = Some $"Send failed: {err}" }, []
202 | PollResult(incoming, status, newCursor) ->
197 | PollResult(incoming, status, newCursor) ->
203 let model' =
198 let model' =
@@ -219,25 +214,28 @@ module App =
220 model', cmds @ (if not incoming.IsEmpty then saveCmd model' else [])
215 model', cmds @ (if not incoming.IsEmpty then saveCmd model' else [])
222− | SetNewPubkey pk -> { model with NewPubkey = pk }, []
217+ | SetNewPubkey pk ->
218+ match model.Page with
219+ | AddContact(_, nn) -> { model with Page = AddContact(pk, nn) }, []
223− | SetNewNickname nn -> { model with NewNickname = nn }, []
222+ | SetNewNickname nn ->
223+ match model.Page with
224+ | AddContact(pk, _) -> { model with Page = AddContact(pk, nn) }, []
225 | DoSaveContact ->
227 | DoSaveContact ->
226− if model.NewPubkey <> "" && model.NewNickname <> "" then
228+ match model.Page with
229+ | AddContact(pk, nn) when pk <> "" && nn <> "" ->
227− let contact: Contact =
230+ let contact: Contact = { Pubkey = pk; Nickname = nn }
228− { Pubkey = model.NewPubkey
229− Nickname = model.NewNickname }
231 let model' =
232 let model' =
232 { model with
233 { model with
233 Contacts = model.Contacts |> upsertContact contact
234 Contacts = model.Contacts |> upsertContact contact
236 Page = Conversations }
235 Page = Conversations }
238 model', saveCmd model'
237 model', saveCmd model'
240− model, []
238+ | _ -> model, []
242 | CopyPubKey ->
240 | CopyPubKey ->
243 match model.Identity with
241 match model.Identity with
@@ -247,7 +245,7 @@ module App =
247 | DismissError -> { model with Error = None }, []
245 | DismissError -> { model with Error = None }, []
249 | StateLoaded(identity, dataOpt) ->
247 | StateLoaded(identity, dataOpt) ->
250− let model' = { model with Identity = Some identity; Conn = Connecting identity }
248+ let model' = { model with Identity = Some identity; Conn = Connecting }
252 let model' =
250 let model' =
253 match dataOpt with
251 match dataOpt with
@@ -319,9 +317,9 @@ module App =
319 let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
317 let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
320 let blobHex = Crypto.toHex blob
318 let blobHex = Crypto.toHex blob
321 let! result = ApiClient.sendMessage session.Url session.Token recipientHex blobHex ts
319 let! result = ApiClient.sendMessage session.Url session.Token recipientHex blobHex ts
322− return Sent result
320+ return Sent(Ok result)
323 with ex ->
321 with ex ->
324− return DismissError // TODO: show send errors properly
322+ return Sent(Error ex.Message)
327 | CmdPoll(session, cursor) ->
325 | CmdPoll(session, cursor) ->
@@ -373,7 +371,7 @@ module App =
373 |> Async.Start)
371 |> Async.Start)
375 | CmdSaveIdentity identity ->
373 | CmdSaveIdentity identity ->
376− Cmd.ofEffect (fun _ -> Store.saveIdentity identity |> ignore)
374+ Cmd.ofEffect (fun _ -> Store.saveIdentity identity |> Async.Start)
378 | CmdSaveData data ->
376 | CmdSaveData data ->
379 Cmd.ofEffect (fun _ -> Store.saveData data)
377 Cmd.ofEffect (fun _ -> Store.saveData data)
@@ -428,7 +426,7 @@ module App =
429 match model.Conn with
427 match model.Conn with
430 | Offline -> Button("Connect", DoConnect)
428 | Offline -> Button("Connect", DoConnect)
431− | Connecting _ -> Label("Connecting...").centerTextHorizontal()
429+ | Connecting -> Label("Connecting...").centerTextHorizontal()
432 | Online _ ->
430 | Online _ ->
433 Button("Disconnect", DoDisconnect)
431 Button("Disconnect", DoDisconnect)
434 Button("Go to Conversations", Nav Conversations)
432 Button("Go to Conversations", Nav Conversations)
@@ -441,7 +439,7 @@ module App =
441 let connStr =
439 let connStr =
442 match model.Conn with
440 match model.Conn with
443 | Offline -> "OFFLINE"
441 | Offline -> "OFFLINE"
444− | Connecting _ -> "CONNECTING"
442+ | Connecting -> "CONNECTING"
445 | Online _ -> "ONLINE"
443 | Online _ -> "ONLINE"
447 let contactNames =
445 let contactNames =
@@ -459,7 +457,7 @@ module App =
459 Label("Conversations")
457 Label("Conversations")
460 .font(size = 24.)
458 .font(size = 24.)
462− Button("+", Nav AddContact)
460+ Button("+", Nav(AddContact("", "")))
463 Button("Settings", Nav Settings)
461 Button("Settings", Nav Settings)
@@ -492,13 +490,13 @@ module App =
492 let preview =
490 let preview =
493 if lastMsg.Length > 40 then lastMsg.[..39] + "..." else lastMsg
491 if lastMsg.Length > 40 then lastMsg.[..39] + "..." else lastMsg
495− Button($"{c.Nickname}\n{preview}", Nav(Chat c.Pubkey))
493+ Button($"{c.Nickname}\n{preview}", Nav(Chat(c.Pubkey, "")))
497 .padding(20.)
495 .padding(20.)
501− let private viewChat model pk =
499+ let private viewChat model pk compose =
502 let name = contactName model.Contacts pk
500 let name = contactName model.Contacts pk
503 let msgs = messagesFor pk model.Messages
501 let msgs = messagesFor pk model.Messages
@@ -519,10 +517,6 @@ module App =
519 .font(size = 11.)
517 .font(size = 11.)
520 | None -> ()
518 | None -> ()
522− Label($"[{msgs.Length} message(s), compose: \"{model.ComposeText}\", send: \"{model.LastSendStatus}\"]")
523− .textColor(Colors.Gray)
526 let allMessages =
520 let allMessages =
528 |> List.map (fun m ->
522 |> List.map (fun m ->
@@ -534,7 +528,7 @@ module App =
534 .font(size = 14.)
528 .font(size = 14.)
536 HStack(spacing = 8.) {
530 HStack(spacing = 8.) {
537− Entry(model.ComposeText, SetCompose)
531+ Entry(compose, SetCompose)
538 .placeholder("Message...")
532 .placeholder("Message...")
540 Button("Send", DoSend)
534 Button("Send", DoSend)
@@ -543,7 +537,7 @@ module App =
543 .padding(12.)
537 .padding(12.)
546− let private viewAddContact model =
540+ let private viewAddContact pk nn =
547 ContentPage(
541 ContentPage(
548 (VStack(spacing = 16.) {
542 (VStack(spacing = 16.) {
549 HStack(spacing = 8.) {
543 HStack(spacing = 8.) {
@@ -554,10 +548,10 @@ module App =
556 Label("Public Key (hex):")
550 Label("Public Key (hex):")
557− Entry(model.NewPubkey, SetNewPubkey)
551+ Entry(pk, SetNewPubkey)
559 Label("Nickname:")
553 Label("Nickname:")
560− Entry(model.NewNickname, SetNewNickname)
554+ Entry(nn, SetNewNickname)
562 Button("Save Contact", DoSaveContact)
556 Button("Save Contact", DoSaveContact)
563 .centerHorizontal()
557 .centerHorizontal()
@@ -571,8 +565,8 @@ module App =
571 | Setup -> viewSetup ()
565 | Setup -> viewSetup ()
572 | Settings -> viewSettings model
566 | Settings -> viewSettings model
573 | Conversations -> viewConversations model
567 | Conversations -> viewConversations model
574− | Chat pk -> viewChat model pk
568+ | Chat(pk, compose) -> viewChat model pk compose
575− | AddContact -> viewAddContact model
569+ | AddContact(pk, nn) -> viewAddContact pk nn
577 Application() { Window(page) }
571 Application() { Window(page) }