Post reaction

This commit is contained in:
LittleSheep 2024-11-12 20:47:40 +08:00
parent e5239a6ca0
commit 5368f8ebb0
9 changed files with 271 additions and 93 deletions

View File

@ -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",

View File

@ -93,6 +93,14 @@
"postRepostingNotice": "你正在转发由 {} 发布的帖子。",
"postReact": "反应",
"postPosted": "帖子已经发表。",
"postReactions": "帖子的反应",
"postReactionPoints": {
"zero": "{} 点",
"one": "{} 点",
"other": "{} 点"
},
"postReactCompleted": "反应已被添加。",
"postReactUncompleted": "反应已被移除。",
"postComments": {
"zero": "评论",
"one": "{} 条评论",

View File

@ -136,6 +136,13 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
postReplyId: _data!.id,
onPost: () {
_childListKey.currentState!.refresh();
setState(() {
_data = _data!.copyWith(
metric: _data!.metric.copyWith(
replyCount: _data!.metric.replyCount + 1,
),
);
});
},
),
),

View File

@ -20,7 +20,6 @@ class SnPost with _$SnPost {
required String? aliasPrefix,
required List<dynamic> tags,
required List<dynamic> 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<String, int> reactionList,
}) = _SnMetric;
factory SnMetric.fromJson(Map<String, Object?> json) =>

View File

@ -31,7 +31,6 @@ mixin _$SnPost {
String? get aliasPrefix => throw _privateConstructorUsedError;
List<dynamic> get tags => throw _privateConstructorUsedError;
List<dynamic> 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<dynamic> tags,
List<dynamic> 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<dynamic>,
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<dynamic> tags,
List<dynamic> 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<dynamic>,
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<dynamic> tags,
required final List<dynamic> 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<dynamic> tags,
required final List<dynamic> 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<dynamic> 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<String, dynamic> json) {
mixin _$SnMetric {
int get replyCount => throw _privateConstructorUsedError;
int get reactionCount => throw _privateConstructorUsedError;
Map<String, int> get reactionList => throw _privateConstructorUsedError;
/// Serializes this SnMetric to a JSON map.
Map<String, dynamic> 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<String, int> 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<String, int>,
) 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<String, int> 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<String, int>,
));
}
}
@ -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<String, int> reactionList = const {}})
: _reactionList = reactionList;
factory _$SnMetricImpl.fromJson(Map<String, dynamic> json) =>
_$$SnMetricImplFromJson(json);
@ -1458,10 +1410,18 @@ class _$SnMetricImpl implements _SnMetric {
final int replyCount;
@override
final int reactionCount;
final Map<String, int> _reactionList;
@override
@JsonKey()
Map<String, int> 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<String, int> reactionList}) = _$SnMetricImpl;
factory _SnMetric.fromJson(Map<String, dynamic> json) =
_$SnMetricImpl.fromJson;
@ -1507,6 +1471,8 @@ abstract class _SnMetric implements SnMetric {
int get replyCount;
@override
int get reactionCount;
@override
Map<String, int> get reactionList;
/// Create a copy of SnMetric
/// with the given fields replaced by the non-null parameter values.

View File

@ -20,7 +20,6 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
aliasPrefix: json['alias_prefix'] as String?,
tags: json['tags'] as List<dynamic>,
categories: json['categories'] as List<dynamic>,
reactions: json['reactions'],
replies: json['replies'],
replyId: json['reply_id'],
repostId: json['repost_id'],
@ -47,8 +46,6 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> 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<String, dynamic>),
@ -71,7 +68,6 @@ Map<String, dynamic> _$$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<String, dynamic> _$$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<String, dynamic> json) =>
_$SnMetricImpl(
replyCount: (json['reply_count'] as num).toInt(),
reactionCount: (json['reaction_count'] as num).toInt(),
reactionList: (json['reaction_list'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()),
) ??
const {},
);
Map<String, dynamic> _$$SnMetricImplToJson(_$SnMetricImpl instance) =>
<String, dynamic>{
'reply_count': instance.replyCount,
'reaction_count': instance.reactionCount,
'reaction_list': instance.reactionList,
};
_$SnPublisherImpl _$$SnPublisherImplFromJson(Map<String, dynamic> json) =>

20
lib/types/reaction.dart Normal file
View File

@ -0,0 +1,20 @@
class ReactInfo {
final String icon;
final int attitude;
const ReactInfo({required this.icon, required this.attitude});
}
const Map<String, ReactInfo> 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),
};

View File

@ -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(

View File

@ -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<String, int> value, bool isPositive, int delta)? onChanged;
const PostReactionPopup({super.key, required this.data, this.onChanged});
@override
State<PostReactionPopup> createState() => _PostReactionPopupState();
}
class _PostReactionPopupState extends State<PostReactionPopup> {
bool _isSubmitting = false;
late Map<String, int> _reactions;
Future<void> _reactPost(String symbol, int attitude) async {
if (_isSubmitting) return;
final sn = context.read<SnNetworkProvider>();
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(),
),
),
],
),
);
}
}