on master help sign in
Plumb safe area insets to model and extend bottom bar into inset padding

Reads UIViewController safe area insets on iOS, dispatches as SafeAreaChanged events, and applies top inset to main content padding. Bottom bars now extend into bottom safe area with padding, collapsing when keyboard is visible.

commitc8bf966dc4e2432623806c40620a4d27b201960b authoriko <mail@iko.soy> dateWed 22 Apr 2026, 23:20 (6 months ago) parent25c6d89
9 files changed+41−11
client/App.fs+9−3 client/AppTypes.fs+5−1 client/Keyboard.fs+5 client/Platform/iOS/AppDelegate.fs+15−2 client/Styles.fs+2−1 client/ViewAddContact.fs+1−1 client/ViewChat.fs+2−1 client/ViewConversations.fs+1−1 client/ViewEditProfile.fs+1−1
client/App.fs+9−3
@@ -135,8 +135,10 @@ module App =
135135 Profile = None
136136 FlushingOutbox = false
137137 PollRetries = 0
138− KeyboardHeight = 0. },
139− [ CmdLoadState; CmdSubscribeKeyboard ]
138+ KeyboardHeight = 0.
139+ SafeAreaTop = 0.
140+ SafeAreaBottom = 0. },
141+ [ CmdLoadState; CmdSubscribeKeyboard; CmdSubscribeSafeArea ]
140142
141143 // ── Update ──
142144
@@ -367,6 +369,7 @@ module App =
367369 | Search _ -> model, []
368370
369371 | KeyboardHeightChanged h -> { model with KeyboardHeight = h }, []
372+ | SafeAreaChanged(top, bottom) -> { model with SafeAreaTop = top; SafeAreaBottom = bottom }, []
370373
371374 | DoScanQR -> model, [ CmdScanQR ]
372375
@@ -698,6 +701,9 @@ module App =
698701 | CmdSubscribeKeyboard ->
699702 Cmd.ofEffect (fun dispatch -> Keyboard.heightChanged.Add(fun h -> dispatch (KeyboardHeightChanged h)))
700703
704+ | CmdSubscribeSafeArea ->
705+ Cmd.ofEffect (fun dispatch -> SafeArea.insetsChanged.Add(fun (t, b) -> dispatch (SafeAreaChanged(t, b))))
706+
701707 // ── View ──
702708
703709 let private pageContent model =
@@ -712,7 +718,7 @@ module App =
712718
713719 Border(page)
714720 .background(SolidColorBrush(Colors.White))
715− .padding (Avalonia.Thickness(0., 0., 0., model.KeyboardHeight))
721+ .padding (Avalonia.Thickness(0., model.SafeAreaTop, 0., model.KeyboardHeight))
716722
717723 let view model =
718724#if MOBILE
client/AppTypes.fs+5−1
@@ -60,7 +60,9 @@ module AppTypes =
6060 Profile: Profile option
6161 FlushingOutbox: bool
6262 PollRetries: int
63− KeyboardHeight: float }
63+ KeyboardHeight: float
64+ SafeAreaTop: float
65+ SafeAreaBottom: float }
6466
6567 // ── Msg ──
6668
@@ -86,6 +88,7 @@ module AppTypes =
8688 | Search of string
8789 | TokenExpired
8890 | KeyboardHeightChanged of float
91+ | SafeAreaChanged of top: float * bottom: float
8992 | DoScanQR
9093 | QRScanned of string option
9194
@@ -103,6 +106,7 @@ module AppTypes =
103106 | CmdPickPhoto
104107 | CmdSaveProfile of Profile
105108 | CmdSubscribeKeyboard
109+ | CmdSubscribeSafeArea
106110 | CmdScanQR
107111
108112 // ── Shared Helpers ──
client/Keyboard.fs+5
@@ -4,3 +4,8 @@ module Keyboard =
44 let private evt = Event<float>()
55 let heightChanged = evt.Publish
66 let triggerHeightChanged h = evt.Trigger h
7+
8+module SafeArea =
9+ let private evt = Event<float * float>()
10+ let insetsChanged = evt.Publish
11+ let triggerInsetsChanged (top, bottom) = evt.Trigger(top, bottom)
client/Platform/iOS/AppDelegate.fs+15−2
@@ -12,10 +12,23 @@ open Skrepka
1212type AppDelegate() =
1313 inherit AvaloniaAppDelegate<FabApplication>()
1414
15+ let readSafeArea () =
16+ let w = UIApplication.SharedApplication.KeyWindow
17+ if not (isNull w) then
18+ SafeArea.triggerInsetsChanged(float w.SafeAreaInsets.Top, float w.SafeAreaInsets.Bottom)
19+
1520 override this.CreateAppBuilder() =
16− NSTimer.CreateScheduledTimer(0.0, false, fun _ ->
21+ NSTimer.CreateScheduledTimer(0.1, false, fun _ ->
1722 if this.Window <> null then
18− this.Window.BackgroundColor <- UIColor.White)
23+ this.Window.BackgroundColor <- UIColor.White
24+ readSafeArea())
25+ |> ignore
26+
27+ UIDevice.CurrentDevice.BeginGeneratingDeviceOrientationNotifications()
28+ NSNotificationCenter.DefaultCenter.AddObserver(
29+ UIDevice.OrientationDidChangeNotification,
30+ Action<NSNotification>(fun _ ->
31+ NSTimer.CreateScheduledTimer(0.1, false, fun _ -> readSafeArea()) |> ignore))
1932 |> ignore
2033
2134 NSNotificationCenter.DefaultCenter.AddObserver(
client/Styles.fs+2−1
@@ -20,7 +20,7 @@ module Styles =
2020
2121 s :> IStyle
2222
23− let withBottomBar (bar: WidgetBuilder<'msg, 'a>) (content: WidgetBuilder<'msg, 'b>) =
23+ let withBottomBar safeAreaBottom (bar: WidgetBuilder<'msg, 'a>) (content: WidgetBuilder<'msg, 'b>) =
2424 Grid([], [ Dimension.Star; Dimension.Auto ]) {
2525 AnyView(content).gridRow (0)
2626
@@ -28,6 +28,7 @@ module Styles =
2828 .borderThickness(Avalonia.Thickness(0., 4., 0., 0.))
2929 .borderBrush(SolidColorBrush(Colors.Black))
3030 .background(Constants.accentColor)
31+ .padding(Avalonia.Thickness(0., 0., 0., safeAreaBottom))
3132 .gridRow (1)
3233 }
3334
client/ViewAddContact.fs+1−1
@@ -67,4 +67,4 @@ module ViewAddContact =
6767
6868 let bar = HStack(8.) { backButton (SetPage Conversations) }
6969
70− withBottomBar bar content
70+ withBottomBar model.SafeAreaBottom bar content
client/ViewChat.fs+2−1
@@ -168,4 +168,5 @@ module ViewChat =
168168 msgView).gridRow (2)
169169 })
170170
171− Styles.withBottomBar bar content
171+ let barInset = if model.KeyboardHeight > 0. then 0. else model.SafeAreaBottom
172+ Styles.withBottomBar barInset bar content
client/ViewConversations.fs+1−1
@@ -98,4 +98,4 @@ module ViewConversations =
9898 .horizontalAlignment (HorizontalAlignment.Stretch)
9999 }
100100
101− withBottomBar bar content
101+ withBottomBar model.SafeAreaBottom bar content
client/ViewEditProfile.fs+1−1
@@ -38,4 +38,4 @@ module ViewEditProfile =
3838 (button Secondary "save" DoSaveProfile).margin (8.)
3939 }
4040
41− withBottomBar bar content
41+ withBottomBar model.SafeAreaBottom bar content