diff --git a/CHANGELOG.md b/CHANGELOG.md index d16c78c..b413cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Set an optional max length on the default post title input field - Add a postCreationFloatingActionButtonColor to the timeline theme to set the color of the floating action button - Add a post and a category to the postViewOpenPageBuilder function +- Add a refresh functionality to the timeline with a pull to refresh callback to allow additional functionality when refreshing the timeline ## 3.0.1 diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart index 0b6da31..88a3927 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_gorouter_userstory.dart @@ -33,6 +33,7 @@ List getTimelineStoryRoutes({ var timelineScreen = TimelineScreen( userId: config.getUserId?.call(context) ?? config.userId, onUserTap: (user) => config.onUserTap?.call(context, user), + onRefresh: config.onRefresh, service: service, options: config.optionsBuilder(context), onPostTap: (post) async => diff --git a/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart index 802876f..054cf4a 100644 --- a/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart +++ b/packages/flutter_timeline/lib/src/flutter_timeline_navigator_userstory.dart @@ -60,6 +60,7 @@ Widget _timelineScreenRoute({ ), ), ), + onRefresh: config.onRefresh, filterEnabled: config.filterEnabled, postWidgetBuilder: config.postWidgetBuilder, ); diff --git a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart index dffadeb..3d35d6c 100644 --- a/packages/flutter_timeline/lib/src/models/timeline_configuration.dart +++ b/packages/flutter_timeline/lib/src/models/timeline_configuration.dart @@ -57,6 +57,7 @@ class TimelineUserStoryConfiguration { this.postOverviewOpenPageBuilder, this.onPostTap, this.onUserTap, + this.onRefresh, this.onPostDelete, this.filterEnabled = false, this.postWidgetBuilder, @@ -127,6 +128,9 @@ class TimelineUserStoryConfiguration { /// A callback function invoked when the user's profile is tapped. final Function(BuildContext context, String userId)? onUserTap; + /// A callback function invoked when the timeline is refreshed by pulling down + final Function(BuildContext context, String? category)? onRefresh; + /// A callback function invoked when a post deletion is requested. final Widget Function(BuildContext context, TimelinePost post)? onPostDelete; diff --git a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart index a9d7575..cdebf17 100644 --- a/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart +++ b/packages/flutter_timeline_view/lib/src/screens/timeline_screen.dart @@ -16,6 +16,7 @@ class TimelineScreen extends StatefulWidget { this.onPostTap, this.scrollController, this.onUserTap, + this.onRefresh, this.posts, this.timelineCategory, this.postWidgetBuilder, @@ -45,6 +46,9 @@ class TimelineScreen extends StatefulWidget { /// Called when a post is tapped final Function(TimelinePost)? onPostTap; + /// Called when the timeline is refreshed by pulling down + final Function(BuildContext context, String? category)? onRefresh; + /// If this is not null, the user can tap on the user avatar or name final Function(String userId)? onUserTap; @@ -218,72 +222,81 @@ class _TimelineScreenState extends State { height: 12, ), Expanded( - child: SingleChildScrollView( - controller: controller, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - /// Add a optional custom header to the list of posts - widget.options.listHeaderBuilder?.call(context, category) ?? - const SizedBox.shrink(), - ...posts.map( - (post) => Padding( - padding: widget.options.postPadding, - child: widget.postWidgetBuilder?.call(post) ?? - TimelinePostWidget( - service: service, - userId: widget.userId, - options: widget.options, - post: post, - onTap: () async { - if (widget.onPostTap != null) { - widget.onPostTap!.call(post); + child: RefreshIndicator( + onRefresh: () async { + await widget.onRefresh?.call(context, category); + await loadPosts(); + }, + child: SingleChildScrollView( + controller: controller, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + /// Add a optional custom header to the list of posts + widget.options.listHeaderBuilder + ?.call(context, category) ?? + const SizedBox.shrink(), + ...posts.map( + (post) => Padding( + padding: widget.options.postPadding, + child: widget.postWidgetBuilder?.call(post) ?? + TimelinePostWidget( + service: service, + userId: widget.userId, + options: widget.options, + post: post, + onTap: () async { + if (widget.onPostTap != null) { + widget.onPostTap!.call(post); - return; - } + return; + } - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => Scaffold( - body: TimelinePostScreen( - userId: 'test_user', - service: service, - options: widget.options, - post: post, - onPostDelete: () { - service.postService.deletePost(post); - Navigator.of(context).pop(); - }, + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Scaffold( + body: TimelinePostScreen( + userId: 'test_user', + service: service, + options: widget.options, + post: post, + onPostDelete: () { + service.postService + .deletePost(post); + Navigator.of(context).pop(); + }, + ), ), ), - ), - ); - }, - onTapLike: () async => service.postService - .likePost(widget.userId, post), - onTapUnlike: () async => service.postService - .unlikePost(widget.userId, post), - onPostDelete: () async => - service.postService.deletePost(post), - onUserTap: widget.onUserTap, - ), - ), - ), - if (posts.isEmpty) - Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - category == null - ? widget.options.translations.noPosts! - : widget - .options.translations.noPostsWithFilter!, - style: widget.options.theme.textStyles.noPostsStyle, - ), + ); + }, + onTapLike: () async => service.postService + .likePost(widget.userId, post), + onTapUnlike: () async => service.postService + .unlikePost(widget.userId, post), + onPostDelete: () async => + service.postService.deletePost(post), + onUserTap: widget.onUserTap, + ), ), ), - ], + if (posts.isEmpty) + Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + category == null + ? widget.options.translations.noPosts! + : widget + .options.translations.noPostsWithFilter!, + style: + widget.options.theme.textStyles.noPostsStyle, + ), + ), + ), + ], + ), ), ), ),