import 'dart:io'; import 'package:comunic/enums/likes_type.dart'; import 'package:comunic/enums/post_kind.dart'; import 'package:comunic/enums/post_visibility_level.dart'; import 'package:comunic/helpers/comments_helper.dart'; import 'package:comunic/helpers/likes_helper.dart'; import 'package:comunic/helpers/posts_helper.dart'; import 'package:comunic/lists/groups_list.dart'; import 'package:comunic/lists/users_list.dart'; import 'package:comunic/models/comment.dart'; import 'package:comunic/models/like_element.dart'; import 'package:comunic/models/new_comment.dart'; import 'package:comunic/models/post.dart'; import 'package:comunic/models/user.dart'; import 'package:comunic/ui/tiles/comment_tile.dart'; import 'package:comunic/ui/widgets/account_image_widget.dart'; import 'package:comunic/ui/widgets/network_image_widget.dart'; import 'package:comunic/ui/widgets/survey_widget.dart'; import 'package:comunic/utils/date_utils.dart'; import 'package:comunic/utils/files_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/navigation_utils.dart'; import 'package:comunic/utils/post_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; /// Single posts tile /// /// @author Pierre HUBERT /// User style const TextStyle _userNameStyle = TextStyle( color: Color.fromRGBO(0x72, 0xaf, 0xd2, 1.0), //#72afd2 fontWeight: FontWeight.w600, fontSize: 16.0); /// Post actions enum _PostActions { DELETE, UPDATE_CONTENT } class PostTile extends StatefulWidget { final Post post; final UsersList usersInfo; final GroupsList groupsInfo; final void Function(Post) onDeletedPost; final bool showPostTarget; final bool userNamesClickable; const PostTile({ Key key, @required this.post, @required this.usersInfo, @required this.onDeletedPost, @required this.showPostTarget, @required this.groupsInfo, @required this.userNamesClickable, }) : assert(post != null), assert(usersInfo != null), assert(onDeletedPost != null), assert(showPostTarget != null), assert(groupsInfo != null), assert(userNamesClickable != null), super(key: key); @override State createState() => _PostTileState(); } class _PostTileState extends State { // Helpers final _postsHelper = PostsHelper(); final _likesHelper = LikesHelper(); final _commentsHelper = CommentsHelper(); // Class members TextEditingController _commentController = TextEditingController(); File _commentImage; bool _submitting = false; User get _user => widget.usersInfo.getUser(widget.post.userID); bool get _commentValid => _commentController.text.toString().length > 3; bool get _hasImage => _commentImage != null; bool get _canSubmitComment => !_submitting && (_commentValid || _hasImage); set _sendingComment(bool sending) => setState(() => _submitting = sending); String _getPostTarget() { if (!widget.showPostTarget || (!widget.post.isGroupPost && widget.post.userID == widget.post.userPageID)) return ""; return " > " + (widget.post.isGroupPost ? widget.groupsInfo[widget.post.groupID].displayName : widget.usersInfo.getUser(widget.post.userPageID).displayName); } Widget _buildHeaderRow() { // Header row return Row( children: [ // User account image Padding( padding: const EdgeInsets.only(right: 8.0, left: 8.0), child: InkWell( child: AccountImageWidget(user: _user), onTap: widget.userNamesClickable ? () => openUserPage( userID: _user.id, context: context, ) : null, ), ), // Column with user name + post target Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _user.displayName + _getPostTarget(), style: _userNameStyle, ), Text(diffTimeFromNowToStr(widget.post.timeSent)), ], ), ), InkWell( child: Icon( PostVisibilityLevelsMapIcons[widget.post.visibilityLevel], color: Colors.grey, ), onTap: widget.post.canUpdate ? updatePostVisibilityLevel : null, ), PopupMenuButton<_PostActions>( itemBuilder: (c) => [ // Update post content PopupMenuItem( child: Text(tr("Update content")), value: _PostActions.UPDATE_CONTENT, enabled: widget.post.canUpdate, ), // Delete post PopupMenuItem( child: Text(tr("Delete")), value: _PostActions.DELETE, enabled: widget.post.canDelete, ), ], onSelected: _selectedPostMenuAction, ) ], ); } Widget _buildContentRow() { Widget postContent; switch (widget.post.kind) { case PostKind.IMAGE: postContent = _buildPostImage(); break; case PostKind.YOUTUBE: postContent = _buildPostYouTube(); break; case PostKind.WEB_LINK: postContent = _buildPostWebLink(); break; case PostKind.SURVEY: postContent = _buildPostSurvey(); break; default: } return Column( children: [ // Post "rich" content Container(child: postContent), // Post text Container( child: widget.post.hasContent ? Text(widget.post.content) : null), ], ); } Widget _buildButtonsArea() { return Padding( padding: const EdgeInsets.only(top: 16.0, left: 8.0, right: 8.0, bottom: 16.0), child: Column( children: [ // Like button Center( child: InkWell( onTap: () => _updatePostLike(), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0), child: Icon( Icons.thumb_up, color: widget.post.userLike ? Colors.blue : null, ), ), Text(widget.post.likes < 2 ? tr("%num% like", args: {"num": widget.post.likes.toString()}) : tr("%num% likes", args: {"num": widget.post.likes.toString()})) ], ), ), ), ], ), ); } @override Widget build(BuildContext context) { return Card( elevation: 1.0, child: Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ _buildHeaderRow(), _buildContentRow(), _buildButtonsArea(), ], ), ), Container( child: widget.post.hasComments ? _buildComments() : null, ), ], ), ); } Widget _buildPostImage() { return NetworkImageWidget( url: widget.post.fileURL, allowFullScreen: true, roundedEdges: false, loadingHeight: 150, ); } Widget _buildPostYouTube() { return RaisedButton( color: Colors.red, textColor: Colors.white, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.ondemand_video), Text(tr("YouTube movie")) ], ), onPressed: () => launch("https://youtube.com/watch/?v=" + widget.post.filePath), ); } Widget _buildPostWebLink() { return Card( color: Color.fromRGBO(0xf7, 0xf7, 0xf7, 1), child: InkWell( onTap: () => launch(widget.post.linkURL), child: Row( children: [ Padding( padding: const EdgeInsets.only(right: 8.0), child: widget.post.hasLinkImage ? NetworkImageWidget( url: widget.post.linkImage, width: 70, roundedEdges: false, ) : Icon( Icons.link, size: 70, ), ), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(htmlDecodeCharacters(widget.post.linkTitle), style: TextStyle(fontSize: 20.0)), Text( widget.post.linkURL, maxLines: 3, ), Text(htmlDecodeCharacters(widget.post.linkDescription)) ], ), ) ], ), ), ); } /// Build post survey Widget _buildPostSurvey() { return SurveyWidget( survey: widget.post.survey, ); } /// Build the list of comments Widget _buildComments() { assert(widget.post.hasComments); final comments = List.generate( widget.post.comments.length, (num) => CommentTile( comment: widget.post.comments[num], user: widget.usersInfo.getUser(widget.post.comments[num].userID), onUpdateLike: _updateCommentLike, onUpdateComment: _updateCommentContent, onDeleteComment: _deleteComment, ), ); // Add comments form comments.add(_buildCommentsForm()); return Container( color: darkTheme() ? Colors.black38 : Colors.grey[300], child: Padding( padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), child: Column( children: comments, ), ), ); } /// Build comments form Widget _buildCommentsForm() { return Padding( padding: EdgeInsets.only( left: 8.0, right: 8.0, top: (widget.post.comments.length > 0 ? 8.0 : 0)), child: Row( children: [ // Comment input Expanded( child: TextField( // Comment max size maxLength: 255, maxLines: null, buildCounter: smartInputCounterWidgetBuilder, controller: _commentController, onChanged: (s) => setState(() {}), onSubmitted: _canSubmitComment ? (s) => _submitComment() : null, style: TextStyle( color: darkTheme() ? Colors.white : null, ), decoration: InputDecoration( hintText: tr("New comment..."), hintStyle: TextStyle(color: Colors.grey, fontSize: 12), fillColor: darkTheme() ? Colors.black38 : Colors.white, filled: true, border: OutlineInputBorder( borderSide: BorderSide(width: 0.5, color: Colors.grey), borderRadius: BorderRadius.only( topLeft: Radius.circular(3), bottomLeft: Radius.circular(3), ), gapPadding: 1), contentPadding: EdgeInsets.only( left: 10, right: 10, bottom: 5, top: 5, ), ), ), ), // Image button Container( width: 30, child: FlatButton( padding: EdgeInsets.only(), onPressed: _pickImageForComment, child: Icon( Icons.image, color: _hasImage ? Colors.blue : Colors.grey, ), ), ), // Submit button Container( width: 40, child: FlatButton( padding: EdgeInsets.only(), onPressed: _canSubmitComment ? () => _submitComment() : null, child: Icon( Icons.send, color: _canSubmitComment ? Colors.blue : Colors.grey, ), ), ), ], ), ); } /// Clear comments submitting form void clearCommentForm() { setState(() { _commentController.text = ""; _commentImage = null; }); } /// Pick an image Future _pickImageForComment() async { // Ask the user to confirm image removal if there is already one selected if (_hasImage) { if (await showConfirmDialog( context: context, title: tr("Remove selected image"), message: tr("Do you want to unselected currently selected image ?"), )) { setState(() { _commentImage = null; }); } return; } // Pick a new image final newImage = await pickImage(context); setState(() { _commentImage = newImage; }); } /// Submit comment entered by the user Future _submitComment() async { _sendingComment = true; final commentID = await _commentsHelper.createComment(NewComment( postID: widget.post.id, content: _commentController.text, image: _commentImage, )); _sendingComment = false; if (commentID < 1) return showSimpleSnack(context, tr("Could not create comment!")); clearCommentForm(); // Get and show new comment final newComment = await _commentsHelper.getSingle(commentID); if (newComment == null) return showSimpleSnack( context, tr("Could not retrieve created comment!")); setState(() { widget.post.comments.add(newComment); }); } /// Update like status Future _updatePostLike() async { _updateElementLike(LikesType.POST, widget.post); } /// Invert comment like status Future _updateCommentLike(Comment comment) async { _updateElementLike(LikesType.COMMENT, comment); } /// Invert element like status Future _updateElementLike(LikesType type, LikeElement element) async { // Update liking status _likesHelper.setLiking( type: type, like: !element.userLike, id: element.id, ); // Save new like status setState(() { element.userLike = !element.userLike; element.userLike ? element.likes++ : element.likes--; }); } /// Update comment content Future _updateCommentContent(Comment comment) async { final newContent = await askUserString( context: context, title: tr("Update comment content"), message: tr("New content:"), defaultValue: comment.content, hint: tr("New content..."), ); if (!(await _commentsHelper.updateContent(comment.id, newContent))) return showSimpleSnack(context, tr("Could not update comment content!")); setState(() { comment.content = newContent; }); } /// Process the deletion of a user Future _deleteComment(Comment comment) async { if (!await showConfirmDialog( context: context, message: tr("Do you really want to delete this comment ?"), title: tr("Delete comment"))) return; if (!await _commentsHelper.delete(comment.id)) { showSimpleSnack(context, tr("Could not delete the comment!")); return; } // Remove the comment from the list setState(() { widget.post.comments.remove(comment); }); } /// Method called each time the user has selected an option void _selectedPostMenuAction(_PostActions value) { switch (value) { // Update post content case _PostActions.UPDATE_CONTENT: updateContent(); break; // Delete post case _PostActions.DELETE: confirmDelete(); break; } } /// Update post visibility level Future updatePostVisibilityLevel() async { final newLevel = await showPostVisibilityPicker( context: context, initialLevel: widget.post.visibilityLevel, isGroup: widget.post.isGroupPost, ); if (newLevel == null || newLevel == widget.post.visibilityLevel) return; // Update post visibility if (!await _postsHelper.setVisibility(widget.post.id, newLevel)) { showSimpleSnack(context, tr("Could not update post visibility!")); return; } setState(() => widget.post.visibilityLevel = newLevel); } /// Update post content Future updateContent() async { final newContent = await askUserString( context: context, title: tr("Update post content"), message: tr("Please enter message content: "), defaultValue: widget.post.content == null ? "" : widget.post.content, hint: tr("Post content"), ); if (newContent == null) return; if (!await _postsHelper.updateContent(widget.post.id, newContent)) { showSimpleSnack(context, tr("Could not update post content!")); return; } setState(() => widget.post.content = newContent); } /// Perform the deletion of the post Future confirmDelete() async { // Ask user confirmation if (!await showConfirmDialog( context: context, message: tr( "Do you really want to delete this post ? The operation can not be reverted !"), )) return; if (!await _postsHelper.delete(widget.post.id)) { showSimpleSnack(context, tr("Could not delete the post!")); return; } widget.onDeletedPost(widget.post); } }