diff --git a/lib/main.dart b/lib/main.dart index fb7597b..bd2fb8f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:solian/providers/account.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/content/attachment.dart'; +import 'package:solian/providers/content/post_explore.dart'; import 'package:solian/providers/friend.dart'; import 'package:solian/router.dart'; import 'package:solian/theme.dart'; @@ -31,6 +32,7 @@ class SolianApp extends StatelessWidget { onInit: () { Get.lazyPut(() => AuthProvider()); Get.lazyPut(() => FriendProvider()); + Get.lazyPut(() => PostProvider()); Get.lazyPut(() => AttachmentProvider()); Get.lazyPut(() => AccountProvider()); diff --git a/lib/providers/content/post_explore.dart b/lib/providers/content/post_explore.dart index 0c6fdbc..509b47c 100644 --- a/lib/providers/content/post_explore.dart +++ b/lib/providers/content/post_explore.dart @@ -16,6 +16,15 @@ class PostProvider extends GetConnect { return resp; } + Future listPostReplies(String alias, int page) async { + final resp = await get('/api/posts/$alias/replies?take=${10}&offset=$page'); + if (resp.statusCode != 200) { + throw Exception(resp.body); + } + + return resp; + } + Future getPost(String alias) async { final resp = await get('/api/posts/$alias'); if (resp.statusCode != 200) { diff --git a/lib/screens/posts/post_detail.dart b/lib/screens/posts/post_detail.dart index bc408c0..c2f289a 100644 --- a/lib/screens/posts/post_detail.dart +++ b/lib/screens/posts/post_detail.dart @@ -4,6 +4,7 @@ import 'package:solian/exts.dart'; import 'package:solian/models/post.dart'; import 'package:solian/providers/content/post_explore.dart'; import 'package:solian/widgets/posts/post_item.dart'; +import 'package:solian/widgets/posts/post_replies.dart'; class PostDetailScreen extends StatefulWidget { final String alias; @@ -43,13 +44,22 @@ class _PostDetailScreenState extends State { ); } - return Column( + return ListView( children: [ PostItem( item: item!, isClickable: true, isShowReply: false, ), + const Divider(thickness: 0.3, height: 0.3), + Text( + 'postReplies'.tr, + style: Theme.of(context).textTheme.headlineSmall, + ).paddingOnly(left: 24, right: 24, top: 16), + PostReplyList( + item: item!, + shrinkWrap: true, + ), ], ); }, diff --git a/lib/screens/social.dart b/lib/screens/social.dart index 164e6a3..923579f 100644 --- a/lib/screens/social.dart +++ b/lib/screens/social.dart @@ -8,8 +8,7 @@ import 'package:solian/providers/content/post_explore.dart'; import 'package:solian/router.dart'; import 'package:solian/screens/account/notification.dart'; import 'package:solian/theme.dart'; -import 'package:solian/widgets/posts/post_action.dart'; -import 'package:solian/widgets/posts/post_item.dart'; +import 'package:solian/widgets/posts/post_list.dart'; class SocialScreen extends StatefulWidget { const SocialScreen({super.key}); @@ -44,7 +43,6 @@ class _SocialScreenState extends State { @override void initState() { - Get.lazyPut(() => PostProvider()); super.initState(); _pagingController.addPageRequestListener(getPosts); @@ -99,40 +97,7 @@ class _SocialScreenState extends State { context: context, child: RefreshIndicator( onRefresh: () => Future.sync(() => _pagingController.refresh()), - child: PagedListView.separated( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - return GestureDetector( - child: PostItem( - key: Key('p${item.alias}'), - item: item, - isClickable: true, - ).paddingSymmetric( - vertical: - (item.attachments?.isEmpty ?? false) ? 8 : 0, - ), - onTap: () { - AppRouter.instance.pushNamed( - 'postDetail', - pathParameters: {'alias': item.alias}, - ); - }, - onLongPress: () { - showModalBottomSheet( - useRootNavigator: true, - context: context, - builder: (context) => PostAction(item: item), - ).then((value) { - if (value == true) _pagingController.refresh(); - }); - }, - ); - }, - ), - separatorBuilder: (_, __) => - const Divider(thickness: 0.3, height: 0.3), - ), + child: PostListWidget(controller: _pagingController), ), ), ), diff --git a/lib/translations.dart b/lib/translations.dart index 38d02ff..4ba95e9 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -63,11 +63,13 @@ class SolianMessages extends Translations { 'notifyEmptyCaption': 'It seems like nothing happened recently', 'postAction': 'Post', 'postDetail': 'Post', + 'postReplies': 'Replies', 'postPublishing': 'Post a post', 'postIdentityNotify': 'You will post this post as', 'postContentPlaceholder': 'What\'s happened?!', 'postReaction': 'Reactions of the Post', 'postActionList': 'Actions of Post', + 'postReplyAction': 'Make a reply', 'postRepliedNotify': 'Replied a post from @username.', 'postRepostedNotify': 'Reposted a post from @username.', 'postEditingNotify': 'You\'re editing as post from you.', @@ -142,11 +144,13 @@ class SolianMessages extends Translations { 'notifyEmptyCaption': '看起来最近没发生什么呢', 'postAction': '发表', 'postDetail': '帖子详情', + 'postReplies': '帖子回复', 'postPublishing': '发表帖子', 'postIdentityNotify': '你将会以本身份发表帖子', 'postContentPlaceholder': '发生什么事了?!', 'postReaction': '帖子的反应', 'postActionList': '帖子的操作', + 'postReplyAction': '发表一则回复', 'postRepliedNotify': '回了一个 @username 的帖子', 'postRepostedNotify': '转了一个 @username 的帖子', 'postEditingNotify': '你正在编辑一个你发布的帖子', diff --git a/lib/widgets/posts/post_item.dart b/lib/widgets/posts/post_item.dart index 4a0c2a1..05aebd7 100644 --- a/lib/widgets/posts/post_item.dart +++ b/lib/widgets/posts/post_item.dart @@ -16,6 +16,7 @@ class PostItem extends StatefulWidget { final bool isCompact; final bool isReactable; final bool isShowReply; + final bool isShowEmbed; const PostItem({ super.key, @@ -24,6 +25,7 @@ class PostItem extends StatefulWidget { this.isCompact = false, this.isReactable = true, this.isShowReply = true, + this.isShowEmbed = true, }); @override @@ -162,7 +164,7 @@ class _PostItemState extends State { data: item.content, padding: const EdgeInsets.all(0), ).paddingOnly(left: 12, right: 8), - if (widget.item.replyTo != null) + if (widget.item.replyTo != null && widget.isShowEmbed) GestureDetector( child: buildReply(context).paddingOnly(top: 4), onTap: () { @@ -175,7 +177,7 @@ class _PostItemState extends State { ); }, ), - if (widget.item.repostTo != null) + if (widget.item.repostTo != null && widget.isShowEmbed) GestureDetector( child: buildRepost(context).paddingOnly(top: 4), onTap: () { diff --git a/lib/widgets/posts/post_list.dart b/lib/widgets/posts/post_list.dart new file mode 100644 index 0000000..67c9331 --- /dev/null +++ b/lib/widgets/posts/post_list.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:solian/models/post.dart'; +import 'package:solian/router.dart'; +import 'package:solian/widgets/posts/post_action.dart'; +import 'package:solian/widgets/posts/post_item.dart'; + +class PostListWidget extends StatelessWidget { + final bool shrinkWrap; + final bool isShowEmbed; + final bool isClickable; + final bool isNestedClickable; + final PagingController controller; + + const PostListWidget({ + super.key, + required this.controller, + this.shrinkWrap = false, + this.isShowEmbed = true, + this.isClickable = true, + this.isNestedClickable = true, + }); + + @override + Widget build(BuildContext context) { + return PagedListView.separated( + shrinkWrap: shrinkWrap, + pagingController: controller, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return GestureDetector( + child: PostItem( + key: Key('p${item.alias}'), + item: item, + isShowEmbed: isShowEmbed, + isClickable: isNestedClickable, + ).paddingSymmetric( + vertical: (item.attachments?.isEmpty ?? false) ? 8 : 0, + ), + onTap: () { + if (!isClickable) return; + AppRouter.instance.pushNamed( + 'postDetail', + pathParameters: {'alias': item.alias}, + ); + }, + onLongPress: () { + showModalBottomSheet( + useRootNavigator: true, + context: context, + builder: (context) => PostAction(item: item), + ).then((value) { + if (value == true) controller.refresh(); + }); + }, + ); + }, + ), + separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3), + ); + } +} diff --git a/lib/widgets/posts/post_quick_action.dart b/lib/widgets/posts/post_quick_action.dart index 073ea3b..1397b32 100644 --- a/lib/widgets/posts/post_quick_action.dart +++ b/lib/widgets/posts/post_quick_action.dart @@ -6,6 +6,7 @@ import 'package:solian/models/reaction.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/services.dart'; import 'package:solian/widgets/posts/post_reaction.dart'; +import 'package:solian/widgets/posts/post_replies.dart'; class PostQuickAction extends StatefulWidget { final Post item; @@ -100,7 +101,15 @@ class _PostQuickActionState extends State { avatar: const Icon(Icons.comment), label: Text(widget.item.replyCount.toString()), visualDensity: density, - onPressed: () {}, + onPressed: () { + showModalBottomSheet( + useRootNavigator: true, + context: context, + builder: (context) { + return PostReplyListPopup(item: widget.item); + }, + ); + }, ), if (widget.isReactable && widget.isShowReply) const VerticalDivider( diff --git a/lib/widgets/posts/post_replies.dart b/lib/widgets/posts/post_replies.dart new file mode 100644 index 0000000..5499ce8 --- /dev/null +++ b/lib/widgets/posts/post_replies.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:solian/models/pagination.dart'; +import 'package:solian/models/post.dart'; +import 'package:solian/providers/content/post_explore.dart'; +import 'package:solian/widgets/posts/post_list.dart'; + +class PostReplyList extends StatefulWidget { + final Post item; + final bool shrinkWrap; + + const PostReplyList({ + super.key, + required this.item, + this.shrinkWrap = false, + }); + + @override + State createState() => _PostReplyListState(); +} + +class _PostReplyListState extends State { + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + Future getReplies(int pageKey) async { + final PostProvider provider = Get.find(); + + Response resp; + try { + resp = await provider.listPostReplies(widget.item.alias, pageKey); + } catch (e) { + _pagingController.error = e; + return; + } + + final PaginationResult result = PaginationResult.fromJson(resp.body); + final parsed = result.data?.map((e) => Post.fromJson(e)).toList(); + if (parsed != null && parsed.length >= 10) { + _pagingController.appendPage(parsed, pageKey + parsed.length); + } else if (parsed != null) { + _pagingController.appendLastPage(parsed); + } + } + + @override + void initState() { + super.initState(); + + _pagingController.addPageRequestListener(getReplies); + } + + @override + Widget build(BuildContext context) { + return PostListWidget( + isShowEmbed: false, + shrinkWrap: widget.shrinkWrap, + controller: _pagingController, + ); + } +} + +class PostReplyListPopup extends StatelessWidget { + final Post item; + + const PostReplyListPopup({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'postReplies'.tr, + style: Theme.of(context).textTheme.headlineSmall, + ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), + Expanded( + child: PostReplyList( + item: item, + ), + ), + ], + ); + } +}