@@ -40,8 +40,8 @@ module App =
4040 type Page =
4141 | Setup
4242 | Conversations
43− | Chat of pubkey: string
44− | AddContact
43+ | Chat of pubkey: string * compose: string
44+ | AddContact of pubkey: string * nickname: string
4545 | Settings
4646
4747 type Session = { Url: string; Token: string; Identity: Identity }
@@ -55,6 +55,14 @@ module App =
5555 | NoIdentity
5656 | Identified of identity: Identity * conn: ConnStatus
5757
58+ type IncomingMessage = { SenderPubkey: string; Message: ChatMessage }
59+
60+ type PollStatus =
61+ | Idle
62+ | Polling
63+ | Received of events: int * messages: int * errors: int * firstError: string option
64+ | PollError of string
65+
5866 // ── Model ──
5967
6068 type Model =
@@ -62,13 +70,10 @@ module App =
6270 Auth: AuthState
6371 ServerUrl: string
6472 Contacts: Contact list
65− PollStatus: string
73+ PollStatus: PollStatus
6674 Messages: Map<string, ChatMessage list>
6775 Error: string option
68− PollCursor: int64
69− Compose: string
70− ContactPubkey: string
71− ContactNickname: string }
76+ PollCursor: int64 }
7277
7378 // ── Msg ──
7479
@@ -92,7 +97,7 @@ module App =
9297 | DoSend
9398 | SendFailed of string
9499 | DoSaveContact
95− | PollResult of messages: (string * ChatMessage) list * status: string * cursor: int64
100+ | PollResult of messages: IncomingMessage list * status: PollStatus * cursor: int64
96101 | CopyPubKey
97102 | DismissError
98103 | StateLoaded of StateLoadResult
@@ -111,12 +116,6 @@ module App =
111116
112117 // ── Helpers ──
113118
114− let private nowStr () =
115− DateTimeOffset.UtcNow.ToString("HH:mm:ss")
116−
117− let private truncate maxLen (s: string) =
118− if s.Length > maxLen then s.[..maxLen - 1] + "..." else s
119−
120119 let private hexToOb (hex: string) =
121120 Crypto.fromHex hex |> Phonemic.toOb
122121
@@ -178,29 +177,35 @@ module App =
178177 { Page = Setup
179178 Auth = NoIdentity
180179 ServerUrl = "http://localhost:8080"
181− PollStatus = ""
180+ PollStatus = Idle
182181 Contacts = []
183182 Messages = Map.empty
184183 Error = None
185− PollCursor = 0L
186− Compose = ""
187− ContactPubkey = ""
188− ContactNickname = "" },
184+ PollCursor = 0L },
189185 [ CmdLoadState ]
190186
191187 // ── Update ──
192188
193189 let update msg model =
194190 match msg with
195− | SetPage(Chat pk) ->
196− { model with Page = Chat pk; Compose = "" }, []
197− | SetPage AddContact ->
198− { model with Page = AddContact; ContactPubkey = ""; ContactNickname = "" }, []
191+ | SetPage(Chat(pk, _)) ->
192+ { model with Page = Chat(pk, "") }, []
193+ | SetPage(AddContact _) ->
194+ { model with Page = AddContact("", "") }, []
199195 | SetPage page -> { model with Page = page }, []
200196
201− | SetCompose text -> { model with Compose = text }, []
202− | SetContactPubkey pk -> { model with ContactPubkey = pk }, []
203− | SetContactNickname nn -> { model with ContactNickname = nn }, []
197+ | SetCompose text ->
198+ match model.Page with
199+ | Chat(pk, _) -> { model with Page = Chat(pk, text) }, []
200+ | _ -> model, []
201+ | SetContactPubkey pk ->
202+ match model.Page with
203+ | AddContact(_, nn) -> { model with Page = AddContact(pk, nn) }, []
204+ | _ -> model, []
205+ | SetContactNickname nn ->
206+ match model.Page with
207+ | AddContact(pk, _) -> { model with Page = AddContact(pk, nn) }, []
208+ | _ -> model, []
204209
205210 | GenIdentity -> model, [ CmdGenIdentity ]
206211
@@ -230,21 +235,20 @@ module App =
230235
231236 | DoSend ->
232237 match model.Page, trySession model with
233− | Chat pk, Some session when model.Compose <> "" ->
234− let text = model.Compose
238+ | Chat(pk, compose), Some session when compose <> "" ->
235239 let id = Guid.NewGuid().ToString()
236240 let outMsg =
237241 { Id = id
238− Body = text
242+ Body = compose
239243 Timestamp = DateTimeOffset.UtcNow
240244 IsOutgoing = true }
241245
242246 let model' =
243247 { model with
244− Compose = ""
248+ Page = Chat(pk, "")
245249 Messages = model.Messages |> appendMessage pk outMsg }
246250
247− model', [ CmdSend(session, pk, text, id); saveCmdMsg model' ]
251+ model', [ CmdSend(session, pk, compose, id); saveCmdMsg model' ]
248252 | _ -> model, []
249253
250254 | SendFailed err -> { model with Error = Some err }, []
@@ -253,10 +257,10 @@ module App =
253257 let model' =
254258 incoming
255259 |> List.fold
256− (fun (m: Model) (fromPubkey, chatMsg) ->
260+ (fun (m: Model) msg ->
257261 { m with
258− Contacts = m.Contacts |> addContactIfNew { Pubkey = fromPubkey; Nickname = truncKey fromPubkey }
259− Messages = m.Messages |> appendMessage fromPubkey chatMsg })
262+ Contacts = m.Contacts |> addContactIfNew { Pubkey = msg.SenderPubkey; Nickname = truncKey msg.SenderPubkey }
263+ Messages = m.Messages |> appendMessage msg.SenderPubkey msg.Message })
260264 { model with PollStatus = status; PollCursor = newCursor }
261265
262266 model',
@@ -266,9 +270,8 @@ module App =
266270 if not incoming.IsEmpty then saveCmdMsg model' ]
267271
268272 | DoSaveContact ->
269− let pk, nn = model.ContactPubkey, model.ContactNickname
270273 match model.Page with
271− | AddContact when pk <> "" && nn <> "" ->
274+ | AddContact(pk, nn) when pk <> "" && nn <> "" ->
272275 match tryParsePubkey pk with
273276 | Some bytes ->
274277 let contact: Contact = { Pubkey = Crypto.toHex bytes; Nickname = nn }
@@ -323,11 +326,12 @@ module App =
323326
324327 match envelope.Type with
325328 | Text ->
326− Ok(senderHex,
327− { Id = envelope.Id
328− Body = envelope.Body
329− Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
330− IsOutgoing = false })
329+ Ok { SenderPubkey = senderHex
330+ Message =
331+ { Id = envelope.Id
332+ Body = envelope.Body
333+ Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
334+ IsOutgoing = false } }
331335 | UnknownEnvelope t -> Error $"unknown envelope type: {t}"
332336 | None -> Error "decrypt failed"
333337 with ex ->
@@ -388,20 +392,14 @@ module App =
388392 if not ackIds.IsEmpty then
389393 do! ackMessages session.Url session.Token ackIds
390394
391− let now = nowStr ()
392−
393− let status =
394− $"[{now}] evts:{response.Events.Length} msgs:{messages.Length} errs:{errors.Length}"
395− + (match errors with err :: _ -> $" [{err}]" | [] -> "")
396−
395+ let status = Received(response.Events.Length, messages.Length, errors.Length, List.tryHead errors)
397396 return PollResult(messages, status, response.Cursor)
398397 with
399398 | :? TimeoutException ->
400− return PollResult([], "polling...", cursor)
399+ return PollResult([], Polling, cursor)
401400 | ex ->
402− let now = nowStr ()
403401 do! Async.Sleep 3000
404− return PollResult([], $"[{now}] poll error: {ex.Message}", cursor)
402+ return PollResult([], PollError ex.Message, cursor)
405403 })
406404
407405 | CmdCopyToClipboard text ->
@@ -428,6 +426,14 @@ module App =
428426
429427 // ── View ──
430428
429+ let private formatPollStatus = function
430+ | Idle -> ""
431+ | Polling -> "polling..."
432+ | Received(evts, msgs, errs, firstErr) ->
433+ $"evts:{evts} msgs:{msgs} errs:{errs}"
434+ + (match firstErr with Some e -> $" [{e}]" | None -> "")
435+ | PollError msg -> $"poll error: {msg}"
436+
431437 let private viewSetup () =
432438 ContentPage(
433439 (VStack(spacing = 24.) {
@@ -495,14 +501,6 @@ module App =
495501 | Identified(_, Connecting) -> "CONNECTING"
496502 | Identified(_, Online _) -> "ONLINE"
497503
498− let contactNames =
499− model.Contacts
500− |> List.map _.Nickname
501− |> String.concat ", "
502−
503− let totalMsgs =
504− model.Messages |> Map.values |> Seq.sumBy List.length
505−
506504 ContentPage(
507505 ScrollView(
508506 (VStack(spacing = 8.) {
@@ -510,22 +508,19 @@ module App =
510508 Label("Conversations")
511509 .font(size = 24.)
512510
513− Button("+", SetPage AddContact)
511+ Button("+", SetPage(AddContact("", "")))
514512 Button("Settings", SetPage Settings)
515513 }
516514
517− Label($"conn:{connStr} contacts:{model.Contacts.Length} msgs:{totalMsgs}")
518− .font(size = 10.)
519− .textColor(Colors.DimGray)
520−
521− Label($"poll: {model.PollStatus}")
515+ Label($"{connStr} | {formatPollStatus model.PollStatus}")
522516 .font(size = 10.)
523517 .textColor(Colors.DimGray)
524518
525− if contactNames <> "" then
526− Label($"names: {contactNames}")
527− .font(size = 10.)
528− .textColor(Colors.DimGray)
519+ match model.Error with
520+ | Some err ->
521+ Label(err).textColor(Colors.Red).font(size = 12.)
522+ Button("Dismiss", DismissError)
523+ | None -> ()
529524
530525 if model.Contacts.IsEmpty then
531526 Label("No contacts yet. Tap + to add one.")
@@ -536,16 +531,16 @@ module App =
536531 let preview =
537532 messagesFor c.Pubkey model.Messages
538533 |> List.tryLast
539− |> Option.map (fun m -> truncate 40 m.Body)
534+ |> Option.map (fun m -> if m.Body.Length > 40 then m.Body.[..39] + "..." else m.Body)
540535 |> Option.defaultValue ""
541536
542− Button($"{c.Nickname}\n{preview}", SetPage(Chat c.Pubkey))
537+ Button($"{c.Nickname}\n{preview}", SetPage(Chat(c.Pubkey, "")))
543538 })
544539 .padding(20.)
545540 )
546541 )
547542
548− let private viewChat model pk =
543+ let private viewChat model pk compose =
549544 let name = contactName model.Contacts pk
550545 let msgs = messagesFor pk model.Messages
551546
@@ -563,7 +558,9 @@ module App =
563558 | Some err ->
564559 Label(err)
565560 .textColor(Colors.Red)
566− .font(size = 11.)
561+ .font(size = 12.)
562+
563+ Button("Dismiss", DismissError)
567564 | None -> ()
568565
569566 if msgs.IsEmpty then
@@ -577,7 +574,7 @@ module App =
577574 .font(size = 14.)
578575
579576 HStack(spacing = 8.) {
580− Entry(model.Compose, SetCompose)
577+ Entry(compose, SetCompose)
581578 .placeholder("Message...")
582579
583580 Button("Send", DoSend)
@@ -586,7 +583,7 @@ module App =
586583 .padding(12.)
587584 )
588585
589− let private viewAddContact model =
586+ let private viewAddContact model pubkey nickname =
590587 ContentPage(
591588 (VStack(spacing = 16.) {
592589 HStack(spacing = 8.) {
@@ -596,12 +593,18 @@ module App =
596593 .font(size = 20.)
597594 }
598595
596+ match model.Error with
597+ | Some err ->
598+ Label(err).textColor(Colors.Red).font(size = 12.)
599+ Button("Dismiss", DismissError)
600+ | None -> ()
601+
599602 Label("Public Key:")
600− Entry(model.ContactPubkey, SetContactPubkey)
603+ Entry(pubkey, SetContactPubkey)
601604 .placeholder("sampel-palnet-...")
602605
603606 Label("Nickname:")
604− Entry(model.ContactNickname, SetContactNickname)
607+ Entry(nickname, SetContactNickname)
605608
606609 Button("Save Contact", DoSaveContact)
607610 .centerHorizontal()
@@ -615,8 +618,8 @@ module App =
615618 | Setup -> viewSetup ()
616619 | Settings -> viewSettings model
617620 | Conversations -> viewConversations model
618− | Chat pk -> viewChat model pk
619− | AddContact -> viewAddContact model
621+ | Chat(pk, compose) -> viewChat model pk compose
622+ | AddContact(pubkey, nickname) -> viewAddContact model pubkey nickname
620623
621624 Application() { Window(page) }
622625