1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-12-29 06:18:51 +00:00
comunicmobile/lib/ui/tiles/post_tile.dart

625 lines
17 KiB
Dart
Raw Normal View History

2019-05-18 07:45:15 +00:00
import 'dart:io';
2019-05-11 07:48:01 +00:00
import 'package:comunic/enums/post_kind.dart';
2019-05-20 07:35:03 +00:00
import 'package:comunic/enums/post_visibility_level.dart';
2019-05-18 12:58:35 +00:00
import 'package:comunic/helpers/comments_helper.dart';
2019-05-19 12:54:09 +00:00
import 'package:comunic/helpers/posts_helper.dart';
import 'package:comunic/helpers/users_helper.dart';
2019-06-10 07:47:02 +00:00
import 'package:comunic/lists/groups_list.dart';
2019-05-10 17:15:11 +00:00
import 'package:comunic/lists/users_list.dart';
2019-05-18 13:54:10 +00:00
import 'package:comunic/models/comment.dart';
2019-05-18 12:58:35 +00:00
import 'package:comunic/models/new_comment.dart';
2019-05-10 17:15:11 +00:00
import 'package:comunic/models/post.dart';
2019-05-11 07:48:01 +00:00
import 'package:comunic/models/user.dart';
2019-05-16 12:52:22 +00:00
import 'package:comunic/ui/tiles/comment_tile.dart';
2019-05-11 07:48:01 +00:00
import 'package:comunic/ui/widgets/account_image_widget.dart';
2019-11-02 19:14:34 +00:00
import 'package:comunic/ui/widgets/countdown_widget.dart';
2020-04-15 17:07:15 +00:00
import 'package:comunic/ui/widgets/like_widget.dart';
2019-05-11 07:48:01 +00:00
import 'package:comunic/ui/widgets/network_image_widget.dart';
2019-06-28 09:32:36 +00:00
import 'package:comunic/ui/widgets/survey_widget.dart';
2020-04-16 07:53:19 +00:00
import 'package:comunic/ui/widgets/text_widget.dart';
2019-05-11 07:48:01 +00:00
import 'package:comunic/utils/date_utils.dart';
2019-05-18 07:45:15 +00:00
import 'package:comunic/utils/files_utils.dart';
2019-05-11 13:35:07 +00:00
import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/navigation_utils.dart';
2019-05-23 16:27:43 +00:00
import 'package:comunic/utils/post_utils.dart';
2019-05-18 07:45:15 +00:00
import 'package:comunic/utils/ui_utils.dart';
2019-05-10 17:15:11 +00:00
import 'package:flutter/material.dart';
2019-06-24 08:48:31 +00:00
import 'package:url_launcher/url_launcher.dart';
2019-05-10 17:15:11 +00:00
/// Single posts tile
///
/// @author Pierre HUBERT
2019-05-11 07:48:01 +00:00
/// User style
const TextStyle _userNameStyle = TextStyle(
color: Color.fromRGBO(0x72, 0xaf, 0xd2, 1.0), //#72afd2
fontWeight: FontWeight.w600,
fontSize: 16.0);
2019-05-19 12:54:09 +00:00
/// Post actions
2019-05-19 15:42:09 +00:00
enum _PostActions { DELETE, UPDATE_CONTENT }
2019-05-19 12:54:09 +00:00
2019-05-18 07:45:15 +00:00
class PostTile extends StatefulWidget {
2019-05-10 17:15:11 +00:00
final Post post;
final UsersList usersInfo;
2019-06-10 07:47:02 +00:00
final GroupsList groupsInfo;
2019-05-19 12:54:09 +00:00
final void Function(Post) onDeletedPost;
2019-05-23 16:37:56 +00:00
final bool showPostTarget;
final bool userNamesClickable;
const PostTile({
Key key,
@required this.post,
@required this.usersInfo,
2019-05-19 12:54:09 +00:00
@required this.onDeletedPost,
2019-05-23 16:37:56 +00:00
@required this.showPostTarget,
2019-06-10 07:47:02 +00:00
@required this.groupsInfo,
@required this.userNamesClickable,
}) : assert(post != null),
2019-05-10 17:15:11 +00:00
assert(usersInfo != null),
2019-05-19 12:54:09 +00:00
assert(onDeletedPost != null),
2019-05-23 16:37:56 +00:00
assert(showPostTarget != null),
2019-06-10 07:47:02 +00:00
assert(groupsInfo != null),
assert(userNamesClickable != null),
2019-05-10 17:15:11 +00:00
super(key: key);
2019-05-18 07:45:15 +00:00
@override
State<StatefulWidget> createState() => _PostTileState();
}
class _PostTileState extends State<PostTile> {
2019-05-18 13:05:26 +00:00
// Helpers
2019-05-19 12:54:09 +00:00
final _postsHelper = PostsHelper();
2019-05-18 13:05:26 +00:00
final _commentsHelper = CommentsHelper();
2019-05-18 07:45:15 +00:00
// Class members
2019-05-18 12:58:35 +00:00
TextEditingController _commentController = TextEditingController();
2019-05-18 07:45:15 +00:00
File _commentImage;
2019-05-18 12:58:35 +00:00
bool _submitting = false;
2019-05-18 07:45:15 +00:00
User get _user => widget.usersInfo.getUser(widget.post.userID);
2019-05-18 12:58:35 +00:00
bool get _commentValid => _commentController.text.toString().length > 3;
2019-05-18 07:45:15 +00:00
bool get _hasImage => _commentImage != null;
2019-05-11 07:48:01 +00:00
2019-05-18 12:58:35 +00:00
bool get _canSubmitComment => !_submitting && (_commentValid || _hasImage);
set _sendingComment(bool sending) => setState(() => _submitting = sending);
2019-05-23 16:37:56 +00:00
String _getPostTarget() {
if (!widget.showPostTarget ||
(!widget.post.isGroupPost &&
widget.post.userID == widget.post.userPageID)) return "";
return " > " +
(widget.post.isGroupPost
2019-06-10 07:47:02 +00:00
? widget.groupsInfo[widget.post.groupID].displayName
2019-05-23 16:37:56 +00:00
: widget.usersInfo.getUser(widget.post.userPageID).displayName);
}
2019-05-11 07:48:01 +00:00
Widget _buildHeaderRow() {
// Header row
return Row(
children: <Widget>[
// 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,
),
2019-05-11 07:48:01 +00:00
),
// Column with user name + post target
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
2019-05-23 16:37:56 +00:00
_user.displayName + _getPostTarget(),
2019-05-11 07:48:01 +00:00
style: _userNameStyle,
),
2019-05-18 07:45:15 +00:00
Text(diffTimeFromNowToStr(widget.post.timeSent)),
2019-05-11 07:48:01 +00:00
],
),
),
2019-05-23 16:27:43 +00:00
InkWell(
2019-05-20 07:35:03 +00:00
child: Icon(
PostVisibilityLevelsMapIcons[widget.post.visibilityLevel],
color: Colors.grey,
),
2019-05-23 16:27:43 +00:00
onTap: widget.post.canUpdate ? updatePostVisibilityLevel : null,
2019-05-20 07:35:03 +00:00
),
2019-05-19 12:54:09 +00:00
PopupMenuButton<_PostActions>(
itemBuilder: (c) => [
2019-11-01 13:17:46 +00:00
// 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,
),
],
2019-05-19 12:54:09 +00:00
onSelected: _selectedPostMenuAction,
2019-05-11 07:48:01 +00:00
)
],
);
}
Widget _buildContentRow() {
Widget postContent;
2019-05-18 07:45:15 +00:00
switch (widget.post.kind) {
2019-05-11 07:48:01 +00:00
case PostKind.IMAGE:
postContent = _buildPostImage();
break;
2019-07-01 10:18:10 +00:00
case PostKind.YOUTUBE:
postContent = _buildPostYouTube();
break;
2019-06-24 08:48:31 +00:00
case PostKind.WEB_LINK:
postContent = _buildPostWebLink();
break;
2019-11-16 08:24:20 +00:00
case PostKind.PDF:
postContent = _buildPostPDF();
break;
2019-11-02 19:14:34 +00:00
case PostKind.COUNTDOWN:
postContent = _buildCountDownTimer();
break;
2019-06-28 09:32:36 +00:00
case PostKind.SURVEY:
postContent = _buildPostSurvey();
break;
2019-05-11 07:48:01 +00:00
default:
}
return Column(
children: <Widget>[
// Post "rich" content
Container(child: postContent),
// Post text
2019-05-18 07:45:15 +00:00
Container(
2020-04-16 07:53:19 +00:00
child: widget.post.hasContent
2020-04-16 08:14:45 +00:00
? TextWidget(
content: widget.post.content,
parseBBcode: true,
)
2020-04-16 07:53:19 +00:00
: null),
2019-05-11 07:48:01 +00:00
],
);
}
2019-05-11 13:35:07 +00:00
Widget _buildButtonsArea() {
return Padding(
padding:
const EdgeInsets.only(top: 16.0, left: 8.0, right: 8.0, bottom: 16.0),
child: Column(
children: <Widget>[
// Like button
Center(
2020-04-15 17:26:51 +00:00
child: LikeWidget(
likeElement: widget.post,
buttonIconSize: null,
),
2020-04-15 17:17:29 +00:00
),
2019-05-11 13:35:07 +00:00
],
),
);
}
2019-05-10 17:15:11 +00:00
@override
Widget build(BuildContext context) {
2019-05-11 07:48:01 +00:00
return Card(
elevation: 1.0,
2019-05-16 12:52:22 +00:00
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
_buildHeaderRow(),
_buildContentRow(),
_buildButtonsArea(),
],
),
),
Container(
2019-05-18 07:45:15 +00:00
child: widget.post.hasComments ? _buildComments() : null,
2019-05-16 12:52:22 +00:00
),
],
2019-05-11 07:48:01 +00:00
),
);
}
Widget _buildPostImage() {
return NetworkImageWidget(
2019-05-18 07:45:15 +00:00
url: widget.post.fileURL,
2019-05-11 07:48:01 +00:00
allowFullScreen: true,
roundedEdges: false,
2019-05-20 07:13:12 +00:00
loadingHeight: 150,
2019-05-10 17:15:11 +00:00
);
}
2019-05-16 12:52:22 +00:00
2019-07-01 10:18:10 +00:00
Widget _buildPostYouTube() {
return RaisedButton(
color: Colors.red,
textColor: Colors.white,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.ondemand_video),
Text(tr("YouTube movie"))
],
),
onPressed: () =>
launch("https://youtube.com/watch/?v=" + widget.post.filePath),
);
}
2019-06-24 08:48:31 +00:00
Widget _buildPostWebLink() {
return Card(
2019-11-02 11:48:47 +00:00
color:
darkTheme() ? darkerAccentColor : Color.fromRGBO(0xf7, 0xf7, 0xf7, 1),
2019-06-24 08:48:31 +00:00
child: InkWell(
onTap: () => launch(widget.post.linkURL),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 8.0),
2019-06-24 18:27:20 +00:00
child: widget.post.hasLinkImage
? NetworkImageWidget(
url: widget.post.linkImage,
width: 70,
roundedEdges: false,
)
: Icon(
Icons.link,
size: 70,
),
2019-06-24 08:48:31 +00:00
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(htmlDecodeCharacters(widget.post.linkTitle),
style: TextStyle(fontSize: 20.0)),
Text(
widget.post.linkURL,
maxLines: 3,
),
Text(htmlDecodeCharacters(widget.post.linkDescription))
],
),
)
],
),
),
);
}
2019-11-16 08:24:20 +00:00
Widget _buildPostPDF() {
return RaisedButton.icon(
onPressed: () {
launch(widget.post.fileURL);
},
icon: Icon(Icons.picture_as_pdf),
label: Text(tr("PDF")),
);
}
2019-11-02 19:14:34 +00:00
Widget _buildCountDownTimer() {
return CountdownWidget(
startTime: widget.post.timeSent,
endTime: widget.post.timeEnd,
);
}
2019-06-28 09:32:36 +00:00
/// Build post survey
Widget _buildPostSurvey() {
return SurveyWidget(
survey: widget.post.survey,
);
}
2019-05-16 12:52:22 +00:00
/// Build the list of comments
Widget _buildComments() {
2019-05-18 07:45:15 +00:00
assert(widget.post.hasComments);
2019-05-16 12:52:22 +00:00
2019-05-18 07:45:15 +00:00
final comments = List<Widget>.generate(
widget.post.comments.length,
2019-05-16 12:52:22 +00:00
(num) => CommentTile(
2019-11-01 13:17:46 +00:00
comment: widget.post.comments[num],
user: widget.usersInfo.getUser(widget.post.comments[num].userID),
onUpdateComment: _updateCommentContent,
onDeleteComment: _deleteComment,
),
2019-05-16 12:52:22 +00:00
);
2019-05-18 07:45:15 +00:00
// Add comments form
comments.add(_buildCommentsForm());
2019-05-16 12:52:22 +00:00
return Container(
2019-11-01 13:17:46 +00:00
color: darkTheme() ? Colors.black38 : Colors.grey[300],
2019-05-16 12:52:22 +00:00
child: Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
child: Column(
children: comments,
),
),
);
}
2019-05-18 07:45:15 +00:00
/// 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: <Widget>[
// Comment input
Expanded(
child: TextField(
2019-05-20 07:20:11 +00:00
// Comment max size
maxLength: 255,
maxLines: null,
buildCounter: smartInputCounterWidgetBuilder,
2019-05-18 12:58:35 +00:00
controller: _commentController,
2019-05-18 07:45:15 +00:00
onChanged: (s) => setState(() {}),
2019-05-18 12:58:35 +00:00
onSubmitted: _canSubmitComment ? (s) => _submitComment() : null,
2019-11-01 13:17:46 +00:00
style: TextStyle(
color: darkTheme() ? Colors.white : null,
),
2019-05-18 07:45:15 +00:00
decoration: InputDecoration(
2019-05-20 07:20:11 +00:00
hintText: tr("New comment..."),
hintStyle: TextStyle(color: Colors.grey, fontSize: 12),
2019-11-01 13:17:46 +00:00
fillColor: darkTheme() ? Colors.black38 : Colors.white,
2019-05-20 07:20:11 +00:00
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,
),
),
2019-05-18 07:45:15 +00:00
),
),
// Image button
Container(
width: 30,
child: FlatButton(
padding: EdgeInsets.only(),
onPressed: _pickImageForComment,
2019-05-18 07:45:15 +00:00
child: Icon(
Icons.image,
color: _hasImage ? Colors.blue : Colors.grey,
),
),
),
// Submit button
Container(
width: 40,
child: FlatButton(
padding: EdgeInsets.only(),
2019-05-18 12:58:35 +00:00
onPressed: _canSubmitComment ? () => _submitComment() : null,
2019-05-18 07:45:15 +00:00
child: Icon(
Icons.send,
2019-05-18 12:58:35 +00:00
color: _canSubmitComment ? Colors.blue : Colors.grey,
2019-05-18 07:45:15 +00:00
),
),
),
],
),
);
}
2019-05-18 12:58:35 +00:00
/// Clear comments submitting form
void clearCommentForm() {
setState(() {
_commentController.text = "";
_commentImage = null;
});
}
2019-05-18 07:45:15 +00:00
/// Pick an image
Future<void> _pickImageForComment() async {
2019-05-18 07:45:15 +00:00
// Ask the user to confirm image removal if there is already one selected
if (_hasImage) {
2019-05-18 14:48:19 +00:00
if (await showConfirmDialog(
2019-05-18 07:45:15 +00:00
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
2019-05-18 12:58:35 +00:00
Future<void> _submitComment() async {
try {
_sendingComment = true;
2019-05-18 12:58:35 +00:00
final commentID = await _commentsHelper.createComment(NewComment(
postID: widget.post.id,
content: _commentController.text,
image: _commentImage,
));
2019-05-18 12:58:35 +00:00
_sendingComment = false;
2019-05-18 12:58:35 +00:00
if (commentID < 1) throw new Exception("Comment ID is inferior to 1!");
2019-05-18 12:58:35 +00:00
clearCommentForm();
2019-05-18 12:58:35 +00:00
// Get and show new comment
final newComment = await _commentsHelper.getSingle(commentID);
2019-05-18 13:05:26 +00:00
if (newComment == null)
return showSimpleSnack(
context, tr("Could not retrieve created comment!"));
2019-05-18 13:05:26 +00:00
// Get information about the user who created the comment (if required)
if (!widget.usersInfo.hasUser(newComment.userID))
widget.usersInfo
.add(await UsersHelper().getSingleWithThrow(newComment.userID));
setState(() {
widget.post.comments.add(newComment);
});
} catch (e) {
print(e);
showSimpleSnack(context, tr("Could not create comment!"));
}
2019-05-18 12:58:35 +00:00
}
2019-05-18 16:48:12 +00:00
/// Update comment content
Future<void> _updateCommentContent(Comment comment) async {
final newContent = await askUserString(
context: context,
title: tr("Update comment content"),
message: tr("New content:"),
2020-04-16 12:07:21 +00:00
defaultValue: comment.content.isNull ? "" : comment.content.content,
2019-05-18 16:48:12 +00:00
hint: tr("New content..."),
);
if (!(await _commentsHelper.updateContent(comment.id, newContent)))
return showSimpleSnack(context, tr("Could not update comment content!"));
setState(() {
2020-04-16 12:07:21 +00:00
comment.content.content = newContent;
2019-05-18 16:48:12 +00:00
});
}
2019-05-18 14:48:19 +00:00
/// Process the deletion of a user
Future<void> _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);
});
}
2019-05-19 12:54:09 +00:00
/// Method called each time the user has selected an option
void _selectedPostMenuAction(_PostActions value) {
switch (value) {
2019-05-19 15:42:09 +00:00
// Update post content
case _PostActions.UPDATE_CONTENT:
updateContent();
break;
// Delete post
2019-05-19 12:54:09 +00:00
case _PostActions.DELETE:
confirmDelete();
break;
}
}
2019-05-23 16:27:43 +00:00
/// Update post visibility level
Future<void> 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);
}
2019-05-19 15:42:09 +00:00
/// Update post content
Future<void> updateContent() async {
final newContent = await askUserString(
context: context,
title: tr("Update post content"),
message: tr("Please enter message content: "),
2020-04-16 12:07:21 +00:00
defaultValue: widget.post.content.isNull == null ? "" : widget.post.content.content,
2019-05-19 15:42:09 +00:00
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;
}
2020-04-16 12:07:21 +00:00
setState(() => widget.post.content.content = newContent);
2019-05-19 15:42:09 +00:00
}
2019-05-19 12:54:09 +00:00
/// Perform the deletion of the post
Future<void> 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);
}
2019-05-10 17:15:11 +00:00
}