Post reaction

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

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(),
),
),
],
),
);
}
}