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

652 lines
18 KiB
Dart
Raw Permalink Normal View History

import 'dart:math';
2019-05-18 07:45:15 +00:00
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';
2022-03-18 17:45:58 +00:00
import 'package:comunic/enums/report_target_type.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';
2022-03-18 17:45:58 +00:00
import 'package:comunic/helpers/server_config_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';
2022-03-18 17:45:58 +00:00
import 'package:comunic/models/report_target.dart';
2019-05-11 07:48:01 +00:00
import 'package:comunic/models/user.dart';
2021-03-17 16:53:07 +00:00
import 'package:comunic/ui/dialogs/post_visibility_picker_dialog.dart';
2022-03-18 17:45:58 +00:00
import 'package:comunic/ui/dialogs/report_dialog.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';
2020-05-16 07:10:09 +00:00
import 'package:comunic/ui/widgets/post_container_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-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';
2022-06-11 13:04:11 +00:00
import 'package:url_launcher/url_launcher_string.dart';
2019-05-10 17:15:11 +00:00
2021-03-13 17:03:20 +00:00
import '../../models/api_request.dart';
import '../../utils/log_utils.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
2022-03-18 17:45:58 +00:00
enum _PostActions { DELETE, UPDATE_CONTENT, REPORT }
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,
required this.onDeletedPost,
required this.showPostTarget,
required this.groupsInfo,
required this.userNamesClickable,
2022-03-11 15:40:56 +00:00
}) : super(key: key);
2019-05-10 17:15:11 +00:00
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();
BytesFile? _commentImage;
2019-05-18 12:58:35 +00:00
bool _submitting = false;
int _maxNumberOfCommentToShow = 10;
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
? 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(
2022-03-11 15:21:35 +00:00
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,
),
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")!),
2019-11-01 13:17:46 +00:00
value: _PostActions.UPDATE_CONTENT,
enabled: widget.post.canUpdate,
),
// Delete post
PopupMenuItem(
child: Text(tr("Delete")!),
2019-11-01 13:17:46 +00:00
value: _PostActions.DELETE,
enabled: widget.post.canDelete,
),
]..addAll(srvConfig!.isReportingEnabled &&
(!widget.post.isOwner ||
srvConfig!.reportPolicy!.canUserReportHisOwnContent)
2022-03-18 17:45:58 +00:00
? [
PopupMenuItem(
child: Text(tr("Report abuse")!),
value: _PostActions.REPORT,
)
]
: []),
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) {
2020-05-16 07:10:09 +00:00
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(),
],
),
2019-05-16 12:52:22 +00:00
),
2020-05-16 07:10:09 +00:00
Container(
child: widget.post.hasComments ? _buildComments() : null,
),
],
),
2019-05-11 07:48:01 +00:00
),
);
}
Widget _buildPostImage() {
return NetworkImageWidget(
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() {
2021-03-13 14:38:43 +00:00
return ElevatedButton(
style:
ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.red)),
2019-07-01 10:18:10 +00:00
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.ondemand_video),
Text(tr("YouTube movie")!)
2019-07-01 10:18:10 +00:00
],
),
2022-06-11 13:04:11 +00:00
onPressed: () => launchUrlString(
"https://youtube.com/watch/?v=" + widget.post.filePath!),
2019-07-01 10:18:10 +00:00
);
}
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(
2022-06-11 13:04:11 +00:00
onTap: () => launchUrlString(widget.post.linkURL!),
2019-06-24 08:48:31 +00:00
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!,
2019-06-24 18:27:20 +00:00
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!,
2019-06-24 08:48:31 +00:00
maxLines: 3,
),
Text(htmlDecodeCharacters(widget.post.linkDescription))
],
),
)
],
),
),
);
}
2019-11-16 08:24:20 +00:00
Widget _buildPostPDF() {
2021-03-13 14:38:43 +00:00
return ElevatedButton.icon(
2019-11-16 08:24:20 +00:00
onPressed: () {
2022-06-11 13:04:11 +00:00
launchUrlString(widget.post.fileURL!);
2019-11-16 08:24:20 +00:00
},
icon: Icon(Icons.picture_as_pdf),
label: Text(tr("PDF")!),
2019-11-16 08:24:20 +00:00
);
}
2019-11-02 19:14:34 +00:00
Widget _buildCountDownTimer() {
return CountdownWidget(
startTime: widget.post.timeSent,
endTime: widget.post.timeEnd!,
2019-11-02 19:14:34 +00:00
);
}
2019-06-28 09:32:36 +00:00
/// Build post survey
Widget _buildPostSurvey() {
return SurveyWidget(
survey: widget.post.survey!,
2020-05-18 17:15:16 +00:00
onUpdated: (s) => setState(() => widget.post.survey = s),
2019-06-28 09:32:36 +00:00
);
}
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(
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,
2022-03-18 18:05:21 +00:00
onReportComment: _reportComment,
);
},
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: (widget.post.comments!.length > _maxNumberOfCommentToShow
? [_buildShowMoreCommentsButton()]
: [])
..addAll(comments),
2019-05-16 12:52:22 +00:00
),
),
);
}
2019-05-18 07:45:15 +00:00
2020-05-16 07:17:49 +00:00
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()),
2020-05-16 07:17:49 +00:00
),
),
);
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)),
2019-05-18 07:45:15 +00:00
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,
2021-03-13 14:38:43 +00:00
child: TextButton(
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,
2021-03-13 14:38:43 +00:00
child: TextButton(
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;
}
2021-03-13 17:03:20 +00:00
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!")!);
2021-03-13 17:03:20 +00:00
}
2019-05-18 07:45:15 +00:00
}
/// 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,
2022-03-11 15:40:56 +00:00
));
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();
} 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:")!,
defaultValue: comment.content.isNull ? "" : comment.content.content!,
hint: tr("New content...")!,
2019-05-18 16:48:12 +00:00
);
if (!(await _commentsHelper.updateContent(comment.id, newContent)))
return showSimpleSnack(context, tr("Could not update comment content!")!);
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!")!);
2019-05-18 14:48:19 +00:00
return;
}
}
2019-05-19 12:54:09 +00:00
2022-03-18 18:05:21 +00:00
/// Process a report request
void _reportComment(Comment comment) => showReportDialog(
ctx: context, target: ReportTarget(ReportTargetType.Comment, comment.id));
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;
2022-03-18 17:45:58 +00:00
// Report content
case _PostActions.REPORT:
reportContent();
break;
2019-05-19 12:54:09 +00:00
}
}
2019-05-23 16:27:43 +00:00
/// Update post visibility level
Future<void> updatePostVisibilityLevel() async {
2021-03-17 16:53:07 +00:00
final newLevel = await showPostVisibilityPickerDialog(
2019-05-23 16:27:43 +00:00
context: context,
initialLevel: widget.post.visibilityLevel,
isGroup: widget.post.isGroupPost,
);
2022-03-11 15:40:56 +00:00
if (newLevel == widget.post.visibilityLevel) return;
2019-05-23 16:27:43 +00:00
// Update post visibility
if (!await _postsHelper.setVisibility(widget.post.id, newLevel)) {
showSimpleSnack(context, tr("Could not update post visibility!")!);
2019-05-23 16:27:43 +00:00
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-18 14:07:56 +00:00
defaultValue:
widget.post.content.isNull ? "" : widget.post.content.content!,
hint: tr("Post content")!,
2019-05-19 15:42:09 +00:00
);
if (newContent == null) return;
if (!await _postsHelper.updateContent(widget.post.id, newContent)) {
showSimpleSnack(context, tr("Could not update post content!")!);
2019-05-19 15:42:09 +00:00
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!")!);
2019-05-19 12:54:09 +00:00
return;
}
widget.onDeletedPost(widget.post);
}
2022-03-18 17:45:58 +00:00
/// Report post
void reportContent() async => await showReportDialog(
ctx: context,
target: ReportTarget(ReportTargetType.Post, widget.post.id));
2019-05-10 17:15:11 +00:00
}