@@ -97,6 +97,31 @@ class MessageListAppBarTitle extends StatelessWidget {
9797 }
9898}
9999
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+ }
100125
101126class MessageList extends StatefulWidget {
102127 const MessageList ({super .key, required this .narrow});
@@ -109,6 +134,9 @@ class MessageList extends StatefulWidget {
109134
110135class _MessageListState extends State <MessageList > {
111136 MessageListView ? model;
137+ final ScrollController scrollController = ScrollController ();
138+
139+ final ValueNotifier <bool > _scrollToBottomVisibleValue = ValueNotifier <bool >(false );
112140
113141 @override
114142 void didChangeDependencies () {
@@ -161,7 +189,26 @@ class _MessageListState extends State<MessageList> {
161189 child: Center (
162190 child: ConstrainedBox (
163191 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+ ))))));
165212 }
166213
167214 Widget _buildListView (context) {
@@ -179,6 +226,7 @@ class _MessageListState extends State<MessageList> {
179226 _ => ScrollViewKeyboardDismissBehavior .manual,
180227 },
181228
229+ controller: scrollController,
182230 itemCount: length,
183231 // Setting reverse: true means the scroll starts at the bottom.
184232 // Flipping the indexes (in itemBuilder) means the start/bottom
0 commit comments