@@ -4,6 +4,7 @@ open System
44open System.Text.Json
55open Fabulous
66open Fabulous.Maui
7+open System.Collections.Generic
78open Microsoft.Maui.Graphics
89
910open type Fabulous.Maui.View
@@ -81,6 +82,7 @@ module App =
8182 | DoPoll
8283 | CopyPubKey
8384 | DismissError
85+ | StateLoaded of identity: (byte[] * string) option * data: Store.DataDto option
8486
8587 // ── CmdMsg ──
8688
@@ -90,6 +92,9 @@ module App =
9092 | CmdSend of session: Session * recipientHex: string * text: string * messageId: string
9193 | CmdPoll of session: Session * cursor: uint64
9294 | CmdCopyToClipboard of string
95+ | CmdLoadState
96+ | CmdSaveIdentity of privKey: byte[] * pubKeyHex: string
97+ | CmdSaveData of contacts: Contact list * messages: Map<string, ChatMessage list> * serverUrl: string * cursor: uint64
9398
9499 // ── Helpers ──
95100
@@ -123,6 +128,9 @@ module App =
123128 | Online session -> Some session
124129 | _ -> None
125130
131+ let private saveCmd model =
132+ [ CmdSaveData(model.Contacts, model.Messages, model.ServerUrl, model.PollCursor) ]
133+
126134 // ── Init ──
127135
128136 let init () =
@@ -139,7 +147,7 @@ module App =
139147 Error = None
140148 PollCursor = 0UL
141149 LastSendStatus = "" },
142− []
150+ [ CmdLoadState ]
143151
144152 // ── Update ──
145153
@@ -150,7 +158,8 @@ module App =
150158 | GenIdentity -> model, [ CmdGenIdentity ]
151159
152160 | IdentityReady identity ->
153− { model with Identity = Some identity; Page = Settings }, []
161+ { model with Identity = Some identity; Page = Settings },
162+ [ CmdSaveIdentity(identity.PrivKey, identity.PubKeyHex) ]
154163
155164 | SetServerUrl url -> { model with ServerUrl = url }, []
156165
@@ -187,10 +196,12 @@ module App =
187196 IsOutgoing = true }
188197 let msgs = messagesFor pk model.Messages
189198
190− { model with
191− ComposeText = ""
192− Messages = model.Messages |> Map.add pk (msgs @ [ outMsg ]) },
193− [ CmdSend(session, pk, text, id) ]
199+ let model' =
200+ { model with
201+ ComposeText = ""
202+ Messages = model.Messages |> Map.add pk (msgs @ [ outMsg ]) }
203+
204+ model', CmdSend(session, pk, text, id) :: saveCmd model'
194205 | _ -> model, []
195206
196207 | Sent status -> { model with LastSendStatus = status }, []
@@ -216,9 +227,12 @@ module App =
216227 Messages = m.Messages |> Map.add msg.FromPubkey (msgs @ [ chatMsg ]) })
217228 { model with PollStatus = status; PollCursor = newCursor }
218229
219− match trySession model' with
220− | Some session -> model', [ CmdPoll(session, model'.PollCursor) ]
221− | None -> model', []
230+ let cmds =
231+ match trySession model' with
232+ | Some session -> [ CmdPoll(session, model'.PollCursor) ]
233+ | None -> []
234+
235+ model', cmds @ (if not incoming.IsEmpty then saveCmd model' else [])
222236
223237 | DoPoll ->
224238 match trySession model with
@@ -234,19 +248,23 @@ module App =
234248 { Pubkey = model.NewPubkey
235249 Nickname = model.NewNickname }
236250
237− { model with
238− Contacts = model.Contacts |> upsertContact contact
239− NewPubkey = ""
240− NewNickname = ""
241− Page = Conversations },
242− []
251+ let model' =
252+ { model with
253+ Contacts = model.Contacts |> upsertContact contact
254+ NewPubkey = ""
255+ NewNickname = ""
256+ Page = Conversations }
257+
258+ model', saveCmd model'
243259 else
244260 model, []
245261
246262 | DoDeleteContact pk ->
247− { model with
248− Contacts = model.Contacts |> List.filter (fun c -> c.Pubkey <> pk) },
249− []
263+ let model' =
264+ { model with
265+ Contacts = model.Contacts |> List.filter (fun c -> c.Pubkey <> pk) }
266+
267+ model', saveCmd model'
250268
251269 | CopyPubKey ->
252270 match model.Identity with
@@ -255,6 +273,56 @@ module App =
255273
256274 | DismissError -> { model with Error = None }, []
257275
276+ | StateLoaded(identityOpt, dataOpt) ->
277+ match identityOpt with
278+ | None -> model, []
279+ | Some(privKey, pubKeyHex) ->
280+ let identity = { PrivKey = privKey; PubKeyHex = pubKeyHex }
281+
282+ let model' =
283+ match dataOpt with
284+ | Some data ->
285+ let contacts =
286+ data.Contacts
287+ |> Array.toList
288+ |> List.map (fun c ->
289+ { Pubkey = c.Pubkey; Nickname = c.Nickname }: Contact)
290+
291+ let messages =
292+ data.Messages
293+ |> Seq.fold
294+ (fun acc (KeyValue(pk, dtos)) ->
295+ let msgs =
296+ dtos
297+ |> Array.toList
298+ |> List.map (fun m ->
299+ { Id = m.Id
300+ Body = m.Body
301+ Timestamp = DateTimeOffset.FromUnixTimeSeconds m.TimestampUnix
302+ IsOutgoing = m.IsOutgoing })
303+
304+ acc |> Map.add pk msgs)
305+ Map.empty
306+
307+ { model with
308+ Identity = Some identity
309+ Contacts = contacts
310+ Messages = messages
311+ ServerUrl =
312+ if data.ServerUrl <> "" then data.ServerUrl
313+ else model.ServerUrl
314+ PollCursor = data.PollCursor
315+ Page = Conversations
316+ Conn = Connecting }
317+
318+ | None ->
319+ { model with
320+ Identity = Some identity
321+ Page = Settings
322+ Conn = Connecting }
323+
324+ model', [ CmdConnect(model'.ServerUrl, identity) ]
325+
258326 // ── mapCmd ──
259327
260328 let private asyncCmd (op: Async<Msg>) : Cmd<Msg> =
@@ -369,6 +437,44 @@ module App =
369437 Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.Default.SetTextAsync(text)
370438 |> ignore)
371439
440+ | CmdLoadState ->
441+ asyncCmd (async {
442+ let! identity = Store.loadIdentity ()
443+ let data = Store.loadData ()
444+ return StateLoaded(identity, data)
445+ })
446+
447+ | CmdSaveIdentity(privKey, pubKeyHex) ->
448+ Cmd.ofEffect (fun _ -> Store.saveIdentity privKey pubKeyHex |> ignore)
449+
450+ | CmdSaveData(contacts, messages, serverUrl, cursor) ->
451+ Cmd.ofEffect (fun _ ->
452+ let contactDtos =
453+ contacts
454+ |> List.map (fun c ->
455+ { Store.ContactDto.Pubkey = c.Pubkey
456+ Store.ContactDto.Nickname = c.Nickname })
457+ |> Array.ofList
458+
459+ let messageDtos = Dictionary<string, Store.MessageDto array>()
460+
461+ messages
462+ |> Map.iter (fun pk msgs ->
463+ messageDtos[pk] <-
464+ msgs
465+ |> List.map (fun m ->
466+ { Store.MessageDto.Id = m.Id
467+ Store.MessageDto.Body = m.Body
468+ Store.MessageDto.TimestampUnix = m.Timestamp.ToUnixTimeSeconds()
469+ Store.MessageDto.IsOutgoing = m.IsOutgoing })
470+ |> Array.ofList)
471+
472+ Store.saveData
473+ { Contacts = contactDtos
474+ Messages = messageDtos
475+ ServerUrl = serverUrl
476+ PollCursor = cursor })
477+
372478 // ── View ──
373479
374480 let private viewSetup () =