@@ -97,6 +97,31 @@ class MessageListAppBarTitle extends StatelessWidget {
97
97
}
98
98
}
99
99
100
+ class ScrollToBottomButton extends StatelessWidget {
101
+ const ScrollToBottomButton ({super .key, required this .scrollController, required this .visibleValue});
102
+ final ValueNotifier <bool > visibleValue;
103
+ final ScrollController scrollController;
104
+
105
+ Future <void > _navigateToBottom (BuildContext context) async {
106
+ scrollController.animateTo (0 , duration: const Duration (milliseconds: 300 ), curve: Curves .easeIn);
107
+ }
108
+
109
+ @override
110
+ Widget build (BuildContext context) {
111
+ return ValueListenableBuilder <bool >(
112
+ builder: (BuildContext context, bool value, Widget ? child) {
113
+ return (value && child != null ) ? child : const SizedBox .shrink ();
114
+ },
115
+ valueListenable: visibleValue,
116
+ // TODO: fix hardcoded values for size and style here
117
+ child: IconButton (
118
+ tooltip: "Scroll to bottom" ,
119
+ icon: const Icon (Icons .expand_circle_down_rounded),
120
+ iconSize: 40 ,
121
+ style: IconButton .styleFrom (foregroundColor: const HSLColor .fromAHSL (0.5 ,240 ,0.96 ,0.68 ).toColor ()),
122
+ onPressed: () => _navigateToBottom (context)));
123
+ }
124
+ }
100
125
101
126
class MessageList extends StatefulWidget {
102
127
const MessageList ({super .key, required this .narrow});
@@ -109,6 +134,9 @@ class MessageList extends StatefulWidget {
109
134
110
135
class _MessageListState extends State <MessageList > {
111
136
MessageListView ? model;
137
+ final ScrollController scrollController = ScrollController ();
138
+
139
+ final ValueNotifier <bool > _scrollToBottomVisibleValue = ValueNotifier <bool >(false );
112
140
113
141
@override
114
142
void didChangeDependencies () {
@@ -161,7 +189,26 @@ class _MessageListState extends State<MessageList> {
161
189
child: Center (
162
190
child: ConstrainedBox (
163
191
constraints: const BoxConstraints (maxWidth: 760 ),
164
- child: _buildListView (context))))));
192
+ child: NotificationListener <ScrollEndNotification >(
193
+ onNotification: (scrollEnd) {
194
+ final metrics = scrollEnd.metrics;
195
+ if (metrics.atEdge && metrics.pixels == 0 ) {
196
+ _scrollToBottomVisibleValue.value = false ;
197
+ } else {
198
+ _scrollToBottomVisibleValue.value = true ;
199
+ }
200
+ return true ;
201
+ },
202
+ child: Stack (
203
+ children: < Widget > [
204
+ _buildListView (context),
205
+ Container (
206
+ alignment: Alignment .bottomRight,
207
+ child: ScrollToBottomButton (scrollController: scrollController, visibleValue: _scrollToBottomVisibleValue),
208
+ ),
209
+ ]
210
+ ),
211
+ ))))));
165
212
}
166
213
167
214
Widget _buildListView (context) {
@@ -179,6 +226,7 @@ class _MessageListState extends State<MessageList> {
179
226
_ => ScrollViewKeyboardDismissBehavior .manual,
180
227
},
181
228
229
+ controller: scrollController,
182
230
itemCount: length,
183
231
// Setting reverse: true means the scroll starts at the bottom.
184
232
// Flipping the indexes (in itemBuilder) means the start/bottom
0 commit comments