Reactions

This commit is contained in:
LittleSheep 2024-03-24 13:34:29 +08:00
parent cba4f19b17
commit 7c8c1025e2
3 changed files with 225 additions and 14 deletions

View File

@ -4,22 +4,51 @@ import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:solaragent/models/feed.dart'; import 'package:solaragent/models/feed.dart';
import 'package:solaragent/widgets/image.dart'; import 'package:solaragent/widgets/image.dart';
import 'package:solaragent/widgets/posts/comments.dart'; import 'package:solaragent/widgets/posts/comments.dart';
import 'package:solaragent/widgets/posts/reactions.dart';
class FeedItem extends StatelessWidget { class FeedItem extends StatefulWidget {
final Feed item; final Feed item;
const FeedItem({super.key, required this.item}); const FeedItem({super.key, required this.item});
@override
State<FeedItem> createState() => _FeedItemState();
}
class _FeedItemState extends State<FeedItem> {
int reactionCount = 0;
Map<String, dynamic>? reactionList;
void viewComments(BuildContext context) { void viewComments(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: (context) => CommentListWidget(parent: item), builder: (context) => CommentList(parent: widget.item),
);
}
void viewReactions(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ReactionList(
parent: widget.item,
onReact: (symbol, num) {
setState(() {
if (!reactionList!.containsKey(symbol)) {
reactionList![symbol] = 0;
}
reactionCount += num;
reactionList![symbol] += num;
});
},
),
); );
} }
bool hasAttachments() => bool hasAttachments() =>
item.attachments != null && item.attachments!.isNotEmpty; widget.item.attachments != null && widget.item.attachments!.isNotEmpty;
String getDescription(String desc) => String getDescription(String desc) =>
desc.isEmpty ? "No description yet." : desc; desc.isEmpty ? "No description yet." : desc;
@ -27,6 +56,13 @@ class FeedItem extends StatelessWidget {
String getFileUrl(String fileId) => String getFileUrl(String fileId) =>
'https://co.solsynth.dev/api/attachments/o/$fileId'; 'https://co.solsynth.dev/api/attachments/o/$fileId';
@override
void initState() {
reactionCount = widget.item.reactionCount;
reactionList = widget.item.reactionList ?? <String, dynamic>{};
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -35,12 +71,12 @@ class FeedItem extends StatelessWidget {
Container( Container(
color: Colors.grey[50], color: Colors.grey[50],
child: ListTile( child: ListTile(
title: Text(item.author.name), title: Text(widget.item.author.name),
leading: CircleAvatar( leading: CircleAvatar(
backgroundImage: NetworkImage(item.author.avatar), backgroundImage: NetworkImage(widget.item.author.avatar),
), ),
subtitle: Text( subtitle: Text(
getDescription(item.author.description), getDescription(widget.item.author.description),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
softWrap: false, softWrap: false,
@ -49,7 +85,7 @@ class FeedItem extends StatelessWidget {
), ),
// Content // Content
Markdown( Markdown(
data: item.content, data: widget.item.content,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
), ),
@ -67,7 +103,7 @@ class FeedItem extends StatelessWidget {
showIndicator: true, showIndicator: true,
slideIndicator: const CircularSlideIndicator(), slideIndicator: const CircularSlideIndicator(),
), ),
items: item.attachments?.map((x) { items: widget.item.attachments?.map((x) {
return Builder( return Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return SizedBox( return SizedBox(
@ -104,9 +140,15 @@ class FeedItem extends StatelessWidget {
children: [ children: [
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.comment), icon: const Icon(Icons.comment),
label: Text(item.commentCount.toString()), label: Text(widget.item.commentCount.toString()),
onPressed: () => viewComments(context), onPressed: () => viewComments(context),
) ),
TextButton.icon(
icon: const Icon(Icons.emoji_emotions),
label: Text(reactionCount.toString()),
style: TextButton.styleFrom(foregroundColor: Colors.teal),
onPressed: () => viewReactions(context),
),
], ],
), ),
), ),

View File

@ -9,16 +9,16 @@ import 'package:solaragent/models/pagination.dart';
import 'package:solaragent/router.dart'; import 'package:solaragent/router.dart';
import 'package:solaragent/widgets/feed.dart'; import 'package:solaragent/widgets/feed.dart';
class CommentListWidget extends StatefulWidget { class CommentList extends StatefulWidget {
final Feed parent; final Feed parent;
const CommentListWidget({super.key, required this.parent}); const CommentList({super.key, required this.parent});
@override @override
State<CommentListWidget> createState() => _CommentListWidgetState(); State<CommentList> createState() => _CommentListState();
} }
class _CommentListWidgetState extends State<CommentListWidget> { class _CommentListState extends State<CommentList> {
static const pageSize = 5; static const pageSize = 5;
final client = Client(); final client = Client();

View File

@ -0,0 +1,169 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:solaragent/auth.dart';
import 'package:solaragent/models/feed.dart';
class ReactionList extends StatefulWidget {
static const Map<String, Map<String, dynamic>> emojis = {
'thumb_up': {'icon': '👍', 'attitude': 1},
'clap': {'icon': '👏', 'attitude': 1}
};
final Feed parent;
final void Function(String symbol, int num) onReact;
const ReactionList({super.key, required this.parent, required this.onReact});
@override
State<ReactionList> createState() => _ReactionListState();
}
class _ReactionListState extends State<ReactionList> {
bool isSubmitting = false;
void viewReactMenu(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) {
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: ReactionList.emojis.length,
itemBuilder: (BuildContext context, int index) {
var element = ReactionList.emojis.entries.toList()[index];
return InkWell(
borderRadius: const BorderRadius.all(
Radius.circular(64),
),
onTap: () async {
await doReact(element.key, element.value['attitude']);
},
child: ListTile(
title: Text(element.value['icon']),
subtitle: Text(
":${element.key}:",
style: const TextStyle(fontFamily: "monospace"),
),
),
);
});
},
);
}
Future<void> doReact(String symbol, int attitude) async {
if (!await authClient.isAuthorized()) return;
var dataset = "${widget.parent.modelType}s";
var alias = widget.parent.id;
var uri = Uri.parse(
"https://co.solsynth.dev/api/p/$dataset/$alias/react",
);
setState(() => isSubmitting = true);
var res = await authClient.client!.post(
uri,
headers: <String, String>{
'Content-Type': 'application/json',
},
body: jsonEncode(<String, dynamic>{
'symbol': symbol,
'attitude': attitude,
}),
);
if (res.statusCode == 201) {
widget.onReact(symbol, 1);
} else if (res.statusCode == 204) {
widget.onReact(symbol, -1);
} else {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
}
setState(() => isSubmitting = false);
}
List<MapEntry<String, dynamic>> getReactionEntries() =>
widget.parent.reactionList?.entries.toList() ?? List.empty();
@override
Widget build(BuildContext context) {
return Column(
children: [
// Title
Container(
padding: const EdgeInsets.only(left: 10, right: 10, top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 12.0,
),
child: Text(
'Reactions',
style: Theme
.of(context)
.textTheme
.headlineSmall,
),
),
FutureBuilder(
future: authClient.isAuthorized(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) {
return TextButton.icon(
icon: const Icon(Icons.add_reaction),
label: const Text("REACT"),
onPressed:
isSubmitting ? null : () => viewReactMenu(context),
);
} else {
return Container();
}
},
),
],
),
),
// Loading indicator
isSubmitting ? const LinearProgressIndicator() : Container(),
// Data list
Expanded(
child: ListView.separated(
itemCount: getReactionEntries().length,
itemBuilder: (BuildContext context, int index) {
var element = getReactionEntries()[index];
return InkWell(
onTap: isSubmitting
? null
: () {
doReact(
element.key,
ReactionList.emojis[element.key]!['attitude'],
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: ListTile(
title: Text(
"${ReactionList.emojis[element.key]!['icon']} x${element
.value.toString()}",
),
subtitle: Text(
":${element.key}:",
style: const TextStyle(fontFamily: "monospace"),
),
),
));
},
separatorBuilder: (context, index) => const Divider(),
),
),
],
);
}
}