From 5368f8ebb05020dc6b7db8cdcaaefaebfd31c982 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 12 Nov 2024 20:47:40 +0800 Subject: [PATCH] :sparkles: Post reaction --- assets/translations/en-US.json | 8 ++ assets/translations/zh-CN.json | 8 ++ lib/screens/post/post_detail.dart | 7 ++ lib/types/post.dart | 4 +- lib/types/post.freezed.dart | 108 ++++++++--------------- lib/types/post.g.dart | 11 ++- lib/types/reaction.dart | 20 +++++ lib/widgets/post/post_item.dart | 70 ++++++++++++--- lib/widgets/post/post_reaction.dart | 128 ++++++++++++++++++++++++++++ 9 files changed, 271 insertions(+), 93 deletions(-) create mode 100644 lib/types/reaction.dart create mode 100644 lib/widgets/post/post_reaction.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index f29bc8b..f678900 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -93,6 +93,14 @@ "postReplyingNotice": "You're about to reply to a post that posted {}.", "postRepostingNotice": "You're about to repost a post that posted {}.", "postReact": "React", + "postReactions": "Reactions of Post", + "postReactionPoints": { + "zero": "{}pt", + "one": "{}pt", + "other": "{}pts" + }, + "postReactCompleted": "Reaction has been added.", + "postReactUncompleted": "Reaction has been removed.", "postComments": { "zero": "Comment", "one": "{} comment", diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 4fd5621..7e00e53 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -93,6 +93,14 @@ "postRepostingNotice": "你正在转发由 {} 发布的帖子。", "postReact": "反应", "postPosted": "帖子已经发表。", + "postReactions": "帖子的反应", + "postReactionPoints": { + "zero": "{} 点", + "one": "{} 点", + "other": "{} 点" + }, + "postReactCompleted": "反应已被添加。", + "postReactUncompleted": "反应已被移除。", "postComments": { "zero": "评论", "one": "{} 条评论", diff --git a/lib/screens/post/post_detail.dart b/lib/screens/post/post_detail.dart index 6bcdb73..4ded56d 100644 --- a/lib/screens/post/post_detail.dart +++ b/lib/screens/post/post_detail.dart @@ -136,6 +136,13 @@ class _PostDetailScreenState extends State { postReplyId: _data!.id, onPost: () { _childListKey.currentState!.refresh(); + setState(() { + _data = _data!.copyWith( + metric: _data!.metric.copyWith( + replyCount: _data!.metric.replyCount + 1, + ), + ); + }); }, ), ), diff --git a/lib/types/post.dart b/lib/types/post.dart index c7b65dc..8107f8f 100644 --- a/lib/types/post.dart +++ b/lib/types/post.dart @@ -20,7 +20,6 @@ class SnPost with _$SnPost { required String? aliasPrefix, required List tags, required List categories, - required dynamic reactions, required dynamic replies, required dynamic replyId, required dynamic repostId, @@ -37,8 +36,6 @@ class SnPost with _$SnPost { required DateTime? publishedUntil, required int totalUpvote, required int totalDownvote, - required int? realmId, - required dynamic realm, required int publisherId, required SnPublisher publisher, required SnMetric metric, @@ -81,6 +78,7 @@ class SnMetric with _$SnMetric { const factory SnMetric({ required int replyCount, required int reactionCount, + @Default({}) Map reactionList, }) = _SnMetric; factory SnMetric.fromJson(Map json) => diff --git a/lib/types/post.freezed.dart b/lib/types/post.freezed.dart index 888fae3..2f59d58 100644 --- a/lib/types/post.freezed.dart +++ b/lib/types/post.freezed.dart @@ -31,7 +31,6 @@ mixin _$SnPost { String? get aliasPrefix => throw _privateConstructorUsedError; List get tags => throw _privateConstructorUsedError; List get categories => throw _privateConstructorUsedError; - dynamic get reactions => throw _privateConstructorUsedError; dynamic get replies => throw _privateConstructorUsedError; dynamic get replyId => throw _privateConstructorUsedError; dynamic get repostId => throw _privateConstructorUsedError; @@ -48,8 +47,6 @@ mixin _$SnPost { DateTime? get publishedUntil => throw _privateConstructorUsedError; int get totalUpvote => throw _privateConstructorUsedError; int get totalDownvote => throw _privateConstructorUsedError; - int? get realmId => throw _privateConstructorUsedError; - dynamic get realm => throw _privateConstructorUsedError; int get publisherId => throw _privateConstructorUsedError; SnPublisher get publisher => throw _privateConstructorUsedError; SnMetric get metric => throw _privateConstructorUsedError; @@ -81,7 +78,6 @@ abstract class $SnPostCopyWith<$Res> { String? aliasPrefix, List tags, List categories, - dynamic reactions, dynamic replies, dynamic replyId, dynamic repostId, @@ -98,8 +94,6 @@ abstract class $SnPostCopyWith<$Res> { DateTime? publishedUntil, int totalUpvote, int totalDownvote, - int? realmId, - dynamic realm, int publisherId, SnPublisher publisher, SnMetric metric, @@ -136,7 +130,6 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> Object? aliasPrefix = freezed, Object? tags = null, Object? categories = null, - Object? reactions = freezed, Object? replies = freezed, Object? replyId = freezed, Object? repostId = freezed, @@ -153,8 +146,6 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> Object? publishedUntil = freezed, Object? totalUpvote = null, Object? totalDownvote = null, - Object? realmId = freezed, - Object? realm = freezed, Object? publisherId = null, Object? publisher = null, Object? metric = null, @@ -205,10 +196,6 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> ? _value.categories : categories // ignore: cast_nullable_to_non_nullable as List, - reactions: freezed == reactions - ? _value.reactions - : reactions // ignore: cast_nullable_to_non_nullable - as dynamic, replies: freezed == replies ? _value.replies : replies // ignore: cast_nullable_to_non_nullable @@ -273,14 +260,6 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost> ? _value.totalDownvote : totalDownvote // ignore: cast_nullable_to_non_nullable as int, - realmId: freezed == realmId - ? _value.realmId - : realmId // ignore: cast_nullable_to_non_nullable - as int?, - realm: freezed == realm - ? _value.realm - : realm // ignore: cast_nullable_to_non_nullable - as dynamic, publisherId: null == publisherId ? _value.publisherId : publisherId // ignore: cast_nullable_to_non_nullable @@ -354,7 +333,6 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> { String? aliasPrefix, List tags, List categories, - dynamic reactions, dynamic replies, dynamic replyId, dynamic repostId, @@ -371,8 +349,6 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> { DateTime? publishedUntil, int totalUpvote, int totalDownvote, - int? realmId, - dynamic realm, int publisherId, SnPublisher publisher, SnMetric metric, @@ -410,7 +386,6 @@ class __$$SnPostImplCopyWithImpl<$Res> Object? aliasPrefix = freezed, Object? tags = null, Object? categories = null, - Object? reactions = freezed, Object? replies = freezed, Object? replyId = freezed, Object? repostId = freezed, @@ -427,8 +402,6 @@ class __$$SnPostImplCopyWithImpl<$Res> Object? publishedUntil = freezed, Object? totalUpvote = null, Object? totalDownvote = null, - Object? realmId = freezed, - Object? realm = freezed, Object? publisherId = null, Object? publisher = null, Object? metric = null, @@ -479,10 +452,6 @@ class __$$SnPostImplCopyWithImpl<$Res> ? _value._categories : categories // ignore: cast_nullable_to_non_nullable as List, - reactions: freezed == reactions - ? _value.reactions - : reactions // ignore: cast_nullable_to_non_nullable - as dynamic, replies: freezed == replies ? _value.replies : replies // ignore: cast_nullable_to_non_nullable @@ -547,14 +516,6 @@ class __$$SnPostImplCopyWithImpl<$Res> ? _value.totalDownvote : totalDownvote // ignore: cast_nullable_to_non_nullable as int, - realmId: freezed == realmId - ? _value.realmId - : realmId // ignore: cast_nullable_to_non_nullable - as int?, - realm: freezed == realm - ? _value.realm - : realm // ignore: cast_nullable_to_non_nullable - as dynamic, publisherId: null == publisherId ? _value.publisherId : publisherId // ignore: cast_nullable_to_non_nullable @@ -590,7 +551,6 @@ class _$SnPostImpl extends _SnPost { required this.aliasPrefix, required final List tags, required final List categories, - required this.reactions, required this.replies, required this.replyId, required this.repostId, @@ -607,8 +567,6 @@ class _$SnPostImpl extends _SnPost { required this.publishedUntil, required this.totalUpvote, required this.totalDownvote, - required this.realmId, - required this.realm, required this.publisherId, required this.publisher, required this.metric, @@ -661,8 +619,6 @@ class _$SnPostImpl extends _SnPost { return EqualUnmodifiableListView(_categories); } - @override - final dynamic reactions; @override final dynamic replies; @override @@ -696,10 +652,6 @@ class _$SnPostImpl extends _SnPost { @override final int totalDownvote; @override - final int? realmId; - @override - final dynamic realm; - @override final int publisherId; @override final SnPublisher publisher; @@ -710,7 +662,7 @@ class _$SnPostImpl extends _SnPost { @override String toString() { - return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, reactions: $reactions, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, realm: $realm, publisherId: $publisherId, publisher: $publisher, metric: $metric, preload: $preload)'; + return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, publisherId: $publisherId, publisher: $publisher, metric: $metric, preload: $preload)'; } @override @@ -735,7 +687,6 @@ class _$SnPostImpl extends _SnPost { const DeepCollectionEquality().equals(other._tags, _tags) && const DeepCollectionEquality() .equals(other._categories, _categories) && - const DeepCollectionEquality().equals(other.reactions, reactions) && const DeepCollectionEquality().equals(other.replies, replies) && const DeepCollectionEquality().equals(other.replyId, replyId) && const DeepCollectionEquality().equals(other.repostId, repostId) && @@ -762,8 +713,6 @@ class _$SnPostImpl extends _SnPost { other.totalUpvote == totalUpvote) && (identical(other.totalDownvote, totalDownvote) || other.totalDownvote == totalDownvote) && - (identical(other.realmId, realmId) || other.realmId == realmId) && - const DeepCollectionEquality().equals(other.realm, realm) && (identical(other.publisherId, publisherId) || other.publisherId == publisherId) && (identical(other.publisher, publisher) || @@ -787,7 +736,6 @@ class _$SnPostImpl extends _SnPost { aliasPrefix, const DeepCollectionEquality().hash(_tags), const DeepCollectionEquality().hash(_categories), - const DeepCollectionEquality().hash(reactions), const DeepCollectionEquality().hash(replies), const DeepCollectionEquality().hash(replyId), const DeepCollectionEquality().hash(repostId), @@ -804,8 +752,6 @@ class _$SnPostImpl extends _SnPost { publishedUntil, totalUpvote, totalDownvote, - realmId, - const DeepCollectionEquality().hash(realm), publisherId, publisher, metric, @@ -841,7 +787,6 @@ abstract class _SnPost extends SnPost { required final String? aliasPrefix, required final List tags, required final List categories, - required final dynamic reactions, required final dynamic replies, required final dynamic replyId, required final dynamic repostId, @@ -858,8 +803,6 @@ abstract class _SnPost extends SnPost { required final DateTime? publishedUntil, required final int totalUpvote, required final int totalDownvote, - required final int? realmId, - required final dynamic realm, required final int publisherId, required final SnPublisher publisher, required final SnMetric metric, @@ -891,8 +834,6 @@ abstract class _SnPost extends SnPost { @override List get categories; @override - dynamic get reactions; - @override dynamic get replies; @override dynamic get replyId; @@ -925,10 +866,6 @@ abstract class _SnPost extends SnPost { @override int get totalDownvote; @override - int? get realmId; - @override - dynamic get realm; - @override int get publisherId; @override SnPublisher get publisher; @@ -1356,6 +1293,7 @@ SnMetric _$SnMetricFromJson(Map json) { mixin _$SnMetric { int get replyCount => throw _privateConstructorUsedError; int get reactionCount => throw _privateConstructorUsedError; + Map get reactionList => throw _privateConstructorUsedError; /// Serializes this SnMetric to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -1372,7 +1310,7 @@ abstract class $SnMetricCopyWith<$Res> { factory $SnMetricCopyWith(SnMetric value, $Res Function(SnMetric) then) = _$SnMetricCopyWithImpl<$Res, SnMetric>; @useResult - $Res call({int replyCount, int reactionCount}); + $Res call({int replyCount, int reactionCount, Map reactionList}); } /// @nodoc @@ -1392,6 +1330,7 @@ class _$SnMetricCopyWithImpl<$Res, $Val extends SnMetric> $Res call({ Object? replyCount = null, Object? reactionCount = null, + Object? reactionList = null, }) { return _then(_value.copyWith( replyCount: null == replyCount @@ -1402,6 +1341,10 @@ class _$SnMetricCopyWithImpl<$Res, $Val extends SnMetric> ? _value.reactionCount : reactionCount // ignore: cast_nullable_to_non_nullable as int, + reactionList: null == reactionList + ? _value.reactionList + : reactionList // ignore: cast_nullable_to_non_nullable + as Map, ) as $Val); } } @@ -1414,7 +1357,7 @@ abstract class _$$SnMetricImplCopyWith<$Res> __$$SnMetricImplCopyWithImpl<$Res>; @override @useResult - $Res call({int replyCount, int reactionCount}); + $Res call({int replyCount, int reactionCount, Map reactionList}); } /// @nodoc @@ -1432,6 +1375,7 @@ class __$$SnMetricImplCopyWithImpl<$Res> $Res call({ Object? replyCount = null, Object? reactionCount = null, + Object? reactionList = null, }) { return _then(_$SnMetricImpl( replyCount: null == replyCount @@ -1442,6 +1386,10 @@ class __$$SnMetricImplCopyWithImpl<$Res> ? _value.reactionCount : reactionCount // ignore: cast_nullable_to_non_nullable as int, + reactionList: null == reactionList + ? _value._reactionList + : reactionList // ignore: cast_nullable_to_non_nullable + as Map, )); } } @@ -1449,7 +1397,11 @@ class __$$SnMetricImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$SnMetricImpl implements _SnMetric { - const _$SnMetricImpl({required this.replyCount, required this.reactionCount}); + const _$SnMetricImpl( + {required this.replyCount, + required this.reactionCount, + final Map reactionList = const {}}) + : _reactionList = reactionList; factory _$SnMetricImpl.fromJson(Map json) => _$$SnMetricImplFromJson(json); @@ -1458,10 +1410,18 @@ class _$SnMetricImpl implements _SnMetric { final int replyCount; @override final int reactionCount; + final Map _reactionList; + @override + @JsonKey() + Map get reactionList { + if (_reactionList is EqualUnmodifiableMapView) return _reactionList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_reactionList); + } @override String toString() { - return 'SnMetric(replyCount: $replyCount, reactionCount: $reactionCount)'; + return 'SnMetric(replyCount: $replyCount, reactionCount: $reactionCount, reactionList: $reactionList)'; } @override @@ -1472,12 +1432,15 @@ class _$SnMetricImpl implements _SnMetric { (identical(other.replyCount, replyCount) || other.replyCount == replyCount) && (identical(other.reactionCount, reactionCount) || - other.reactionCount == reactionCount)); + other.reactionCount == reactionCount) && + const DeepCollectionEquality() + .equals(other._reactionList, _reactionList)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, replyCount, reactionCount); + int get hashCode => Object.hash(runtimeType, replyCount, reactionCount, + const DeepCollectionEquality().hash(_reactionList)); /// Create a copy of SnMetric /// with the given fields replaced by the non-null parameter values. @@ -1498,7 +1461,8 @@ class _$SnMetricImpl implements _SnMetric { abstract class _SnMetric implements SnMetric { const factory _SnMetric( {required final int replyCount, - required final int reactionCount}) = _$SnMetricImpl; + required final int reactionCount, + final Map reactionList}) = _$SnMetricImpl; factory _SnMetric.fromJson(Map json) = _$SnMetricImpl.fromJson; @@ -1507,6 +1471,8 @@ abstract class _SnMetric implements SnMetric { int get replyCount; @override int get reactionCount; + @override + Map get reactionList; /// Create a copy of SnMetric /// with the given fields replaced by the non-null parameter values. diff --git a/lib/types/post.g.dart b/lib/types/post.g.dart index 3637687..2dd8b31 100644 --- a/lib/types/post.g.dart +++ b/lib/types/post.g.dart @@ -20,7 +20,6 @@ _$SnPostImpl _$$SnPostImplFromJson(Map json) => _$SnPostImpl( aliasPrefix: json['alias_prefix'] as String?, tags: json['tags'] as List, categories: json['categories'] as List, - reactions: json['reactions'], replies: json['replies'], replyId: json['reply_id'], repostId: json['repost_id'], @@ -47,8 +46,6 @@ _$SnPostImpl _$$SnPostImplFromJson(Map json) => _$SnPostImpl( : DateTime.parse(json['published_until'] as String), totalUpvote: (json['total_upvote'] as num).toInt(), totalDownvote: (json['total_downvote'] as num).toInt(), - realmId: (json['realm_id'] as num?)?.toInt(), - realm: json['realm'], publisherId: (json['publisher_id'] as num).toInt(), publisher: SnPublisher.fromJson(json['publisher'] as Map), @@ -71,7 +68,6 @@ Map _$$SnPostImplToJson(_$SnPostImpl instance) => 'alias_prefix': instance.aliasPrefix, 'tags': instance.tags, 'categories': instance.categories, - 'reactions': instance.reactions, 'replies': instance.replies, 'reply_id': instance.replyId, 'repost_id': instance.repostId, @@ -88,8 +84,6 @@ Map _$$SnPostImplToJson(_$SnPostImpl instance) => 'published_until': instance.publishedUntil?.toIso8601String(), 'total_upvote': instance.totalUpvote, 'total_downvote': instance.totalDownvote, - 'realm_id': instance.realmId, - 'realm': instance.realm, 'publisher_id': instance.publisherId, 'publisher': instance.publisher.toJson(), 'metric': instance.metric.toJson(), @@ -131,12 +125,17 @@ _$SnMetricImpl _$$SnMetricImplFromJson(Map json) => _$SnMetricImpl( replyCount: (json['reply_count'] as num).toInt(), reactionCount: (json['reaction_count'] as num).toInt(), + reactionList: (json['reaction_list'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toInt()), + ) ?? + const {}, ); Map _$$SnMetricImplToJson(_$SnMetricImpl instance) => { 'reply_count': instance.replyCount, 'reaction_count': instance.reactionCount, + 'reaction_list': instance.reactionList, }; _$SnPublisherImpl _$$SnPublisherImplFromJson(Map json) => diff --git a/lib/types/reaction.dart b/lib/types/reaction.dart new file mode 100644 index 0000000..ad9a92b --- /dev/null +++ b/lib/types/reaction.dart @@ -0,0 +1,20 @@ +class ReactInfo { + final String icon; + final int attitude; + + const ReactInfo({required this.icon, required this.attitude}); +} + +const Map kTemplateReactions = { + 'thumb_up': ReactInfo(icon: '👍', attitude: 1), + 'thumb_down': ReactInfo(icon: '👎', attitude: 2), + 'just_okay': ReactInfo(icon: '😅', attitude: 0), + 'cry': ReactInfo(icon: '😭', attitude: 0), + 'confuse': ReactInfo(icon: '🧐', attitude: 0), + 'clap': ReactInfo(icon: '👏', attitude: 1), + 'laugh': ReactInfo(icon: '😂', attitude: 1), + 'angry': ReactInfo(icon: '😡', attitude: 2), + 'party': ReactInfo(icon: '🎉', attitude: 1), + 'joy': ReactInfo(icon: '🤣', attitude: 1), + 'pray': ReactInfo(icon: '🙏', attitude: 1), +}; diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 3451f06..b5eabf7 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -12,16 +12,25 @@ import 'package:surface/widgets/attachment/attachment_list.dart'; import 'package:surface/widgets/markdown_content.dart'; import 'package:gap/gap.dart'; import 'package:surface/widgets/post/post_comment_list.dart'; +import 'package:surface/widgets/post/post_reaction.dart'; class PostItem extends StatelessWidget { final SnPost data; + final bool showReactions; final bool showComments; + final Function(SnPost data)? onChanged; const PostItem({ super.key, required this.data, + this.showReactions = true, this.showComments = true, + this.onChanged, }); + void _onChanged(SnPost data) { + if (onChanged != null) onChanged!(data); + } + @override Widget build(BuildContext context) { return Column( @@ -34,8 +43,12 @@ class PostItem extends StatelessWidget { data: data.preload!.attachments!, bordered: true, ), - _PostBottomAction(data: data, showComments: showComments) - .padding(left: 12, right: 18), + _PostBottomAction( + data: data, + showComments: showComments, + showReactions: showReactions, + onChanged: _onChanged, + ).padding(left: 12, right: 18), ], ); } @@ -44,7 +57,14 @@ class PostItem extends StatelessWidget { class _PostBottomAction extends StatelessWidget { final SnPost data; final bool showComments; - const _PostBottomAction({required this.data, required this.showComments}); + final bool showReactions; + final Function(SnPost data) onChanged; + const _PostBottomAction({ + required this.data, + required this.showComments, + required this.showReactions, + required this.onChanged, + }); @override Widget build(BuildContext context) { @@ -56,16 +76,40 @@ class _PostBottomAction extends StatelessWidget { children: [ Row( children: [ - InkWell( - child: Row( - children: [ - Icon(Symbols.add_reaction, size: 20, color: iconColor), - const Gap(8), - Text('postReact').tr(), - ], - ).padding(horizontal: 8, vertical: 8), - onTap: () {}, - ), + if (showReactions) + InkWell( + child: Row( + children: [ + Icon(Symbols.add_reaction, size: 20, color: iconColor), + const Gap(8), + if (data.totalDownvote > 0 || data.totalUpvote > 0) + Text('postReactionPoints').plural( + data.totalUpvote - data.totalDownvote, + ) + else + Text('postReact').tr(), + ], + ).padding(horizontal: 8, vertical: 8), + onTap: () { + showModalBottomSheet( + context: context, + builder: (context) => PostReactionPopup( + data: data, + onChanged: (value, isPositive, delta) { + onChanged(data.copyWith( + totalUpvote: isPositive + ? data.totalUpvote + delta + : data.totalUpvote, + totalDownvote: !isPositive + ? data.totalDownvote + delta + : data.totalDownvote, + metric: data.metric.copyWith(reactionList: value), + )); + }, + ), + ); + }, + ), if (showComments) InkWell( child: Row( diff --git a/lib/widgets/post/post_reaction.dart b/lib/widgets/post/post_reaction.dart new file mode 100644 index 0000000..232a34b --- /dev/null +++ b/lib/widgets/post/post_reaction.dart @@ -0,0 +1,128 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/post.dart'; +import 'package:surface/types/reaction.dart'; +import 'package:surface/widgets/dialog.dart'; + +class PostReactionPopup extends StatefulWidget { + final SnPost data; + final Function(Map value, bool isPositive, int delta)? onChanged; + const PostReactionPopup({super.key, required this.data, this.onChanged}); + + @override + State createState() => _PostReactionPopupState(); +} + +class _PostReactionPopupState extends State { + bool _isSubmitting = false; + late Map _reactions; + + Future _reactPost(String symbol, int attitude) async { + if (_isSubmitting) return; + + final sn = context.read(); + + try { + setState(() => _isSubmitting = true); + final resp = await sn.client.post( + '/cgi/co/posts/${widget.data.id}/react', + data: { + 'symbol': symbol, + 'attitude': attitude, + }, + ); + if (resp.statusCode == 201) { + _reactions[symbol] = (_reactions[symbol] ?? 0) + 1; + // ignore: use_build_context_synchronously + if (context.mounted) context.showSnackbar('postReactCompleted'.tr()); + if (widget.onChanged != null) { + widget.onChanged!( + _reactions, + kTemplateReactions[symbol]!.attitude == 1, + 1, + ); + } + } else if (resp.statusCode == 204) { + _reactions[symbol] = (_reactions[symbol] ?? 0) - 1; + // ignore: use_build_context_synchronously + if (context.mounted) context.showSnackbar('postReactUncompleted'.tr()); + if (widget.onChanged != null) { + widget.onChanged!( + _reactions, + kTemplateReactions[symbol]!.attitude == 1, + -1, + ); + } + } + } catch (err) { + // ignore: use_build_context_synchronously + if (context.mounted) context.showErrorDialog(err); + } finally { + setState(() => _isSubmitting = false); + } + } + + @override + void initState() { + super.initState(); + _reactions = Map.from(widget.data.metric.reactionList); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Symbols.mood, size: 24), + const Gap(16), + Text('postReactions') + .tr() + .textStyle(Theme.of(context).textTheme.titleLarge!), + ], + ).padding(horizontal: 20, top: 16, bottom: 12), + Expanded( + child: GridView.count( + crossAxisSpacing: 4, + mainAxisSpacing: 4, + crossAxisCount: 4, + children: kTemplateReactions.entries.map((e) { + return InkWell( + onTap: () { + if (widget.onChanged == null) return; + _reactPost(e.key, e.value.attitude).then((_) { + if (context.mounted) Navigator.pop(context); + }); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(e.value.icon).fontSize(40), + Text( + e.key, + style: const TextStyle(fontFamily: 'monospace'), + ), + const Gap(6), + Text( + 'x${_reactions[e.key]?.toString() ?? '0'}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ); + }).toList(), + ), + ), + ], + ), + ); + } +}