From c1e10916ee764fc40a2ccac2fad65ad247816717 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 10 Nov 2024 20:07:26 +0800 Subject: [PATCH] :sparkles: Repostable and replyable post --- assets/translations/en-US.json | 4 +- assets/translations/zh-CN.json | 6 ++- lib/screens/post/post_editor.dart | 63 +++++++++++++++++++++++++------ lib/types/post.dart | 2 +- lib/types/post.freezed.dart | 24 ++++++------ lib/types/post.g.dart | 6 ++- lib/widgets/post/post_item.dart | 55 +++++++++++++++++++++++---- 7 files changed, 123 insertions(+), 37 deletions(-) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 1ba2f40..91b1fd6 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -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 {}." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 1cdd65d..3771aa0 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -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": "你正在转发由 {} 发布的帖子。" } diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 3aa1122..392d90d 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -191,6 +191,8 @@ class _PostEditorScreenState extends State { '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 { 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: [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: [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 { title: Text('postEditingNotice') .fontSize(15) .tr(args: ['@${_editingOg!.publisher.name}']), - children: [ - PostItem(data: _editingOg!), - ], + children: [PostItem(data: _editingOg!)], ), ), const Divider(height: 1), - const Gap(8) ], ), // Content Input Area @@ -430,7 +468,8 @@ class _PostEditorScreenState extends State { onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ) - ], + ].expand((ele) => [ele, const Gap(8)]).toList() + ..removeLast(), ), ), ), @@ -448,7 +487,7 @@ class _PostEditorScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - LoadingIndicator(isActive: _isBusy), + LoadingIndicator(isActive: _isLoading), if (_isBusy && _progress != null) TweenAnimationBuilder( tween: Tween(begin: 0, end: 1), diff --git a/lib/types/post.dart b/lib/types/post.dart index 5f7551d..b625bed 100644 --- a/lib/types/post.dart +++ b/lib/types/post.dart @@ -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, diff --git a/lib/types/post.freezed.dart b/lib/types/post.freezed.dart index 5bed330..1c27ec2 100644 --- a/lib/types/post.freezed.dart +++ b/lib/types/post.freezed.dart @@ -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 diff --git a/lib/types/post.g.dart b/lib/types/post.g.dart index 6646cfd..39fc426 100644 --- a/lib/types/post.g.dart +++ b/lib/types/post.g.dart @@ -39,7 +39,9 @@ _$SnPostImpl _$$SnPostImplFromJson(Map 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 _$$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, diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index cd2975d..9d2fbd5 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -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(); + 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) => [ + 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(