mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-11-26 23:09:21 +00:00
625 lines
17 KiB
Dart
625 lines
17 KiB
Dart
import 'dart:math';
|
|
|
|
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/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/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/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/post_utils.dart';
|
|
import 'package:comunic/utils/ui_utils.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:image_picker/image_picker.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<StatefulWidget> createState() => _PostTileState();
|
|
}
|
|
|
|
class _PostTileState extends State<PostTile> {
|
|
// Helpers
|
|
final _postsHelper = PostsHelper();
|
|
final _commentsHelper = CommentsHelper();
|
|
|
|
// Class members
|
|
TextEditingController _commentController = TextEditingController();
|
|
PickedFile _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,
|
|
),
|
|
],
|
|
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 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),
|
|
);
|
|
}
|
|
|
|
Widget _buildPostWebLink() {
|
|
return Card(
|
|
color:
|
|
darkTheme() ? darkerAccentColor : Color.fromRGBO(0xf7, 0xf7, 0xf7, 1),
|
|
child: InkWell(
|
|
onTap: () => launch(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 RaisedButton.icon(
|
|
onPressed: () {
|
|
launch(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];
|
|
return CommentTile(
|
|
comment: comment,
|
|
user: widget.usersInfo.getUser(comment.userID),
|
|
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: (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: 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<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;
|
|
}
|
|
|
|
// Pick a new image
|
|
final newImage = await pickImage(context);
|
|
setState(() {
|
|
_commentImage = newImage;
|
|
});
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
}
|
|
|
|
/// 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<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);
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
}
|