@@ -4,6 +4,7 @@ open System
4open System.Text.Json
4open System.Text.Json
5open Fabulous
5open Fabulous
6open Fabulous.Maui
6open Fabulous.Maui
7+open System.Collections.Generic
7open Microsoft.Maui.Graphics
8open Microsoft.Maui.Graphics
9open type Fabulous.Maui.View
10open type Fabulous.Maui.View
@@ -81,6 +82,7 @@ module App =
82 | CopyPubKey
83 | CopyPubKey
83 | DismissError
84 | DismissError
85+ | StateLoaded of identity: (byte[] * string) option * data: Store.DataDto option
85 // ── CmdMsg ──
87 // ── CmdMsg ──
@@ -90,6 +92,9 @@ module App =
90 | CmdSend of session: Session * recipientHex: string * text: string * messageId: string
92 | CmdSend of session: Session * recipientHex: string * text: string * messageId: string
91 | CmdPoll of session: Session * cursor: uint64
93 | CmdPoll of session: Session * cursor: uint64
92 | CmdCopyToClipboard of string
94 | CmdCopyToClipboard of string
96+ | CmdSaveIdentity of privKey: byte[] * pubKeyHex: string
97+ | CmdSaveData of contacts: Contact list * messages: Map<string, ChatMessage list> * serverUrl: string * cursor: uint64
94 // ── Helpers ──
99 // ── Helpers ──
@@ -123,6 +128,9 @@ module App =
123 | Online session -> Some session
128 | Online session -> Some session
124 | _ -> None
129 | _ -> None
131+ let private saveCmd model =
132+ [ CmdSaveData(model.Contacts, model.Messages, model.ServerUrl, model.PollCursor) ]
126 // ── Init ──
134 // ── Init ──
128 let init () =
136 let init () =
@@ -139,7 +147,7 @@ module App =
139 Error = None
147 Error = None
140 PollCursor = 0UL
148 PollCursor = 0UL
141 LastSendStatus = "" },
149 LastSendStatus = "" },
142− []
150+ [ CmdLoadState ]
144 // ── Update ──
152 // ── Update ──
@@ -150,7 +158,8 @@ module App =
150 | GenIdentity -> model, [ CmdGenIdentity ]
158 | GenIdentity -> model, [ CmdGenIdentity ]
152 | IdentityReady identity ->
160 | IdentityReady identity ->
153− { model with Identity = Some identity; Page = Settings }, []
161+ { model with Identity = Some identity; Page = Settings },
162+ [ CmdSaveIdentity(identity.PrivKey, identity.PubKeyHex) ]
155 | SetServerUrl url -> { model with ServerUrl = url }, []
164 | SetServerUrl url -> { model with ServerUrl = url }, []
@@ -187,10 +196,12 @@ module App =
187 IsOutgoing = true }
196 IsOutgoing = true }
188 let msgs = messagesFor pk model.Messages
197 let msgs = messagesFor pk model.Messages
190− { model with
200+ { model with
191− ComposeText = ""
201+ ComposeText = ""
192− Messages = model.Messages |> Map.add pk (msgs @ [ outMsg ]) },
202+ Messages = model.Messages |> Map.add pk (msgs @ [ outMsg ]) }
193− [ CmdSend(session, pk, text, id) ]
204+ model', CmdSend(session, pk, text, id) :: saveCmd model'
194 | _ -> model, []
205 | _ -> model, []
196 | Sent status -> { model with LastSendStatus = status }, []
207 | Sent status -> { model with LastSendStatus = status }, []
@@ -216,9 +227,12 @@ module App =
216 Messages = m.Messages |> Map.add msg.FromPubkey (msgs @ [ chatMsg ]) })
227 Messages = m.Messages |> Map.add msg.FromPubkey (msgs @ [ chatMsg ]) })
217 { model with PollStatus = status; PollCursor = newCursor }
228 { model with PollStatus = status; PollCursor = newCursor }
219− match trySession model' with
231+ match trySession model' with
220− | Some session -> model', [ CmdPoll(session, model'.PollCursor) ]
232+ | Some session -> [ CmdPoll(session, model'.PollCursor) ]
221− | None -> model', []
233+ | None -> []
235+ model', cmds @ (if not incoming.IsEmpty then saveCmd model' else [])
223 | DoPoll ->
237 | DoPoll ->
224 match trySession model with
238 match trySession model with
@@ -234,19 +248,23 @@ module App =
234 { Pubkey = model.NewPubkey
248 { Pubkey = model.NewPubkey
235 Nickname = model.NewNickname }
249 Nickname = model.NewNickname }
237− { model with
252+ { model with
238− Contacts = model.Contacts |> upsertContact contact
253+ Contacts = model.Contacts |> upsertContact contact
239− NewPubkey = ""
254+ NewPubkey = ""
240− NewNickname = ""
255+ NewNickname = ""
241− Page = Conversations },
256+ Page = Conversations }
258+ model', saveCmd model'
244 model, []
260 model, []
246 | DoDeleteContact pk ->
262 | DoDeleteContact pk ->
247− { model with
264+ { model with
248− Contacts = model.Contacts |> List.filter (fun c -> c.Pubkey <> pk) },
265+ Contacts = model.Contacts |> List.filter (fun c -> c.Pubkey <> pk) }
267+ model', saveCmd model'
251 | CopyPubKey ->
269 | CopyPubKey ->
252 match model.Identity with
270 match model.Identity with
@@ -255,6 +273,56 @@ module App =
256 | DismissError -> { model with Error = None }, []
274 | DismissError -> { model with Error = None }, []
276+ | StateLoaded(identityOpt, dataOpt) ->
277+ match identityOpt with
279+ | Some(privKey, pubKeyHex) ->
280+ let identity = { PrivKey = privKey; PubKeyHex = pubKeyHex }
288+ |> List.map (fun c ->
289+ { Pubkey = c.Pubkey; Nickname = c.Nickname }: Contact)
294+ (fun acc (KeyValue(pk, dtos)) ->
298+ |> List.map (fun m ->
301+ Timestamp = DateTimeOffset.FromUnixTimeSeconds m.TimestampUnix
302+ IsOutgoing = m.IsOutgoing })
304+ acc |> Map.add pk msgs)
308+ Identity = Some identity
312+ if data.ServerUrl <> "" then data.ServerUrl
313+ else model.ServerUrl
314+ PollCursor = data.PollCursor
315+ Page = Conversations
320+ Identity = Some identity
324+ model', [ CmdConnect(model'.ServerUrl, identity) ]
258 // ── mapCmd ──
326 // ── mapCmd ──
260 let private asyncCmd (op: Async<Msg>) : Cmd<Msg> =
328 let private asyncCmd (op: Async<Msg>) : Cmd<Msg> =
@@ -369,6 +437,44 @@ module App =
369 Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.Default.SetTextAsync(text)
437 Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.Default.SetTextAsync(text)
370 |> ignore)
438 |> ignore)
442+ let! identity = Store.loadIdentity ()
443+ let data = Store.loadData ()
444+ return StateLoaded(identity, data)
447+ | CmdSaveIdentity(privKey, pubKeyHex) ->
448+ Cmd.ofEffect (fun _ -> Store.saveIdentity privKey pubKeyHex |> ignore)
450+ | CmdSaveData(contacts, messages, serverUrl, cursor) ->
451+ Cmd.ofEffect (fun _ ->
454+ |> List.map (fun c ->
455+ { Store.ContactDto.Pubkey = c.Pubkey
456+ Store.ContactDto.Nickname = c.Nickname })
459+ let messageDtos = Dictionary<string, Store.MessageDto array>()
462+ |> Map.iter (fun pk 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 })
473+ { Contacts = contactDtos
474+ Messages = messageDtos
475+ ServerUrl = serverUrl
476+ PollCursor = cursor })
372 // ── View ──
478 // ── View ──
374 let private viewSetup () =
480 let private viewSetup () =