✨ 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/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),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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();
|
||||||
|
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