@@ -12,6 +12,73 @@ module ViewChat =
12 open Buttons
12 open Buttons
13 open TextFields
13 open TextFields
15+ let private scrollRef = Fabulous.ViewRef<Avalonia.Controls.ScrollViewer>()
16+ let mutable private lastMsgCount = 0
17+ let mutable private pageSize = 50
18+ let mutable private loadingMore = false
19+ let mutable private isAtBottom = true
21+ let private scrollToEnd () =
22+ Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(fun () ->
23+ match scrollRef.TryValue with
24+ | Some sv -> sv.ScrollToEnd()
26+ , Avalonia.Threading.DispatcherPriority.Background) |> ignore
28+ let private messageItem m =
29+ let align, cols, col =
30+ match m.Direction with
31+ | Outgoing _ -> HorizontalAlignment.Right, [ Dimension.Stars 3.; Dimension.Stars 7. ], 1
32+ | Incoming -> HorizontalAlignment.Left, [ Dimension.Stars 7.; Dimension.Stars 3. ], 0
35+ match m.Direction with
36+ | Outgoing DeliveryStatus.Sent -> true
42+ .stroke(Avalonia.Media.Colors.Black)
44+ .strokeDashArray([ 4.; 2. ])
45+ .isHitTestVisible(false)
46+ .onLoaded(fun args ->
47+ let rect = args.Source :?> Avalonia.Controls.Shapes.Rectangle
48+ let anim = Avalonia.Animation.Animation()
49+ anim.Duration <- System.TimeSpan.FromSeconds 3.
50+ anim.IterationCount <- Avalonia.Animation.IterationCount.Infinite
51+ anim.Easing <- Avalonia.Animation.Easings.LinearEasing()
52+ let kf1 = Avalonia.Animation.KeyFrame()
53+ kf1.Setters.Add(Avalonia.Styling.Setter(Avalonia.Controls.Shapes.Shape.StrokeDashOffsetProperty, box 0.))
54+ kf1.Cue <- Avalonia.Animation.Cue(0.)
55+ let kf2 = Avalonia.Animation.KeyFrame()
56+ kf2.Setters.Add(Avalonia.Styling.Setter(Avalonia.Controls.Shapes.Shape.StrokeDashOffsetProperty, box 6.))
57+ kf2.Cue <- Avalonia.Animation.Cue(1.)
58+ anim.Children.Add(kf1)
59+ anim.Children.Add(kf2)
60+ anim.RunAsync(rect) |> ignore
63+ Rectangle().stroke(Avalonia.Media.Colors.Black).strokeThickness(4.).isHitTestVisible (false)
65+ Grid(cols, [ Dimension.Auto ]) {
70+ .fontFamily(Constants.fontFamily)
71+ .fontWeight(Avalonia.Media.FontWeight.Bold)
72+ .textWrapping(Avalonia.Media.TextWrapping.Wrap)
77+ .horizontalAlignment(align)
15 let viewChat model pk compose =
82 let viewChat model pk compose =
16 let name = contactName model.Contacts pk
83 let name = contactName model.Contacts pk
17 let msgs = messagesFor pk model.Messages
84 let msgs = messagesFor pk model.Messages
@@ -20,8 +87,11 @@ module ViewChat =
20 (Grid([ Dimension.Auto; Dimension.Star; Dimension.Auto ], [ Dimension.Auto ]) {
87 (Grid([ Dimension.Auto; Dimension.Star; Dimension.Auto ], [ Dimension.Auto ]) {
21 (backButton (SetPage Conversations)).gridColumn (0)
88 (backButton (SetPage Conversations)).gridColumn (0)
23− (textField compose (fun text -> SetPage(Chat(pk, text))))
90+ TextBox(compose, fun text -> SetPage(Chat(pk, text)))
91+ .watermark("Message...")
92+ .fontFamily(Constants.fontFamily)
24 .verticalAlignment(VerticalAlignment.Stretch)
93 .verticalAlignment(VerticalAlignment.Stretch)
94+ .verticalContentAlignment(VerticalAlignment.Center)
25 .margin(8.)
95 .margin(8.)
26 .gridColumn (1)
96 .gridColumn (1)
@@ -35,39 +105,50 @@ module ViewChat =
36 (viewErrorBanner model.Error).gridRow (1)
106 (viewErrorBanner model.Error).gridRow (1)
38− (if msgs.IsEmpty then
109+ if msgs.IsEmpty then
39− AnyView(TextBlock("No messages yet").padding(8.).fontSize (14.))
112+ AnyView(TextBlock("No messages yet").padding(8.).fontSize (14.))
41− let layout = Avalonia.Layout.StackLayout()
114+ if msgs.Length <> lastMsgCount then
42− layout.DisableVirtualization <- true
115+ lastMsgCount <- msgs.Length
43−
116+ if isAtBottom then scrollToEnd ()
45− ScrollViewer(
118+ let skip = max 0 (msgs.Length - pageSize)
46− ItemsRepeater(msgs, fun m ->
119+ let visible = msgs |> List.skip skip
48− match m.Direction with
121+ AnyView(
49− | Outgoing DeliveryStatus.Delivered -> " ✓"
122+ ScrollViewer(
50− | Outgoing DeliveryStatus.Sent -> ""
123+ VStack(0.) {
51− | Incoming -> ""
124+ for m in visible do
54− match m.Direction with
127+ .reference(scrollRef)
55− | Incoming -> HorizontalAlignment.Right
128+ .onLoaded(fun _ ->
56− | Outgoing _ -> HorizontalAlignment.Left
129+ pageSize <- 50
57−
130+ loadingMore <- false
58− Border(
131+ isAtBottom <- true
59− TextBlock($"{m.Body}{tick}")
132+ scrollToEnd ()
60− .fontSize(14.)
133+ DismissError)
61− .padding (8.))
134+ .onScrollChanged(fun args ->
62− .borderThickness(Avalonia.Thickness(2.))
135+ let sv = args.Source :?> Avalonia.Controls.ScrollViewer
63− .borderBrush(Avalonia.Media.SolidColorBrush(Avalonia.Media.Colors.Black))
136+ isAtBottom <- sv.Offset.Y + sv.Viewport.Height >= sv.Extent.Height - 20.
64− .horizontalAlignment(align)
137+ if sv.Offset.Y < 50. && not loadingMore && skip > 0 then
65− .margin (4.))
138+ loadingMore <- true
66− .layout(layout)
139+ let oldHeight = sv.Extent.Height
67− )
140+ pageSize <- min msgs.Length (pageSize + 50)
68− .horizontalScrollBarVisibility(Avalonia.Controls.Primitives.ScrollBarVisibility.Disabled)
141+ Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(fun () ->
69− .offset(Avalonia.Vector(0., System.Double.MaxValue))))
142+ match scrollRef.TryValue with
144+ let newHeight = sv2.Extent.Height
145+ sv2.Offset <- Avalonia.Vector(0., sv2.Offset.Y + (newHeight - oldHeight))
146+ loadingMore <- false
147+ | None -> loadingMore <- false
148+ , Avalonia.Threading.DispatcherPriority.Background) |> ignore
149+ SetPage(Chat(pk, compose))))
70− .gridRow (2)
151+ msgView).gridRow (2)
73 Styles.withBottomBar bar content
154 Styles.withBottomBar bar content