@@ -25,7 +25,7 @@ module App =
25 | Chat of pubkey: string * compose: string
25 | Chat of pubkey: string * compose: string
26 | AddContact of pubkey: string * nickname: string
26 | AddContact of pubkey: string * nickname: string
27 | Settings
27 | Settings
28− | EditProfile of displayName: string * bio: string * photo: string
28+ | EditProfile of displayName: string * bio: string * photo: byte[]
30 type Session =
30 type Session =
31 { Url: string
31 { Url: string
@@ -83,7 +83,7 @@ module App =
83 | DismissError
83 | DismissError
84 | StateLoaded of Identity option * Data option * Profile option
84 | StateLoaded of Identity option * Data option * Profile option
85 | DoPickPhoto
85 | DoPickPhoto
86− | PhotoPicked of string
86+ | PhotoPicked of byte[]
87 | DoSaveProfile
87 | DoSaveProfile
89 // ── CmdMsg ──
89 // ── CmdMsg ──
@@ -156,13 +156,13 @@ module App =
156 Nickname = nickname
156 Nickname = nickname
157 DisplayName = ""
157 DisplayName = ""
159− PhotoBase64 = "" }
159+ Photo = [||] }
161 let private withProfile (p: Profile) (c: Contact) =
161 let private withProfile (p: Profile) (c: Contact) =
163 DisplayName = p.DisplayName
163 DisplayName = p.DisplayName
164 Bio = p.Bio
164 Bio = p.Bio
165− PhotoBase64 = p.PhotoBase64 }
165+ Photo = p.Photo }
167 let private applyEvent (m: Model) (evt: IncomingEvent) =
167 let private applyEvent (m: Model) (evt: IncomingEvent) =
168 match evt.Envelope with
168 match evt.Envelope with
@@ -242,7 +242,7 @@ module App =
243 { model with
243 { model with
244 Auth = Identified(identity, Offline)
244 Auth = Identified(identity, Offline)
245− Page = EditProfile("", "", "") },
245+ Page = EditProfile("", "", [||]) },
246 [ CmdSaveIdentity identity ]
246 [ CmdSaveIdentity identity ]
248 | SetServerUrl url -> { model with ServerUrl = url }, []
248 | SetServerUrl url -> { model with ServerUrl = url }, []
@@ -372,7 +372,7 @@ module App =
372 let profile: Profile =
372 let profile: Profile =
373 { DisplayName = name
373 { DisplayName = name
374 Bio = bio
374 Bio = bio
375− PhotoBase64 = photo }
375+ Photo = photo }
377 { model with
377 { model with
378 Profile = Some profile
378 Profile = Some profile
@@ -431,7 +431,7 @@ module App =
431 {| ``type`` = "profile"
431 {| ``type`` = "profile"
432 display_name = profile.DisplayName
432 display_name = profile.DisplayName
433 bio = profile.Bio
433 bio = profile.Bio
434− photo = profile.PhotoBase64 |}
434+ photo = Convert.ToBase64String profile.Photo |}
437 let private sendEnvelope (session: Session) (recipientHex: string) (envelope: Envelope) =
437 let private sendEnvelope (session: Session) (recipientHex: string) (envelope: Envelope) =
@@ -472,7 +472,7 @@ module App =
472 Envelope.ProfileMessage
472 Envelope.ProfileMessage
473 { DisplayName = displayName
473 { DisplayName = displayName
474 Bio = bio
474 Bio = bio
475− PhotoBase64 = photo }
475+ Photo = if photo = "" then [||] else Convert.FromBase64String photo }
477 | _ -> None
477 | _ -> None
@@ -510,9 +510,7 @@ module App =
511 match app.ApplicationLifetime with
511 match app.ApplicationLifetime with
512 | :? Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime as desktop ->
512 | :? Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime as desktop ->
515− |> Option.map (fun w -> w.StorageProvider)
513+ desktop.MainWindow |> Option.ofObj |> Option.map (fun w -> w.StorageProvider)
516 | _ -> None
514 | _ -> None
518 let mapCmd cmdMsg =
516 let mapCmd cmdMsg =
@@ -638,25 +636,31 @@ module App =
639 match getStorageProvider () with
637 match getStorageProvider () with
640 | Some sp ->
638 | Some sp ->
641− let opts = Avalonia.Platform.Storage.FilePickerOpenOptions(
640+ Avalonia.Platform.Storage.FilePickerOpenOptions(
642− AllowMultiple = false,
641+ AllowMultiple = false,
643− Title = "Pick Photo",
642+ Title = "Pick Photo",
644− FileTypeFilter = [
643+ FileTypeFilter =
645− Avalonia.Platform.Storage.FilePickerFileType("Images",
644+ [ Avalonia.Platform.Storage.FilePickerFileType(
646− Patterns = [ "*.png"; "*.jpg"; "*.jpeg"; "*.gif"; "*.bmp" ])
646+ Patterns = [ "*.png"; "*.jpg"; "*.jpeg"; "*.gif"; "*.bmp" ]
648 let! files = sp.OpenFilePickerAsync(opts)
650 let! files = sp.OpenFilePickerAsync(opts)
649 match files |> Seq.tryHead with
652 match files |> Seq.tryHead with
650− | None -> dispatch (PhotoPicked "")
653+ | None -> dispatch (PhotoPicked [||])
651 | Some file ->
654 | Some file ->
652 use! stream = file.OpenReadAsync()
655 use! stream = file.OpenReadAsync()
653 use ms = new MemoryStream()
656 use ms = new MemoryStream()
654 do! stream.CopyToAsync(ms)
657 do! stream.CopyToAsync(ms)
655− dispatch (PhotoPicked(Convert.ToBase64String(ms.ToArray())))
658+ dispatch (PhotoPicked(ms.ToArray()))
656− | None -> dispatch (PhotoPicked "")
659+ | None -> dispatch (PhotoPicked [||])
657 with _ ->
660 with _ ->
658− dispatch (PhotoPicked "")
661+ dispatch (PhotoPicked [||])
659− } :> System.Threading.Tasks.Task)
663+ :> System.Threading.Tasks.Task)
660 |> ignore)
664 |> ignore)
662 | CmdSaveProfile profile -> Cmd.ofEffect (fun _ -> Store.saveProfile profile)
666 | CmdSaveProfile profile -> Cmd.ofEffect (fun _ -> Store.saveProfile profile)
@@ -667,7 +671,7 @@ module App =
667 VStack(4.) {
671 VStack(4.) {
668 match error with
672 match error with
669 | Some err ->
673 | Some err ->
670− TextBlock(err).foreground(SolidColorBrush(Colors.Red)).fontSize(12.)
674+ TextBlock(err).foreground(SolidColorBrush(Colors.Red)).fontSize (12.)
671 Button("Dismiss", DismissError)
675 Button("Dismiss", DismissError)
672 | None -> ()
676 | None -> ()
@@ -676,77 +680,73 @@ module App =
676 (VStack(24.) {
680 (VStack(24.) {
677 Image("avares://Skrepka/Assets/Images/logo.png", Avalonia.Media.Stretch.Uniform)
681 Image("avares://Skrepka/Assets/Images/logo.png", Avalonia.Media.Stretch.Uniform)
678 .margin(8., 0., 8., 0.)
682 .margin(8., 0., 8., 0.)
679− .maxWidth(300.)
683+ .maxWidth (300.)
681 TextBlock("Skrepka")
685 TextBlock("Skrepka")
682 .fontSize(32.)
686 .fontSize(32.)
683 .fontFamily(FontFamily("avares://Skrepka/Assets/Fonts#Unbounded"))
687 .fontFamily(FontFamily("avares://Skrepka/Assets/Fonts#Unbounded"))
684 .fontWeight(FontWeight.Bold)
688 .fontWeight(FontWeight.Bold)
685− .centerText()
689+ .centerText ()
687− TextBlock("").height(50.)
691+ TextBlock("").height (50.)
689 button "Next" GenIdentity
693 button "Next" GenIdentity
691 .margin(30.)
695 .margin(30.)
692− .centerVertical()
696+ .centerVertical ()
694 let private viewSettings model =
698 let private viewSettings model =
695 ScrollViewer(
699 ScrollViewer(
696 (VStack(16.) {
700 (VStack(16.) {
697− TextBlock("Settings")
701+ TextBlock("Settings").fontSize(24.).centerText ()
701 match model.Auth with
703 match model.Auth with
702 | Identified(id, conn) ->
704 | Identified(id, conn) ->
703 TextBlock("Your Public Key:")
705 TextBlock("Your Public Key:")
705− TextBlock(truncKey id.PubKeyHex).fontSize(14.)
707+ TextBlock(truncKey id.PubKeyHex).fontSize (14.)
707 Button("Copy Public Key", CopyPubKey)
709 Button("Copy Public Key", CopyPubKey)
709− TextBlock("Profile:").fontSize(18.)
711+ TextBlock("Profile:").fontSize (18.)
711 let displayName, bio, photo =
713 let displayName, bio, photo =
712 match model.Profile with
714 match model.Profile with
713− | Some p -> p.DisplayName, p.Bio, p.PhotoBase64
715+ | Some p -> p.DisplayName, p.Bio, p.Photo
714− | None -> "", "", ""
716+ | None -> "", "", [||]
716 if displayName <> "" then
718 if displayName <> "" then
717− TextBlock($"Name: {displayName}").fontSize(14.)
719+ TextBlock($"Name: {displayName}").fontSize (14.)
719 if bio <> "" then
721 if bio <> "" then
720− TextBlock($"Bio: {bio}")
722− .foreground(SolidColorBrush(Colors.DimGray))
722+ TextBlock($"Bio: {bio}").fontSize(14.).foreground (SolidColorBrush(Colors.DimGray))
724 Button("Edit Profile", SetPage(EditProfile(displayName, bio, photo)))
724 Button("Edit Profile", SetPage(EditProfile(displayName, bio, photo)))
726 TextBlock("Server:")
726 TextBlock("Server:")
728 match conn with
728 match conn with
729− | Online _ -> TextBlock(model.ServerUrl).fontSize(14.)
729+ | Online _ -> TextBlock(model.ServerUrl).fontSize (14.)
730 | _ -> TextBox(model.ServerUrl, SetServerUrl)
730 | _ -> TextBox(model.ServerUrl, SetServerUrl)
732 viewErrorBanner model.Error
732 viewErrorBanner model.Error
734 match conn with
734 match conn with
735 | Offline -> Button("Connect", DoConnect)
735 | Offline -> Button("Connect", DoConnect)
736− | Connecting -> TextBlock("Connecting...").centerText()
736+ | Connecting -> TextBlock("Connecting...").centerText ()
737 | Online _ ->
737 | Online _ ->
738 Button("Disconnect", DoDisconnect)
738 Button("Disconnect", DoDisconnect)
739 Button("Go to Conversations", SetPage Conversations)
739 Button("Go to Conversations", SetPage Conversations)
740 | NoIdentity -> ()
740 | NoIdentity -> ()
742− .margin(20.)
742+ .margin (20.)
745 let private viewConversations model =
745 let private viewConversations model =
746 ScrollViewer(
746 ScrollViewer(
747 (VStack(8.) {
747 (VStack(8.) {
748 HStack(8.) {
748 HStack(8.) {
749− TextBlock("Conversations").fontSize(24.)
749+ TextBlock("Conversations").fontSize (24.)
751 Button("+", SetPage(AddContact("", "")))
751 Button("+", SetPage(AddContact("", "")))
752 Button("Settings", SetPage Settings)
752 Button("Settings", SetPage Settings)
@@ -765,14 +765,14 @@ module App =
766 TextBlock($"{connStatusLabel model.Auth} [{pubPrefix}]{pollInfo}")
766 TextBlock($"{connStatusLabel model.Auth} [{pubPrefix}]{pollInfo}")
767 .fontSize(10.)
767 .fontSize(10.)
768− .foreground(SolidColorBrush(Colors.DimGray))
768+ .foreground (SolidColorBrush(Colors.DimGray))
770 viewErrorBanner model.Error
770 viewErrorBanner model.Error
772 if model.Contacts.IsEmpty then
772 if model.Contacts.IsEmpty then
773 TextBlock("No contacts yet. Tap + to add one.")
773 TextBlock("No contacts yet. Tap + to add one.")
774 .centerText()
774 .centerText()
775− .foreground(SolidColorBrush(Colors.Gray))
775+ .foreground (SolidColorBrush(Colors.Gray))
777 for c in model.Contacts.Values do
777 for c in model.Contacts.Values do
778 let preview =
778 let preview =
@@ -783,7 +783,7 @@ module App =
784 Button($"{contactName model.Contacts c.Pubkey}\n{preview}", SetPage(Chat(c.Pubkey, "")))
784 Button($"{contactName model.Contacts c.Pubkey}\n{preview}", SetPage(Chat(c.Pubkey, "")))
786− .margin(20.)
786+ .margin (20.)
789 let private viewChat model pk compose =
789 let private viewChat model pk compose =
@@ -794,7 +794,7 @@ module App =
794 HStack(8.) {
794 HStack(8.) {
795 Button("< Back", SetPage Conversations)
795 Button("< Back", SetPage Conversations)
797− TextBlock(name).fontSize(20.).centerVertical()
797+ TextBlock(name).fontSize(20.).centerVertical ()
800 viewErrorBanner model.Error
800 viewErrorBanner model.Error
@@ -802,7 +802,7 @@ module App =
802 ScrollViewer(
802 ScrollViewer(
803 VStack(4.) {
803 VStack(4.) {
804 if msgs.IsEmpty then
804 if msgs.IsEmpty then
805− TextBlock("No messages yet").padding(8.).fontSize(14.)
805+ TextBlock("No messages yet").padding(8.).fontSize (14.)
807 for m in msgs do
807 for m in msgs do
808 let prefix, tick =
808 let prefix, tick =
@@ -811,76 +811,74 @@ module App =
811 | Outgoing DeliveryStatus.Sent -> "You: ", ""
811 | Outgoing DeliveryStatus.Sent -> "You: ", ""
812 | Incoming -> "", ""
812 | Incoming -> "", ""
814− TextBlock($"{prefix}{m.Body}{tick}").padding(8.).fontSize(14.)
814+ TextBlock($"{prefix}{m.Body}{tick}").padding(8.).fontSize (14.)
818 HStack(8.) {
818 HStack(8.) {
819 TextBox(compose, fun text -> SetPage(Chat(pk, text)))
819 TextBox(compose, fun text -> SetPage(Chat(pk, text)))
820 .watermark("Message...")
820 .watermark("Message...")
821− .horizontalAlignment(Avalonia.Layout.HorizontalAlignment.Stretch)
821+ .horizontalAlignment (Avalonia.Layout.HorizontalAlignment.Stretch)
823 Button("Send", DoSend)
823 Button("Send", DoSend)
826− .margin(12.)
826+ .margin (12.)
828 let private viewAddContact model cpk cnn =
828 let private viewAddContact model cpk cnn =
829 (VStack(16.) {
829 (VStack(16.) {
830 HStack(8.) {
830 HStack(8.) {
831 Button("< Back", SetPage Conversations)
831 Button("< Back", SetPage Conversations)
833− TextBlock("Add Contact").fontSize(20.)
833+ TextBlock("Add Contact").fontSize (20.)
836 viewErrorBanner model.Error
836 viewErrorBanner model.Error
838 TextBlock("Public Key:")
838 TextBlock("Public Key:")
839− TextBox(cpk, fun text -> SetPage(AddContact(text, cnn))).watermark("sampel-palnet-...")
839+ TextBox(cpk, fun text -> SetPage(AddContact(text, cnn))).watermark ("sampel-palnet-...")
841 TextBlock("Nickname:")
841 TextBlock("Nickname:")
842 TextBox(cnn, fun text -> SetPage(AddContact(cpk, text)))
842 TextBox(cnn, fun text -> SetPage(AddContact(cpk, text)))
844− Button("Save Contact", DoSaveContact).centerHorizontal()
844+ Button("Save Contact", DoSaveContact).centerHorizontal ()
846− .margin(20.)
846+ .margin (20.)
848− let private viewEditProfile model displayName bio photo =
848+ let private viewEditProfile model displayName bio (photo: byte[]) =
849 ScrollViewer(
849 ScrollViewer(
850 (VStack(16.) {
850 (VStack(16.) {
851 HStack(8.) {
851 HStack(8.) {
852 Button("< Back", SetPage Settings)
852 Button("< Back", SetPage Settings)
853− TextBlock("Edit Profile").fontSize(20.)
853+ TextBlock("Edit Profile").fontSize (20.)
856 viewErrorBanner model.Error
856 viewErrorBanner model.Error
858− if photo <> "" then
858+ if photo.Length > 0 then
860− new Avalonia.Media.Imaging.Bitmap(new MemoryStream(Convert.FromBase64String(photo))),
860+ new Avalonia.Media.Imaging.Bitmap(new MemoryStream(photo: byte[])),
861− Avalonia.Media.Stretch.Uniform)
861+ Avalonia.Media.Stretch.Uniform
862 .width(120.)
863 .width(120.)
863 .height(120.)
864 .height(120.)
864− .centerHorizontal()
865+ .centerHorizontal ()
866− TextBlock("No Photo")
869− .foreground(SolidColorBrush(Colors.Gray))
867+ TextBlock("No Photo").fontSize(16.).centerText().foreground (SolidColorBrush(Colors.Gray))
871− Button("Change Photo", DoPickPhoto).centerHorizontal()
869+ Button("Change Photo", DoPickPhoto).centerHorizontal ()
873 TextBlock("Display Name:")
871 TextBlock("Display Name:")
874− TextBox(displayName, fun text -> SetPage(EditProfile(text, bio, photo))).watermark("Your name")
872+ TextBox(displayName, fun text -> SetPage(EditProfile(text, bio, photo))).watermark ("Your name")
876 TextBlock("Bio:")
874 TextBlock("Bio:")
878 TextBox(bio, fun text -> SetPage(EditProfile(displayName, text, photo)))
876 TextBox(bio, fun text -> SetPage(EditProfile(displayName, text, photo)))
879− .watermark("Tell something about yourself")
877+ .watermark ("Tell something about yourself")
881− Button("Save Profile", DoSaveProfile).centerHorizontal()
879+ Button("Save Profile", DoSaveProfile).centerHorizontal ()
883− .margin(20.)
881+ .margin (20.)
886 let private pageContent model =
884 let private pageContent model =
@@ -893,7 +891,7 @@ module App =
893 | AddContact(pk, nn) -> AnyView(viewAddContact model pk nn)
891 | AddContact(pk, nn) -> AnyView(viewAddContact model pk nn)
894 | EditProfile(displayName, bio, photo) -> AnyView(viewEditProfile model displayName bio photo)
892 | EditProfile(displayName, bio, photo) -> AnyView(viewEditProfile model displayName bio photo)
896− Border(page).background(SolidColorBrush(Colors.White))
894+ Border(page).background (SolidColorBrush(Colors.White))
898 let view model =
896 let view model =
899#if MOBILE
897#if MOBILE
@@ -902,11 +900,13 @@ module App =
902 DesktopApplication() {
900 DesktopApplication() {
903 Window(pageContent model)
901 Window(pageContent model)
904 .title("Skrepka")
902 .title("Skrepka")
903+ .icon("avares://Skrepka/Assets/Images/logo.png")
905 .width(450.)
904 .width(450.)
906− .height(750.)
905+ .height (750.)
910 let program = Program.statefulWithCmdMsg init update mapCmd |> Program.withView view
909 let program = Program.statefulWithCmdMsg init update mapCmd |> Program.withView view
912− let create () = FabulousAppBuilder.Configure(FluentTheme, program)
912+ FabulousAppBuilder.Configure(FluentTheme, program)