import 'dart:math'; import 'package:comunic/helpers/events_helper.dart'; import 'package:comunic/helpers/groups_helper.dart'; import 'package:comunic/helpers/posts_helper.dart'; import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/lists/groups_list.dart'; import 'package:comunic/lists/posts_list.dart'; import 'package:comunic/lists/users_list.dart'; import 'package:comunic/models/comment.dart'; import 'package:comunic/models/post.dart'; import 'package:comunic/ui/screens/conversation_screen.dart'; import 'package:comunic/ui/tiles/post_tile.dart'; import 'package:comunic/ui/widgets/safe_state.dart'; import 'package:comunic/ui/widgets/scroll_watcher.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/material.dart'; /// Posts list widget /// /// Displays a list of posts /// /// @author Pierre HUBERT class PostsListWidget extends StatefulWidget { final List? topWidgets; final Future Function() getPostsList; final Future Function(int from)? getOlder; final bool showPostsTarget; final bool buildListView; final bool userNamesClickable; final bool disablePullToRefresh; const PostsListWidget({ Key? key, required this.getPostsList, required this.showPostsTarget, this.userNamesClickable = true, this.buildListView = true, this.getOlder, this.topWidgets, this.disablePullToRefresh = false, }) : assert(getPostsList != null), assert(showPostsTarget != null), assert(buildListView != null), assert(userNamesClickable != null), assert(disablePullToRefresh != null), super(key: key); @override State createState() => PostsListWidgetState(); } class PostsListWidgetState extends SafeState { // Helpers final UsersHelper _usersHelper = UsersHelper(); final GroupsHelper _groupsHelper = GroupsHelper(); // Class members PostsList? _list; late UsersList _users; late GroupsList _groups; ScrollWatcher? _scrollController; ErrorLevel _error = ErrorLevel.NONE; bool _loading = false; final _registeredPosts = Set(); set error(ErrorLevel err) => setState(() => _error = err); int get _numberTopWidgets => widget.topWidgets == null ? 0 : widget.topWidgets!.length; @override void initState() { super.initState(); _scrollController = ScrollWatcher(onReachBottom: reachedPostsBottom); // Register to events this.listen((ev) => this._addComment(ev.comment)); this.listenChangeState( (ev) => this._updateComment(ev.comment)); this.listenChangeState( (ev) => this._removeComment(ev.commentID)); } @override void dispose() { super.dispose(); _unregisterAllPosts(); } @override void setState(fn) { if (!mounted) return; super.setState(fn); // Register for posts update _registerRequiredPosts(); } @override void didChangeDependencies() { super.didChangeDependencies(); loadPostsList(); } void _loadError() { error = _list == null ? ErrorLevel.MAJOR : ErrorLevel.MINOR; _loading = false; } /// Load the list of posts Future loadPostsList({bool getOlder = false}) async { if (_loading) return; _loading = true; try { final list = !getOlder ? await widget.getPostsList() : await widget.getOlder!(_list!.oldestID); if (list == null) return _loadError(); final users = await _usersHelper.getList(list.usersID); final groups = await _groupsHelper.getList(list.groupsID); if (groups == null) return _loadError(); if (!mounted) return; setState(() { if (!getOlder) { _list = list; _users = users; _groups = groups; } else { _list!.addAll(list); _users.addAll(users); _groups.addAll(groups); } }); } catch (e, s) { print("Failed to load post information ! $e => $s"); _loadError(); } _loading = false; } /// Register for potential new posts void _registerRequiredPosts() async { if (_list == null) return; final missing = _list! .where((f) => !_registeredPosts.contains(f.id)) .map((f) => f.id) .toSet(); _registeredPosts.addAll(missing); for (final postID in missing) { await PostsHelper().registerPostEvents(postID); } } /// Unregister from all posts /// /// This method should be called only once void _unregisterAllPosts() async { for (final postID in _registeredPosts) { // We put the try - catch here because we must absolutely unregister ALL // POSTS event if one post fails to remove try { await PostsHelper().unregisterPostEvents(postID); } catch (e, stack) { print("Could not unregister post! $e $stack"); } } } Widget _buildErrorCard() { return buildErrorCard(tr("Could not get the list of posts !")); } Widget _buildNoPostNotice() { return Center( child: Padding( padding: const EdgeInsets.all(8.0), child: Text(tr("There is no post to display here yet.")!), ), ); } Widget _buildListViewWithRefreshIndicator() => RefreshIndicator( child: _buildListView(), onRefresh: () => loadPostsList(), ); Widget _buildListView() { return ListView.builder( // We use max function here to display to post notice in case there are not posts to display but there are custom widgets... itemCount: max(_list!.length, 1) + _numberTopWidgets, itemBuilder: _buildItem, controller: _scrollController, ); } Widget _buildColumn() { return Column( children: List.generate( _list!.length, (i) => _buildItem(null, i), ), ); } Widget _buildItem(BuildContext? context, int index) { if (index < _numberTopWidgets) return widget.topWidgets![index]; // Show no posts notice if required if (_list!.length == 0) return _buildNoPostNotice(); return PostTile( post: _list![index - _numberTopWidgets], usersInfo: _users, groupsInfo: _groups, onDeletedPost: _removePost, showPostTarget: widget.showPostsTarget, userNamesClickable: widget.userNamesClickable, ); } @override Widget build(BuildContext context) { if (_error == ErrorLevel.MAJOR) return _buildErrorCard(); if (_list == null) return buildCenteredProgressBar(); if (_list!.length == 0 && _numberTopWidgets == 0) return _buildNoPostNotice(); if (!widget.buildListView) return _buildColumn(); if (widget.disablePullToRefresh) return _buildListView(); return _buildListViewWithRefreshIndicator(); } void _removePost(Post post) => setState(() => _list!.remove(post)); void reachedPostsBottom() { if (widget.getOlder != null) loadPostsList(getOlder: true); } /// Add new comment void _addComment(Comment c) async { if (_list == null) return; try { // This command will throw if post could not be found final Post p = _list!.singleWhere((p) => p.id == c.postID); if (!_users.hasUser(c.userID)) _users.add(await UsersHelper().getSingleWithThrow(c.userID)); setState(() { p.comments!.add(c); }); } catch (e, stack) { print("$e\n$stack"); } } /// Update a comment void _updateComment(Comment c) { if (_list == null) return; try { final Post p = _list!.singleWhere((p) => p.id == c.postID); final index = p.comments!.indexWhere((d) => d.id == c.id); if (index > -1) p.comments![index] = c; } catch (e, stack) { print("$e\n$stack"); } } /// Remove a comment from the list void _removeComment(int? commentID) { if (_list == null) return; _list!.forEach((p) => p.comments!.removeWhere((c) => c.id == commentID)); } }