From 7e42d959049e7a7948c4255f9a86f2c106329f55 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 15 Apr 2024 23:08:32 +0800 Subject: [PATCH] :sparkles: Reactions --- lib/models/reaction.dart | 12 ++ lib/providers/auth.dart | 29 ++-- lib/widgets/posts/attachment_editor.dart | 16 +- lib/widgets/posts/attachment_screen.dart | 29 ++-- lib/widgets/posts/content/article.dart | 13 +- lib/widgets/posts/content/attachment.dart | 43 +++-- lib/widgets/posts/content/moment.dart | 8 + lib/widgets/posts/item.dart | 189 +++++++++++++--------- lib/widgets/posts/item_action.dart | 11 +- lib/widgets/posts/reaction_action.dart | 144 +++++++++++++++++ lib/widgets/posts/reaction_list.dart | 89 ++++++++++ pubspec.lock | 2 +- pubspec.yaml | 1 + 13 files changed, 449 insertions(+), 137 deletions(-) create mode 100644 lib/models/reaction.dart create mode 100644 lib/widgets/posts/reaction_action.dart create mode 100644 lib/widgets/posts/reaction_list.dart diff --git a/lib/models/reaction.dart b/lib/models/reaction.dart new file mode 100644 index 0000000..985d931 --- /dev/null +++ b/lib/models/reaction.dart @@ -0,0 +1,12 @@ +class ReactInfo { + final String icon; + final int attitude; + + ReactInfo({required this.icon, required this.attitude}); +} + +final Map reactions = { + 'thumb_up': ReactInfo(icon: '👍', attitude: 1), + 'thumb_down': ReactInfo(icon: '👎', attitude: -1), + 'clap': ReactInfo(icon: '👏', attitude: 1), +}; diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 0aac0cc..6a7164b 100755 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -8,9 +7,7 @@ import 'package:oauth2/oauth2.dart' as oauth2; import 'package:solian/utils/service_url.dart'; class AuthProvider { - AuthProvider() { - pickClient(); - } + AuthProvider(); final deviceEndpoint = getRequestUri('passport', '/api/notifications/subscribe'); @@ -26,7 +23,10 @@ class AuthProvider { static const storageKey = "identity"; static const profileKey = "profiles"; + /// Before use this variable to make request + /// **MAKE SURE YOU HAVE CALL THE isAuthorized() METHOD** oauth2.Client? client; + DateTime? lastRefreshedAt; Future pickClient() async { @@ -83,9 +83,9 @@ class AuthProvider { Future refreshToken() async { if (client != null) { - final credentials = await client?.credentials.refresh( + final credentials = await client!.credentials.refresh( identifier: clientId, secret: clientSecret, basicAuth: false); - client = oauth2.Client(credentials!, + client = oauth2.Client(credentials, identifier: clientId, secret: clientSecret); storage.write(key: storageKey, value: credentials.toJson()); } @@ -106,14 +106,15 @@ class AuthProvider { Future isAuthorized() async { const storage = FlutterSecureStorage(); if (await storage.containsKey(key: storageKey)) { - if (client != null) { - if (lastRefreshedAt == null || - lastRefreshedAt! - .add(const Duration(minutes: 3)) - .isBefore(DateTime.now())) { - await refreshToken(); - lastRefreshedAt = DateTime.now(); - } + if (client == null) { + await pickClient(); + } + if (lastRefreshedAt == null || + DateTime.now() + .subtract(const Duration(minutes: 3)) + .isAfter(lastRefreshedAt!)) { + await refreshToken(); + lastRefreshedAt = DateTime.now(); } return true; } else { diff --git a/lib/widgets/posts/attachment_editor.dart b/lib/widgets/posts/attachment_editor.dart index c871e31..8472db0 100755 --- a/lib/widgets/posts/attachment_editor.dart +++ b/lib/widgets/posts/attachment_editor.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:math' as math; import 'package:crypto/crypto.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; import 'package:image_picker/image_picker.dart'; @@ -27,7 +26,7 @@ class AttachmentEditor extends StatefulWidget { class _AttachmentEditorState extends State { final _imagePicker = ImagePicker(); - bool isSubmitting = false; + bool _isSubmitting = false; List _attachments = List.empty(growable: true); @@ -47,7 +46,7 @@ class _AttachmentEditorState extends State { final image = await _imagePicker.pickImage(source: ImageSource.gallery); if (image == null) return; - setState(() => isSubmitting = true); + setState(() => _isSubmitting = true); final file = File(image.path); final hashcode = await calculateSha256(file); @@ -63,7 +62,7 @@ class _AttachmentEditorState extends State { SnackBar(content: Text("Something went wrong... $err")), ); } finally { - setState(() => isSubmitting = false); + setState(() => _isSubmitting = false); } } @@ -77,7 +76,6 @@ class _AttachmentEditorState extends State { req.fields['hashcode'] = hashcode; var res = await auth.client!.send(req); - print(res); if (res.statusCode == 200) { var result = Attachment.fromJson( jsonDecode(utf8.decode(await res.stream.toBytes()))["info"], @@ -98,7 +96,7 @@ class _AttachmentEditorState extends State { getRequestUri('interactive', '/api/attachments/${item.id}'), ); - setState(() => isSubmitting = true); + setState(() => _isSubmitting = true); var res = await auth.client!.send(req); if (res.statusCode == 200) { setState(() => _attachments.removeAt(index)); @@ -109,7 +107,7 @@ class _AttachmentEditorState extends State { SnackBar(content: Text("Something went wrong... $err")), ); } - setState(() => isSubmitting = false); + setState(() => _isSubmitting = false); } Future calculateSha256(File file) async { @@ -186,7 +184,7 @@ class _AttachmentEditorState extends State { builder: (context, snapshot) { if (snapshot.hasData && snapshot.data == true) { return TextButton( - onPressed: isSubmitting + onPressed: _isSubmitting ? null : () => viewAttachMethods(context), style: TextButton.styleFrom(shape: const CircleBorder()), @@ -200,7 +198,7 @@ class _AttachmentEditorState extends State { ], ), ), - isSubmitting ? const LinearProgressIndicator() : Container(), + _isSubmitting ? const LinearProgressIndicator() : Container(), Expanded( child: ListView.separated( itemCount: _attachments.length, diff --git a/lib/widgets/posts/attachment_screen.dart b/lib/widgets/posts/attachment_screen.dart index 5f95684..0a43c6c 100755 --- a/lib/widgets/posts/attachment_screen.dart +++ b/lib/widgets/posts/attachment_screen.dart @@ -1,29 +1,28 @@ import 'package:flutter/material.dart'; class AttachmentScreen extends StatelessWidget { - final String tag; final String url; + final String? tag; - const AttachmentScreen({super.key, required this.tag, required this.url}); + const AttachmentScreen({super.key, this.tag, required this.url}); @override Widget build(BuildContext context) { + final image = SizedBox( + height: MediaQuery.of(context).size.height, + width: MediaQuery.of(context).size.width, + child: InteractiveViewer( + boundaryMargin: const EdgeInsets.all(128), + minScale: 0.1, + maxScale: 16.0, + child: Image.network(url, fit: BoxFit.contain), + ), + ); + return Scaffold( body: GestureDetector( child: Center( - child: SizedBox( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, - child: InteractiveViewer( - boundaryMargin: const EdgeInsets.all(128), - minScale: 0.1, - maxScale: 16.0, - child: Hero( - tag: tag, - child: Image.network(url, fit: BoxFit.contain), - ), - ), - ), + child: tag != null ? Hero(tag: tag!, child: image) : image, ), onTap: () { Navigator.pop(context); diff --git a/lib/widgets/posts/content/article.dart b/lib/widgets/posts/content/article.dart index 2930b93..5af5070 100644 --- a/lib/widgets/posts/content/article.dart +++ b/lib/widgets/posts/content/article.dart @@ -3,6 +3,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:solian/models/post.dart'; import 'package:markdown/markdown.dart' as markdown; import 'package:solian/utils/service_url.dart'; +import 'package:solian/widgets/posts/content/attachment.dart'; import 'package:url_launcher/url_launcher_string.dart'; class ArticleContent extends StatelessWidget { @@ -53,13 +54,17 @@ class ArticleContent extends StatelessWidget { ); }, imageBuilder: (url, _, __) { + Uri uri; if (url.toString().startsWith("/api/attachments")) { - return Image.network( - getRequestUri('interactive', url.toString()) - .toString()); + uri = getRequestUri('interactive', url.toString()); } else { - return Image.network(url.toString()); + uri = url; } + + return AttachmentItem( + type: 1, + url: uri.toString(), + ); }, ), ], diff --git a/lib/widgets/posts/content/attachment.dart b/lib/widgets/posts/content/attachment.dart index 4e8c5b6..a8e23de 100644 --- a/lib/widgets/posts/content/attachment.dart +++ b/lib/widgets/posts/content/attachment.dart @@ -1,47 +1,53 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:chewie/chewie.dart'; import 'package:solian/models/post.dart'; import 'package:solian/utils/service_url.dart'; import 'package:flutter_carousel_widget/flutter_carousel_widget.dart'; import 'package:solian/widgets/posts/attachment_screen.dart'; +import 'package:uuid/uuid.dart'; import 'package:video_player/video_player.dart'; class AttachmentItem extends StatefulWidget { - final Attachment item; + final int type; + final String url; + final String? tag; final String? badge; - const AttachmentItem({super.key, required this.item, this.badge}); + const AttachmentItem({ + super.key, + required this.type, + required this.url, + this.tag, + this.badge, + }); @override State createState() => _AttachmentItemState(); } class _AttachmentItemState extends State { - String getTag() => 'attachment-${widget.item.fileId}'; - - Uri getFileUri() => - getRequestUri('interactive', '/api/attachments/o/${widget.item.fileId}'); + String getTag() => 'attachment-${widget.tag ?? const Uuid().v4()}'; VideoPlayerController? _vpController; ChewieController? _chewieController; @override Widget build(BuildContext context) { - const borderRadius = Radius.circular(16); + const borderRadius = Radius.circular(8); + final tag = getTag(); Widget content; - if (widget.item.type == 1) { + if (widget.type == 1) { content = GestureDetector( child: ClipRRect( borderRadius: const BorderRadius.all(borderRadius), child: Hero( - tag: getTag(), + tag: tag, child: Stack( children: [ Image.network( - getFileUri().toString(), + widget.url, width: double.infinity, height: double.infinity, fit: BoxFit.cover, @@ -62,15 +68,15 @@ class _AttachmentItemState extends State { context, MaterialPageRoute(builder: (_) { return AttachmentScreen( - tag: getTag(), - url: getFileUri().toString(), + tag: tag, + url: widget.url, ); }), ); }, ); } else { - _vpController = VideoPlayerController.networkUrl(getFileUri()); + _vpController = VideoPlayerController.networkUrl(Uri.parse(widget.url)); _chewieController = ChewieController( videoPlayerController: _vpController!, ); @@ -123,6 +129,9 @@ class AttachmentList extends StatelessWidget { const AttachmentList({super.key, required this.items}); + Uri getFileUri(String fileId) => + getRequestUri('interactive', '/api/attachments/o/$fileId'); + @override Widget build(BuildContext context) { var renderProgress = 0; @@ -140,7 +149,11 @@ class AttachmentList extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: AttachmentItem( - item: item, badge: items.length <= 1 ? null : badge), + type: item.type, + tag: item.fileId, + url: getFileUri(item.fileId).toString(), + badge: items.length <= 1 ? null : badge, + ), ); }, ); diff --git a/lib/widgets/posts/content/moment.dart b/lib/widgets/posts/content/moment.dart index 386dc48..b966eca 100644 --- a/lib/widgets/posts/content/moment.dart +++ b/lib/widgets/posts/content/moment.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:solian/models/post.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class MomentContent extends StatelessWidget { final Post item; @@ -16,6 +17,13 @@ class MomentContent extends StatelessWidget { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(0), + onTapLink: (text, href, title) async { + if (href == null) return; + await launchUrlString( + href, + mode: LaunchMode.externalApplication, + ); + }, ); } } diff --git a/lib/widgets/posts/item.dart b/lib/widgets/posts/item.dart index d77b111..5bf5518 100644 --- a/lib/widgets/posts/item.dart +++ b/lib/widgets/posts/item.dart @@ -4,14 +4,22 @@ import 'package:solian/widgets/posts/content/article.dart'; import 'package:solian/widgets/posts/content/attachment.dart'; import 'package:solian/widgets/posts/content/moment.dart'; import 'package:solian/widgets/posts/item_action.dart'; +import 'package:solian/widgets/posts/reaction_list.dart'; import 'package:timeago/timeago.dart' as timeago; class PostItem extends StatefulWidget { final Post item; final bool? brief; final Function? onUpdate; + final Function? onDelete; - const PostItem({super.key, required this.item, this.brief, this.onUpdate}); + const PostItem({ + super.key, + required this.item, + this.brief, + this.onUpdate, + this.onDelete, + }); @override State createState() => _PostItemState(); @@ -53,10 +61,39 @@ class _PostItemState extends State { } } + Widget renderReactions() { + if (reactionList != null && reactionList!.isNotEmpty) { + return Container( + height: 48, + padding: const EdgeInsets.only(top: 8, left: 4, right: 4), + child: ReactionList( + item: widget.item, + reactionList: reactionList, + onReact: (symbol, changes) { + setState(() { + if (!reactionList!.containsKey(symbol)) { + reactionList![symbol] = 0; + } + reactionList![symbol] += changes; + }); + }, + ), + ); + } else { + return Container(); + } + } + String getAuthorDescribe() => widget.item.author.description.isNotEmpty ? widget.item.author.description : 'No description yet.'; + @override + void initState() { + reactionList = widget.item.reactionList; + super.initState(); + } + @override Widget build(BuildContext context) { final headingParts = [ @@ -75,89 +112,89 @@ class _PostItemState extends State { ), ]; + Widget content; + if (widget.brief ?? true) { - return GestureDetector( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CircleAvatar( - backgroundImage: NetworkImage(widget.item.author.avatar), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...headingParts, - Padding( - padding: const EdgeInsets.only( - left: 12, right: 12, top: 4), - child: renderContent(), - ), - renderAttachments(), - ], - ), - ), - ], - ), - ], - ), - ), - onLongPress: () { - viewActions(context); - }, - ); - } else { - return GestureDetector( + content = Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: Column( children: [ - Padding( - padding: const EdgeInsets.only(left: 12, right: 12, top: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CircleAvatar( - backgroundImage: NetworkImage(widget.item.author.avatar), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + backgroundImage: NetworkImage(widget.item.author.avatar), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...headingParts, + Padding( + padding: + const EdgeInsets.only(left: 12, right: 12, top: 4), + child: renderContent(), + ), + renderAttachments(), + renderReactions(), + ], ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...headingParts, - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - getAuthorDescribe(), - maxLines: 1, - ), - ), - ], - ), - ), - ], - ), + ), + ], ), - const Padding( - padding: EdgeInsets.only(top: 6), - child: Divider(thickness: 0.3), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: renderContent(), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: renderAttachments(), - ) ], ), - onLongPress: () { - viewActions(context); - }, + ); + } else { + content = Column( + children: [ + Padding( + padding: const EdgeInsets.only(left: 12, right: 12, top: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + backgroundImage: NetworkImage(widget.item.author.avatar), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...headingParts, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + getAuthorDescribe(), + maxLines: 1, + ), + ), + ], + ), + ), + ], + ), + ), + const Padding( + padding: EdgeInsets.only(top: 6), + child: Divider(thickness: 0.3), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: renderContent(), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: renderAttachments(), + ) + ], ); } + + return GestureDetector( + child: content, + onLongPress: () { + viewActions(context); + }, + ); } } diff --git a/lib/widgets/posts/item_action.dart b/lib/widgets/posts/item_action.dart index 53221b0..5530ba0 100644 --- a/lib/widgets/posts/item_action.dart +++ b/lib/widgets/posts/item_action.dart @@ -9,8 +9,14 @@ import 'package:solian/widgets/posts/item_deletion.dart'; class PostItemAction extends StatelessWidget { final Post item; final Function? onUpdate; + final Function? onDelete; - const PostItemAction({super.key, required this.item, this.onUpdate}); + const PostItemAction({ + super.key, + required this.item, + this.onUpdate, + this.onDelete, + }); @override Widget build(BuildContext context) { @@ -32,7 +38,6 @@ class PostItemAction extends StatelessWidget { child: FutureBuilder( future: auth.getProfiles(), builder: (context, snapshot) { - print(snapshot); if (snapshot.hasData) { final authorizedItems = [ ListTile( @@ -59,7 +64,7 @@ class PostItemAction extends StatelessWidget { item: item, dataset: dataset, onDelete: (did) { - if(did == true && onUpdate != null) onUpdate!(); + if (did == true && onDelete != null) onDelete!(); }, ), ); diff --git a/lib/widgets/posts/reaction_action.dart b/lib/widgets/posts/reaction_action.dart new file mode 100644 index 0000000..81b9cb3 --- /dev/null +++ b/lib/widgets/posts/reaction_action.dart @@ -0,0 +1,144 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:solian/models/reaction.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/utils/service_url.dart'; + +Future doReact( + String dataset, + int id, + String symbol, + int attitude, + final void Function(String symbol, int num) onReact, + BuildContext context, +) async { + final auth = context.read(); + if (!await auth.isAuthorized()) return; + + var uri = getRequestUri( + 'interactive', + '/api/p/$dataset/$id/react', + ); + + var res = await auth.client!.post( + uri, + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'symbol': symbol, + 'attitude': attitude, + }), + ); + + if (res.statusCode == 201) { + onReact(symbol, 1); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Your reaction has been added onto this post."), + ), + ); + } else if (res.statusCode == 204) { + onReact(symbol, -1); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Your reaction has been removed from this post."), + ), + ); + } else { + var message = utf8.decode(res.bodyBytes); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Something went wrong... $message")), + ); + } + + if (Navigator.canPop(context)) { + Navigator.pop(context); + } +} + +class ReactionActionPopup extends StatefulWidget { + final String dataset; + final int id; + final void Function(String symbol, int num) onReact; + + const ReactionActionPopup({ + super.key, + required this.dataset, + required this.id, + required this.onReact, + }); + + @override + State createState() => _ReactionActionPopupState(); +} + +class _ReactionActionPopupState extends State { + bool _isSubmitting = false; + + Future doWidgetReact( + String symbol, + int attitude, + BuildContext context, + ) async { + if (_isSubmitting) return; + + setState(() => _isSubmitting = true); + await doReact( + widget.dataset, + widget.id, + symbol, + attitude, + widget.onReact, + context, + ); + setState(() => _isSubmitting = false); + } + + @override + Widget build(BuildContext context) { + final reactEntries = reactions.entries.toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.only(left: 8, right: 8, top: 20), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 12, + ), + child: Text( + 'Add a reaction', + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + ), + _isSubmitting ? const LinearProgressIndicator() : Container(), + Expanded( + child: ListView.builder( + itemCount: reactions.length, + itemBuilder: (BuildContext context, int index) { + var info = reactEntries[index]; + return InkWell( + onTap: () async { + await doWidgetReact(info.key, info.value.attitude, context); + }, + child: ListTile( + title: Text(info.value.icon), + subtitle: Text( + ":${info.key}:", + style: const TextStyle(fontFamily: "monospace"), + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/posts/reaction_list.dart b/lib/widgets/posts/reaction_list.dart new file mode 100644 index 0000000..def4c41 --- /dev/null +++ b/lib/widgets/posts/reaction_list.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:solian/models/post.dart'; +import 'package:solian/models/reaction.dart'; +import 'package:solian/widgets/posts/reaction_action.dart'; + +class ReactionList extends StatefulWidget { + final Post item; + final Map? reactionList; + final void Function(String symbol, int num) onReact; + + const ReactionList({ + super.key, + required this.item, + this.reactionList, + required this.onReact, + }); + + @override + State createState() => _ReactionListState(); +} + +class _ReactionListState extends State { + bool _isSubmitting = false; + + void viewReactMenu(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (context) => ReactionActionPopup( + dataset: '${widget.item.modelType}s', + id: widget.item.id, + onReact: widget.onReact, + ), + ); + } + + Future doWidgetReact( + String symbol, + int attitude, + BuildContext context, + ) async { + if (_isSubmitting) return; + + setState(() => _isSubmitting = true); + await doReact( + '${widget.item.modelType}s', + widget.item.id, + symbol, + attitude, + widget.onReact, + context, + ); + setState(() => _isSubmitting = false); + } + + @override + Widget build(BuildContext context) { + const density = VisualDensity(horizontal: -4, vertical: -2); + + final reactEntries = widget.reactionList?.entries ?? List.empty(); + + return ListView( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: [ + ...reactEntries.map((x) { + final info = reactions[x.key]; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: ActionChip( + avatar: Text(info!.icon), + label: Text(x.value.toString()), + tooltip: ':${x.key}:', + visualDensity: density, + onPressed: _isSubmitting + ? null + : () => doWidgetReact(x.key, info.attitude, context), + ), + ); + }), + ActionChip( + avatar: const Icon(Icons.add_reaction, color: Colors.teal), + label: const Text("React"), + visualDensity: density, + onPressed: () => viewReactMenu(context), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ce3494c..64df652 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -859,7 +859,7 @@ packages: source: hosted version: "3.1.1" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" diff --git a/pubspec.yaml b/pubspec.yaml index 75e6ded..088ebc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: webview_flutter: ^4.7.0 crypto: ^3.0.3 image_picker: ^1.0.8 + uuid: ^4.4.0 dev_dependencies: flutter_test: