✨ Reactions
This commit is contained in:
parent
cba4f19b17
commit
7c8c1025e2
@ -4,22 +4,51 @@ import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:solaragent/models/feed.dart';
|
||||
import 'package:solaragent/widgets/image.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;
|
||||
|
||||
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) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
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() =>
|
||||
item.attachments != null && item.attachments!.isNotEmpty;
|
||||
widget.item.attachments != null && widget.item.attachments!.isNotEmpty;
|
||||
|
||||
String getDescription(String desc) =>
|
||||
desc.isEmpty ? "No description yet." : desc;
|
||||
@ -27,6 +56,13 @@ class FeedItem extends StatelessWidget {
|
||||
String getFileUrl(String 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
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@ -35,12 +71,12 @@ class FeedItem extends StatelessWidget {
|
||||
Container(
|
||||
color: Colors.grey[50],
|
||||
child: ListTile(
|
||||
title: Text(item.author.name),
|
||||
title: Text(widget.item.author.name),
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: NetworkImage(item.author.avatar),
|
||||
backgroundImage: NetworkImage(widget.item.author.avatar),
|
||||
),
|
||||
subtitle: Text(
|
||||
getDescription(item.author.description),
|
||||
getDescription(widget.item.author.description),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
@ -49,7 +85,7 @@ class FeedItem extends StatelessWidget {
|
||||
),
|
||||
// Content
|
||||
Markdown(
|
||||
data: item.content,
|
||||
data: widget.item.content,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
),
|
||||
@ -67,7 +103,7 @@ class FeedItem extends StatelessWidget {
|
||||
showIndicator: true,
|
||||
slideIndicator: const CircularSlideIndicator(),
|
||||
),
|
||||
items: item.attachments?.map((x) {
|
||||
items: widget.item.attachments?.map((x) {
|
||||
return Builder(
|
||||
builder: (BuildContext context) {
|
||||
return SizedBox(
|
||||
@ -104,9 +140,15 @@ class FeedItem extends StatelessWidget {
|
||||
children: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.comment),
|
||||
label: Text(item.commentCount.toString()),
|
||||
label: Text(widget.item.commentCount.toString()),
|
||||
onPressed: () => viewComments(context),
|
||||
)
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.emoji_emotions),
|
||||
label: Text(reactionCount.toString()),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.teal),
|
||||
onPressed: () => viewReactions(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -9,16 +9,16 @@ import 'package:solaragent/models/pagination.dart';
|
||||
import 'package:solaragent/router.dart';
|
||||
import 'package:solaragent/widgets/feed.dart';
|
||||
|
||||
class CommentListWidget extends StatefulWidget {
|
||||
class CommentList extends StatefulWidget {
|
||||
final Feed parent;
|
||||
|
||||
const CommentListWidget({super.key, required this.parent});
|
||||
const CommentList({super.key, required this.parent});
|
||||
|
||||
@override
|
||||
State<CommentListWidget> createState() => _CommentListWidgetState();
|
||||
State<CommentList> createState() => _CommentListState();
|
||||
}
|
||||
|
||||
class _CommentListWidgetState extends State<CommentListWidget> {
|
||||
class _CommentListState extends State<CommentList> {
|
||||
static const pageSize = 5;
|
||||
|
||||
final client = Client();
|
||||
|
169
lib/widgets/posts/reactions.dart
Normal file
169
lib/widgets/posts/reactions.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user