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