✨ Repostable and replyable post
This commit is contained in:
		| @@ -78,5 +78,7 @@ | ||||
|   "fieldPostTitle": "Title", | ||||
|   "fieldPostDescription": "Description", | ||||
|   "postPublish": "Publish", | ||||
|   "postEditingNotice": "You're about to editing a post that posted {}." | ||||
|   "postEditingNotice": "You're about to editing a post that posted {}.", | ||||
|   "postReplyingNotice": "You're about to reply to a post that posted {}.", | ||||
|   "postRepostingNotice": "You're about to repost a post that posted {}." | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "nextVersionAlert": "高强度开发提示", | ||||
|   "nextVersionNotice": "您正在使用的是 Solian 2.0 的抢先体验版本,目前稳定分支(sn.solsynth.dev)版本为 1.4。该版本还在剧烈的开发中,部分功能可能不稳定,也并非所有功能都支持了。您可以通过 TestFlight 回滚到 1.4.X 或者继续体验新版本(sn-next.solsynth.dev)。", | ||||
|   "nextVersionNotice": "您正在使用的是 Solian 2.0 的抢先体验版本,目前稳定分支(sn.solsynth.dev)版本为 1.4。该版本还在持续的开发中,部分功能可能不稳定,也并非所有功能都支持了。您可以通过 TestFlight 回滚到 1.4.X 或者继续体验新版本(sn-next.solsynth.dev)。", | ||||
|   "screen": "页面", | ||||
|   "screenHome": "首页", | ||||
|   "screenExplore": "探索", | ||||
| @@ -78,5 +78,7 @@ | ||||
|   "fieldPostTitle": "标题", | ||||
|   "fieldPostDescription": "描述", | ||||
|   "postPublish": "发布", | ||||
|   "postEditingNotice": "你正在修改由 {} 发布的帖子。" | ||||
|   "postEditingNotice": "你正在修改由 {} 发布的帖子。", | ||||
|   "postReplyingNotice": "你正在回复由 {} 发布的帖子。", | ||||
|   "postRepostingNotice": "你正在转发由 {} 发布的帖子。" | ||||
| } | ||||
|   | ||||
| @@ -191,6 +191,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|           'title': _title, | ||||
|           'description': _description, | ||||
|           'attachments': _attachments.map((e) => e.rid).toList(), | ||||
|           if (_replyingTo != null) 'reply_to': _replyingTo!.id, | ||||
|           if (_repostingTo != null) 'repost_to': _repostingTo!.id, | ||||
|         }, | ||||
|         onSendProgress: (count, total) { | ||||
|           setState(() { | ||||
| @@ -385,19 +387,58 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|           const Divider(height: 1), | ||||
|           Expanded( | ||||
|             child: SingleChildScrollView( | ||||
|               padding: EdgeInsets.only( | ||||
|                 top: _editingOg == null ? 8 : 0, | ||||
|                 bottom: 8, | ||||
|               ), | ||||
|               padding: EdgeInsets.only(bottom: 8), | ||||
|               child: Column( | ||||
|                 children: [ | ||||
|                   // Replying Notice | ||||
|                   if (_replyingTo != null) | ||||
|                     Column( | ||||
|                       children: [ | ||||
|                         Theme( | ||||
|                           data: Theme.of(context).copyWith( | ||||
|                             dividerColor: Colors.transparent, | ||||
|                           ), | ||||
|                           child: ExpansionTile( | ||||
|                             minTileHeight: 48, | ||||
|                             leading: const Icon(Symbols.reply).padding(left: 4), | ||||
|                             title: Text('postReplyingNotice') | ||||
|                                 .fontSize(15) | ||||
|                                 .tr(args: ['@${_replyingTo!.publisher.name}']), | ||||
|                             children: <Widget>[PostItem(data: _replyingTo!)], | ||||
|                           ), | ||||
|                         ), | ||||
|                         const Divider(height: 1), | ||||
|                       ], | ||||
|                     ), | ||||
|                   // Reposting Notice | ||||
|                   if (_repostingTo != null) | ||||
|                     Column( | ||||
|                       children: [ | ||||
|                         Theme( | ||||
|                           data: Theme.of(context).copyWith( | ||||
|                             dividerColor: Colors.transparent, | ||||
|                           ), | ||||
|                           child: ExpansionTile( | ||||
|                             minTileHeight: 48, | ||||
|                             leading: | ||||
|                                 const Icon(Symbols.forward).padding(left: 4), | ||||
|                             title: Text('postRepostingNotice') | ||||
|                                 .fontSize(15) | ||||
|                                 .tr(args: ['@${_repostingTo!.publisher.name}']), | ||||
|                             children: <Widget>[PostItem(data: _repostingTo!)], | ||||
|                           ), | ||||
|                         ), | ||||
|                         const Divider(height: 1), | ||||
|                       ], | ||||
|                     ), | ||||
|                   // Editing Notice | ||||
|                   if (_editingOg != null) | ||||
|                     Column( | ||||
|                       children: [ | ||||
|                         Theme( | ||||
|                           data: Theme.of(context) | ||||
|                               .copyWith(dividerColor: Colors.transparent), | ||||
|                           data: Theme.of(context).copyWith( | ||||
|                             dividerColor: Colors.transparent, | ||||
|                           ), | ||||
|                           child: ExpansionTile( | ||||
|                             minTileHeight: 48, | ||||
|                             leading: | ||||
| @@ -405,13 +446,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                             title: Text('postEditingNotice') | ||||
|                                 .fontSize(15) | ||||
|                                 .tr(args: ['@${_editingOg!.publisher.name}']), | ||||
|                             children: <Widget>[ | ||||
|                               PostItem(data: _editingOg!), | ||||
|                             ], | ||||
|                             children: <Widget>[PostItem(data: _editingOg!)], | ||||
|                           ), | ||||
|                         ), | ||||
|                         const Divider(height: 1), | ||||
|                         const Gap(8) | ||||
|                       ], | ||||
|                     ), | ||||
|                   // Content Input Area | ||||
| @@ -430,7 +468,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                     onTapOutside: (_) => | ||||
|                         FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   ) | ||||
|                 ], | ||||
|                 ].expand((ele) => [ele, const Gap(8)]).toList() | ||||
|                   ..removeLast(), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
| @@ -448,7 +487,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 LoadingIndicator(isActive: _isBusy), | ||||
|                 LoadingIndicator(isActive: _isLoading), | ||||
|                 if (_isBusy && _progress != null) | ||||
|                   TweenAnimationBuilder<double>( | ||||
|                     tween: Tween(begin: 0, end: 1), | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class SnPost with _$SnPost { | ||||
|     required DateTime? pinnedAt, | ||||
|     required DateTime? lockedAt, | ||||
|     required bool isDraft, | ||||
|     required DateTime publishedAt, | ||||
|     required DateTime? publishedAt, | ||||
|     required dynamic publishedUntil, | ||||
|     required int totalUpvote, | ||||
|     required int totalDownvote, | ||||
|   | ||||
| @@ -44,7 +44,7 @@ mixin _$SnPost { | ||||
|   DateTime? get pinnedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get lockedAt => throw _privateConstructorUsedError; | ||||
|   bool get isDraft => throw _privateConstructorUsedError; | ||||
|   DateTime get publishedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get publishedAt => throw _privateConstructorUsedError; | ||||
|   dynamic get publishedUntil => throw _privateConstructorUsedError; | ||||
|   int get totalUpvote => throw _privateConstructorUsedError; | ||||
|   int get totalDownvote => throw _privateConstructorUsedError; | ||||
| @@ -94,7 +94,7 @@ abstract class $SnPostCopyWith<$Res> { | ||||
|       DateTime? pinnedAt, | ||||
|       DateTime? lockedAt, | ||||
|       bool isDraft, | ||||
|       DateTime publishedAt, | ||||
|       DateTime? publishedAt, | ||||
|       dynamic publishedUntil, | ||||
|       int totalUpvote, | ||||
|       int totalDownvote, | ||||
| @@ -149,7 +149,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> | ||||
|     Object? pinnedAt = freezed, | ||||
|     Object? lockedAt = freezed, | ||||
|     Object? isDraft = null, | ||||
|     Object? publishedAt = null, | ||||
|     Object? publishedAt = freezed, | ||||
|     Object? publishedUntil = freezed, | ||||
|     Object? totalUpvote = null, | ||||
|     Object? totalDownvote = null, | ||||
| @@ -257,10 +257,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> | ||||
|           ? _value.isDraft | ||||
|           : isDraft // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       publishedAt: null == publishedAt | ||||
|       publishedAt: freezed == publishedAt | ||||
|           ? _value.publishedAt | ||||
|           : publishedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|               as DateTime?, | ||||
|       publishedUntil: freezed == publishedUntil | ||||
|           ? _value.publishedUntil | ||||
|           : publishedUntil // ignore: cast_nullable_to_non_nullable | ||||
| @@ -367,7 +367,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> { | ||||
|       DateTime? pinnedAt, | ||||
|       DateTime? lockedAt, | ||||
|       bool isDraft, | ||||
|       DateTime publishedAt, | ||||
|       DateTime? publishedAt, | ||||
|       dynamic publishedUntil, | ||||
|       int totalUpvote, | ||||
|       int totalDownvote, | ||||
| @@ -423,7 +423,7 @@ class __$$SnPostImplCopyWithImpl<$Res> | ||||
|     Object? pinnedAt = freezed, | ||||
|     Object? lockedAt = freezed, | ||||
|     Object? isDraft = null, | ||||
|     Object? publishedAt = null, | ||||
|     Object? publishedAt = freezed, | ||||
|     Object? publishedUntil = freezed, | ||||
|     Object? totalUpvote = null, | ||||
|     Object? totalDownvote = null, | ||||
| @@ -531,10 +531,10 @@ class __$$SnPostImplCopyWithImpl<$Res> | ||||
|           ? _value.isDraft | ||||
|           : isDraft // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       publishedAt: null == publishedAt | ||||
|       publishedAt: freezed == publishedAt | ||||
|           ? _value.publishedAt | ||||
|           : publishedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|               as DateTime?, | ||||
|       publishedUntil: freezed == publishedUntil | ||||
|           ? _value.publishedUntil | ||||
|           : publishedUntil // ignore: cast_nullable_to_non_nullable | ||||
| @@ -688,7 +688,7 @@ class _$SnPostImpl extends _SnPost { | ||||
|   @override | ||||
|   final bool isDraft; | ||||
|   @override | ||||
|   final DateTime publishedAt; | ||||
|   final DateTime? publishedAt; | ||||
|   @override | ||||
|   final dynamic publishedUntil; | ||||
|   @override | ||||
| @@ -854,7 +854,7 @@ abstract class _SnPost extends SnPost { | ||||
|       required final DateTime? pinnedAt, | ||||
|       required final DateTime? lockedAt, | ||||
|       required final bool isDraft, | ||||
|       required final DateTime publishedAt, | ||||
|       required final DateTime? publishedAt, | ||||
|       required final dynamic publishedUntil, | ||||
|       required final int totalUpvote, | ||||
|       required final int totalDownvote, | ||||
| @@ -917,7 +917,7 @@ abstract class _SnPost extends SnPost { | ||||
|   @override | ||||
|   bool get isDraft; | ||||
|   @override | ||||
|   DateTime get publishedAt; | ||||
|   DateTime? get publishedAt; | ||||
|   @override | ||||
|   dynamic get publishedUntil; | ||||
|   @override | ||||
|   | ||||
| @@ -39,7 +39,9 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl( | ||||
|           ? null | ||||
|           : DateTime.parse(json['locked_at'] as String), | ||||
|       isDraft: json['is_draft'] as bool, | ||||
|       publishedAt: DateTime.parse(json['published_at'] as String), | ||||
|       publishedAt: json['published_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['published_at'] as String), | ||||
|       publishedUntil: json['published_until'], | ||||
|       totalUpvote: (json['total_upvote'] as num).toInt(), | ||||
|       totalDownvote: (json['total_downvote'] as num).toInt(), | ||||
| @@ -80,7 +82,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) => | ||||
|       'pinned_at': instance.pinnedAt?.toIso8601String(), | ||||
|       'locked_at': instance.lockedAt?.toIso8601String(), | ||||
|       'is_draft': instance.isDraft, | ||||
|       'published_at': instance.publishedAt.toIso8601String(), | ||||
|       'published_at': instance.publishedAt?.toIso8601String(), | ||||
|       'published_until': instance.publishedUntil, | ||||
|       'total_upvote': instance.totalUpvote, | ||||
|       'total_downvote': instance.totalDownvote, | ||||
|   | ||||
| @@ -2,8 +2,10 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_list.dart'; | ||||
| @@ -34,6 +36,9 @@ class _PostContentHeader extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|     final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user!.id; | ||||
|  | ||||
|     return Row( | ||||
|       children: [ | ||||
|         AccountImage(content: data.publisher.avatar), | ||||
| @@ -47,8 +52,9 @@ class _PostContentHeader extends StatelessWidget { | ||||
|                 children: [ | ||||
|                   Text('@${data.publisher.name}').fontSize(13), | ||||
|                   const Gap(4), | ||||
|                   Text(RelativeTime(context).format(data.publishedAt)) | ||||
|                       .fontSize(13), | ||||
|                   Text(RelativeTime(context).format( | ||||
|                     data.publishedAt ?? data.createdAt, | ||||
|                   )).fontSize(13), | ||||
|                 ], | ||||
|               ).opacity(0.8), | ||||
|             ], | ||||
| @@ -60,30 +66,65 @@ class _PostContentHeader extends StatelessWidget { | ||||
|             visualDensity: VisualDensity(horizontal: -4, vertical: -4), | ||||
|           ), | ||||
|           itemBuilder: (BuildContext context) => <PopupMenuEntry>[ | ||||
|             if (isAuthor) | ||||
|               PopupMenuItem( | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
|                     const Icon(Symbols.edit), | ||||
|                     const Gap(16), | ||||
|                     Text('edit').tr(), | ||||
|                   ], | ||||
|                 ), | ||||
|                 onTap: () { | ||||
|                   GoRouter.of(context).pushNamed( | ||||
|                     'postEditor', | ||||
|                     pathParameters: {'mode': data.typePlural}, | ||||
|                     queryParameters: {'editing': data.id.toString()}, | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|             if (isAuthor) | ||||
|               PopupMenuItem( | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
|                     const Icon(Symbols.delete), | ||||
|                     const Gap(16), | ||||
|                     Text('delete').tr(), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             if (isAuthor) const PopupMenuDivider(), | ||||
|             PopupMenuItem( | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   const Icon(Symbols.edit), | ||||
|                   const Icon(Symbols.reply), | ||||
|                   const Gap(16), | ||||
|                   Text('edit').tr(), | ||||
|                   Text('reply').tr(), | ||||
|                 ], | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 GoRouter.of(context).pushNamed( | ||||
|                   'postEditor', | ||||
|                   pathParameters: {'mode': data.typePlural}, | ||||
|                   queryParameters: {'editing': data.id.toString()}, | ||||
|                   queryParameters: {'replying': data.id.toString()}, | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|             PopupMenuItem( | ||||
|               child: Row( | ||||
|                 children: [ | ||||
|                   const Icon(Symbols.delete), | ||||
|                   const Icon(Symbols.forward), | ||||
|                   const Gap(16), | ||||
|                   Text('delete').tr(), | ||||
|                   Text('repost').tr(), | ||||
|                 ], | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 GoRouter.of(context).pushNamed( | ||||
|                   'postEditor', | ||||
|                   pathParameters: {'mode': data.typePlural}, | ||||
|                   queryParameters: {'reposting': data.id.toString()}, | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|             const PopupMenuDivider(), | ||||
|             PopupMenuItem( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user