1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-11-22 21:09:21 +00:00
comunicmobile/lib/ui/tiles/post_tile.dart

650 lines
18 KiB
Dart

import 'dart:math';
import 'package:comunic/enums/post_kind.dart';
import 'package:comunic/enums/post_visibility_level.dart';
import 'package:comunic/enums/report_target_type.dart';
import 'package:comunic/helpers/comments_helper.dart';
import 'package:comunic/helpers/posts_helper.dart';
import 'package:comunic/helpers/server_config_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/new_comment.dart';
import 'package:comunic/models/post.dart';
import 'package:comunic/models/report_target.dart';
import 'package:comunic/models/user.dart';
import 'package:comunic/ui/dialogs/post_visibility_picker_dialog.dart';
import 'package:comunic/ui/dialogs/report_dialog.dart';
import 'package:comunic/ui/tiles/comment_tile.dart';
import 'package:comunic/ui/widgets/account_image_widget.dart';
import 'package:comunic/ui/widgets/countdown_widget.dart';
import 'package:comunic/ui/widgets/like_widget.dart';
import 'package:comunic/ui/widgets/network_image_widget.dart';
import 'package:comunic/ui/widgets/post_container_widget.dart';
import 'package:comunic/ui/widgets/survey_widget.dart';
import 'package:comunic/ui/widgets/text_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/ui_utils.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../../models/api_request.dart';
import '../../utils/log_utils.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, REPORT }
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,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _PostTileState();
}
class _PostTileState extends State<PostTile> {
// Helpers
final _postsHelper = PostsHelper();
final _commentsHelper = CommentsHelper();
// Class members
TextEditingController _commentController = TextEditingController();
BytesFile? _commentImage;
bool _submitting = false;
int _maxNumberOfCommentToShow = 10;
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: <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,
),
),
// Column with user name + post target
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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,
),
]..addAll(srvConfig!.isReportingEnabled && !widget.post.isOwner
? [
PopupMenuItem(
child: Text(tr("Report abuse")!),
value: _PostActions.REPORT,
)
]
: []),
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.PDF:
postContent = _buildPostPDF();
break;
case PostKind.COUNTDOWN:
postContent = _buildCountDownTimer();
break;
case PostKind.SURVEY:
postContent = _buildPostSurvey();
break;
default:
}
return Column(
children: <Widget>[
// Post "rich" content
Container(child: postContent),
// Post text
Container(
child: widget.post.hasContent
? TextWidget(
content: widget.post.content,
parseBBcode: true,
)
: null),
],
);
}
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(
child: LikeWidget(
likeElement: widget.post,
buttonIconSize: null,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return PostContainer(
child: Card(
elevation: 1.0,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
_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 ElevatedButton(
style:
ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.red)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.ondemand_video),
Text(tr("YouTube movie")!)
],
),
onPressed: () => launchUrlString(
"https://youtube.com/watch/?v=" + widget.post.filePath!),
);
}
Widget _buildPostWebLink() {
return Card(
color:
darkTheme() ? darkerAccentColor : Color.fromRGBO(0xf7, 0xf7, 0xf7, 1),
child: InkWell(
onTap: () => launchUrlString(widget.post.linkURL!),
child: Row(
children: <Widget>[
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: <Widget>[
Text(htmlDecodeCharacters(widget.post.linkTitle),
style: TextStyle(fontSize: 20.0)),
Text(
widget.post.linkURL!,
maxLines: 3,
),
Text(htmlDecodeCharacters(widget.post.linkDescription))
],
),
)
],
),
),
);
}
Widget _buildPostPDF() {
return ElevatedButton.icon(
onPressed: () {
launchUrlString(widget.post.fileURL!);
},
icon: Icon(Icons.picture_as_pdf),
label: Text(tr("PDF")!),
);
}
Widget _buildCountDownTimer() {
return CountdownWidget(
startTime: widget.post.timeSent,
endTime: widget.post.timeEnd!,
);
}
/// Build post survey
Widget _buildPostSurvey() {
return SurveyWidget(
survey: widget.post.survey!,
onUpdated: (s) => setState(() => widget.post.survey = s),
);
}
/// Build the list of comments
Widget _buildComments() {
assert(widget.post.hasComments);
final comments = List<Widget>.generate(
min(widget.post.comments!.length, _maxNumberOfCommentToShow),
(num) {
final index = num +
max(0, widget.post.comments!.length - _maxNumberOfCommentToShow);
final comment = widget.post.comments![index as int];
return CommentTile(
comment: comment,
user: widget.usersInfo.getUser(comment.userID),
onUpdateComment: _updateCommentContent,
onDeleteComment: _deleteComment,
onReportComment: _reportComment,
);
},
);
// 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: (widget.post.comments!.length > _maxNumberOfCommentToShow
? [_buildShowMoreCommentsButton()]
: [])
..addAll(comments),
),
),
);
}
Widget _buildShowMoreCommentsButton() => Material(
color: Colors.transparent,
child: InkWell(
onTap: () => setState(() => _maxNumberOfCommentToShow += 10),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(tr("Show more comments")!.toUpperCase()),
),
),
);
/// 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(
// 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: TextButton(
onPressed: _pickImageForComment,
child: Icon(
Icons.image,
color: _hasImage ? Colors.blue : Colors.grey,
),
),
),
// Submit button
Container(
width: 40,
child: TextButton(
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<void> _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;
}
try {
// Pick a new image
final newImage = await pickImage(context);
setState(() {
_commentImage = newImage;
});
} catch (e, s) {
logError(e, s);
snack(context, tr("Failed to choose an image!")!);
}
}
/// Submit comment entered by the user
Future<void> _submitComment() async {
try {
_sendingComment = true;
final commentID = await _commentsHelper.createComment(NewComment(
postID: widget.post.id,
content: _commentController.text,
image: _commentImage,
));
_sendingComment = false;
if (commentID < 1) throw new Exception("Comment ID is inferior to 1!");
clearCommentForm();
} catch (e) {
print(e);
showSimpleSnack(context, tr("Could not create comment!")!);
}
}
/// Update comment content
Future<void> _updateCommentContent(Comment comment) async {
final newContent = await askUserString(
context: context,
title: tr("Update comment content")!,
message: tr("New content:")!,
defaultValue: comment.content.isNull ? "" : comment.content.content!,
hint: tr("New content...")!,
);
if (!(await _commentsHelper.updateContent(comment.id, newContent)))
return showSimpleSnack(context, tr("Could not update comment content!")!);
}
/// 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;
}
}
/// Process a report request
void _reportComment(Comment comment) => showReportDialog(
ctx: context, target: ReportTarget(ReportTargetType.Comment, comment.id));
/// 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;
// Report content
case _PostActions.REPORT:
reportContent();
break;
}
}
/// Update post visibility level
Future<void> updatePostVisibilityLevel() async {
final newLevel = await showPostVisibilityPickerDialog(
context: context,
initialLevel: widget.post.visibilityLevel,
isGroup: widget.post.isGroupPost,
);
if (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<void> updateContent() async {
final newContent = await askUserString(
context: context,
title: tr("Update post content")!,
message: tr("Please enter message content: ")!,
defaultValue:
widget.post.content.isNull ? "" : widget.post.content.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.content = newContent);
}
/// 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);
}
/// Report post
void reportContent() async => await showReportDialog(
ctx: context,
target: ReportTarget(ReportTargetType.Post, widget.post.id));
}