From 4b0438f748adfa7f1fa1762e10bbc253df158041 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Wed, 23 Oct 2024 18:45:07 +0530 Subject: [PATCH 01/77] Basic setup --- .../news/widgets/news_item/news_side_bar.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index e091a5d7ecf2..af09dddcad50 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -6,6 +6,7 @@ import 'package:acter/common/themes/colors/color_scheme.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/default_bottom_sheet.dart'; import 'package:acter/common/widgets/like_button.dart'; +import 'package:acter/features/comments/widgets/comments_section.dart'; import 'package:acter/features/news/model/keys.dart'; import 'package:acter/features/news/providers/news_providers.dart'; import 'package:acter/router/utils.dart'; @@ -60,6 +61,19 @@ class NewsSideBar extends ConsumerWidget { }, ), const SizedBox(height: 10), + IconButton( + onPressed: () { + showModalBottomSheet( + context: context, + showDragHandle: true, + builder: (context) => CommentsSection( + manager: news.comments(), + ), + ); + }, + icon: const Icon(Atlas.comment_blank), + ), + const SizedBox(height: 10), InkWell( key: NewsUpdateKeys.newsSidebarActionBottomSheet, onTap: () => showModalBottomSheet( From bc9803f67dbdb01b0723e2bacb3999ef8d5ec366 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 12:23:40 +0530 Subject: [PATCH 02/77] All new comment section --- .../comments/actions/sbumit_comment.dart | 42 ++++++ .../comments/widgets/add_comment_widget.dart | 65 +++++++++ ...{comment.dart => comment_item_widget.dart} | 4 +- .../comment_list_empty_state_widget.dart | 10 ++ .../widgets/comment_list_skeleton_widget.dart | 33 +++++ .../comments/widgets/comment_list_widget.dart | 67 ++++++++++ .../comments/widgets/comments_list.dart | 116 ---------------- .../comments/widgets/comments_section.dart | 64 ++++----- .../comments/widgets/create_comment.dart | 125 ------------------ .../features/pins/pages/pin_details_page.dart | 3 +- app/lib/l10n/app_en.arb | 1 + 11 files changed, 251 insertions(+), 279 deletions(-) create mode 100644 app/lib/features/comments/actions/sbumit_comment.dart create mode 100644 app/lib/features/comments/widgets/add_comment_widget.dart rename app/lib/features/comments/widgets/{comment.dart => comment_item_widget.dart} (96%) create mode 100644 app/lib/features/comments/widgets/comment_list_empty_state_widget.dart create mode 100644 app/lib/features/comments/widgets/comment_list_skeleton_widget.dart create mode 100644 app/lib/features/comments/widgets/comment_list_widget.dart delete mode 100644 app/lib/features/comments/widgets/comments_list.dart delete mode 100644 app/lib/features/comments/widgets/create_comment.dart diff --git a/app/lib/features/comments/actions/sbumit_comment.dart b/app/lib/features/comments/actions/sbumit_comment.dart new file mode 100644 index 000000000000..99359b57f50d --- /dev/null +++ b/app/lib/features/comments/actions/sbumit_comment.dart @@ -0,0 +1,42 @@ +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('a3::submit::comment'); + +Future submitComment( + BuildContext context, + String plainDescription, + String htmlBodyDescription, + CommentsManager manager, +) async { + final lang = L10n.of(context); + if (plainDescription.isEmpty) { + EasyLoading.showToast(lang.youNeedToEnterAComment); + return; + } + EasyLoading.show(status: lang.submittingComment); + try { + final draft = manager.commentDraft(); + draft.contentFormatted(plainDescription, htmlBodyDescription); + await draft.send(); + FocusManager.instance.primaryFocus?.unfocus(); + if (!context.mounted) { + EasyLoading.dismiss(); + return; + } + EasyLoading.showToast(lang.commentSubmitted); + } catch (e, s) { + _log.severe('Failed to submit comment', e, s); + if (!context.mounted) { + EasyLoading.dismiss(); + return; + } + EasyLoading.showError( + lang.errorSubmittingComment(e), + duration: const Duration(seconds: 3), + ); + } +} diff --git a/app/lib/features/comments/widgets/add_comment_widget.dart b/app/lib/features/comments/widgets/add_comment_widget.dart new file mode 100644 index 000000000000..bd980774a4e2 --- /dev/null +++ b/app/lib/features/comments/widgets/add_comment_widget.dart @@ -0,0 +1,65 @@ +import 'package:acter/common/providers/common_providers.dart'; +import 'package:acter/common/widgets/edit_html_description_sheet.dart'; +import 'package:acter/features/comments/actions/sbumit_comment.dart'; +import 'package:acter_avatar/acter_avatar.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AddCommentWidget extends ConsumerWidget { + final CommentsManager manager; + + const AddCommentWidget({ + super.key, + required this.manager, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final avatarInfo = ref.watch(accountAvatarInfoProvider); + return InkWell( + onTap: () { + showEditHtmlDescriptionBottomSheet( + context: context, + bottomSheetTitle: L10n.of(context).addComment, + onSave: (htmlBodyDescription, plainDescription) async { + await submitComment( + context, + plainDescription, + htmlBodyDescription, + manager, + ); + if (!context.mounted) return; + Navigator.pop(context); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + ActerAvatar(options: AvatarOptions.DM(avatarInfo, size: 22)), + const SizedBox(width: 12), + Expanded(child: addCommentBoxUI(context)), + ], + ), + ), + ); + } + + Widget addCommentBoxUI(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).unselectedWidgetColor.withAlpha(30), + border: Border.all(color: Theme.of(context).unselectedWidgetColor), + borderRadius: const BorderRadius.all(Radius.circular(24)), + ), + child: Text( + L10n.of(context).addComment, + style: Theme.of(context).textTheme.labelMedium, + ), + ); + } +} diff --git a/app/lib/features/comments/widgets/comment.dart b/app/lib/features/comments/widgets/comment_item_widget.dart similarity index 96% rename from app/lib/features/comments/widgets/comment.dart rename to app/lib/features/comments/widgets/comment_item_widget.dart index 8d746e0723c4..045c96564f94 100644 --- a/app/lib/features/comments/widgets/comment.dart +++ b/app/lib/features/comments/widgets/comment_item_widget.dart @@ -6,11 +6,11 @@ import 'package:dart_date/dart_date.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -class CommentWidget extends ConsumerWidget { +class CommentItemWidget extends ConsumerWidget { final Comment comment; final CommentsManager manager; - const CommentWidget({ + const CommentItemWidget({ super.key, required this.comment, required this.manager, diff --git a/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart b/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart new file mode 100644 index 000000000000..9d31be4f555f --- /dev/null +++ b/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class CommentListEmptyStateWidget extends StatelessWidget { + const CommentListEmptyStateWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/app/lib/features/comments/widgets/comment_list_skeleton_widget.dart b/app/lib/features/comments/widgets/comment_list_skeleton_widget.dart new file mode 100644 index 000000000000..ece78684e2dd --- /dev/null +++ b/app/lib/features/comments/widgets/comment_list_skeleton_widget.dart @@ -0,0 +1,33 @@ +import 'package:atlas_icons/atlas_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +class CommentListSkeletonWidget extends StatelessWidget { + const CommentListSkeletonWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Skeletonizer( + child: ListView( + shrinkWrap: true, + children: [ + listItem(), + listItem(), + listItem(), + listItem(), + listItem(), + ], + ), + ); + } + + Widget listItem() { + return const ListTile( + leading: Icon(Atlas.chats, size: 60), + title: Text('Title Title Title Title Title'), + subtitle: Text( + 'Sub-title Sub-title Sub-title Sub-title Sub-title Sub-title Sub-title', + ), + ); + } +} diff --git a/app/lib/features/comments/widgets/comment_list_widget.dart b/app/lib/features/comments/widgets/comment_list_widget.dart new file mode 100644 index 000000000000..51f8690cdb58 --- /dev/null +++ b/app/lib/features/comments/widgets/comment_list_widget.dart @@ -0,0 +1,67 @@ +import 'package:acter/common/toolkit/errors/error_page.dart'; +import 'package:acter/features/comments/providers/comments.dart'; +import 'package:acter/features/comments/widgets/comment_item_widget.dart'; +import 'package:acter/features/comments/widgets/comment_list_skeleton_widget.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +final _log = Logger('a3::comments::list::widget'); + +class CommentListWidget extends ConsumerWidget { + final CommentsManager manager; + final bool shrinkWrap; + final Widget emptyState; + + const CommentListWidget({ + super.key, + required this.manager, + this.shrinkWrap = true, + this.emptyState = const SizedBox.shrink(), + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final commentsLoader = ref.watch(commentsListProvider(manager)); + return commentsLoader.when( + data: (commentList) => buildCommentListUI(context, commentList), + error: (error, stack) => + commentListErrorWidget(context, ref, error, stack), + loading: () => const CommentListSkeletonWidget(), + ); + } + + Widget buildCommentListUI(BuildContext context, List commentList) { + if (commentList.isEmpty) return emptyState; + return ListView.builder( + shrinkWrap: shrinkWrap, + itemCount: commentList.length, + padding: EdgeInsets.zero, + physics: shrinkWrap ? const NeverScrollableScrollPhysics() : null, + itemBuilder: (context, index) { + return CommentItemWidget( + comment: commentList[index], + manager: manager, + ); + }, + ); + } + + Widget commentListErrorWidget( + BuildContext context, + WidgetRef ref, + Object error, + StackTrace stack, + ) { + _log.severe('Failed to load comments', error, stack); + return ErrorPage( + background: const CommentListSkeletonWidget(), + error: error, + stack: stack, + textBuilder: L10n.of(context).loadingFailed, + onRetryTap: () => ref.invalidate(commentsListProvider(manager)), + ); + } +} diff --git a/app/lib/features/comments/widgets/comments_list.dart b/app/lib/features/comments/widgets/comments_list.dart deleted file mode 100644 index 40deea61a344..000000000000 --- a/app/lib/features/comments/widgets/comments_list.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:acter/common/toolkit/buttons/inline_text_button.dart'; -import 'package:acter/features/comments/providers/comments.dart'; -import 'package:acter/features/comments/widgets/comment.dart'; -import 'package:acter/features/comments/widgets/create_comment.dart'; -import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logging/logging.dart'; - -final _log = Logger('a3::comments::list'); - -class CommentsList extends ConsumerStatefulWidget { - final CommentsManager manager; - - const CommentsList({ - super.key, - required this.manager, - }); - - @override - ConsumerState createState() => _CommentsListState(); -} - -class _CommentsListState extends ConsumerState { - bool editorOpened = false; - - @override - Widget build(BuildContext context) { - final commentsLoader = ref.watch(commentsListProvider(widget.manager)); - return commentsLoader.when( - data: (comments) { - if (comments.isEmpty) { - return commentEmptyState(context); - } else { - return commentListUI(context, comments); - } - }, - error: (e, s) { - _log.severe('Failed to load comments', e, s); - return onError(context, e); - }, - loading: () => loading(context), - ); - } - - Widget createComment() { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: CreateCommentWidget( - manager: widget.manager, - onClose: () => setState(() => editorOpened = false), - ), - ); - } - - Widget commentListUI(BuildContext context, List comments) { - final commentList = comments - .map( - (c) => CommentWidget( - comment: c, - manager: widget.manager, - ), - ) - .toList(); - return Column( - children: [ - Column(children: commentList), - if (editorOpened) - createComment() - else - Container( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextButton( - child: Text(L10n.of(context).createComment), - onPressed: () => setState(() => editorOpened = true), - ), - ), - ), - ], - ); - } - - Widget commentEmptyState(BuildContext context) { - if (editorOpened) return createComment(); - final lang = L10n.of(context); - return Row( - children: [ - Text(lang.commentEmptyStateTitle), - if (!editorOpened) - ActerInlineTextButton( - onPressed: () => setState(() => editorOpened = true), - child: Text(lang.commentEmptyStateAction), - ), - ], - ); - } - - Widget onError(BuildContext context, Object error) { - return Column( - children: [ - Text(L10n.of(context).commentsListError(error)), - ], - ); - } - - Widget loading(BuildContext context) { - return Column( - children: [ - Text(L10n.of(context).loadingCommentsList), - ], - ); - } -} diff --git a/app/lib/features/comments/widgets/comments_section.dart b/app/lib/features/comments/widgets/comments_section.dart index f343eb8b9645..da72057e7bfa 100644 --- a/app/lib/features/comments/widgets/comments_section.dart +++ b/app/lib/features/comments/widgets/comments_section.dart @@ -1,7 +1,9 @@ +import 'package:acter/common/toolkit/errors/error_page.dart'; import 'package:acter/features/comments/providers/comments.dart'; -import 'package:acter/features/comments/widgets/comments_list.dart'; +import 'package:acter/features/comments/widgets/add_comment_widget.dart'; +import 'package:acter/features/comments/widgets/comment_list_skeleton_widget.dart'; +import 'package:acter/features/comments/widgets/comment_list_widget.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; -import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -21,51 +23,43 @@ class CommentsSection extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final managerLoader = ref.watch(commentsManagerProvider(manager)); return managerLoader.when( - data: (manager) => found(context, manager), - error: (e, s) { - _log.severe('Failed to load comment manager', e, s); - return onError(context, e); - }, - loading: () => loading(context), + data: (commentManager) => buildCommentSectionUI(context, commentManager), + error: (error, stack) => + commentManagerErrorWidget(context, ref, error, stack), + loading: () => const CommentListSkeletonWidget(), ); } - static Widget _inBox(BuildContext context, Widget child) { + Widget buildCommentSectionUI( + BuildContext context, + CommentsManager commentManager, + ) { return Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - const Icon( - Atlas.comment_blank_thin, - size: 14, - ), - const SizedBox(width: 5), - Text( - L10n.of(context).comments, - style: Theme.of(context).textTheme.labelLarge, - ), - const SizedBox(width: 5), - ], - ), - const SizedBox(height: 10), - child, + Text(L10n.of(context).comments), + CommentListWidget(manager: commentManager), + AddCommentWidget(manager: commentManager), ], ), ); } - Widget found(BuildContext context, CommentsManager manager) { - return _inBox(context, CommentsList(manager: manager)); - } - - Widget onError(BuildContext context, Object error) { - return _inBox(context, Text(L10n.of(context).loadingFailed(error))); - } - - static Widget loading(BuildContext context) { - return _inBox(context, Text(L10n.of(context).loading)); + Widget commentManagerErrorWidget( + BuildContext context, + WidgetRef ref, + Object error, + StackTrace stack, + ) { + _log.severe('Failed to load comment manager', error, stack); + return ErrorPage( + background: const CommentListSkeletonWidget(), + error: error, + stack: stack, + textBuilder: L10n.of(context).loadingFailed, + onRetryTap: () => ref.invalidate(commentsManagerProvider(manager)), + ); } } diff --git a/app/lib/features/comments/widgets/create_comment.dart b/app/lib/features/comments/widgets/create_comment.dart deleted file mode 100644 index 3fe9cccc20e4..000000000000 --- a/app/lib/features/comments/widgets/create_comment.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:acter/common/toolkit/buttons/primary_action_button.dart'; -import 'package:acter/common/widgets/html_editor.dart'; -import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logging/logging.dart'; - -final _log = Logger('a3::comments::create_comment'); - -class CreateCommentWidget extends ConsumerStatefulWidget { - final CommentsManager manager; - final void Function() onClose; - static const commentField = Key('create-comment-input-field'); - - const CreateCommentWidget({ - super.key, - required this.manager, - required this.onClose, - }); - - @override - ConsumerState createState() => - _CreateCommentWidgetState(); -} - -class _CreateCommentWidgetState extends ConsumerState { - EditorState textEditorState = EditorState.blank(); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(vertical: 12), - child: commentInputUI(), - ); - } - - Widget commentInputUI() { - final lang = L10n.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Text(lang.createComment), - ), - Container( - height: 200, - margin: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), - ), - child: HtmlEditor( - key: CreateCommentWidget.commentField, - editable: true, - autoFocus: false, - editorState: textEditorState, - footer: const SizedBox(), - onChanged: (body, html) { - textEditorState = EditorState( - document: ActerDocumentHelpers.parse( - body, - htmlContent: html, - ), - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const Spacer(), - OutlinedButton( - onPressed: widget.onClose, - child: Text(lang.cancel), - ), - const SizedBox(width: 22), - ActerPrimaryActionButton( - onPressed: onSubmit, - child: Text(lang.submit), - ), - ], - ), - ), - ], - ); - } - - Future onSubmit() async { - final lang = L10n.of(context); - final plainDescription = textEditorState.intoMarkdown().trim(); - final htmlBodyDescription = textEditorState.intoHtml(); - if (plainDescription.isEmpty) { - EasyLoading.showToast(lang.youNeedToEnterAComment); - return; - } - EasyLoading.show(status: lang.submittingComment); - try { - final draft = widget.manager.commentDraft(); - draft.contentFormatted(plainDescription, htmlBodyDescription); - await draft.send(); - FocusManager.instance.primaryFocus?.unfocus(); - if (!mounted) { - EasyLoading.dismiss(); - return; - } - EasyLoading.showToast(lang.commentSubmitted); - } catch (e, s) { - _log.severe('Failed to submit comment', e, s); - if (!mounted) { - EasyLoading.dismiss(); - return; - } - EasyLoading.showError( - lang.errorSubmittingComment(e), - duration: const Duration(seconds: 3), - ); - } - } -} diff --git a/app/lib/features/pins/pages/pin_details_page.dart b/app/lib/features/pins/pages/pin_details_page.dart index bf8add56b0c5..7ecd7d7dc06e 100644 --- a/app/lib/features/pins/pages/pin_details_page.dart +++ b/app/lib/features/pins/pages/pin_details_page.dart @@ -8,6 +8,7 @@ import 'package:acter/common/widgets/edit_html_description_sheet.dart'; import 'package:acter/common/widgets/edit_title_sheet.dart'; import 'package:acter/common/widgets/render_html.dart'; import 'package:acter/features/attachments/widgets/attachment_section.dart'; +import 'package:acter/features/comments/widgets/comment_list_skeleton_widget.dart'; import 'package:acter/features/comments/widgets/comments_section.dart'; import 'package:acter/features/home/widgets/space_chip.dart'; import 'package:acter/features/pins/actions/edit_pin_actions.dart'; @@ -104,7 +105,7 @@ class _PinDetailsPageState extends ConsumerState { const SizedBox(height: 20), AttachmentSectionWidget.loading(), const SizedBox(height: 20), - CommentsSection.loading(context), + const CommentListSkeletonWidget(), ], ), ); diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index d90bc9ef8028..3fa53be67033 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -2264,5 +2264,6 @@ "@calenderWithEvents": {}, "addBoost": "Add Boost", "addTaskList": "Add TaskList", + "addComment": "Add Comment", "@addBoost": {} } From 65c4dcefb81cad8f76c3e68fc8df16fdd586413a Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 12:26:16 +0530 Subject: [PATCH 03/77] File rename --- .../{comments_section.dart => comments_section_widget.dart} | 4 ++-- app/lib/features/events/pages/event_details_page.dart | 4 ++-- app/lib/features/news/widgets/news_item/news_side_bar.dart | 4 ++-- app/lib/features/pins/pages/pin_details_page.dart | 4 ++-- app/lib/features/tasks/pages/task_item_detail_page.dart | 4 ++-- app/lib/features/tasks/pages/task_list_details_page.dart | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) rename app/lib/features/comments/widgets/{comments_section.dart => comments_section_widget.dart} (96%) diff --git a/app/lib/features/comments/widgets/comments_section.dart b/app/lib/features/comments/widgets/comments_section_widget.dart similarity index 96% rename from app/lib/features/comments/widgets/comments_section.dart rename to app/lib/features/comments/widgets/comments_section_widget.dart index da72057e7bfa..b125e99c5cec 100644 --- a/app/lib/features/comments/widgets/comments_section.dart +++ b/app/lib/features/comments/widgets/comments_section_widget.dart @@ -11,10 +11,10 @@ import 'package:logging/logging.dart'; final _log = Logger('a3::comments::section'); -class CommentsSection extends ConsumerWidget { +class CommentsSectionWidget extends ConsumerWidget { final Future manager; - const CommentsSection({ + const CommentsSectionWidget({ super.key, required this.manager, }); diff --git a/app/lib/features/events/pages/event_details_page.dart b/app/lib/features/events/pages/event_details_page.dart index f0e877853840..7e7ccb8f84b0 100644 --- a/app/lib/features/events/pages/event_details_page.dart +++ b/app/lib/features/events/pages/event_details_page.dart @@ -13,7 +13,7 @@ import 'package:acter/common/widgets/render_html.dart'; import 'package:acter/features/attachments/widgets/attachment_section.dart'; import 'package:acter/features/bookmarks/types.dart'; import 'package:acter/features/bookmarks/widgets/bookmark_action.dart'; -import 'package:acter/features/comments/widgets/comments_section.dart'; +import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/events/providers/event_type_provider.dart'; import 'package:acter/features/events/model/keys.dart'; import 'package:acter/features/events/providers/event_providers.dart'; @@ -282,7 +282,7 @@ class _EventDetailPageConsumerState extends ConsumerState { const SizedBox(height: 40), AttachmentSectionWidget(manager: calendarEvent.attachments()), const SizedBox(height: 40), - CommentsSection(manager: calendarEvent.comments()), + CommentsSectionWidget(manager: calendarEvent.comments()), const SizedBox(height: 40), ], ), diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index af09dddcad50..ab505f624aa5 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -6,7 +6,7 @@ import 'package:acter/common/themes/colors/color_scheme.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/default_bottom_sheet.dart'; import 'package:acter/common/widgets/like_button.dart'; -import 'package:acter/features/comments/widgets/comments_section.dart'; +import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/news/model/keys.dart'; import 'package:acter/features/news/providers/news_providers.dart'; import 'package:acter/router/utils.dart'; @@ -66,7 +66,7 @@ class NewsSideBar extends ConsumerWidget { showModalBottomSheet( context: context, showDragHandle: true, - builder: (context) => CommentsSection( + builder: (context) => CommentsSectionWidget( manager: news.comments(), ), ); diff --git a/app/lib/features/pins/pages/pin_details_page.dart b/app/lib/features/pins/pages/pin_details_page.dart index 7ecd7d7dc06e..6782dd56be16 100644 --- a/app/lib/features/pins/pages/pin_details_page.dart +++ b/app/lib/features/pins/pages/pin_details_page.dart @@ -9,7 +9,7 @@ import 'package:acter/common/widgets/edit_title_sheet.dart'; import 'package:acter/common/widgets/render_html.dart'; import 'package:acter/features/attachments/widgets/attachment_section.dart'; import 'package:acter/features/comments/widgets/comment_list_skeleton_widget.dart'; -import 'package:acter/features/comments/widgets/comments_section.dart'; +import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/home/widgets/space_chip.dart'; import 'package:acter/features/pins/actions/edit_pin_actions.dart'; import 'package:acter/features/pins/actions/pin_update_actions.dart'; @@ -75,7 +75,7 @@ class _PinDetailsPageState extends ConsumerState { AttachmentSectionWidget(manager: pin.attachments()), FakeLinkAttachmentItem(pinId: pin.eventIdStr()), const SizedBox(height: 20), - CommentsSection(manager: pin.comments()), + CommentsSectionWidget(manager: pin.comments()), ], ), ); diff --git a/app/lib/features/tasks/pages/task_item_detail_page.dart b/app/lib/features/tasks/pages/task_item_detail_page.dart index c70a4d9afff7..1c4a53d593c0 100644 --- a/app/lib/features/tasks/pages/task_item_detail_page.dart +++ b/app/lib/features/tasks/pages/task_item_detail_page.dart @@ -11,7 +11,7 @@ import 'package:acter/common/widgets/edit_html_description_sheet.dart'; import 'package:acter/common/widgets/edit_title_sheet.dart'; import 'package:acter/common/widgets/render_html.dart'; import 'package:acter/features/attachments/widgets/attachment_section.dart'; -import 'package:acter/features/comments/widgets/comments_section.dart'; +import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/tasks/providers/task_items_providers.dart'; import 'package:acter/features/tasks/widgets/due_picker.dart'; import 'package:acter/features/tasks/widgets/skeleton/task_item_detail_page_skeleton.dart'; @@ -223,7 +223,7 @@ class TaskItemDetailPage extends ConsumerWidget { const SizedBox(height: 20), AttachmentSectionWidget(manager: task.attachments()), const SizedBox(height: 20), - CommentsSection(manager: task.comments()), + CommentsSectionWidget(manager: task.comments()), const SizedBox(height: 20), ], ), diff --git a/app/lib/features/tasks/pages/task_list_details_page.dart b/app/lib/features/tasks/pages/task_list_details_page.dart index 9580565518df..7da73254979a 100644 --- a/app/lib/features/tasks/pages/task_list_details_page.dart +++ b/app/lib/features/tasks/pages/task_list_details_page.dart @@ -9,7 +9,7 @@ import 'package:acter/common/widgets/edit_html_description_sheet.dart'; import 'package:acter/common/widgets/edit_title_sheet.dart'; import 'package:acter/common/widgets/render_html.dart'; import 'package:acter/features/attachments/widgets/attachment_section.dart'; -import 'package:acter/features/comments/widgets/comments_section.dart'; +import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/tasks/actions/update_tasklist.dart'; import 'package:acter/features/tasks/providers/tasklists_providers.dart'; import 'package:acter/features/tasks/widgets/task_items_list_widget.dart'; @@ -291,7 +291,7 @@ class _TaskListPageState extends ConsumerState { const SizedBox(height: 20), AttachmentSectionWidget(manager: taskListData.attachments()), const SizedBox(height: 20), - CommentsSection(manager: taskListData.comments()), + CommentsSectionWidget(manager: taskListData.comments()), const SizedBox(height: 20), ], ); From c7099b745d32492d8283962377952abfd49e8464 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 12:29:46 +0530 Subject: [PATCH 04/77] Full screen comment list bottom sheet for boosts --- app/lib/features/news/widgets/news_item/news_side_bar.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index ab505f624aa5..58e1da88f596 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -65,10 +65,10 @@ class NewsSideBar extends ConsumerWidget { onPressed: () { showModalBottomSheet( context: context, + isScrollControlled: true, showDragHandle: true, - builder: (context) => CommentsSectionWidget( - manager: news.comments(), - ), + builder: (context) => + CommentsSectionWidget(manager: news.comments()), ); }, icon: const Icon(Atlas.comment_blank), From f0734a41fdf6e15258bebbd25c02f1f7d23c013c Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 12:36:12 +0530 Subject: [PATCH 05/77] Manage comment scrolling behavior --- .../comments/widgets/comments_section_widget.dart | 11 ++++++++++- .../news/widgets/news_item/news_side_bar.dart | 7 ++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/lib/features/comments/widgets/comments_section_widget.dart b/app/lib/features/comments/widgets/comments_section_widget.dart index b125e99c5cec..e856a12d88fa 100644 --- a/app/lib/features/comments/widgets/comments_section_widget.dart +++ b/app/lib/features/comments/widgets/comments_section_widget.dart @@ -13,9 +13,11 @@ final _log = Logger('a3::comments::section'); class CommentsSectionWidget extends ConsumerWidget { final Future manager; + final bool shrinkWrap; const CommentsSectionWidget({ super.key, + this.shrinkWrap = true, required this.manager, }); @@ -40,13 +42,20 @@ class CommentsSectionWidget extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(L10n.of(context).comments), - CommentListWidget(manager: commentManager), + commentListUI(commentManager), AddCommentWidget(manager: commentManager), ], ), ); } + Widget commentListUI(CommentsManager commentManager) { + if (shrinkWrap) return CommentListWidget(manager: commentManager); + return Expanded( + child: CommentListWidget(manager: commentManager, shrinkWrap: shrinkWrap), + ); + } + Widget commentManagerErrorWidget( BuildContext context, WidgetRef ref, diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index 58e1da88f596..8f19ecf4d5b1 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -65,10 +65,11 @@ class NewsSideBar extends ConsumerWidget { onPressed: () { showModalBottomSheet( context: context, - isScrollControlled: true, showDragHandle: true, - builder: (context) => - CommentsSectionWidget(manager: news.comments()), + builder: (context) => CommentsSectionWidget( + manager: news.comments(), + shrinkWrap: false, + ), ); }, icon: const Icon(Atlas.comment_blank), From 406762460e226b353920944a58ba5920293bd677 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 12:41:50 +0530 Subject: [PATCH 06/77] Manage comment title label position --- .../widgets/comments_section_widget.dart | 27 +++++++++++++------ .../news/widgets/news_item/news_side_bar.dart | 1 + 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/lib/features/comments/widgets/comments_section_widget.dart b/app/lib/features/comments/widgets/comments_section_widget.dart index e856a12d88fa..af79536292f2 100644 --- a/app/lib/features/comments/widgets/comments_section_widget.dart +++ b/app/lib/features/comments/widgets/comments_section_widget.dart @@ -14,10 +14,12 @@ final _log = Logger('a3::comments::section'); class CommentsSectionWidget extends ConsumerWidget { final Future manager; final bool shrinkWrap; + final bool centerTitle; const CommentsSectionWidget({ super.key, this.shrinkWrap = true, + this.centerTitle = false, required this.manager, }); @@ -36,15 +38,24 @@ class CommentsSectionWidget extends ConsumerWidget { BuildContext context, CommentsManager commentManager, ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 12), + commentTitleUI(context), + commentListUI(commentManager), + AddCommentWidget(manager: commentManager), + ], + ); + } + + Widget commentTitleUI(BuildContext context) { return Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(L10n.of(context).comments), - commentListUI(commentManager), - AddCommentWidget(manager: commentManager), - ], + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + L10n.of(context).comments, + style: Theme.of(context).textTheme.titleMedium, + textAlign: centerTitle ? TextAlign.center : TextAlign.start, ), ); } diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index 8f19ecf4d5b1..b817106f614d 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -69,6 +69,7 @@ class NewsSideBar extends ConsumerWidget { builder: (context) => CommentsSectionWidget( manager: news.comments(), shrinkWrap: false, + centerTitle: true, ), ); }, From 9bb5357633b9bfee046bd5b1f8a3ed928dcf1674 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 13:07:57 +0530 Subject: [PATCH 07/77] Comment Item Code Improvement --- .../comments/widgets/add_comment_widget.dart | 2 +- .../comments/widgets/comment_item_widget.dart | 95 ++++++++++++------- 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/app/lib/features/comments/widgets/add_comment_widget.dart b/app/lib/features/comments/widgets/add_comment_widget.dart index bd980774a4e2..d272e7819fd2 100644 --- a/app/lib/features/comments/widgets/add_comment_widget.dart +++ b/app/lib/features/comments/widgets/add_comment_widget.dart @@ -39,7 +39,7 @@ class AddCommentWidget extends ConsumerWidget { padding: const EdgeInsets.all(12), child: Row( children: [ - ActerAvatar(options: AvatarOptions.DM(avatarInfo, size: 22)), + ActerAvatar(options: AvatarOptions.DM(avatarInfo, size: 18)), const SizedBox(width: 12), Expanded(child: addCommentBoxUI(context)), ], diff --git a/app/lib/features/comments/widgets/comment_item_widget.dart b/app/lib/features/comments/widgets/comment_item_widget.dart index 045c96564f94..345ae092b5b5 100644 --- a/app/lib/features/comments/widgets/comment_item_widget.dart +++ b/app/lib/features/comments/widgets/comment_item_widget.dart @@ -20,52 +20,75 @@ class CommentItemWidget extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final roomID = manager.roomIdStr(); final userId = comment.sender().toString(); - final msgContent = comment.msgContent(); - final formatted = msgContent.formattedBody(); - final commentTime = DateTime.fromMillisecondsSinceEpoch( - comment.originServerTs(), - isUtc: true, - ); - final time = commentTime.toLocal().timeago(); final avatarInfo = ref.watch( memberAvatarInfoProvider((roomId: roomID, userId: userId)), ); - final displayName = avatarInfo.displayName; - return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( children: [ - ListTile( - leading: ActerAvatar( - options: AvatarOptions.DM( - avatarInfo, - size: 18, + userAvatarUI(context, avatarInfo), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + userNameUI(context, avatarInfo), + messageContentUI(context), + const SizedBox(height: 4), + messageTimeUI(context), + ], ), ), - title: Text( - displayName ?? userId, - style: Theme.of(context).textTheme.titleSmall, - ), - subtitle: displayName == null ? null : Text(userId), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: formatted != null - ? RenderHtml(text: formatted) - : Text(msgContent.body()), - ), - const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - time, - style: Theme.of(context).textTheme.labelMedium, - ), - ), - const SizedBox(height: 16), ], ), ); } + + Widget userAvatarUI(BuildContext context, AvatarInfo avatarInfo) { + return ActerAvatar(options: AvatarOptions.DM(avatarInfo, size: 18)); + } + + Widget userNameUI(BuildContext context, AvatarInfo avatarInfo) { + final userId = comment.sender().toString(); + final displayName = avatarInfo.displayName; + final displayNameTextStyle = Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold); + final usrNameTextStyle = Theme.of(context).textTheme.labelMedium; + + return Row( + children: [ + Text(displayName ?? userId, style: displayNameTextStyle), + const SizedBox(width: 8), + if (displayName != null) Text(userId, style: usrNameTextStyle) + ], + ); + } + + Widget messageContentUI(BuildContext context) { + final msgContent = comment.msgContent(); + final formatted = msgContent.formattedBody(); + final messageTextStyle = Theme.of(context).textTheme.bodyMedium; + + return formatted != null + ? RenderHtml(text: formatted, defaultTextStyle: messageTextStyle) + : Text(msgContent.body(), style: messageTextStyle); + } + + Widget messageTimeUI(BuildContext context) { + final commentTime = DateTime.fromMillisecondsSinceEpoch( + comment.originServerTs(), + isUtc: true, + ); + final time = commentTime.toLocal().timeago(); + return Text( + time, + style: Theme.of(context).textTheme.labelMedium, + ); + } } From 78100469fa11d599ff8acd4cf8feb756c19c0099 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 13:09:03 +0530 Subject: [PATCH 08/77] Minor Adjustments --- app/lib/features/comments/widgets/add_comment_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/features/comments/widgets/add_comment_widget.dart b/app/lib/features/comments/widgets/add_comment_widget.dart index d272e7819fd2..1b993098483f 100644 --- a/app/lib/features/comments/widgets/add_comment_widget.dart +++ b/app/lib/features/comments/widgets/add_comment_widget.dart @@ -50,7 +50,7 @@ class AddCommentWidget extends ConsumerWidget { Widget addCommentBoxUI(BuildContext context) { return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Theme.of(context).unselectedWidgetColor.withAlpha(30), border: Border.all(color: Theme.of(context).unselectedWidgetColor), From 0deb90c32981d5a5c321d1f27c6ad31ec3df9c10 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 13:10:37 +0530 Subject: [PATCH 09/77] Minor Adjustments --- app/lib/features/comments/widgets/comment_item_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/features/comments/widgets/comment_item_widget.dart b/app/lib/features/comments/widgets/comment_item_widget.dart index 345ae092b5b5..cdc6d7f6e662 100644 --- a/app/lib/features/comments/widgets/comment_item_widget.dart +++ b/app/lib/features/comments/widgets/comment_item_widget.dart @@ -57,11 +57,11 @@ class CommentItemWidget extends ConsumerWidget { final displayName = avatarInfo.displayName; final displayNameTextStyle = Theme.of(context) .textTheme - .bodyMedium + .bodySmall ?.copyWith(fontWeight: FontWeight.bold); final usrNameTextStyle = Theme.of(context).textTheme.labelMedium; - return Row( + return Wrap( children: [ Text(displayName ?? userId, style: displayNameTextStyle), const SizedBox(width: 8), From 4ab963a80e0770e33a09af352a1aa5530f027de1 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 13:36:19 +0530 Subject: [PATCH 10/77] Comment Empty State Management --- .../comment_list_empty_state_widget.dart | 61 ++++++++++++++++++- .../comments/widgets/comment_list_widget.dart | 9 ++- .../widgets/comments_section_widget.dart | 15 ++++- .../news/widgets/news_item/news_side_bar.dart | 1 + 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart b/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart index 9d31be4f555f..f5caba869d43 100644 --- a/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart +++ b/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart @@ -1,10 +1,67 @@ +import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class CommentListEmptyStateWidget extends StatelessWidget { - const CommentListEmptyStateWidget({super.key}); + final bool useCompactView; + + const CommentListEmptyStateWidget({super.key, this.useCompactView = true}); @override Widget build(BuildContext context) { - return const Placeholder(); + final color = Theme.of(context).unselectedWidgetColor; + return useCompactView + ? compactEmptyState(context, color) + : fullViewEmptyState(context, color); + } + + Widget fullViewEmptyState(BuildContext context, Color color) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Atlas.chats, color: color, size: 52), + const SizedBox(height: 16), + Text( + L10n.of(context).commentEmptyStateTitle, + style: + Theme.of(context).textTheme.titleMedium?.copyWith(color: color), + ), + Text( + L10n.of(context).commentEmptyStateAction, + style: Theme.of(context).textTheme.labelLarge?.copyWith(color: color), + ), + ], + ); + } + + Widget compactEmptyState(BuildContext context, Color color) { + return Padding( + padding: const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Atlas.chats, color: color, size: 40), + const SizedBox(width: 16), + Column( + children: [ + Text( + L10n.of(context).commentEmptyStateTitle, + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith(color: color), + ), + Text( + L10n.of(context).commentEmptyStateAction, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: color), + ), + ], + ) + ], + ), + ); } } diff --git a/app/lib/features/comments/widgets/comment_list_widget.dart b/app/lib/features/comments/widgets/comment_list_widget.dart index 51f8690cdb58..9185332baa3d 100644 --- a/app/lib/features/comments/widgets/comment_list_widget.dart +++ b/app/lib/features/comments/widgets/comment_list_widget.dart @@ -1,6 +1,7 @@ import 'package:acter/common/toolkit/errors/error_page.dart'; import 'package:acter/features/comments/providers/comments.dart'; import 'package:acter/features/comments/widgets/comment_item_widget.dart'; +import 'package:acter/features/comments/widgets/comment_list_empty_state_widget.dart'; import 'package:acter/features/comments/widgets/comment_list_skeleton_widget.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; @@ -13,13 +14,13 @@ final _log = Logger('a3::comments::list::widget'); class CommentListWidget extends ConsumerWidget { final CommentsManager manager; final bool shrinkWrap; - final Widget emptyState; + final bool useCompactEmptyState; const CommentListWidget({ super.key, required this.manager, this.shrinkWrap = true, - this.emptyState = const SizedBox.shrink(), + this.useCompactEmptyState = true, }); @override @@ -34,7 +35,9 @@ class CommentListWidget extends ConsumerWidget { } Widget buildCommentListUI(BuildContext context, List commentList) { - if (commentList.isEmpty) return emptyState; + if (commentList.isEmpty) { + return CommentListEmptyStateWidget(useCompactView: useCompactEmptyState); + } return ListView.builder( shrinkWrap: shrinkWrap, itemCount: commentList.length, diff --git a/app/lib/features/comments/widgets/comments_section_widget.dart b/app/lib/features/comments/widgets/comments_section_widget.dart index af79536292f2..d9e18ddc5c98 100644 --- a/app/lib/features/comments/widgets/comments_section_widget.dart +++ b/app/lib/features/comments/widgets/comments_section_widget.dart @@ -15,11 +15,13 @@ class CommentsSectionWidget extends ConsumerWidget { final Future manager; final bool shrinkWrap; final bool centerTitle; + final bool useCompactEmptyState; const CommentsSectionWidget({ super.key, this.shrinkWrap = true, this.centerTitle = false, + this.useCompactEmptyState = true, required this.manager, }); @@ -61,9 +63,18 @@ class CommentsSectionWidget extends ConsumerWidget { } Widget commentListUI(CommentsManager commentManager) { - if (shrinkWrap) return CommentListWidget(manager: commentManager); + if (shrinkWrap) { + return CommentListWidget( + manager: commentManager, + useCompactEmptyState: useCompactEmptyState, + ); + } return Expanded( - child: CommentListWidget(manager: commentManager, shrinkWrap: shrinkWrap), + child: CommentListWidget( + manager: commentManager, + shrinkWrap: shrinkWrap, + useCompactEmptyState: useCompactEmptyState, + ), ); } diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index b817106f614d..f809e3869c9e 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -70,6 +70,7 @@ class NewsSideBar extends ConsumerWidget { manager: news.comments(), shrinkWrap: false, centerTitle: true, + useCompactEmptyState: false, ), ); }, From a526e2a1704d1c1b8f50476eda7d7b84d2ab6913 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 13:42:15 +0530 Subject: [PATCH 11/77] Fixes lint errors --- app/lib/features/comments/widgets/comment_item_widget.dart | 2 +- .../comments/widgets/comment_list_empty_state_widget.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/features/comments/widgets/comment_item_widget.dart b/app/lib/features/comments/widgets/comment_item_widget.dart index cdc6d7f6e662..6e09f2eda181 100644 --- a/app/lib/features/comments/widgets/comment_item_widget.dart +++ b/app/lib/features/comments/widgets/comment_item_widget.dart @@ -65,7 +65,7 @@ class CommentItemWidget extends ConsumerWidget { children: [ Text(displayName ?? userId, style: displayNameTextStyle), const SizedBox(width: 8), - if (displayName != null) Text(userId, style: usrNameTextStyle) + if (displayName != null) Text(userId, style: usrNameTextStyle), ], ); } diff --git a/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart b/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart index f5caba869d43..16335d379c8b 100644 --- a/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart +++ b/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart @@ -59,7 +59,7 @@ class CommentListEmptyStateWidget extends StatelessWidget { ?.copyWith(color: color), ), ], - ) + ), ], ), ); From a23dba2929a34582494aed67207dc8434cbd756c Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 15:24:50 +0530 Subject: [PATCH 12/77] Comment counts provider --- app/lib/features/comments/providers/comments.dart | 9 +++++++++ .../news/widgets/news_item/news_side_bar.dart | 12 ++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/lib/features/comments/providers/comments.dart b/app/lib/features/comments/providers/comments.dart index 0f5e01aaf50a..8f4615f65e25 100644 --- a/app/lib/features/comments/providers/comments.dart +++ b/app/lib/features/comments/providers/comments.dart @@ -42,3 +42,12 @@ final commentsListProvider = FutureProvider.family .autoDispose, CommentsManager>((ref, manager) async { return (await manager.comments()).toList(); }); + +final commentsCountProvider = FutureProvider.family + .autoDispose>((ref, manager) async { + final commentManager = + await ref.watch(commentsManagerProvider(manager).future); + final commentList = + await ref.watch(commentsListProvider(commentManager).future); + return commentList.length; +}); diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index f809e3869c9e..903b4d6b6882 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -6,6 +6,7 @@ import 'package:acter/common/themes/colors/color_scheme.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/default_bottom_sheet.dart'; import 'package:acter/common/widgets/like_button.dart'; +import 'package:acter/features/comments/providers/comments.dart'; import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/news/model/keys.dart'; import 'package:acter/features/news/providers/news_providers.dart'; @@ -36,7 +37,8 @@ class NewsSideBar extends ConsumerWidget { final likesCount = ref.watch(totalLikesForNewsProvider(news)); final space = ref.watch(briefSpaceItemProvider(roomId)); final style = Theme.of(context).textTheme.bodyLarge!.copyWith(fontSize: 13); - + final commentCount = + ref.watch(commentsCountProvider(news.comments())).valueOrNull ?? 0; return Align( alignment: Alignment.bottomRight, child: Column( @@ -74,7 +76,13 @@ class NewsSideBar extends ConsumerWidget { ), ); }, - icon: const Icon(Atlas.comment_blank), + icon: Column( + children: [ + const Icon(Atlas.comment_blank), + const SizedBox(height: 4), + Text(commentCount.toString(), style: style), + ], + ), ), const SizedBox(height: 10), InkWell( From 3ac86e1af4c30f85db34d0c688cedbba7ed5e8d5 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Fri, 25 Oct 2024 15:53:23 +0530 Subject: [PATCH 13/77] Static comment counts --- app/lib/features/news/widgets/news_item/news_side_bar.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index 903b4d6b6882..409489e86d01 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -6,7 +6,6 @@ import 'package:acter/common/themes/colors/color_scheme.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/default_bottom_sheet.dart'; import 'package:acter/common/widgets/like_button.dart'; -import 'package:acter/features/comments/providers/comments.dart'; import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/news/model/keys.dart'; import 'package:acter/features/news/providers/news_providers.dart'; @@ -37,8 +36,9 @@ class NewsSideBar extends ConsumerWidget { final likesCount = ref.watch(totalLikesForNewsProvider(news)); final space = ref.watch(briefSpaceItemProvider(roomId)); final style = Theme.of(context).textTheme.bodyLarge!.copyWith(fontSize: 13); - final commentCount = - ref.watch(commentsCountProvider(news.comments())).valueOrNull ?? 0; + // final commentCount = + // ref.watch(commentsCountProvider(news.comments())).valueOrNull ?? 0; + const commentCount = 0; return Align( alignment: Alignment.bottomRight, child: Column( From fd9c44f2e19587f708d73cd22a326bba8d2d66bb Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Mon, 28 Oct 2024 19:02:09 +0530 Subject: [PATCH 14/77] Minor fixes --- app/lib/features/comments/providers/comments.dart | 7 ++++--- app/lib/features/comments/widgets/comment_item_widget.dart | 5 +++-- app/lib/features/news/widgets/news_item/news_side_bar.dart | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/lib/features/comments/providers/comments.dart b/app/lib/features/comments/providers/comments.dart index 8f4615f65e25..4fa4f665d52d 100644 --- a/app/lib/features/comments/providers/comments.dart +++ b/app/lib/features/comments/providers/comments.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' - show Comment, CommentsManager; + show Comment, CommentsManager, NewsEntry; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; @@ -43,8 +43,9 @@ final commentsListProvider = FutureProvider.family return (await manager.comments()).toList(); }); -final commentsCountProvider = FutureProvider.family - .autoDispose>((ref, manager) async { +final newsCommentsCountProvider = + FutureProvider.family.autoDispose((ref, newsEntry) async { + final manager = newsEntry.comments(); final commentManager = await ref.watch(commentsManagerProvider(manager).future); final commentList = diff --git a/app/lib/features/comments/widgets/comment_item_widget.dart b/app/lib/features/comments/widgets/comment_item_widget.dart index 6e09f2eda181..9d86961bf521 100644 --- a/app/lib/features/comments/widgets/comment_item_widget.dart +++ b/app/lib/features/comments/widgets/comment_item_widget.dart @@ -25,13 +25,14 @@ class CommentItemWidget extends ConsumerWidget { ); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ userAvatarUI(context, avatarInfo), Expanded( child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index 9968eb63601b..657b2273cd25 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -6,6 +6,7 @@ import 'package:acter/common/themes/colors/color_scheme.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/default_bottom_sheet.dart'; import 'package:acter/common/widgets/like_button.dart'; +import 'package:acter/features/comments/providers/comments.dart'; import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/news/model/keys.dart'; import 'package:acter/features/news/providers/news_providers.dart'; @@ -37,9 +38,8 @@ class NewsSideBar extends ConsumerWidget { final likesCount = ref.watch(totalLikesForNewsProvider(news)); final space = ref.watch(briefSpaceItemProvider(roomId)); final style = Theme.of(context).textTheme.bodyLarge!.copyWith(fontSize: 13); - // final commentCount = - // ref.watch(commentsCountProvider(news.comments())).valueOrNull ?? 0; - const commentCount = 0; + final commentCount = + ref.watch(newsCommentsCountProvider(news)).valueOrNull ?? 0; return Align( alignment: Alignment.bottomRight, child: Column( From d3a0e3cd5e9a2683c58c65f537feabc22b32a01d Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Mon, 28 Oct 2024 20:10:06 +0530 Subject: [PATCH 15/77] Manage comment input box --- .../comments/widgets/add_comment_widget.dart | 119 ++++++++++++------ 1 file changed, 81 insertions(+), 38 deletions(-) diff --git a/app/lib/features/comments/widgets/add_comment_widget.dart b/app/lib/features/comments/widgets/add_comment_widget.dart index 1b993098483f..7f4116ef115a 100644 --- a/app/lib/features/comments/widgets/add_comment_widget.dart +++ b/app/lib/features/comments/widgets/add_comment_widget.dart @@ -3,11 +3,13 @@ import 'package:acter/common/widgets/edit_html_description_sheet.dart'; import 'package:acter/features/comments/actions/sbumit_comment.dart'; import 'package:acter_avatar/acter_avatar.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; -class AddCommentWidget extends ConsumerWidget { +class AddCommentWidget extends ConsumerStatefulWidget { final CommentsManager manager; const AddCommentWidget({ @@ -16,50 +18,91 @@ class AddCommentWidget extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _AddCommentWidgetState(); +} + +class _AddCommentWidgetState extends ConsumerState { + final ValueNotifier showSendButton = ValueNotifier(false); + final TextEditingController _commentController = TextEditingController(); + + @override + Widget build(BuildContext context) { final avatarInfo = ref.watch(accountAvatarInfoProvider); - return InkWell( - onTap: () { - showEditHtmlDescriptionBottomSheet( - context: context, - bottomSheetTitle: L10n.of(context).addComment, - onSave: (htmlBodyDescription, plainDescription) async { - await submitComment( - context, - plainDescription, - htmlBodyDescription, - manager, - ); - if (!context.mounted) return; - Navigator.pop(context); - }, - ); - }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - ActerAvatar(options: AvatarOptions.DM(avatarInfo, size: 18)), - const SizedBox(width: 12), - Expanded(child: addCommentBoxUI(context)), - ], - ), + return Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ActerAvatar(options: AvatarOptions.DM(avatarInfo, size: 18)), + const SizedBox(width: 12), + Expanded(child: addCommentBoxUI(context)), + sendButton(), + ], ), ); } Widget addCommentBoxUI(BuildContext context) { - return Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Theme.of(context).unselectedWidgetColor.withAlpha(30), - border: Border.all(color: Theme.of(context).unselectedWidgetColor), - borderRadius: const BorderRadius.all(Radius.circular(24)), - ), - child: Text( - L10n.of(context).addComment, - style: Theme.of(context).textTheme.labelMedium, + final lang = L10n.of(context); + return TextField( + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, + controller: _commentController, + onChanged: (value) => showSendButton.value = value.isNotEmpty, + decoration: InputDecoration( + hintText: lang.addComment, + suffixIcon: IconButton( + onPressed: () => showEditHtmlDescriptionBottomSheet( + context: context, + bottomSheetTitle: L10n.of(context).addComment, + descriptionHtmlValue: _commentController.text, + onSave: (htmlBodyDescription, plainDescription) async { + await addComment( + plainDescription: plainDescription, + htmlBodyDescription: htmlBodyDescription, + ); + if (!context.mounted) return; + Navigator.pop(context); + }), + icon: const Icon(Atlas.arrows_up_right_down_left, size: 14), + ), ), ); } + + Widget sendButton() { + return ValueListenableBuilder( + valueListenable: showSendButton, + builder: (context, value, child) { + return value + ? Container( + margin: const EdgeInsets.only(left: 12), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(100)), + ), + child: IconButton( + onPressed: () => + addComment(plainDescription: _commentController.text), + icon: Icon(PhosphorIcons.paperPlaneTilt()), + ), + ) + : const SizedBox.shrink(); + }, + ); + } + + Future addComment({ + required String plainDescription, + String? htmlBodyDescription, + }) async { + await submitComment( + context, + plainDescription, + htmlBodyDescription ?? plainDescription, + widget.manager, + ); + _commentController.clear(); + showSendButton.value = false; + } } From d9291c498a276a4fec2eacbc43fb06e59f25abf5 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 12:35:46 +0530 Subject: [PATCH 16/77] Add Widget Test for Add Comment --- .../comments/widgets/add_comment_widget.dart | 23 +++--- .../features/comments/add_comment_test.dart | 75 +++++++++++++++++++ .../helpers/mock_avatarinfo_provider.dart | 12 +++ app/test/helpers/mock_comment.dart | 4 + 4 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 app/test/features/comments/add_comment_test.dart create mode 100644 app/test/helpers/mock_avatarinfo_provider.dart create mode 100644 app/test/helpers/mock_comment.dart diff --git a/app/lib/features/comments/widgets/add_comment_widget.dart b/app/lib/features/comments/widgets/add_comment_widget.dart index 7f4116ef115a..aeb5120e0870 100644 --- a/app/lib/features/comments/widgets/add_comment_widget.dart +++ b/app/lib/features/comments/widgets/add_comment_widget.dart @@ -53,17 +53,18 @@ class _AddCommentWidgetState extends ConsumerState { hintText: lang.addComment, suffixIcon: IconButton( onPressed: () => showEditHtmlDescriptionBottomSheet( - context: context, - bottomSheetTitle: L10n.of(context).addComment, - descriptionHtmlValue: _commentController.text, - onSave: (htmlBodyDescription, plainDescription) async { - await addComment( - plainDescription: plainDescription, - htmlBodyDescription: htmlBodyDescription, - ); - if (!context.mounted) return; - Navigator.pop(context); - }), + context: context, + bottomSheetTitle: L10n.of(context).addComment, + descriptionHtmlValue: _commentController.text, + onSave: (htmlBodyDescription, plainDescription) async { + await addComment( + plainDescription: plainDescription, + htmlBodyDescription: htmlBodyDescription, + ); + if (!context.mounted) return; + Navigator.pop(context); + }, + ), icon: const Icon(Atlas.arrows_up_right_down_left, size: 14), ), ), diff --git a/app/test/features/comments/add_comment_test.dart b/app/test/features/comments/add_comment_test.dart new file mode 100644 index 000000000000..78a426ce6b44 --- /dev/null +++ b/app/test/features/comments/add_comment_test.dart @@ -0,0 +1,75 @@ +import 'package:acter/common/providers/common_providers.dart'; +import 'package:acter/features/comments/widgets/add_comment_widget.dart'; +import 'package:acter_avatar/acter_avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import '../../helpers/mock_avatarinfo_provider.dart'; +import '../../helpers/mock_comment.dart'; + +void main() { + late MockCommentsManager mockCommentsManager; + late MockAvatarInfo mockAvatarInfo; + + setUp(() { + mockCommentsManager = MockCommentsManager(); + mockAvatarInfo = MockAvatarInfo(); + }); + + testWidgets('should display avatar and comment input', + (WidgetTester tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + // Use overrideWith for the AutoDisposeStateProvider with a mock value + accountAvatarInfoProvider.overrideWith((ref) => mockAvatarInfo), + ], + child: MaterialApp( + localizationsDelegates: const [L10n.delegate], + builder: EasyLoading.init(), + home: Scaffold( + body: AddCommentWidget(manager: mockCommentsManager), + ), + ), + ), + ); // Dummy avatar info + + // Check if avatar is displayed + expect(find.byType(ActerAvatar), findsOneWidget); + + // Check if comment input field is displayed + expect(find.byType(TextField), findsOneWidget); + }); + + testWidgets('send button appears when text is entered', + (WidgetTester tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + // Use overrideWith for the AutoDisposeStateProvider with a mock value + accountAvatarInfoProvider.overrideWith((ref) => mockAvatarInfo), + ], + child: MaterialApp( + localizationsDelegates: const [L10n.delegate], + builder: EasyLoading.init(), + home: Scaffold( + body: AddCommentWidget(manager: mockCommentsManager), + ), + ), + ), + ); + + // Initially, send button should not be visible + expect(find.byIcon(PhosphorIcons.paperPlaneTilt()), findsNothing); + + // Enter text in the comment box + await tester.enterText(find.byType(TextField), 'Test comment'); + await tester.pump(); + + // Send button should now be visible + expect(find.byIcon(PhosphorIcons.paperPlaneTilt()), findsOneWidget); + }); +} diff --git a/app/test/helpers/mock_avatarinfo_provider.dart b/app/test/helpers/mock_avatarinfo_provider.dart new file mode 100644 index 000000000000..bc57793329c1 --- /dev/null +++ b/app/test/helpers/mock_avatarinfo_provider.dart @@ -0,0 +1,12 @@ +// Mock class for the avatar info if needed +import 'package:acter_avatar/acter_avatar.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockAvatarInfo extends Mock implements AvatarInfo { + @override + String get uniqueId => 'mockUniqueId'; + + @override + TooltipStyle get tooltip => TooltipStyle.Combined; + +} diff --git a/app/test/helpers/mock_comment.dart b/app/test/helpers/mock_comment.dart new file mode 100644 index 000000000000..f0fe91cc71b6 --- /dev/null +++ b/app/test/helpers/mock_comment.dart @@ -0,0 +1,4 @@ +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockCommentsManager extends Mock implements CommentsManager {} From e7ace8f46d552fa5d738f5b3e517c15e13acd4fa Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 13:45:51 +0530 Subject: [PATCH 17/77] Added widget testing for Comment Item widget --- .../features/comments/add_comment_test.dart | 8 +- .../features/comments/comment_item_test.dart | 77 +++++++++++++++++++ app/test/helpers/mock_comment.dart | 4 - 3 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 app/test/features/comments/comment_item_test.dart delete mode 100644 app/test/helpers/mock_comment.dart diff --git a/app/test/features/comments/add_comment_test.dart b/app/test/features/comments/add_comment_test.dart index 78a426ce6b44..2d1d4682170d 100644 --- a/app/test/features/comments/add_comment_test.dart +++ b/app/test/features/comments/add_comment_test.dart @@ -1,14 +1,20 @@ import 'package:acter/common/providers/common_providers.dart'; import 'package:acter/features/comments/widgets/add_comment_widget.dart'; import 'package:acter_avatar/acter_avatar.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../helpers/mock_avatarinfo_provider.dart'; -import '../../helpers/mock_comment.dart'; + +class MockCommentsManager extends Mock implements CommentsManager {} + +class MockComment extends Mock implements Comment {} + void main() { late MockCommentsManager mockCommentsManager; diff --git a/app/test/features/comments/comment_item_test.dart b/app/test/features/comments/comment_item_test.dart new file mode 100644 index 000000000000..858c957b9436 --- /dev/null +++ b/app/test/features/comments/comment_item_test.dart @@ -0,0 +1,77 @@ +import 'package:acter/common/models/types.dart'; +import 'package:acter/common/providers/room_providers.dart'; +import 'package:acter/common/widgets/render_html.dart'; +import 'package:acter/features/comments/widgets/comment_item_widget.dart'; +import 'package:acter_avatar/acter_avatar.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mocktail/mocktail.dart'; +import '../../helpers/mock_avatarinfo_provider.dart'; + +// Mocking dependencies with mocktail +class MockComment extends Mock implements Comment {} + +class MockCommentsManager extends Mock implements CommentsManager {} + +class MockUserId extends Mock implements UserId {} + +class MockMsgContent extends Mock implements MsgContent {} + +void main() { + late MockComment mockComment; + late MockCommentsManager mockCommentsManager; + late MockAvatarInfo mockAvatarInfo; + late MockUserId mockUserId; + late MockMsgContent mockMsgContent; + + setUp(() { + mockComment = MockComment(); + mockCommentsManager = MockCommentsManager(); + mockAvatarInfo = MockAvatarInfo(); + mockUserId = MockUserId(); + mockMsgContent = MockMsgContent(); + + // Mock the values expected by the widget + when(() => mockCommentsManager.roomIdStr()).thenReturn('roomId'); + when(() => mockComment.sender()).thenReturn(mockUserId); + when(() => mockComment.msgContent()).thenReturn(mockMsgContent); + when(() => mockMsgContent.body()).thenReturn('This is a test message'); + when(() => mockComment.originServerTs()) + .thenReturn(DateTime.now().millisecondsSinceEpoch); + when(() => mockAvatarInfo.displayName).thenReturn('Test User'); + when(() => mockAvatarInfo.uniqueId).thenReturn('unique-avatar-id'); + }); + + testWidgets( + 'renders CommentItemWidget with avatar, name, content, and timestamp', + (WidgetTester tester) async { + // Wrap in ProviderScope and override the necessary providers + await tester.pumpWidget( + ProviderScope( + overrides: [ + memberAvatarInfoProvider + .overrideWith((ref, MemberInfo memberInfo) => mockAvatarInfo), + ], + child: MaterialApp( + home: Scaffold( + body: CommentItemWidget( + comment: mockComment, + manager: mockCommentsManager, + ), + ), + ), + ), + ); + + // Verify all expected widgets are displayed + expect(find.text('Test User'), findsOneWidget); + expect(find.text('This is a test message'), findsOneWidget); + expect(find.byType(ActerAvatar), findsOneWidget); + expect( + find.byType(RenderHtml), + findsNothing, + ); // Because formattedBody is null + }); +} diff --git a/app/test/helpers/mock_comment.dart b/app/test/helpers/mock_comment.dart deleted file mode 100644 index f0fe91cc71b6..000000000000 --- a/app/test/helpers/mock_comment.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockCommentsManager extends Mock implements CommentsManager {} From 34625dfdd9b753777b074621f08b3fbc6743a22d Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 15:10:12 +0530 Subject: [PATCH 18/77] Re-arrange classes and folders --- app/test/common/mock_data/mock_user_id.dart | 4 ++++ .../mock_providers}/mock_avatarinfo_provider.dart | 0 app/test/features/comments/add_comment_test.dart | 10 ++-------- app/test/features/comments/comment_item_test.dart | 15 +++++---------- .../features/comments/mock_data/mock_comment.dart | 4 ++++ .../comments/mock_data/mock_comments_manager.dart | 4 ++++ .../comments/mock_data/mock_message_content.dart | 4 ++++ .../mock_providers/comment_mock_providers.dart | 0 8 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 app/test/common/mock_data/mock_user_id.dart rename app/test/{helpers => common/mock_providers}/mock_avatarinfo_provider.dart (100%) create mode 100644 app/test/features/comments/mock_data/mock_comment.dart create mode 100644 app/test/features/comments/mock_data/mock_comments_manager.dart create mode 100644 app/test/features/comments/mock_data/mock_message_content.dart create mode 100644 app/test/features/comments/mock_providers/comment_mock_providers.dart diff --git a/app/test/common/mock_data/mock_user_id.dart b/app/test/common/mock_data/mock_user_id.dart new file mode 100644 index 000000000000..4ed746f9f982 --- /dev/null +++ b/app/test/common/mock_data/mock_user_id.dart @@ -0,0 +1,4 @@ +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockUserId extends Mock implements UserId {} diff --git a/app/test/helpers/mock_avatarinfo_provider.dart b/app/test/common/mock_providers/mock_avatarinfo_provider.dart similarity index 100% rename from app/test/helpers/mock_avatarinfo_provider.dart rename to app/test/common/mock_providers/mock_avatarinfo_provider.dart diff --git a/app/test/features/comments/add_comment_test.dart b/app/test/features/comments/add_comment_test.dart index 2d1d4682170d..9dccdaeee513 100644 --- a/app/test/features/comments/add_comment_test.dart +++ b/app/test/features/comments/add_comment_test.dart @@ -1,20 +1,14 @@ import 'package:acter/common/providers/common_providers.dart'; import 'package:acter/features/comments/widgets/add_comment_widget.dart'; import 'package:acter_avatar/acter_avatar.dart'; -import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../../helpers/mock_avatarinfo_provider.dart'; - -class MockCommentsManager extends Mock implements CommentsManager {} - -class MockComment extends Mock implements Comment {} - +import '../../common/mock_providers/mock_avatarinfo_provider.dart'; +import 'mock_data/mock_comments_manager.dart'; void main() { late MockCommentsManager mockCommentsManager; diff --git a/app/test/features/comments/comment_item_test.dart b/app/test/features/comments/comment_item_test.dart index 858c957b9436..cadf7b3e697e 100644 --- a/app/test/features/comments/comment_item_test.dart +++ b/app/test/features/comments/comment_item_test.dart @@ -3,21 +3,16 @@ import 'package:acter/common/providers/room_providers.dart'; import 'package:acter/common/widgets/render_html.dart'; import 'package:acter/features/comments/widgets/comment_item_widget.dart'; import 'package:acter_avatar/acter_avatar.dart'; -import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mocktail/mocktail.dart'; -import '../../helpers/mock_avatarinfo_provider.dart'; +import 'mock_data/mock_comment.dart'; +import 'mock_data/mock_comments_manager.dart'; +import 'mock_data/mock_message_content.dart'; +import '../../common/mock_data/mock_user_id.dart'; +import '../../common/mock_providers/mock_avatarinfo_provider.dart'; -// Mocking dependencies with mocktail -class MockComment extends Mock implements Comment {} - -class MockCommentsManager extends Mock implements CommentsManager {} - -class MockUserId extends Mock implements UserId {} - -class MockMsgContent extends Mock implements MsgContent {} void main() { late MockComment mockComment; diff --git a/app/test/features/comments/mock_data/mock_comment.dart b/app/test/features/comments/mock_data/mock_comment.dart new file mode 100644 index 000000000000..fbbaceda9817 --- /dev/null +++ b/app/test/features/comments/mock_data/mock_comment.dart @@ -0,0 +1,4 @@ +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockComment extends Mock implements Comment {} diff --git a/app/test/features/comments/mock_data/mock_comments_manager.dart b/app/test/features/comments/mock_data/mock_comments_manager.dart new file mode 100644 index 000000000000..f0fe91cc71b6 --- /dev/null +++ b/app/test/features/comments/mock_data/mock_comments_manager.dart @@ -0,0 +1,4 @@ +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockCommentsManager extends Mock implements CommentsManager {} diff --git a/app/test/features/comments/mock_data/mock_message_content.dart b/app/test/features/comments/mock_data/mock_message_content.dart new file mode 100644 index 000000000000..2f7a326408c0 --- /dev/null +++ b/app/test/features/comments/mock_data/mock_message_content.dart @@ -0,0 +1,4 @@ +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockMsgContent extends Mock implements MsgContent {} diff --git a/app/test/features/comments/mock_providers/comment_mock_providers.dart b/app/test/features/comments/mock_providers/comment_mock_providers.dart new file mode 100644 index 000000000000..e69de29bb2d1 From 00e9395c0c6486ef56efa464b34b2f67b900a491 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 15:12:11 +0530 Subject: [PATCH 19/77] Re-arrange classes and folders --- .../mock_avatar_info.dart} | 0 app/test/features/comments/add_comment_test.dart | 2 +- app/test/features/comments/comment_item_test.dart | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename app/test/common/{mock_providers/mock_avatarinfo_provider.dart => mock_data/mock_avatar_info.dart} (100%) diff --git a/app/test/common/mock_providers/mock_avatarinfo_provider.dart b/app/test/common/mock_data/mock_avatar_info.dart similarity index 100% rename from app/test/common/mock_providers/mock_avatarinfo_provider.dart rename to app/test/common/mock_data/mock_avatar_info.dart diff --git a/app/test/features/comments/add_comment_test.dart b/app/test/features/comments/add_comment_test.dart index 9dccdaeee513..a7862d6c44b8 100644 --- a/app/test/features/comments/add_comment_test.dart +++ b/app/test/features/comments/add_comment_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../../common/mock_providers/mock_avatarinfo_provider.dart'; +import '../../common/mock_data/mock_avatar_info.dart'; import 'mock_data/mock_comments_manager.dart'; void main() { diff --git a/app/test/features/comments/comment_item_test.dart b/app/test/features/comments/comment_item_test.dart index cadf7b3e697e..bf58636370ef 100644 --- a/app/test/features/comments/comment_item_test.dart +++ b/app/test/features/comments/comment_item_test.dart @@ -11,7 +11,7 @@ import 'mock_data/mock_comment.dart'; import 'mock_data/mock_comments_manager.dart'; import 'mock_data/mock_message_content.dart'; import '../../common/mock_data/mock_user_id.dart'; -import '../../common/mock_providers/mock_avatarinfo_provider.dart'; +import '../../common/mock_data/mock_avatar_info.dart'; void main() { From 2d301b7fa57aa64d63821cef06f34d346707a237 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 15:14:19 +0530 Subject: [PATCH 20/77] Re-arrange classes and folders --- .../providers/{comments.dart => comments_providers.dart} | 0 app/lib/features/comments/widgets/comment_list_widget.dart | 4 ++-- .../features/comments/widgets/comments_section_widget.dart | 4 ++-- app/lib/features/comments/widgets/create_comment.dart | 0 .../widgets/{ => skeletons}/comment_list_skeleton_widget.dart | 0 app/lib/features/news/widgets/news_item/news_side_bar.dart | 2 +- app/lib/features/pins/pages/pin_details_page.dart | 2 +- .../features/comments/comment_section_test.dart} | 0 8 files changed, 6 insertions(+), 6 deletions(-) rename app/lib/features/comments/providers/{comments.dart => comments_providers.dart} (100%) delete mode 100644 app/lib/features/comments/widgets/create_comment.dart rename app/lib/features/comments/widgets/{ => skeletons}/comment_list_skeleton_widget.dart (100%) rename app/{lib/features/comments/widgets/comment.dart => test/features/comments/comment_section_test.dart} (100%) diff --git a/app/lib/features/comments/providers/comments.dart b/app/lib/features/comments/providers/comments_providers.dart similarity index 100% rename from app/lib/features/comments/providers/comments.dart rename to app/lib/features/comments/providers/comments_providers.dart diff --git a/app/lib/features/comments/widgets/comment_list_widget.dart b/app/lib/features/comments/widgets/comment_list_widget.dart index 9185332baa3d..5146cf1a14f8 100644 --- a/app/lib/features/comments/widgets/comment_list_widget.dart +++ b/app/lib/features/comments/widgets/comment_list_widget.dart @@ -1,8 +1,8 @@ import 'package:acter/common/toolkit/errors/error_page.dart'; -import 'package:acter/features/comments/providers/comments.dart'; +import 'package:acter/features/comments/providers/comments_providers.dart'; import 'package:acter/features/comments/widgets/comment_item_widget.dart'; import 'package:acter/features/comments/widgets/comment_list_empty_state_widget.dart'; -import 'package:acter/features/comments/widgets/comment_list_skeleton_widget.dart'; +import 'package:acter/features/comments/widgets/skeletons/comment_list_skeleton_widget.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/app/lib/features/comments/widgets/comments_section_widget.dart b/app/lib/features/comments/widgets/comments_section_widget.dart index d9e18ddc5c98..e9b5623ec3ee 100644 --- a/app/lib/features/comments/widgets/comments_section_widget.dart +++ b/app/lib/features/comments/widgets/comments_section_widget.dart @@ -1,7 +1,7 @@ import 'package:acter/common/toolkit/errors/error_page.dart'; -import 'package:acter/features/comments/providers/comments.dart'; +import 'package:acter/features/comments/providers/comments_providers.dart'; import 'package:acter/features/comments/widgets/add_comment_widget.dart'; -import 'package:acter/features/comments/widgets/comment_list_skeleton_widget.dart'; +import 'package:acter/features/comments/widgets/skeletons/comment_list_skeleton_widget.dart'; import 'package:acter/features/comments/widgets/comment_list_widget.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; diff --git a/app/lib/features/comments/widgets/create_comment.dart b/app/lib/features/comments/widgets/create_comment.dart deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/app/lib/features/comments/widgets/comment_list_skeleton_widget.dart b/app/lib/features/comments/widgets/skeletons/comment_list_skeleton_widget.dart similarity index 100% rename from app/lib/features/comments/widgets/comment_list_skeleton_widget.dart rename to app/lib/features/comments/widgets/skeletons/comment_list_skeleton_widget.dart diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index 657b2273cd25..e77e35018a5b 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -6,7 +6,7 @@ import 'package:acter/common/themes/colors/color_scheme.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/default_bottom_sheet.dart'; import 'package:acter/common/widgets/like_button.dart'; -import 'package:acter/features/comments/providers/comments.dart'; +import 'package:acter/features/comments/providers/comments_providers.dart'; import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/news/model/keys.dart'; import 'package:acter/features/news/providers/news_providers.dart'; diff --git a/app/lib/features/pins/pages/pin_details_page.dart b/app/lib/features/pins/pages/pin_details_page.dart index 6782dd56be16..b3f7b15c54b8 100644 --- a/app/lib/features/pins/pages/pin_details_page.dart +++ b/app/lib/features/pins/pages/pin_details_page.dart @@ -8,7 +8,7 @@ import 'package:acter/common/widgets/edit_html_description_sheet.dart'; import 'package:acter/common/widgets/edit_title_sheet.dart'; import 'package:acter/common/widgets/render_html.dart'; import 'package:acter/features/attachments/widgets/attachment_section.dart'; -import 'package:acter/features/comments/widgets/comment_list_skeleton_widget.dart'; +import 'package:acter/features/comments/widgets/skeletons/comment_list_skeleton_widget.dart'; import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/home/widgets/space_chip.dart'; import 'package:acter/features/pins/actions/edit_pin_actions.dart'; diff --git a/app/lib/features/comments/widgets/comment.dart b/app/test/features/comments/comment_section_test.dart similarity index 100% rename from app/lib/features/comments/widgets/comment.dart rename to app/test/features/comments/comment_section_test.dart From 3c3457ce1f4e905ad13697a47603fe80bf2e3d75 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 16:28:55 +0530 Subject: [PATCH 21/77] Added more test to comments --- .../features/comments/comment_list_test.dart | 43 +++++++++++++++++++ .../comments/comment_section_test.dart | 33 ++++++++++++++ .../comments/mock_data/mock_comment.dart | 2 + .../comment_mock_providers.dart | 6 +++ 4 files changed, 84 insertions(+) create mode 100644 app/test/features/comments/comment_list_test.dart diff --git a/app/test/features/comments/comment_list_test.dart b/app/test/features/comments/comment_list_test.dart new file mode 100644 index 000000000000..de37acf43f61 --- /dev/null +++ b/app/test/features/comments/comment_list_test.dart @@ -0,0 +1,43 @@ +import 'package:acter/features/comments/widgets/comment_list_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:acter/features/comments/providers/comments_providers.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:acter/features/comments/widgets/comment_list_empty_state_widget.dart'; +import 'mock_data/mock_comments_manager.dart'; + +void main() { + testWidgets('displays empty state when there are no comments', + (tester) async { + // Arrange + final mockCommentsManager = MockCommentsManager(); + + // Build the widget tree with the mocked provider + await tester.pumpWidget( + ProviderScope( + overrides: [ + commentsListProvider.overrideWith((ref, manager) async => []), + ], + child: MaterialApp( + localizationsDelegates: const [L10n.delegate], + home: Scaffold( + body: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager + ), + ), + ), + ), + ); + + // Act + await tester.pumpAndSettle(); // Allow the widget to settle + + // Assert + expect( + find.byType(CommentListEmptyStateWidget), + findsOneWidget, + ); // Ensure the empty state widget is displayed + }); +} diff --git a/app/test/features/comments/comment_section_test.dart b/app/test/features/comments/comment_section_test.dart index e69de29bb2d1..74de2b5bbced 100644 --- a/app/test/features/comments/comment_section_test.dart +++ b/app/test/features/comments/comment_section_test.dart @@ -0,0 +1,33 @@ +import 'package:acter/features/comments/widgets/comments_section_widget.dart'; +import 'package:acter/features/comments/widgets/skeletons/comment_list_skeleton_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'mock_data/mock_comments_manager.dart'; +import 'mock_providers/comment_mock_providers.dart'; + +void main() { + late MockAsyncCommentsManagerNotifier mockNotifier; + late MockCommentsManager mockCommentsManager; + + setUp(() { + mockNotifier = MockAsyncCommentsManagerNotifier(); + mockCommentsManager = MockCommentsManager(); + }); + + testWidgets('displays loading skeleton when loading', (tester) async { + await tester.pumpWidget( + ProviderScope( + child: MaterialApp( + home: Material( + child: CommentsSectionWidget( + manager: Future.value(mockCommentsManager), + ), + ), + ), + ), + ); + + expect(find.byType(CommentListSkeletonWidget), findsOneWidget); + }); +} diff --git a/app/test/features/comments/mock_data/mock_comment.dart b/app/test/features/comments/mock_data/mock_comment.dart index fbbaceda9817..357bf36b5cdd 100644 --- a/app/test/features/comments/mock_data/mock_comment.dart +++ b/app/test/features/comments/mock_data/mock_comment.dart @@ -2,3 +2,5 @@ import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:mocktail/mocktail.dart'; class MockComment extends Mock implements Comment {} + +class MockFfiListComment extends Mock implements FfiListComment {} diff --git a/app/test/features/comments/mock_providers/comment_mock_providers.dart b/app/test/features/comments/mock_providers/comment_mock_providers.dart index e69de29bb2d1..6d2cfea0ae58 100644 --- a/app/test/features/comments/mock_providers/comment_mock_providers.dart +++ b/app/test/features/comments/mock_providers/comment_mock_providers.dart @@ -0,0 +1,6 @@ +import 'package:acter/features/comments/providers/comments_providers.dart'; +import 'package:mocktail/mocktail.dart'; + +// Define a mock class for AsyncCommentsManagerNotifier +class MockAsyncCommentsManagerNotifier extends Mock + implements AsyncCommentsManagerNotifier {} From f59d991a863d8c1e3d24d1b36f5df7f99950bc20 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 16:29:24 +0530 Subject: [PATCH 22/77] Remove lint errors --- app/test/features/comments/comment_list_test.dart | 1 - app/test/features/comments/comment_section_test.dart | 3 --- 2 files changed, 4 deletions(-) diff --git a/app/test/features/comments/comment_list_test.dart b/app/test/features/comments/comment_list_test.dart index de37acf43f61..23b0c6e596c1 100644 --- a/app/test/features/comments/comment_list_test.dart +++ b/app/test/features/comments/comment_list_test.dart @@ -2,7 +2,6 @@ import 'package:acter/features/comments/widgets/comment_list_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:acter/features/comments/providers/comments_providers.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:acter/features/comments/widgets/comment_list_empty_state_widget.dart'; diff --git a/app/test/features/comments/comment_section_test.dart b/app/test/features/comments/comment_section_test.dart index 74de2b5bbced..f9b0bd28b9d5 100644 --- a/app/test/features/comments/comment_section_test.dart +++ b/app/test/features/comments/comment_section_test.dart @@ -4,14 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'mock_data/mock_comments_manager.dart'; -import 'mock_providers/comment_mock_providers.dart'; void main() { - late MockAsyncCommentsManagerNotifier mockNotifier; late MockCommentsManager mockCommentsManager; setUp(() { - mockNotifier = MockAsyncCommentsManagerNotifier(); mockCommentsManager = MockCommentsManager(); }); From 8843e0e2623dbb8a6e5c946aece6dc354b8398b8 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 16:47:05 +0530 Subject: [PATCH 23/77] Removed un-used import due to merge --- app/lib/features/pins/pages/pin_details_page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/app/lib/features/pins/pages/pin_details_page.dart b/app/lib/features/pins/pages/pin_details_page.dart index 490c910dd0ac..327352c9f0df 100644 --- a/app/lib/features/pins/pages/pin_details_page.dart +++ b/app/lib/features/pins/pages/pin_details_page.dart @@ -12,7 +12,6 @@ import 'package:acter/features/comments/widgets/skeletons/comment_list_skeleton_ import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/bookmarks/types.dart'; import 'package:acter/features/bookmarks/widgets/bookmark_action.dart'; -import 'package:acter/features/comments/widgets/comments_section.dart'; import 'package:acter/features/home/widgets/space_chip.dart'; import 'package:acter/features/pins/actions/edit_pin_actions.dart'; import 'package:acter/features/pins/actions/pin_update_actions.dart'; From 5991584ae12c03d67fdf3391d2e08d244b6752c2 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 18:16:50 +0530 Subject: [PATCH 24/77] Resolve error on widget tests --- .../features/comments/add_comment_test.dart | 4 +- .../features/comments/comment_item_test.dart | 19 +-- .../features/comments/comment_list_test.dart | 118 +++++++++++++++++- .../comments/comment_section_test.dart | 17 +-- .../comments/mock_data/mock_comment.dart | 24 +++- .../mock_data/mock_comments_manager.dart | 9 +- .../mock_data/mock_message_content.dart | 11 +- .../comment_mock_providers.dart | 11 +- 8 files changed, 181 insertions(+), 32 deletions(-) diff --git a/app/test/features/comments/add_comment_test.dart b/app/test/features/comments/add_comment_test.dart index a7862d6c44b8..61811ada69d5 100644 --- a/app/test/features/comments/add_comment_test.dart +++ b/app/test/features/comments/add_comment_test.dart @@ -15,7 +15,7 @@ void main() { late MockAvatarInfo mockAvatarInfo; setUp(() { - mockCommentsManager = MockCommentsManager(); + mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); mockAvatarInfo = MockAvatarInfo(); }); @@ -61,7 +61,7 @@ void main() { ), ), ); - + //TODO-Check button with Key instead of IconData // Initially, send button should not be visible expect(find.byIcon(PhosphorIcons.paperPlaneTilt()), findsNothing); diff --git a/app/test/features/comments/comment_item_test.dart b/app/test/features/comments/comment_item_test.dart index bf58636370ef..da7deda8f6aa 100644 --- a/app/test/features/comments/comment_item_test.dart +++ b/app/test/features/comments/comment_item_test.dart @@ -13,30 +13,23 @@ import 'mock_data/mock_message_content.dart'; import '../../common/mock_data/mock_user_id.dart'; import '../../common/mock_data/mock_avatar_info.dart'; - void main() { late MockComment mockComment; late MockCommentsManager mockCommentsManager; late MockAvatarInfo mockAvatarInfo; - late MockUserId mockUserId; - late MockMsgContent mockMsgContent; setUp(() { - mockComment = MockComment(); - mockCommentsManager = MockCommentsManager(); + mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); + mockComment = MockComment( + fakeSender: MockUserId(), + fakeMsgContent: MockMsgContent(bodyText: 'This is a test message'), + fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, + ); mockAvatarInfo = MockAvatarInfo(); - mockUserId = MockUserId(); - mockMsgContent = MockMsgContent(); // Mock the values expected by the widget when(() => mockCommentsManager.roomIdStr()).thenReturn('roomId'); - when(() => mockComment.sender()).thenReturn(mockUserId); - when(() => mockComment.msgContent()).thenReturn(mockMsgContent); - when(() => mockMsgContent.body()).thenReturn('This is a test message'); - when(() => mockComment.originServerTs()) - .thenReturn(DateTime.now().millisecondsSinceEpoch); when(() => mockAvatarInfo.displayName).thenReturn('Test User'); - when(() => mockAvatarInfo.uniqueId).thenReturn('unique-avatar-id'); }); testWidgets( diff --git a/app/test/features/comments/comment_list_test.dart b/app/test/features/comments/comment_list_test.dart index 23b0c6e596c1..a539ff298692 100644 --- a/app/test/features/comments/comment_list_test.dart +++ b/app/test/features/comments/comment_list_test.dart @@ -5,13 +5,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:acter/features/comments/providers/comments_providers.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:acter/features/comments/widgets/comment_list_empty_state_widget.dart'; +import '../../common/mock_data/mock_user_id.dart'; +import '../../helpers/error_helpers.dart'; +import 'mock_data/mock_comment.dart'; import 'mock_data/mock_comments_manager.dart'; +import 'mock_data/mock_message_content.dart'; void main() { testWidgets('displays empty state when there are no comments', (tester) async { // Arrange - final mockCommentsManager = MockCommentsManager(); + final mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); // Build the widget tree with the mocked provider await tester.pumpWidget( @@ -39,4 +43,116 @@ void main() { findsOneWidget, ); // Ensure the empty state widget is displayed }); + + testWidgets( + 'displays error state when there are issue in loading comment list', + (tester) async { + // Arrange + final mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); + + // Build the widget tree with the mocked provider + await tester.pumpWidget( + ProviderScope( + overrides: [ + commentsListProvider + .overrideWith((ref, manager) async => throw 'Some Error'), + ], + child: MaterialApp( + localizationsDelegates: const [L10n.delegate], + home: Scaffold( + body: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager + ), + ), + ), + ), + ); + await tester.ensureErrorPageWorks(); + }); + + testWidgets( + 'displays error state when there are issue in loading comment list and also test retry', + (tester) async { + bool shouldFail = true; + + // Arrange + final mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); + + // Build the widget tree with the mocked provider + await tester.pumpWidget( + ProviderScope( + overrides: [ + commentsListProvider.overrideWith((ref, manager) async { + if (shouldFail) { + shouldFail = false; + throw 'Some Error'; + } else { + return []; + } + }), + ], + child: MaterialApp( + localizationsDelegates: const [L10n.delegate], + home: Scaffold( + body: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager + ), + ), + ), + ), + ); + await tester.ensureErrorPageWithRetryWorks(); + }); + + testWidgets('displays list state when there are comments', (tester) async { + // Arrange + final mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); + final mockUser1 = MockComment( + fakeSender: MockUserId(), + fakeMsgContent: MockMsgContent(bodyText: 'message 1'), + fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, + ); + final mockUser2 = MockComment( + fakeSender: MockUserId(), + fakeMsgContent: MockMsgContent(bodyText: 'message 2'), + fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, + ); + final mockUser3 = MockComment( + fakeSender: MockUserId(), + fakeMsgContent: MockMsgContent(bodyText: 'message 3'), + fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, + ); + + // Build the widget tree with the mocked provider + await tester.pumpWidget( + ProviderScope( + overrides: [ + commentsListProvider.overrideWith( + (ref, manager) async => [ + mockUser1, + mockUser2, + mockUser3, + ], + ), + ], + child: MaterialApp( + localizationsDelegates: const [L10n.delegate], + home: Scaffold( + body: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager + ), + ), + ), + ), + ); + + // Act + await tester.pumpAndSettle(); // Allow the widget to settle + + // Assert + expect( + find.byType(CommentListEmptyStateWidget), + findsNothing, + ); // Ensure the empty state widget is displayed + }); } diff --git a/app/test/features/comments/comment_section_test.dart b/app/test/features/comments/comment_section_test.dart index f9b0bd28b9d5..2734462da13a 100644 --- a/app/test/features/comments/comment_section_test.dart +++ b/app/test/features/comments/comment_section_test.dart @@ -1,27 +1,20 @@ import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/comments/widgets/skeletons/comment_list_skeleton_widget.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../helpers/test_util.dart'; import 'mock_data/mock_comments_manager.dart'; void main() { late MockCommentsManager mockCommentsManager; setUp(() { - mockCommentsManager = MockCommentsManager(); + mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); }); testWidgets('displays loading skeleton when loading', (tester) async { - await tester.pumpWidget( - ProviderScope( - child: MaterialApp( - home: Material( - child: CommentsSectionWidget( - manager: Future.value(mockCommentsManager), - ), - ), - ), + await tester.pumpProviderWidget( + child: CommentsSectionWidget( + manager: Future.value(mockCommentsManager), ), ); diff --git a/app/test/features/comments/mock_data/mock_comment.dart b/app/test/features/comments/mock_data/mock_comment.dart index 357bf36b5cdd..95907cc93bd0 100644 --- a/app/test/features/comments/mock_data/mock_comment.dart +++ b/app/test/features/comments/mock_data/mock_comment.dart @@ -1,6 +1,28 @@ import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:mocktail/mocktail.dart'; -class MockComment extends Mock implements Comment {} +import '../../../common/mock_data/mock_user_id.dart'; +import 'mock_message_content.dart'; + +class MockComment extends Fake implements Comment { + final MockUserId fakeSender; + final MockMsgContent fakeMsgContent; + final int fakeOriginServerTs; + + MockComment({ + required this.fakeSender, + required this.fakeMsgContent, + required this.fakeOriginServerTs, + }); + + @override + MockUserId sender() => fakeSender; + + @override + MockMsgContent msgContent() => fakeMsgContent; + + @override + int originServerTs() => fakeOriginServerTs; +} class MockFfiListComment extends Mock implements FfiListComment {} diff --git a/app/test/features/comments/mock_data/mock_comments_manager.dart b/app/test/features/comments/mock_data/mock_comments_manager.dart index f0fe91cc71b6..f0f4415c5f27 100644 --- a/app/test/features/comments/mock_data/mock_comments_manager.dart +++ b/app/test/features/comments/mock_data/mock_comments_manager.dart @@ -1,4 +1,11 @@ import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:mocktail/mocktail.dart'; -class MockCommentsManager extends Mock implements CommentsManager {} +class MockCommentsManager extends Mock implements CommentsManager { + final String fakeRoomId; + + MockCommentsManager({required this.fakeRoomId}); + + @override + String roomIdStr() => fakeRoomId; +} diff --git a/app/test/features/comments/mock_data/mock_message_content.dart b/app/test/features/comments/mock_data/mock_message_content.dart index 2f7a326408c0..84e9fc96b965 100644 --- a/app/test/features/comments/mock_data/mock_message_content.dart +++ b/app/test/features/comments/mock_data/mock_message_content.dart @@ -1,4 +1,13 @@ import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:mocktail/mocktail.dart'; -class MockMsgContent extends Mock implements MsgContent {} +class MockMsgContent extends Mock implements MsgContent { + final String bodyText; + + MockMsgContent({ + required this.bodyText, + }); + + @override + String body() => bodyText; +} diff --git a/app/test/features/comments/mock_providers/comment_mock_providers.dart b/app/test/features/comments/mock_providers/comment_mock_providers.dart index 6d2cfea0ae58..d8061cc7cf3a 100644 --- a/app/test/features/comments/mock_providers/comment_mock_providers.dart +++ b/app/test/features/comments/mock_providers/comment_mock_providers.dart @@ -1,6 +1,15 @@ +import 'dart:async'; + import 'package:acter/features/comments/providers/comments_providers.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:mocktail/mocktail.dart'; +import '../mock_data/mock_comments_manager.dart'; // Define a mock class for AsyncCommentsManagerNotifier class MockAsyncCommentsManagerNotifier extends Mock - implements AsyncCommentsManagerNotifier {} + implements AsyncCommentsManagerNotifier { + @override + FutureOr build(Future arg) async { + return MockCommentsManager(fakeRoomId: 'roomId'); + } +} From f8721cb78432688cb949c737233ab69d19aaca9a Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 18:20:23 +0530 Subject: [PATCH 25/77] Find by key instead of Type --- app/lib/features/comments/widgets/add_comment_widget.dart | 3 +++ app/test/features/comments/add_comment_test.dart | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/lib/features/comments/widgets/add_comment_widget.dart b/app/lib/features/comments/widgets/add_comment_widget.dart index aeb5120e0870..c1a94587ce8a 100644 --- a/app/lib/features/comments/widgets/add_comment_widget.dart +++ b/app/lib/features/comments/widgets/add_comment_widget.dart @@ -10,6 +10,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; class AddCommentWidget extends ConsumerStatefulWidget { + static const addCommentButton = Key('add-comment-button'); + final CommentsManager manager; const AddCommentWidget({ @@ -83,6 +85,7 @@ class _AddCommentWidgetState extends ConsumerState { borderRadius: const BorderRadius.all(Radius.circular(100)), ), child: IconButton( + key: AddCommentWidget.addCommentButton, onPressed: () => addComment(plainDescription: _commentController.text), icon: Icon(PhosphorIcons.paperPlaneTilt()), diff --git a/app/test/features/comments/add_comment_test.dart b/app/test/features/comments/add_comment_test.dart index 61811ada69d5..bf52afc98011 100644 --- a/app/test/features/comments/add_comment_test.dart +++ b/app/test/features/comments/add_comment_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../common/mock_data/mock_avatar_info.dart'; import 'mock_data/mock_comments_manager.dart'; @@ -61,15 +60,14 @@ void main() { ), ), ); - //TODO-Check button with Key instead of IconData // Initially, send button should not be visible - expect(find.byIcon(PhosphorIcons.paperPlaneTilt()), findsNothing); + expect(find.byKey(AddCommentWidget.addCommentButton), findsNothing); // Enter text in the comment box await tester.enterText(find.byType(TextField), 'Test comment'); await tester.pump(); // Send button should now be visible - expect(find.byIcon(PhosphorIcons.paperPlaneTilt()), findsOneWidget); + expect(find.byKey(AddCommentWidget.addCommentButton), findsOneWidget); }); } From aeef2a3fde4d2381ed4502f708b7a9ea2708ba66 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 18:22:49 +0530 Subject: [PATCH 26/77] Remove un-wanted test --- .../comments/comment_section_test.dart | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 app/test/features/comments/comment_section_test.dart diff --git a/app/test/features/comments/comment_section_test.dart b/app/test/features/comments/comment_section_test.dart deleted file mode 100644 index 2734462da13a..000000000000 --- a/app/test/features/comments/comment_section_test.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:acter/features/comments/widgets/comments_section_widget.dart'; -import 'package:acter/features/comments/widgets/skeletons/comment_list_skeleton_widget.dart'; -import 'package:flutter_test/flutter_test.dart'; -import '../../helpers/test_util.dart'; -import 'mock_data/mock_comments_manager.dart'; - -void main() { - late MockCommentsManager mockCommentsManager; - - setUp(() { - mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); - }); - - testWidgets('displays loading skeleton when loading', (tester) async { - await tester.pumpProviderWidget( - child: CommentsSectionWidget( - manager: Future.value(mockCommentsManager), - ), - ); - - expect(find.byType(CommentListSkeletonWidget), findsOneWidget); - }); -} From 6fa445d3c045fbc92721e941dd8d03b0566e16f4 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 18:24:50 +0530 Subject: [PATCH 27/77] Remove fake roomId --- app/test/features/comments/add_comment_test.dart | 2 +- app/test/features/comments/comment_item_test.dart | 2 +- app/test/features/comments/comment_list_test.dart | 8 ++++---- .../comments/mock_data/mock_comments_manager.dart | 9 +-------- .../comments/mock_providers/comment_mock_providers.dart | 2 +- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/app/test/features/comments/add_comment_test.dart b/app/test/features/comments/add_comment_test.dart index bf52afc98011..a25fea42c364 100644 --- a/app/test/features/comments/add_comment_test.dart +++ b/app/test/features/comments/add_comment_test.dart @@ -14,7 +14,7 @@ void main() { late MockAvatarInfo mockAvatarInfo; setUp(() { - mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); + mockCommentsManager = MockCommentsManager(); mockAvatarInfo = MockAvatarInfo(); }); diff --git a/app/test/features/comments/comment_item_test.dart b/app/test/features/comments/comment_item_test.dart index da7deda8f6aa..a339a2b708b8 100644 --- a/app/test/features/comments/comment_item_test.dart +++ b/app/test/features/comments/comment_item_test.dart @@ -19,7 +19,7 @@ void main() { late MockAvatarInfo mockAvatarInfo; setUp(() { - mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); + mockCommentsManager = MockCommentsManager(); mockComment = MockComment( fakeSender: MockUserId(), fakeMsgContent: MockMsgContent(bodyText: 'This is a test message'), diff --git a/app/test/features/comments/comment_list_test.dart b/app/test/features/comments/comment_list_test.dart index a539ff298692..bca9b3a3567d 100644 --- a/app/test/features/comments/comment_list_test.dart +++ b/app/test/features/comments/comment_list_test.dart @@ -15,7 +15,7 @@ void main() { testWidgets('displays empty state when there are no comments', (tester) async { // Arrange - final mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); + final mockCommentsManager = MockCommentsManager(); // Build the widget tree with the mocked provider await tester.pumpWidget( @@ -48,7 +48,7 @@ void main() { 'displays error state when there are issue in loading comment list', (tester) async { // Arrange - final mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); + final mockCommentsManager = MockCommentsManager(); // Build the widget tree with the mocked provider await tester.pumpWidget( @@ -76,7 +76,7 @@ void main() { bool shouldFail = true; // Arrange - final mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); + final mockCommentsManager = MockCommentsManager(); // Build the widget tree with the mocked provider await tester.pumpWidget( @@ -106,7 +106,7 @@ void main() { testWidgets('displays list state when there are comments', (tester) async { // Arrange - final mockCommentsManager = MockCommentsManager(fakeRoomId: 'roomId'); + final mockCommentsManager = MockCommentsManager(); final mockUser1 = MockComment( fakeSender: MockUserId(), fakeMsgContent: MockMsgContent(bodyText: 'message 1'), diff --git a/app/test/features/comments/mock_data/mock_comments_manager.dart b/app/test/features/comments/mock_data/mock_comments_manager.dart index f0f4415c5f27..f0fe91cc71b6 100644 --- a/app/test/features/comments/mock_data/mock_comments_manager.dart +++ b/app/test/features/comments/mock_data/mock_comments_manager.dart @@ -1,11 +1,4 @@ import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:mocktail/mocktail.dart'; -class MockCommentsManager extends Mock implements CommentsManager { - final String fakeRoomId; - - MockCommentsManager({required this.fakeRoomId}); - - @override - String roomIdStr() => fakeRoomId; -} +class MockCommentsManager extends Mock implements CommentsManager {} diff --git a/app/test/features/comments/mock_providers/comment_mock_providers.dart b/app/test/features/comments/mock_providers/comment_mock_providers.dart index d8061cc7cf3a..de8a542068f6 100644 --- a/app/test/features/comments/mock_providers/comment_mock_providers.dart +++ b/app/test/features/comments/mock_providers/comment_mock_providers.dart @@ -10,6 +10,6 @@ class MockAsyncCommentsManagerNotifier extends Mock implements AsyncCommentsManagerNotifier { @override FutureOr build(Future arg) async { - return MockCommentsManager(fakeRoomId: 'roomId'); + return MockCommentsManager(); } } From b851204ef7b29217379ab46ea4a5b2d73fcebc88 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Tue, 29 Oct 2024 18:55:04 +0530 Subject: [PATCH 28/77] Minor changes --- app/test/features/comments/comment_list_test.dart | 6 +++++- app/test/features/comments/mock_data/mock_comment.dart | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/test/features/comments/comment_list_test.dart b/app/test/features/comments/comment_list_test.dart index bca9b3a3567d..a854d39eef48 100644 --- a/app/test/features/comments/comment_list_test.dart +++ b/app/test/features/comments/comment_list_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:acter/features/comments/providers/comments_providers.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:acter/features/comments/widgets/comment_list_empty_state_widget.dart'; +import 'package:mocktail/mocktail.dart'; import '../../common/mock_data/mock_user_id.dart'; import '../../helpers/error_helpers.dart'; import 'mock_data/mock_comment.dart'; @@ -106,7 +107,7 @@ void main() { testWidgets('displays list state when there are comments', (tester) async { // Arrange - final mockCommentsManager = MockCommentsManager(); + final mockUser1 = MockComment( fakeSender: MockUserId(), fakeMsgContent: MockMsgContent(bodyText: 'message 1'), @@ -123,6 +124,9 @@ void main() { fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, ); + final mockCommentsManager = MockCommentsManager(); + when(() => mockCommentsManager.roomIdStr()).thenReturn('roomId'); + // Build the widget tree with the mocked provider await tester.pumpWidget( ProviderScope( diff --git a/app/test/features/comments/mock_data/mock_comment.dart b/app/test/features/comments/mock_data/mock_comment.dart index 95907cc93bd0..1919ede6b237 100644 --- a/app/test/features/comments/mock_data/mock_comment.dart +++ b/app/test/features/comments/mock_data/mock_comment.dart @@ -25,4 +25,12 @@ class MockComment extends Fake implements Comment { int originServerTs() => fakeOriginServerTs; } -class MockFfiListComment extends Mock implements FfiListComment {} +class MockFfiListComment extends Mock implements FfiListComment { + final List comments; + + MockFfiListComment({required this.comments}); + + @override + List toList({bool growable = false}) => + comments.toList(growable: growable); +} From f8a9844f99bc022512fce6e960565260e9343ea6 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 29 Oct 2024 16:03:49 +0000 Subject: [PATCH 29/77] Switching from rootNavKey.currentContext! to ProviderContainer --- app/integration_test/support/util.dart | 9 ++--- app/lib/config/notifications/init.dart | 40 ++++++++++++------- app/lib/config/notifications/util.dart | 7 ++-- app/lib/config/setup.dart | 3 ++ .../features/calendar_sync/calendar_sync.dart | 9 ++--- app/lib/main.dart | 8 +++- .../shell_routers/chat_shell_router.dart | 27 ++++++------- app/lib/router/utils.dart | 9 ++--- 8 files changed, 60 insertions(+), 52 deletions(-) diff --git a/app/integration_test/support/util.dart b/app/integration_test/support/util.dart index 21d431543f4e..0374c2029841 100644 --- a/app/integration_test/support/util.dart +++ b/app/integration_test/support/util.dart @@ -1,14 +1,13 @@ import 'dart:io'; -import 'package:acter/common/extensions/acter_build_context.dart'; import 'package:acter/common/utils/constants.dart'; import 'package:acter/common/widgets/spaces/select_space_form_field.dart'; +import 'package:acter/config/setup.dart'; import 'package:acter/features/home/data/keys.dart'; import 'package:acter/features/labs/model/labs_features.dart'; import 'package:acter/features/labs/providers/labs_providers.dart'; import 'package:acter/features/search/model/keys.dart'; import 'package:acter/features/settings/widgets/settings_menu.dart'; -import 'package:acter/router/router.dart'; import 'package:convenient_test_dev/convenient_test_dev.dart'; import 'package:image_picker/image_picker.dart'; import 'package:flutter/material.dart'; @@ -62,7 +61,7 @@ extension ActerUtil on ConvenientTest { } Future ensureLabEnabled(LabsFeature feat) async { - if (!rootNavKey.currentContext!.read(isActiveProvider(feat))) { + if (!mainProviderContainer.read(isActiveProvider(feat))) { // ensure we do actually have access to the main nav. await find.byKey(Keys.mainNav).should(findsOneWidget); final quickJumpKey = find.byKey(MainNavKeys.quickJump); @@ -80,7 +79,7 @@ extension ActerUtil on ConvenientTest { final confirmKey = find.byKey(Key('labs-${feat.name}')); await confirmKey.should(findsOneWidget); // let's read again - if (!rootNavKey.currentContext!.read(isActiveProvider(feat))) { + if (!mainProviderContainer.read(isActiveProvider(feat))) { await confirmKey.tap(); } @@ -88,7 +87,7 @@ extension ActerUtil on ConvenientTest { // ensure we are active assert( - rootNavKey.currentContext!.read(isActiveProvider(feat)), + mainProviderContainer.read(isActiveProvider(feat)), 'Could not activate $feat', ); } diff --git a/app/lib/config/notifications/init.dart b/app/lib/config/notifications/init.dart index a2678b82634b..72491245dc5f 100644 --- a/app/lib/config/notifications/init.dart +++ b/app/lib/config/notifications/init.dart @@ -1,11 +1,11 @@ import 'dart:io'; -import 'package:acter/common/extensions/acter_build_context.dart'; import 'package:acter/common/providers/app_state_provider.dart'; import 'package:acter/common/providers/sdk_provider.dart'; import 'package:acter/config/env.g.dart'; import 'package:acter/config/notifications/firebase_options.dart'; import 'package:acter/config/notifications/util.dart'; +import 'package:acter/config/setup.dart'; import 'package:acter/features/labs/model/labs_features.dart'; import 'package:acter/features/labs/providers/labs_providers.dart'; import 'package:acter/router/router.dart'; @@ -94,22 +94,38 @@ Future setupPushNotifications( } bool _handleMessageTap(Map data) { - _log.info('Notification was tapped. Data: \n $data'); + final context = rootNavKey.currentContext; + if (context == null) { + // no context "et", delay by 300ms and try again; + Future.delayed( + const Duration(milliseconds: 300), + () => _handleMessageTap(data), + ); + return false; + } + return _handleMessageTapForContext(context, data); +} + +bool _handleMessageTapForContext( + BuildContext context, + Map data, +) { + _log.info('Notification was tapped. Data: \n $data'); try { final uri = data['payload'] as String?; if (uri != null) { _log.info('Uri found $uri'); if (isCurrentRoute(uri)) { // ensure we reload - rootNavKey.currentContext!.replace(uri); + context.replace(uri); } else { _log.info('Different page, routing'); if (shouldReplaceCurrentRoute(uri)) { // this is a chat-room page, replace this to allow for // a smother "back"-navigation story - rootNavKey.currentContext!.pushReplacement(uri); + context.pushReplacement(uri); } else { - rootNavKey.currentContext!.push(uri); + context.push(uri); } } return true; @@ -123,7 +139,7 @@ bool _handleMessageTap(Map data) { return false; } // fallback support - rootNavKey.currentContext!.push( + context.push( makeForward(roomId: roomId, deviceId: deviceId, eventId: eventId), ); } catch (e, s) { @@ -136,7 +152,7 @@ bool _handleMessageTap(Map data) { bool _isEnabled() { try { // ignore: use_build_context_synchronously - if (!rootNavKey.currentContext! + if (!mainProviderContainer .read(isActiveProvider(LabsFeature.mobilePushNotifications))) { _log.info( 'Showing push notifications has been disabled on this device. Ignoring', @@ -153,7 +169,7 @@ bool _shouldShow(String url) { // we ignore if we are in foreground and looking at that URL if (isCurrentRoute(url) && // ignore: use_build_context_synchronously - rootNavKey.currentContext!.read(isAppInForeground)) { + mainProviderContainer.read(isAppInForeground)) { return false; } return true; @@ -164,13 +180,7 @@ Future> _genCurrentClients() async { 'Received the update information for the token. Updating all clients.', ); List clients = []; - // ignore: use_build_context_synchronously - final currentContext = rootNavKey.currentContext; - if (currentContext == null) { - _log.warning('No currentContext found. skipping setting of new token'); - return clients; - } - final sdk = await currentContext.read(sdkProvider.future); + final sdk = await mainProviderContainer.read(sdkProvider.future); for (final client in sdk.clients) { final deviceId = client.deviceId().toString(); diff --git a/app/lib/config/notifications/util.dart b/app/lib/config/notifications/util.dart index 12da75649b0f..c625e64fa78b 100644 --- a/app/lib/config/notifications/util.dart +++ b/app/lib/config/notifications/util.dart @@ -1,6 +1,5 @@ -import 'package:acter/common/extensions/acter_build_context.dart'; +import 'package:acter/config/setup.dart'; import 'package:acter/router/providers/router_providers.dart'; -import 'package:acter/router/router.dart'; import 'package:acter/router/utils.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -18,7 +17,7 @@ Future setRejected(String deviceId, bool value) async { } bool isCurrentRoute(String uri) { - final currentUri = rootNavKey.currentContext!.read(currentRoutingLocation); + final currentUri = mainProviderContainer.read(currentRoutingLocation); return currentUri == uri; } @@ -27,7 +26,7 @@ bool shouldReplaceCurrentRoute(String uri) { return false; } - final currentUri = rootNavKey.currentContext!.read(currentRoutingLocation); + final currentUri = mainProviderContainer.read(currentRoutingLocation); return currentUri.startsWith(chatRoomUriMatcher); } diff --git a/app/lib/config/setup.dart b/app/lib/config/setup.dart index f81838de47ce..cfafa30e5f24 100644 --- a/app/lib/config/setup.dart +++ b/app/lib/config/setup.dart @@ -2,12 +2,15 @@ import 'dart:io'; import 'package:acter/config/env.g.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; const userAgent = '${Env.rageshakeAppName}/${Env.rageshakeAppName}'; final defaultLogSetting = Platform.environment.containsKey(rustLogKey) ? Platform.environment[rustLogKey] as String : Env.defaultRustLog; +final mainProviderContainer = ProviderContainer(); + void configSetup() { // Pass the configuration to the SDK plugin ActerSdk.setup( diff --git a/app/lib/features/calendar_sync/calendar_sync.dart b/app/lib/features/calendar_sync/calendar_sync.dart index 6ed121af645a..8f3c844d6a8a 100644 --- a/app/lib/features/calendar_sync/calendar_sync.dart +++ b/app/lib/features/calendar_sync/calendar_sync.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'dart:io'; -import 'package:acter/common/extensions/acter_build_context.dart'; import 'package:acter/common/themes/colors/color_scheme.dart'; +import 'package:acter/config/setup.dart'; import 'package:acter/features/calendar_sync/providers/events_to_sync_provider.dart'; import 'package:acter/features/labs/model/labs_features.dart'; import 'package:acter/features/labs/providers/labs_providers.dart'; -import 'package:acter/router/router.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:device_calendar/device_calendar.dart'; @@ -32,7 +31,7 @@ ProviderSubscription>>? _subscription; Future _isEnabled() async { try { - return (await rootNavKey.currentContext! + return (await mainProviderContainer .read(asyncIsActiveProvider(LabsFeature.deviceCalendarSync).future)); } catch (e, s) { _log.severe('Reading current context failed', e, s); @@ -87,9 +86,7 @@ Future initCalendarSync({bool ignoreRejection = false}) async { // clear if it existed before _subscription?.close(); // start listening - _subscription = - ProviderScope.containerOf(rootNavKey.currentContext!, listen: true) - .listen( + _subscription = mainProviderContainer.listen( eventsToSyncProvider, (prev, next) async { final events = next.valueOrNull; diff --git a/app/lib/main.dart b/app/lib/main.dart index 6cdfcbbdb23c..853e1ec58dc2 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -69,6 +69,10 @@ Future _startAppInner(Widget app, bool withSentry) async { app = DesktopSupport(child: app); } + // use the globally defined ProviderContainer + final wrappedApp = + UncontrolledProviderScope(container: mainProviderContainer, child: app); + if (withSentry) { await SentryFlutter.init( (options) { @@ -81,10 +85,10 @@ Future _startAppInner(Widget app, bool withSentry) async { // and prevent reporting otherwise. options.beforeSend = sentryBeforeSend; }, - appRunner: () => runApp(app), + appRunner: () => runApp(wrappedApp), ); } else { - runApp(app); + runApp(wrappedApp); } } diff --git a/app/lib/router/shell_routers/chat_shell_router.dart b/app/lib/router/shell_routers/chat_shell_router.dart index ad4b62693caf..5503216bbccc 100644 --- a/app/lib/router/shell_routers/chat_shell_router.dart +++ b/app/lib/router/shell_routers/chat_shell_router.dart @@ -1,7 +1,7 @@ -import 'package:acter/common/extensions/acter_build_context.dart'; import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/providers/chat_providers.dart'; import 'package:acter/common/utils/routes.dart'; +import 'package:acter/config/setup.dart'; import 'package:acter/features/chat/pages/room_page.dart'; import 'package:acter/features/chat/pages/room_profile_page.dart'; import 'package:acter/features/chat/widgets/chat_layout_builder.dart'; @@ -19,8 +19,7 @@ import 'package:go_router/go_router.dart'; /// the chat-ng feature. Widget _chatLayoutBuilder({Widget? centerChild, Widget? expandedChild}) { final isChatNg = - rootNavKey.currentContext?.read(isActiveProvider(LabsFeature.chatNG)) == - true; + mainProviderContainer.read(isActiveProvider(LabsFeature.chatNG)) == true; return isChatNg ? ChatLayoutBuilder( roomListWidgetBuilder: (s) => RoomsListNGWidget(onSelected: s), @@ -39,9 +38,7 @@ final chatShellRoutes = [ path: Routes.chat.route, redirect: authGuardRedirect, pageBuilder: (context, state) { - rootNavKey.currentContext - ?.read(selectedChatIdProvider.notifier) - .select(null); + mainProviderContainer.read(selectedChatIdProvider.notifier).select(null); return NoTransitionPage( key: state.pageKey, child: _chatLayoutBuilder(), @@ -53,13 +50,13 @@ final chatShellRoutes = [ path: Routes.chatroom.route, redirect: authGuardRedirect, pageBuilder: (context, state) { - final isChatNg = rootNavKey.currentContext - ?.read(isActiveProvider(LabsFeature.chatNG)) == - true; + final isChatNg = + mainProviderContainer.read(isActiveProvider(LabsFeature.chatNG)) == + true; final roomId = state.pathParameters['roomId']!; - rootNavKey.currentContext - ?.read(selectedChatIdProvider.notifier) + mainProviderContainer + .read(selectedChatIdProvider.notifier) .select(roomId); return NoTransitionPage( key: state.pageKey, @@ -82,8 +79,8 @@ final chatShellRoutes = [ pageBuilder: (context, state) { final roomId = state.pathParameters['roomId'] .expect('chatProfile route needs roomId as path param'); - rootNavKey.currentContext - ?.read(selectedChatIdProvider.notifier) + mainProviderContainer + .read(selectedChatIdProvider.notifier) .select(roomId); return NoTransitionPage( key: state.pageKey, @@ -101,8 +98,8 @@ final chatShellRoutes = [ pageBuilder: (context, state) { final roomId = state.pathParameters['roomId'] .expect('chatSettingsVisibility route needs roomId as path param'); - rootNavKey.currentContext - ?.read(selectedChatIdProvider.notifier) + mainProviderContainer + .read(selectedChatIdProvider.notifier) .select(roomId); return NoTransitionPage( key: state.pageKey, diff --git a/app/lib/router/utils.dart b/app/lib/router/utils.dart index 3efc61f0deca..1d636f618b44 100644 --- a/app/lib/router/utils.dart +++ b/app/lib/router/utils.dart @@ -1,7 +1,7 @@ -import 'package:acter/common/extensions/acter_build_context.dart'; import 'package:acter/common/providers/chat_providers.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/config/app_shell.dart'; +import 'package:acter/config/setup.dart'; import 'package:acter/router/providers/router_providers.dart'; import 'package:acter/router/router.dart'; import 'package:flutter/material.dart'; @@ -54,8 +54,7 @@ final chatRoomUriMatcher = RegExp('/chat/.+'); /// helper to figure out how to route to the specific chat room void goToChat(BuildContext localContext, String roomId) { - final context = rootNavKey.currentContext!; - final currentUri = context.read(currentRoutingLocation); + final currentUri = mainProviderContainer.read(currentRoutingLocation); if (!currentUri.startsWith(chatRoomUriMatcher)) { // we are not in a chat room. just a regular push routing // will do @@ -69,13 +68,13 @@ void goToChat(BuildContext localContext, String roomId) { } // we are in a chat page - if (roomId == rootNavKey.currentContext!.read(selectedChatIdProvider)) { + if (roomId == mainProviderContainer.read(selectedChatIdProvider)) { // we are on the same page, nothing to be done return; } // we are on a different chat page. Push replace the current screen - context.pushReplacementNamed( + (rootNavKey.currentContext ?? localContext).pushReplacementNamed( Routes.chatroom.name, pathParameters: {'roomId': roomId}, ); From 05c046b862cde9c543452aa38d7e8c015a8b92da Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 29 Oct 2024 16:30:12 +0000 Subject: [PATCH 30/77] Reporting notification stuff to Sentry --- packages/acter_notifify/lib/acter_notifify.dart | 2 ++ packages/acter_notifify/lib/matrix.dart | 2 ++ packages/acter_notifify/lib/ntfy.dart | 3 +++ packages/acter_notifify/lib/platform/android.dart | 4 ++++ packages/acter_notifify/lib/push.dart | 3 +++ packages/acter_notifify/pubspec.yaml | 1 + 6 files changed, 15 insertions(+) diff --git a/packages/acter_notifify/lib/acter_notifify.dart b/packages/acter_notifify/lib/acter_notifify.dart index 2c366e1a491a..0171e29ebaae 100644 --- a/packages/acter_notifify/lib/acter_notifify.dart +++ b/packages/acter_notifify/lib/acter_notifify.dart @@ -11,6 +11,7 @@ import 'package:acter_notifify/util.dart'; import 'package:acter_notifify/platform/windows.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:logging/logging.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; final _log = Logger('a3::notifify::acter'); @@ -87,6 +88,7 @@ Future initializeNotifify({ ); } catch (error, stack) { final deviceId = client.deviceId().toString(); + Sentry.captureException(error, stackTrace: stack); _log.severe('Failed to setup ntfy for $deviceId', error, stack); } } diff --git a/packages/acter_notifify/lib/matrix.dart b/packages/acter_notifify/lib/matrix.dart index c1a74370b8e8..23aa63dbcd07 100644 --- a/packages/acter_notifify/lib/matrix.dart +++ b/packages/acter_notifify/lib/matrix.dart @@ -14,6 +14,7 @@ import 'package:acter_notifify/platform/windows.dart'; import 'package:convert/convert.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:logging/logging.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; final _log = Logger('a3::notifify::matrix'); int id = 0; @@ -47,6 +48,7 @@ Future handleMatrixMessage( return true; } catch (e, s) { _log.severe('Parsing Notification failed: $message', e, s); + Sentry.captureException(e, stackTrace: s); } return false; } diff --git a/packages/acter_notifify/lib/ntfy.dart b/packages/acter_notifify/lib/ntfy.dart index 81f38fa84ab5..2915e90e1093 100644 --- a/packages/acter_notifify/lib/ntfy.dart +++ b/packages/acter_notifify/lib/ntfy.dart @@ -6,6 +6,7 @@ import 'package:acter_notifify/acter_notifify.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:acter_notifify/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; @@ -58,6 +59,7 @@ Future setupNtfyNotificationsForDevice( ); if (rs.data == null) { _log.severe('Connecting to ntfy server failed: $rs'); + Sentry.captureMessage('Connecting to ntfy server failed: $rs'); return false; } _subscriptions[token] = rs.data!.stream @@ -83,6 +85,7 @@ Future setupNtfyNotificationsForDevice( ); } catch (error, stack) { _log.severe('Failed to show push notification $event', error, stack); + Sentry.captureException(error, stackTrace: stack); } }); return true; diff --git a/packages/acter_notifify/lib/platform/android.dart b/packages/acter_notifify/lib/platform/android.dart index 708774ce57ec..feda9e4c384d 100644 --- a/packages/acter_notifify/lib/platform/android.dart +++ b/packages/acter_notifify/lib/platform/android.dart @@ -5,6 +5,7 @@ import 'package:acter_notifify/local.dart'; import 'package:acter_notifify/matrix.dart'; import 'package:acter_notifify/util.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:logging/logging.dart'; final _log = Logger('a3::notifify::android'); @@ -24,6 +25,7 @@ Future _fetchImage( return ByteArrayAndroidBitmap(image.asTypedList()); } catch (e, s) { _log.severe('fetching image data failed', e, s); + Sentry.captureException(e, stackTrace: s); } } return null; @@ -41,6 +43,7 @@ Future _makeSenderPerson(NotificationItem notification) async { ); } catch (e, s) { _log.severe('fetching image data failed', e, s); + Sentry.captureException(e, stackTrace: s); } } return Person(key: sender.userId(), name: sender.displayName()); @@ -56,6 +59,7 @@ Future _fetchRoomAvatar( return ByteArrayAndroidBitmap(image.asTypedList()); } catch (e, s) { _log.severe('fetching room avatar failed', e, s); + Sentry.captureException(e, stackTrace: s); } } return null; diff --git a/packages/acter_notifify/lib/push.dart b/packages/acter_notifify/lib/push.dart index 645f53d72840..6178c3a2a78e 100644 --- a/packages/acter_notifify/lib/push.dart +++ b/packages/acter_notifify/lib/push.dart @@ -6,6 +6,7 @@ import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:acter_notifify/acter_notifify.dart'; import 'package:acter_notifify/matrix.dart'; import 'package:logging/logging.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:push/push.dart'; final _log = Logger('a3::notifify::push'); @@ -77,12 +78,14 @@ Future initializePush({ pushServerUrl: pushServerUrl); } catch (error, st) { _log.severe('Setting token for $deviceId failed', error, st); + Sentry.captureException(error, stackTrace: st); } } }); } catch (e, s) { // this fails on hot-reload and in integration tests... if so, ignore for now _log.severe('Push initialization error', e, s); + Sentry.captureException(e, stackTrace: s); } } diff --git a/packages/acter_notifify/pubspec.yaml b/packages/acter_notifify/pubspec.yaml index 107ab1b7b4ff..5c89d00246b9 100644 --- a/packages/acter_notifify/pubspec.yaml +++ b/packages/acter_notifify/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: dio: ^5.5.0+1 windows_notification: ^1.2.0 app_badge_plus: ^1.1.5 + sentry: any dev_dependencies: flutter_test: From 578099c219cb091d7d5d99673f988963571f9466 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 29 Oct 2024 16:50:43 +0000 Subject: [PATCH 31/77] Add missing mock --- .../features/comments/comment_list_test.dart | 244 +++++++++--------- 1 file changed, 125 insertions(+), 119 deletions(-) diff --git a/app/test/features/comments/comment_list_test.dart b/app/test/features/comments/comment_list_test.dart index a854d39eef48..3ee62d88df07 100644 --- a/app/test/features/comments/comment_list_test.dart +++ b/app/test/features/comments/comment_list_test.dart @@ -1,4 +1,6 @@ +import 'package:acter/common/providers/room_providers.dart'; import 'package:acter/features/comments/widgets/comment_list_widget.dart'; +import 'package:acter_avatar/acter_avatar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -13,150 +15,154 @@ import 'mock_data/mock_comments_manager.dart'; import 'mock_data/mock_message_content.dart'; void main() { - testWidgets('displays empty state when there are no comments', - (tester) async { - // Arrange - final mockCommentsManager = MockCommentsManager(); + group('Comment List', () { + testWidgets('displays empty state when there are no comments', + (tester) async { + // Arrange + final mockCommentsManager = MockCommentsManager(); - // Build the widget tree with the mocked provider - await tester.pumpWidget( - ProviderScope( - overrides: [ - commentsListProvider.overrideWith((ref, manager) async => []), - ], - child: MaterialApp( - localizationsDelegates: const [L10n.delegate], - home: Scaffold( - body: CommentListWidget( - manager: mockCommentsManager, // Provide the mock manager + // Build the widget tree with the mocked provider + await tester.pumpWidget( + ProviderScope( + overrides: [ + commentsListProvider.overrideWith((ref, manager) async => []), + ], + child: MaterialApp( + localizationsDelegates: const [L10n.delegate], + home: Scaffold( + body: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager + ), ), ), ), - ), - ); + ); - // Act - await tester.pumpAndSettle(); // Allow the widget to settle + // Act + await tester.pumpAndSettle(); // Allow the widget to settle - // Assert - expect( - find.byType(CommentListEmptyStateWidget), - findsOneWidget, - ); // Ensure the empty state widget is displayed - }); + // Assert + expect( + find.byType(CommentListEmptyStateWidget), + findsOneWidget, + ); // Ensure the empty state widget is displayed + }); - testWidgets( - 'displays error state when there are issue in loading comment list', - (tester) async { - // Arrange - final mockCommentsManager = MockCommentsManager(); + testWidgets( + 'displays error state when there are issue in loading comment list', + (tester) async { + // Arrange + final mockCommentsManager = MockCommentsManager(); - // Build the widget tree with the mocked provider - await tester.pumpWidget( - ProviderScope( - overrides: [ - commentsListProvider - .overrideWith((ref, manager) async => throw 'Some Error'), - ], - child: MaterialApp( - localizationsDelegates: const [L10n.delegate], - home: Scaffold( - body: CommentListWidget( - manager: mockCommentsManager, // Provide the mock manager + // Build the widget tree with the mocked provider + await tester.pumpWidget( + ProviderScope( + overrides: [ + commentsListProvider + .overrideWith((ref, manager) async => throw 'Some Error'), + ], + child: MaterialApp( + localizationsDelegates: const [L10n.delegate], + home: Scaffold( + body: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager + ), ), ), ), - ), - ); - await tester.ensureErrorPageWorks(); - }); + ); + await tester.ensureErrorPageWorks(); + }); - testWidgets( - 'displays error state when there are issue in loading comment list and also test retry', - (tester) async { - bool shouldFail = true; + testWidgets( + 'displays error state when there are issue in loading comment list and also test retry', + (tester) async { + bool shouldFail = true; - // Arrange - final mockCommentsManager = MockCommentsManager(); + // Arrange + final mockCommentsManager = MockCommentsManager(); - // Build the widget tree with the mocked provider - await tester.pumpWidget( - ProviderScope( - overrides: [ - commentsListProvider.overrideWith((ref, manager) async { - if (shouldFail) { - shouldFail = false; - throw 'Some Error'; - } else { - return []; - } - }), - ], - child: MaterialApp( - localizationsDelegates: const [L10n.delegate], - home: Scaffold( - body: CommentListWidget( - manager: mockCommentsManager, // Provide the mock manager + // Build the widget tree with the mocked provider + await tester.pumpWidget( + ProviderScope( + overrides: [ + commentsListProvider.overrideWith((ref, manager) async { + if (shouldFail) { + shouldFail = false; + throw 'Some Error'; + } else { + return []; + } + }), + ], + child: MaterialApp( + localizationsDelegates: const [L10n.delegate], + home: Scaffold( + body: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager + ), ), ), ), - ), - ); - await tester.ensureErrorPageWithRetryWorks(); - }); + ); + await tester.ensureErrorPageWithRetryWorks(); + }); - testWidgets('displays list state when there are comments', (tester) async { - // Arrange + testWidgets('displays list state when there are comments', (tester) async { + // Arrange - final mockUser1 = MockComment( - fakeSender: MockUserId(), - fakeMsgContent: MockMsgContent(bodyText: 'message 1'), - fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, - ); - final mockUser2 = MockComment( - fakeSender: MockUserId(), - fakeMsgContent: MockMsgContent(bodyText: 'message 2'), - fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, - ); - final mockUser3 = MockComment( - fakeSender: MockUserId(), - fakeMsgContent: MockMsgContent(bodyText: 'message 3'), - fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, - ); + final mockComment1 = MockComment( + fakeSender: MockUserId(), + fakeMsgContent: MockMsgContent(bodyText: 'message 1'), + fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, + ); + final mockComment2 = MockComment( + fakeSender: MockUserId(), + fakeMsgContent: MockMsgContent(bodyText: 'message 2'), + fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, + ); + final mockComment3 = MockComment( + fakeSender: MockUserId(), + fakeMsgContent: MockMsgContent(bodyText: 'message 3'), + fakeOriginServerTs: DateTime.now().millisecondsSinceEpoch, + ); - final mockCommentsManager = MockCommentsManager(); - when(() => mockCommentsManager.roomIdStr()).thenReturn('roomId'); + final mockCommentsManager = MockCommentsManager(); + when(() => mockCommentsManager.roomIdStr()).thenReturn('roomId'); - // Build the widget tree with the mocked provider - await tester.pumpWidget( - ProviderScope( - overrides: [ - commentsListProvider.overrideWith( - (ref, manager) async => [ - mockUser1, - mockUser2, - mockUser3, - ], - ), - ], - child: MaterialApp( - localizationsDelegates: const [L10n.delegate], - home: Scaffold( - body: CommentListWidget( - manager: mockCommentsManager, // Provide the mock manager + // Build the widget tree with the mocked provider + await tester.pumpWidget( + ProviderScope( + overrides: [ + memberAvatarInfoProvider + .overrideWith((a, i) => const AvatarInfo(uniqueId: 'uniqueId')), + commentsListProvider.overrideWith( + (ref, manager) async => [ + mockComment1, + mockComment2, + mockComment3, + ], + ), + ], + child: MaterialApp( + localizationsDelegates: const [L10n.delegate], + home: Scaffold( + body: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager + ), ), ), ), - ), - ); + ); - // Act - await tester.pumpAndSettle(); // Allow the widget to settle + // Act + await tester.pumpAndSettle(); // Allow the widget to settle - // Assert - expect( - find.byType(CommentListEmptyStateWidget), - findsNothing, - ); // Ensure the empty state widget is displayed + // Assert + expect( + find.byType(CommentListEmptyStateWidget), + findsNothing, + ); // Ensure the empty state widget is displayed + }); }); } From e17d60c4bd6b5e8943d1d20e2913e2fc2aff0c85 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Wed, 30 Oct 2024 11:49:32 +0530 Subject: [PATCH 32/77] PR Feedback Points --- .../widgets/message_content_widget.dart | 22 ++++++++++ app/lib/common/widgets/time_ago_widget.dart | 24 ++++++++++ .../widgets/user_display_name_widget.dart | 21 +++++++++ app/lib/common/widgets/user_id_widget.dart | 18 ++++++++ .../comments/actions/sbumit_comment.dart | 22 ++++------ .../comments/widgets/add_comment_widget.dart | 21 +++++---- .../comments/widgets/comment_item_widget.dart | 44 ++++--------------- 7 files changed, 115 insertions(+), 57 deletions(-) create mode 100644 app/lib/common/widgets/message_content_widget.dart create mode 100644 app/lib/common/widgets/time_ago_widget.dart create mode 100644 app/lib/common/widgets/user_display_name_widget.dart create mode 100644 app/lib/common/widgets/user_id_widget.dart diff --git a/app/lib/common/widgets/message_content_widget.dart b/app/lib/common/widgets/message_content_widget.dart new file mode 100644 index 000000000000..88928b76e1ff --- /dev/null +++ b/app/lib/common/widgets/message_content_widget.dart @@ -0,0 +1,22 @@ +import 'package:acter/common/widgets/render_html.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:flutter/material.dart'; + +class MessageContentWidget extends StatelessWidget { + final MsgContent msgContent; + + const MessageContentWidget({ + super.key, + required this.msgContent, + }); + + @override + Widget build(BuildContext context) { + final formatted = msgContent.formattedBody(); + final messageTextStyle = Theme.of(context).textTheme.bodyMedium; + + return formatted != null + ? RenderHtml(text: formatted, defaultTextStyle: messageTextStyle) + : Text(msgContent.body(), style: messageTextStyle); + } +} diff --git a/app/lib/common/widgets/time_ago_widget.dart b/app/lib/common/widgets/time_ago_widget.dart new file mode 100644 index 000000000000..5fe53be3e911 --- /dev/null +++ b/app/lib/common/widgets/time_ago_widget.dart @@ -0,0 +1,24 @@ +import 'package:dart_date/dart_date.dart'; +import 'package:flutter/material.dart'; + +class TimeAgoWidget extends StatelessWidget { + final int originServerTs; + final TextStyle? textStyle; + + const TimeAgoWidget({ + super.key, + required this.originServerTs, + this.textStyle, + }); + + @override + Widget build(BuildContext context) { + final originServerDateTime = + DateTime.fromMillisecondsSinceEpoch(originServerTs, isUtc: true); + final time = originServerDateTime.toLocal().timeago(); + return Text( + time, + style: textStyle ?? Theme.of(context).textTheme.labelMedium, + ); + } +} diff --git a/app/lib/common/widgets/user_display_name_widget.dart b/app/lib/common/widgets/user_display_name_widget.dart new file mode 100644 index 000000000000..003e1c57cc91 --- /dev/null +++ b/app/lib/common/widgets/user_display_name_widget.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class UserDisplayNameWidget extends StatelessWidget { + final String displayName; + final TextStyle? textStyle; + + const UserDisplayNameWidget({ + super.key, + required this.displayName, + this.textStyle, + }); + + @override + Widget build(BuildContext context) { + final displayNameTextStyle = Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(fontWeight: FontWeight.bold); + return Text(displayName, style: textStyle ?? displayNameTextStyle); + } +} diff --git a/app/lib/common/widgets/user_id_widget.dart b/app/lib/common/widgets/user_id_widget.dart new file mode 100644 index 000000000000..df9171a06eff --- /dev/null +++ b/app/lib/common/widgets/user_id_widget.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class UserIdWidget extends StatelessWidget { + final String userId; + final TextStyle? textStyle; + + const UserIdWidget({ + super.key, + required this.userId, + this.textStyle, + }); + + @override + Widget build(BuildContext context) { + final userNameTextStyle = Theme.of(context).textTheme.labelMedium; + return Text(userId, style: textStyle ?? userNameTextStyle); + } +} diff --git a/app/lib/features/comments/actions/sbumit_comment.dart b/app/lib/features/comments/actions/sbumit_comment.dart index 99359b57f50d..8a70ed5e3660 100644 --- a/app/lib/features/comments/actions/sbumit_comment.dart +++ b/app/lib/features/comments/actions/sbumit_comment.dart @@ -6,37 +6,31 @@ import 'package:logging/logging.dart'; final _log = Logger('a3::submit::comment'); -Future submitComment( - BuildContext context, +Future submitComment( + L10n lang, String plainDescription, String htmlBodyDescription, CommentsManager manager, ) async { - final lang = L10n.of(context); - if (plainDescription.isEmpty) { + final trimmedPlainText = plainDescription.trim(); + if (trimmedPlainText.isEmpty) { EasyLoading.showToast(lang.youNeedToEnterAComment); - return; + return false; } EasyLoading.show(status: lang.submittingComment); try { final draft = manager.commentDraft(); - draft.contentFormatted(plainDescription, htmlBodyDescription); + draft.contentFormatted(trimmedPlainText, htmlBodyDescription); await draft.send(); FocusManager.instance.primaryFocus?.unfocus(); - if (!context.mounted) { - EasyLoading.dismiss(); - return; - } EasyLoading.showToast(lang.commentSubmitted); + return true; } catch (e, s) { _log.severe('Failed to submit comment', e, s); - if (!context.mounted) { - EasyLoading.dismiss(); - return; - } EasyLoading.showError( lang.errorSubmittingComment(e), duration: const Duration(seconds: 3), ); + return false; } } diff --git a/app/lib/features/comments/widgets/add_comment_widget.dart b/app/lib/features/comments/widgets/add_comment_widget.dart index c1a94587ce8a..39a8bc18bc50 100644 --- a/app/lib/features/comments/widgets/add_comment_widget.dart +++ b/app/lib/features/comments/widgets/add_comment_widget.dart @@ -59,12 +59,14 @@ class _AddCommentWidgetState extends ConsumerState { bottomSheetTitle: L10n.of(context).addComment, descriptionHtmlValue: _commentController.text, onSave: (htmlBodyDescription, plainDescription) async { - await addComment( + final success = await addComment( plainDescription: plainDescription, htmlBodyDescription: htmlBodyDescription, ); - if (!context.mounted) return; - Navigator.pop(context); + if (success) { + if (!context.mounted) return; + Navigator.pop(context); + } }, ), icon: const Icon(Atlas.arrows_up_right_down_left, size: 14), @@ -96,17 +98,20 @@ class _AddCommentWidgetState extends ConsumerState { ); } - Future addComment({ + Future addComment({ required String plainDescription, String? htmlBodyDescription, }) async { - await submitComment( - context, + final success = await submitComment( + L10n.of(context), plainDescription, htmlBodyDescription ?? plainDescription, widget.manager, ); - _commentController.clear(); - showSendButton.value = false; + if (success) { + _commentController.clear(); + showSendButton.value = false; + } + return success; } } diff --git a/app/lib/features/comments/widgets/comment_item_widget.dart b/app/lib/features/comments/widgets/comment_item_widget.dart index 9d86961bf521..4dcbfa942844 100644 --- a/app/lib/features/comments/widgets/comment_item_widget.dart +++ b/app/lib/features/comments/widgets/comment_item_widget.dart @@ -1,8 +1,10 @@ import 'package:acter/common/providers/room_providers.dart'; -import 'package:acter/common/widgets/render_html.dart'; +import 'package:acter/common/widgets/message_content_widget.dart'; +import 'package:acter/common/widgets/time_ago_widget.dart'; +import 'package:acter/common/widgets/user_display_name_widget.dart'; +import 'package:acter/common/widgets/user_id_widget.dart'; import 'package:acter_avatar/acter_avatar.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; -import 'package:dart_date/dart_date.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -37,9 +39,9 @@ class CommentItemWidget extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ userNameUI(context, avatarInfo), - messageContentUI(context), + MessageContentWidget(msgContent: comment.msgContent()), const SizedBox(height: 4), - messageTimeUI(context), + TimeAgoWidget(originServerTs: comment.originServerTs()), ], ), ), @@ -54,42 +56,14 @@ class CommentItemWidget extends ConsumerWidget { } Widget userNameUI(BuildContext context, AvatarInfo avatarInfo) { - final userId = comment.sender().toString(); + final userId = avatarInfo.uniqueId; final displayName = avatarInfo.displayName; - final displayNameTextStyle = Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(fontWeight: FontWeight.bold); - final usrNameTextStyle = Theme.of(context).textTheme.labelMedium; - return Wrap( children: [ - Text(displayName ?? userId, style: displayNameTextStyle), + UserDisplayNameWidget(displayName: displayName ?? userId), const SizedBox(width: 8), - if (displayName != null) Text(userId, style: usrNameTextStyle), + if (displayName != null) UserIdWidget(userId: userId), ], ); } - - Widget messageContentUI(BuildContext context) { - final msgContent = comment.msgContent(); - final formatted = msgContent.formattedBody(); - final messageTextStyle = Theme.of(context).textTheme.bodyMedium; - - return formatted != null - ? RenderHtml(text: formatted, defaultTextStyle: messageTextStyle) - : Text(msgContent.body(), style: messageTextStyle); - } - - Widget messageTimeUI(BuildContext context) { - final commentTime = DateTime.fromMillisecondsSinceEpoch( - comment.originServerTs(), - isUtc: true, - ); - final time = commentTime.toLocal().timeago(); - return Text( - time, - style: Theme.of(context).textTheme.labelMedium, - ); - } } From 3cf3dcd2301aa62387d0fe98be0ca8f4122ed349 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Wed, 30 Oct 2024 11:55:10 +0530 Subject: [PATCH 33/77] Sort comment list by ASC of time --- app/lib/features/comments/providers/comments_providers.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/lib/features/comments/providers/comments_providers.dart b/app/lib/features/comments/providers/comments_providers.dart index 4fa4f665d52d..eee22d554df7 100644 --- a/app/lib/features/comments/providers/comments_providers.dart +++ b/app/lib/features/comments/providers/comments_providers.dart @@ -40,7 +40,11 @@ class AsyncCommentsManagerNotifier extends AutoDisposeFamilyAsyncNotifier< final commentsListProvider = FutureProvider.family .autoDispose, CommentsManager>((ref, manager) async { - return (await manager.comments()).toList(); + final commentList = (await manager.comments()).toList(); + commentList.sort( + (a, b) => a.originServerTs().compareTo(b.originServerTs()), + ); + return commentList; }); final newsCommentsCountProvider = From c0cc443475a2f385710602bd2eb1e7e46d5aac55 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Wed, 30 Oct 2024 12:08:37 +0530 Subject: [PATCH 34/77] PR Feedback points --- .../features/comments/add_comment_test.dart | 74 +++++------ .../features/comments/comment_item_test.dart | 30 ++--- .../features/comments/comment_list_test.dart | 117 +++++++----------- 3 files changed, 84 insertions(+), 137 deletions(-) diff --git a/app/test/features/comments/add_comment_test.dart b/app/test/features/comments/add_comment_test.dart index a25fea42c364..f5f1e48a15fa 100644 --- a/app/test/features/comments/add_comment_test.dart +++ b/app/test/features/comments/add_comment_test.dart @@ -2,11 +2,10 @@ import 'package:acter/common/providers/common_providers.dart'; import 'package:acter/features/comments/widgets/add_comment_widget.dart'; import 'package:acter_avatar/acter_avatar.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; + import '../../common/mock_data/mock_avatar_info.dart'; +import '../../helpers/test_util.dart'; import 'mock_data/mock_comments_manager.dart'; void main() { @@ -17,57 +16,40 @@ void main() { mockCommentsManager = MockCommentsManager(); mockAvatarInfo = MockAvatarInfo(); }); - - testWidgets('should display avatar and comment input', - (WidgetTester tester) async { - await tester.pumpWidget( - ProviderScope( + group('Add Comment', () { + testWidgets('should display avatar and comment input', + (WidgetTester tester) async { + await tester.pumpProviderWidget( overrides: [ - // Use overrideWith for the AutoDisposeStateProvider with a mock value accountAvatarInfoProvider.overrideWith((ref) => mockAvatarInfo), ], - child: MaterialApp( - localizationsDelegates: const [L10n.delegate], - builder: EasyLoading.init(), - home: Scaffold( - body: AddCommentWidget(manager: mockCommentsManager), - ), - ), - ), - ); // Dummy avatar info + child: AddCommentWidget(manager: mockCommentsManager), + ); - // Check if avatar is displayed - expect(find.byType(ActerAvatar), findsOneWidget); + // Check if avatar is displayed + expect(find.byType(ActerAvatar), findsOneWidget); - // Check if comment input field is displayed - expect(find.byType(TextField), findsOneWidget); - }); + // Check if comment input field is displayed + expect(find.byType(TextField), findsOneWidget); + }); - testWidgets('send button appears when text is entered', - (WidgetTester tester) async { - await tester.pumpWidget( - ProviderScope( + testWidgets('send button appears when text is entered', + (WidgetTester tester) async { + await tester.pumpProviderWidget( overrides: [ - // Use overrideWith for the AutoDisposeStateProvider with a mock value accountAvatarInfoProvider.overrideWith((ref) => mockAvatarInfo), ], - child: MaterialApp( - localizationsDelegates: const [L10n.delegate], - builder: EasyLoading.init(), - home: Scaffold( - body: AddCommentWidget(manager: mockCommentsManager), - ), - ), - ), - ); - // Initially, send button should not be visible - expect(find.byKey(AddCommentWidget.addCommentButton), findsNothing); - - // Enter text in the comment box - await tester.enterText(find.byType(TextField), 'Test comment'); - await tester.pump(); - - // Send button should now be visible - expect(find.byKey(AddCommentWidget.addCommentButton), findsOneWidget); + child: AddCommentWidget(manager: mockCommentsManager), + ); + // Initially, send button should not be visible + expect(find.byKey(AddCommentWidget.addCommentButton), findsNothing); + + // Enter text in the comment box + await tester.enterText(find.byType(TextField), 'Test comment'); + await tester.pump(); + + // Send button should now be visible + expect(find.byKey(AddCommentWidget.addCommentButton), findsOneWidget); + }); }); } diff --git a/app/test/features/comments/comment_item_test.dart b/app/test/features/comments/comment_item_test.dart index a339a2b708b8..41bd919da66e 100644 --- a/app/test/features/comments/comment_item_test.dart +++ b/app/test/features/comments/comment_item_test.dart @@ -3,15 +3,15 @@ import 'package:acter/common/providers/room_providers.dart'; import 'package:acter/common/widgets/render_html.dart'; import 'package:acter/features/comments/widgets/comment_item_widget.dart'; import 'package:acter_avatar/acter_avatar.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mocktail/mocktail.dart'; + +import '../../common/mock_data/mock_avatar_info.dart'; +import '../../common/mock_data/mock_user_id.dart'; +import '../../helpers/test_util.dart'; import 'mock_data/mock_comment.dart'; import 'mock_data/mock_comments_manager.dart'; import 'mock_data/mock_message_content.dart'; -import '../../common/mock_data/mock_user_id.dart'; -import '../../common/mock_data/mock_avatar_info.dart'; void main() { late MockComment mockComment; @@ -36,20 +36,14 @@ void main() { 'renders CommentItemWidget with avatar, name, content, and timestamp', (WidgetTester tester) async { // Wrap in ProviderScope and override the necessary providers - await tester.pumpWidget( - ProviderScope( - overrides: [ - memberAvatarInfoProvider - .overrideWith((ref, MemberInfo memberInfo) => mockAvatarInfo), - ], - child: MaterialApp( - home: Scaffold( - body: CommentItemWidget( - comment: mockComment, - manager: mockCommentsManager, - ), - ), - ), + await tester.pumpProviderWidget( + overrides: [ + memberAvatarInfoProvider + .overrideWith((ref, MemberInfo memberInfo) => mockAvatarInfo), + ], + child: CommentItemWidget( + comment: mockComment, + manager: mockCommentsManager, ), ); diff --git a/app/test/features/comments/comment_list_test.dart b/app/test/features/comments/comment_list_test.dart index 3ee62d88df07..5d82121bcf9b 100644 --- a/app/test/features/comments/comment_list_test.dart +++ b/app/test/features/comments/comment_list_test.dart @@ -1,15 +1,14 @@ import 'package:acter/common/providers/room_providers.dart'; +import 'package:acter/features/comments/providers/comments_providers.dart'; +import 'package:acter/features/comments/widgets/comment_list_empty_state_widget.dart'; import 'package:acter/features/comments/widgets/comment_list_widget.dart'; import 'package:acter_avatar/acter_avatar.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:acter/features/comments/providers/comments_providers.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:acter/features/comments/widgets/comment_list_empty_state_widget.dart'; import 'package:mocktail/mocktail.dart'; + import '../../common/mock_data/mock_user_id.dart'; import '../../helpers/error_helpers.dart'; +import '../../helpers/test_util.dart'; import 'mock_data/mock_comment.dart'; import 'mock_data/mock_comments_manager.dart'; import 'mock_data/mock_message_content.dart'; @@ -22,19 +21,12 @@ void main() { final mockCommentsManager = MockCommentsManager(); // Build the widget tree with the mocked provider - await tester.pumpWidget( - ProviderScope( - overrides: [ - commentsListProvider.overrideWith((ref, manager) async => []), - ], - child: MaterialApp( - localizationsDelegates: const [L10n.delegate], - home: Scaffold( - body: CommentListWidget( - manager: mockCommentsManager, // Provide the mock manager - ), - ), - ), + await tester.pumpProviderWidget( + overrides: [ + commentsListProvider.overrideWith((ref, manager) async => []), + ], + child: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager ), ); @@ -55,20 +47,13 @@ void main() { final mockCommentsManager = MockCommentsManager(); // Build the widget tree with the mocked provider - await tester.pumpWidget( - ProviderScope( - overrides: [ - commentsListProvider - .overrideWith((ref, manager) async => throw 'Some Error'), - ], - child: MaterialApp( - localizationsDelegates: const [L10n.delegate], - home: Scaffold( - body: CommentListWidget( - manager: mockCommentsManager, // Provide the mock manager - ), - ), - ), + await tester.pumpProviderWidget( + overrides: [ + commentsListProvider + .overrideWith((ref, manager) async => throw 'Some Error'), + ], + child: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager ), ); await tester.ensureErrorPageWorks(); @@ -83,26 +68,19 @@ void main() { final mockCommentsManager = MockCommentsManager(); // Build the widget tree with the mocked provider - await tester.pumpWidget( - ProviderScope( - overrides: [ - commentsListProvider.overrideWith((ref, manager) async { - if (shouldFail) { - shouldFail = false; - throw 'Some Error'; - } else { - return []; - } - }), - ], - child: MaterialApp( - localizationsDelegates: const [L10n.delegate], - home: Scaffold( - body: CommentListWidget( - manager: mockCommentsManager, // Provide the mock manager - ), - ), - ), + await tester.pumpProviderWidget( + overrides: [ + commentsListProvider.overrideWith((ref, manager) async { + if (shouldFail) { + shouldFail = false; + throw 'Some Error'; + } else { + return []; + } + }), + ], + child: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager ), ); await tester.ensureErrorPageWithRetryWorks(); @@ -131,30 +109,23 @@ void main() { when(() => mockCommentsManager.roomIdStr()).thenReturn('roomId'); // Build the widget tree with the mocked provider - await tester.pumpWidget( - ProviderScope( - overrides: [ - memberAvatarInfoProvider - .overrideWith((a, i) => const AvatarInfo(uniqueId: 'uniqueId')), - commentsListProvider.overrideWith( - (ref, manager) async => [ - mockComment1, - mockComment2, - mockComment3, - ], - ), - ], - child: MaterialApp( - localizationsDelegates: const [L10n.delegate], - home: Scaffold( - body: CommentListWidget( - manager: mockCommentsManager, // Provide the mock manager - ), - ), + // Build the widget tree with the mocked provider + await tester.pumpProviderWidget( + overrides: [ + memberAvatarInfoProvider + .overrideWith((a, i) => const AvatarInfo(uniqueId: 'uniqueId')), + commentsListProvider.overrideWith( + (ref, manager) async => [ + mockComment1, + mockComment2, + mockComment3, + ], ), + ], + child: CommentListWidget( + manager: mockCommentsManager, // Provide the mock manager ), ); - // Act await tester.pumpAndSettle(); // Allow the widget to settle From 1e4f8299095cfbfff7755dc08e8feba7dcd2d35c Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Wed, 30 Oct 2024 12:13:35 +0530 Subject: [PATCH 35/77] Minor fixe --- app/test/features/comments/comment_list_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/app/test/features/comments/comment_list_test.dart b/app/test/features/comments/comment_list_test.dart index 5d82121bcf9b..7b8c6d193ab3 100644 --- a/app/test/features/comments/comment_list_test.dart +++ b/app/test/features/comments/comment_list_test.dart @@ -108,7 +108,6 @@ void main() { final mockCommentsManager = MockCommentsManager(); when(() => mockCommentsManager.roomIdStr()).thenReturn('roomId'); - // Build the widget tree with the mocked provider // Build the widget tree with the mocked provider await tester.pumpProviderWidget( overrides: [ From 899d0d1545ba155b2be61ff078244903bf92bd4c Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Wed, 30 Oct 2024 12:21:35 +0530 Subject: [PATCH 36/77] Minor change in empty state --- .../comments/widgets/comment_list_empty_state_widget.dart | 2 +- .../features/comments/widgets/comments_section_widget.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart b/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart index 16335d379c8b..46cd517e7c76 100644 --- a/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart +++ b/app/lib/features/comments/widgets/comment_list_empty_state_widget.dart @@ -36,7 +36,7 @@ class CommentListEmptyStateWidget extends StatelessWidget { Widget compactEmptyState(BuildContext context, Color color) { return Padding( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 12), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/app/lib/features/comments/widgets/comments_section_widget.dart b/app/lib/features/comments/widgets/comments_section_widget.dart index e9b5623ec3ee..fd59c184011f 100644 --- a/app/lib/features/comments/widgets/comments_section_widget.dart +++ b/app/lib/features/comments/widgets/comments_section_widget.dart @@ -66,14 +66,14 @@ class CommentsSectionWidget extends ConsumerWidget { if (shrinkWrap) { return CommentListWidget( manager: commentManager, - useCompactEmptyState: useCompactEmptyState, + useCompactEmptyState: true, ); } return Expanded( child: CommentListWidget( manager: commentManager, shrinkWrap: shrinkWrap, - useCompactEmptyState: useCompactEmptyState, + useCompactEmptyState: true, ), ); } From 66d11360e795b9498b38dfe161c640e587a7b44f Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Wed, 30 Oct 2024 12:24:16 +0530 Subject: [PATCH 37/77] Minor change in attachment header --- .../attachments/widgets/attachment_section.dart | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/lib/features/attachments/widgets/attachment_section.dart b/app/lib/features/attachments/widgets/attachment_section.dart index b6c7e65d4026..fc6760b063c2 100644 --- a/app/lib/features/attachments/widgets/attachment_section.dart +++ b/app/lib/features/attachments/widgets/attachment_section.dart @@ -6,7 +6,6 @@ import 'package:acter/features/attachments/providers/attachment_providers.dart'; import 'package:acter/features/attachments/widgets/attachment_item.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' show Attachment, AttachmentsManager; -import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -122,15 +121,10 @@ class FoundAttachmentSectionWidget extends ConsumerWidget { Widget attachmentHeader(BuildContext context, WidgetRef ref) { final lang = L10n.of(context); - final attachmentTitleTextStyle = Theme.of(context).textTheme.labelLarge; + final attachmentTitleTextStyle = Theme.of(context).textTheme.titleSmall; return Row( children: [ - const Icon(Atlas.paperclip_attachment_thin, size: 14), - const SizedBox(width: 5), - Text( - lang.attachments, - style: attachmentTitleTextStyle, - ), + Text(lang.attachments, style: attachmentTitleTextStyle), const Spacer(), ActerInlineTextButton( onPressed: () => selectAttachment( From 3923daefa5b4803af863434f56f60d8ad6cc0e75 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Wed, 30 Oct 2024 12:29:03 +0530 Subject: [PATCH 38/77] Add changelog data --- .changes/2310-comment-on-boost.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/2310-comment-on-boost.md diff --git a/.changes/2310-comment-on-boost.md b/.changes/2310-comment-on-boost.md new file mode 100644 index 000000000000..8bc2a3605d94 --- /dev/null +++ b/.changes/2310-comment-on-boost.md @@ -0,0 +1 @@ +- [New] : Boost getting even better with comments. \ No newline at end of file From 11b9db228887bf1082adf8cf1905dda9426592d2 Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Wed, 30 Oct 2024 12:33:58 +0530 Subject: [PATCH 39/77] Minor fixes --- .../features/comments/widgets/comments_section_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/features/comments/widgets/comments_section_widget.dart b/app/lib/features/comments/widgets/comments_section_widget.dart index fd59c184011f..e9b5623ec3ee 100644 --- a/app/lib/features/comments/widgets/comments_section_widget.dart +++ b/app/lib/features/comments/widgets/comments_section_widget.dart @@ -66,14 +66,14 @@ class CommentsSectionWidget extends ConsumerWidget { if (shrinkWrap) { return CommentListWidget( manager: commentManager, - useCompactEmptyState: true, + useCompactEmptyState: useCompactEmptyState, ); } return Expanded( child: CommentListWidget( manager: commentManager, shrinkWrap: shrinkWrap, - useCompactEmptyState: true, + useCompactEmptyState: useCompactEmptyState, ), ); } From 1edd0621cca9de970a0ba6ff469ef60897551113 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 30 Oct 2024 12:18:36 +0000 Subject: [PATCH 40/77] Promote DeviceCalendarSync to an always-on labs feature --- app/lib/features/labs/model/labs_features.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/features/labs/model/labs_features.dart b/app/lib/features/labs/model/labs_features.dart index f97b7dfc4067..6a184f745e69 100644 --- a/app/lib/features/labs/model/labs_features.dart +++ b/app/lib/features/labs/model/labs_features.dart @@ -31,6 +31,7 @@ enum LabsFeature { static List get releaseDefaults => [ LabsFeature.mobilePushNotifications, + LabsFeature.deviceCalendarSync, ]; static List get nightlyDefaults => [ From 1c170a739ea32786cb552884b56f50d4d62c6368 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 30 Oct 2024 12:22:06 +0000 Subject: [PATCH 41/77] Remove notifications from lab settings It is still possible to disable it via the Settings->Notifications --- app/lib/features/settings/pages/labs_page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/app/lib/features/settings/pages/labs_page.dart b/app/lib/features/settings/pages/labs_page.dart index 6a11204e1a43..ddc866d7ede5 100644 --- a/app/lib/features/settings/pages/labs_page.dart +++ b/app/lib/features/settings/pages/labs_page.dart @@ -32,7 +32,6 @@ class SettingsLabsPage extends ConsumerWidget { SettingsSection( title: Text(lang.labsAppFeatures), tiles: [ - const LabsNotificationsSettingsTile(), SettingsTile.switchTile( title: Text(lang.encryptionBackupKeyBackup), description: Text(lang.sharedCalendarAndEvents), From 7e3e85f39fac413d83decd12707248a55dc12eb4 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 30 Oct 2024 12:40:38 +0000 Subject: [PATCH 42/77] Changelog --- .changes/2333-calendar-device-sync.md | 1 + app/lib/features/settings/pages/labs_page.dart | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 .changes/2333-calendar-device-sync.md diff --git a/.changes/2333-calendar-device-sync.md b/.changes/2333-calendar-device-sync.md new file mode 100644 index 000000000000..002676bb0d71 --- /dev/null +++ b/.changes/2333-calendar-device-sync.md @@ -0,0 +1 @@ +- [Labs] calendar events should now show up in your device calendar (unless you have rejected them) by default. This is sill under labs, if you run into trouble you can disable it from the Settings -> labs and let us know what problem you had. \ No newline at end of file diff --git a/app/lib/features/settings/pages/labs_page.dart b/app/lib/features/settings/pages/labs_page.dart index ddc866d7ede5..687dc4730f82 100644 --- a/app/lib/features/settings/pages/labs_page.dart +++ b/app/lib/features/settings/pages/labs_page.dart @@ -4,7 +4,6 @@ import 'package:acter/features/calendar_sync/calendar_sync.dart'; import 'package:acter/features/labs/model/labs_features.dart'; import 'package:acter/features/labs/providers/labs_providers.dart'; import 'package:acter/features/settings/pages/settings_page.dart'; -import 'package:acter/features/settings/widgets/labs_notifications_settings_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; From a00ac4bc10c32e6ac09d59809b0541f83a4862b7 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 23 Oct 2024 17:01:42 +0000 Subject: [PATCH 43/77] Translated using Weblate (German) Currently translated at 100.0% (1122 of 1122 strings) Translation: Acter/App Translate-URL: http://weblate.acter.global/projects/acter-app/flutter-app/de/ --- app/lib/l10n/app_de.arb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/lib/l10n/app_de.arb b/app/lib/l10n/app_de.arb index f34d976458e5..7b26de9cbf83 100644 --- a/app/lib/l10n/app_de.arb +++ b/app/lib/l10n/app_de.arb @@ -2253,5 +2253,15 @@ "chatNG": "Next-Generation Chat", "@chatNG": {}, "chatNGExplainer": "Wechsle zum Chat der nächsten Generation. Features sind evtl. noch nicht stabil", - "@chatNGExplainer": {} + "@chatNGExplainer": {}, + "addTaskList": "Aufgabenliste hinzufügen", + "@addTaskList": {}, + "deleteNewsDraftTitle": "Entwurf löschen?", + "@deleteNewsDraftTitle": {}, + "deleteNewsDraftText": "Sicher, dass der Entwurf gelöscht werden soll? Das kann nicht rückgängig gemacht werden.", + "@deleteNewsDraftText": {}, + "deleteDraftBtn": "Entwurf löschen", + "@deleteDraftBtn": {}, + "errorProcessingSlide": "Erstellen von Seite {slideIdx} fehlgeschlagen: {error}", + "@errorProcessingSlide": {} } From 7105aae8f99c03427e0770d0cca900eeb9082d87 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 30 Oct 2024 14:36:59 +0000 Subject: [PATCH 44/77] Translated using Weblate (German) Currently translated at 100.0% (1123 of 1123 strings) Translation: Acter/App Translate-URL: http://weblate.acter.global/projects/acter-app/flutter-app/de/ --- app/lib/l10n/app_de.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/lib/l10n/app_de.arb b/app/lib/l10n/app_de.arb index 7b26de9cbf83..ce0d807d4374 100644 --- a/app/lib/l10n/app_de.arb +++ b/app/lib/l10n/app_de.arb @@ -2263,5 +2263,7 @@ "deleteDraftBtn": "Entwurf löschen", "@deleteDraftBtn": {}, "errorProcessingSlide": "Erstellen von Seite {slideIdx} fehlgeschlagen: {error}", - "@errorProcessingSlide": {} + "@errorProcessingSlide": {}, + "addComment": "Kommentieren", + "@addComment": {} } From 7ab950d9384966c7bfbc34861272deb9be0fb94d Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 30 Oct 2024 16:11:16 +0000 Subject: [PATCH 45/77] Sentry reporting of Sync failures --- app/lib/common/providers/notifiers/sync_notifier.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/lib/common/providers/notifiers/sync_notifier.dart b/app/lib/common/providers/notifiers/sync_notifier.dart index dbcdb9a55cba..dfd2ac5fcb71 100644 --- a/app/lib/common/providers/notifiers/sync_notifier.dart +++ b/app/lib/common/providers/notifiers/sync_notifier.dart @@ -5,6 +5,7 @@ import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/models/sync_state/sync_state.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' as ffi; import 'package:riverpod/riverpod.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; // ignore_for_file: avoid_print class SyncNotifier extends StateNotifier { @@ -64,6 +65,7 @@ class SyncNotifier extends StateNotifier { _errorListener = syncState.syncErrorRx(); // keep it resident in memory _errorPoller = _errorListener.listen((msg) { + Sentry.captureMessage('Sync failure: $msg', level: SentryLevel.error); if (mounted) { if (msg == 'SoftLogout' || msg == 'Unauthorized') { // regular logout, we do nothing here From c1b94601e6c4ed3c7ab9e65818d09e08a82c7968 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 30 Oct 2024 16:29:38 +0000 Subject: [PATCH 46/77] Move registration into external action; log its super invites error --- .../auth/actions/register_action.dart | 44 +++++++++++++++ .../features/auth/pages/register_page.dart | 53 +++++++------------ .../providers/notifiers/auth_notifier.dart | 3 +- 3 files changed, 64 insertions(+), 36 deletions(-) create mode 100644 app/lib/features/auth/actions/register_action.dart diff --git a/app/lib/features/auth/actions/register_action.dart b/app/lib/features/auth/actions/register_action.dart new file mode 100644 index 000000000000..9c10448ba77f --- /dev/null +++ b/app/lib/features/auth/actions/register_action.dart @@ -0,0 +1,44 @@ +import 'package:acter/features/auth/providers/auth_providers.dart'; +import 'package:acter/features/super_invites/providers/super_invites_providers.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +final _log = Logger('a3::auth::register'); + +Future _tryRedeem(SuperInvites superInvites, String token) async { + // try to redeem the token in a fire-and-forget-manner + try { + await superInvites.redeem(token); + } catch (error, stack) { + Sentry.captureMessage('Redeeming post-registration token $token failed'); + Sentry.captureException(error, stackTrace: stack); + _log.warning('redeeming super invite `$token` failed: $error'); + } +} + +Future register({ + required String username, + required String password, + required String name, + required String token, + required WidgetRef ref, +}) async { + final authNotifier = ref.read(authStateProvider.notifier); + final errorMsg = await authNotifier.register( + username, + password, + name, + token, + ); + if (errorMsg != null) { + _log.severe('Failed to register', errorMsg); + throw errorMsg; + } + if (token.isNotEmpty) { + final superInvites = ref.read(superInvitesProvider); + _tryRedeem(superInvites, token); + } + return true; +} diff --git a/app/lib/features/auth/pages/register_page.dart b/app/lib/features/auth/pages/register_page.dart index bc55b06de798..b5cde22a9970 100644 --- a/app/lib/features/auth/pages/register_page.dart +++ b/app/lib/features/auth/pages/register_page.dart @@ -5,9 +5,8 @@ import 'package:acter/common/toolkit/buttons/primary_action_button.dart'; import 'package:acter/common/utils/constants.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/no_internet.dart'; +import 'package:acter/features/auth/actions/register_action.dart'; import 'package:acter/features/auth/providers/auth_providers.dart'; -import 'package:acter/features/super_invites/providers/super_invites_providers.dart'; -import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -20,15 +19,6 @@ import 'package:logging/logging.dart'; final _log = Logger('a3::auth::register'); -Future tryRedeem(SuperInvites superInvites, String token) async { - // try to redeem the token in a fire-and-forget-manner - try { - await superInvites.redeem(token); - } catch (error) { - _log.warning('redeeming super invite failed: $error'); - } -} - class RegisterPage extends ConsumerStatefulWidget { static const usernameField = Key('reg-username-txt'); static const passwordField = Key('reg-password-txt'); @@ -59,32 +49,27 @@ class _RegisterPageState extends ConsumerState { showNoInternetNotification(context); return; } - final authNotifier = ref.read(authStateProvider.notifier); - final errorMsg = await authNotifier.register( - username.text, - password.text, - name.text, - token.text, - context, - ); - if (errorMsg != null) { - _log.severe('Failed to register', errorMsg); - if (!context.mounted) return; + final lang = L10n.of(context); + try { + if (await register( + username: username.text, + password: password.text, + name: name.text, + token: token.text, + ref: ref, + )) { + if (context.mounted) { + context.goNamed( + Routes.saveUsername.name, + queryParameters: {'username': username.text}, + ); + } + } + } catch (errorMsg) { EasyLoading.showError( - L10n.of(context).registerFailed(errorMsg), + lang.registerFailed(errorMsg), duration: const Duration(seconds: 3), ); - return; - } - if (token.text.isNotEmpty) { - final superInvites = ref.read(superInvitesProvider); - tryRedeem(superInvites, token.text); - } - if (context.mounted) { - context.goNamed( - Routes.saveUsername.name, - queryParameters: {'username': username.text}, - ); } } diff --git a/app/lib/features/auth/providers/notifiers/auth_notifier.dart b/app/lib/features/auth/providers/notifiers/auth_notifier.dart index 158e1d4334e8..214136299ee2 100644 --- a/app/lib/features/auth/providers/notifiers/auth_notifier.dart +++ b/app/lib/features/auth/providers/notifiers/auth_notifier.dart @@ -53,7 +53,6 @@ class AuthStateNotifier extends StateNotifier { String password, String displayName, String token, - BuildContext context, ) async { state = true; final sdk = await ref.read(sdkProvider.future); @@ -64,7 +63,7 @@ class AuthStateNotifier extends StateNotifier { return null; } catch (e) { state = false; - return e.toString(); + rethrow; } } From 95c96f4a071e821e1a00da1e939fb06111246aa0 Mon Sep 17 00:00:00 2001 From: Sari Date: Thu, 31 Oct 2024 03:11:46 +0000 Subject: [PATCH 47/77] Releasing v1.24.10310 --- .changes/2279-public-news-views-tracking.md | 1 - .changes/2299-updating-events.md | 1 - .changes/2301-boost-navigation.md | 1 - .changes/2306-space-members-search.md | 1 - .changes/2310-comment-on-boost.md | 1 - .changes/2323-bookmark-pins.md | 1 - .changes/2333-calendar-device-sync.md | 1 - 7 files changed, 7 deletions(-) delete mode 100644 .changes/2279-public-news-views-tracking.md delete mode 100644 .changes/2299-updating-events.md delete mode 100644 .changes/2301-boost-navigation.md delete mode 100644 .changes/2306-space-members-search.md delete mode 100644 .changes/2310-comment-on-boost.md delete mode 100644 .changes/2323-bookmark-pins.md delete mode 100644 .changes/2333-calendar-device-sync.md diff --git a/.changes/2279-public-news-views-tracking.md b/.changes/2279-public-news-views-tracking.md deleted file mode 100644 index 07e463a5acbc..000000000000 --- a/.changes/2279-public-news-views-tracking.md +++ /dev/null @@ -1 +0,0 @@ -- We are now keeping track of how many people have seen any one particular boost \ No newline at end of file diff --git a/.changes/2299-updating-events.md b/.changes/2299-updating-events.md deleted file mode 100644 index 412b4c9cfc40..000000000000 --- a/.changes/2299-updating-events.md +++ /dev/null @@ -1 +0,0 @@ -- Fix: Update event information when time progresses \ No newline at end of file diff --git a/.changes/2301-boost-navigation.md b/.changes/2301-boost-navigation.md deleted file mode 100644 index 721ca73820eb..000000000000 --- a/.changes/2301-boost-navigation.md +++ /dev/null @@ -1 +0,0 @@ -- [New] : Now you can jump to latest boost by doing double-tap on Boost bottom nav button. \ No newline at end of file diff --git a/.changes/2306-space-members-search.md b/.changes/2306-space-members-search.md deleted file mode 100644 index e5cb44216b4c..000000000000 --- a/.changes/2306-space-members-search.md +++ /dev/null @@ -1 +0,0 @@ -- [New] : Space members listing now contains search option to search members by name \ No newline at end of file diff --git a/.changes/2310-comment-on-boost.md b/.changes/2310-comment-on-boost.md deleted file mode 100644 index 8bc2a3605d94..000000000000 --- a/.changes/2310-comment-on-boost.md +++ /dev/null @@ -1 +0,0 @@ -- [New] : Boost getting even better with comments. \ No newline at end of file diff --git a/.changes/2323-bookmark-pins.md b/.changes/2323-bookmark-pins.md deleted file mode 100644 index 2ef0167d7dfd..000000000000 --- a/.changes/2323-bookmark-pins.md +++ /dev/null @@ -1 +0,0 @@ -- You can now bookmark pins to make it easier to find them again (which puts those at the beginning of all lists) \ No newline at end of file diff --git a/.changes/2333-calendar-device-sync.md b/.changes/2333-calendar-device-sync.md deleted file mode 100644 index 002676bb0d71..000000000000 --- a/.changes/2333-calendar-device-sync.md +++ /dev/null @@ -1 +0,0 @@ -- [Labs] calendar events should now show up in your device calendar (unless you have rejected them) by default. This is sill under labs, if you run into trouble you can disable it from the Settings -> labs and let us know what problem you had. \ No newline at end of file From 626f94d54d29f778723914f513d98f39e77a03af Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Thu, 31 Oct 2024 17:30:07 +0530 Subject: [PATCH 48/77] Quick fix related to add comment in ios --- app/lib/features/news/widgets/news_item/news_side_bar.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/features/news/widgets/news_item/news_side_bar.dart b/app/lib/features/news/widgets/news_item/news_side_bar.dart index d7bb608834de..745178536a18 100644 --- a/app/lib/features/news/widgets/news_item/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_item/news_side_bar.dart @@ -76,6 +76,7 @@ class NewsSideBar extends ConsumerWidget { showModalBottomSheet( context: context, showDragHandle: true, + useSafeArea: true, builder: (context) => CommentsSectionWidget( manager: news.comments(), shrinkWrap: false, From 7998407d53c6d38ad48040cc8cb9926df5fc968d Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Thu, 31 Oct 2024 17:33:29 +0530 Subject: [PATCH 49/77] Show latest comment first --- app/lib/features/comments/providers/comments_providers.dart | 2 +- app/lib/features/comments/widgets/comment_list_widget.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/lib/features/comments/providers/comments_providers.dart b/app/lib/features/comments/providers/comments_providers.dart index eee22d554df7..c26d6a916f1f 100644 --- a/app/lib/features/comments/providers/comments_providers.dart +++ b/app/lib/features/comments/providers/comments_providers.dart @@ -42,7 +42,7 @@ final commentsListProvider = FutureProvider.family .autoDispose, CommentsManager>((ref, manager) async { final commentList = (await manager.comments()).toList(); commentList.sort( - (a, b) => a.originServerTs().compareTo(b.originServerTs()), + (a, b) => b.originServerTs().compareTo(a.originServerTs()), ); return commentList; }); diff --git a/app/lib/features/comments/widgets/comment_list_widget.dart b/app/lib/features/comments/widgets/comment_list_widget.dart index 5146cf1a14f8..7a147e72dc81 100644 --- a/app/lib/features/comments/widgets/comment_list_widget.dart +++ b/app/lib/features/comments/widgets/comment_list_widget.dart @@ -42,6 +42,7 @@ class CommentListWidget extends ConsumerWidget { shrinkWrap: shrinkWrap, itemCount: commentList.length, padding: EdgeInsets.zero, + reverse: true, physics: shrinkWrap ? const NeverScrollableScrollPhysics() : null, itemBuilder: (context, index) { return CommentItemWidget( From 0ced7d7a5c2d7b47628127005b5029fc24d77a5e Mon Sep 17 00:00:00 2001 From: kumarpalsinh25 Date: Thu, 31 Oct 2024 17:54:42 +0530 Subject: [PATCH 50/77] Add changelog data --- .changes/2339-comment-fixes.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changes/2339-comment-fixes.md diff --git a/.changes/2339-comment-fixes.md b/.changes/2339-comment-fixes.md new file mode 100644 index 000000000000..00f0f0e64198 --- /dev/null +++ b/.changes/2339-comment-fixes.md @@ -0,0 +1,2 @@ +- [Fix] : Add comment issue in iOS device is fixed now +- [Improvement] : Comment list will show latest comments as default \ No newline at end of file From daf8517bdc9edfed569fa04a3f4d8e7123ffa561 Mon Sep 17 00:00:00 2001 From: Sari Date: Thu, 31 Oct 2024 13:31:53 +0000 Subject: [PATCH 51/77] Releasing v1.24.10311 --- .changes/2279-public-news-views-tracking.md | 1 - .changes/2299-updating-events.md | 1 - .changes/2301-boost-navigation.md | 1 - .changes/2306-space-members-search.md | 1 - .changes/2310-comment-on-boost.md | 1 - .changes/2323-bookmark-pins.md | 1 - .changes/2333-calendar-device-sync.md | 1 - .changes/2339-comment-fixes.md | 2 -- 8 files changed, 9 deletions(-) delete mode 100644 .changes/2279-public-news-views-tracking.md delete mode 100644 .changes/2299-updating-events.md delete mode 100644 .changes/2301-boost-navigation.md delete mode 100644 .changes/2306-space-members-search.md delete mode 100644 .changes/2310-comment-on-boost.md delete mode 100644 .changes/2323-bookmark-pins.md delete mode 100644 .changes/2333-calendar-device-sync.md delete mode 100644 .changes/2339-comment-fixes.md diff --git a/.changes/2279-public-news-views-tracking.md b/.changes/2279-public-news-views-tracking.md deleted file mode 100644 index 07e463a5acbc..000000000000 --- a/.changes/2279-public-news-views-tracking.md +++ /dev/null @@ -1 +0,0 @@ -- We are now keeping track of how many people have seen any one particular boost \ No newline at end of file diff --git a/.changes/2299-updating-events.md b/.changes/2299-updating-events.md deleted file mode 100644 index 412b4c9cfc40..000000000000 --- a/.changes/2299-updating-events.md +++ /dev/null @@ -1 +0,0 @@ -- Fix: Update event information when time progresses \ No newline at end of file diff --git a/.changes/2301-boost-navigation.md b/.changes/2301-boost-navigation.md deleted file mode 100644 index 721ca73820eb..000000000000 --- a/.changes/2301-boost-navigation.md +++ /dev/null @@ -1 +0,0 @@ -- [New] : Now you can jump to latest boost by doing double-tap on Boost bottom nav button. \ No newline at end of file diff --git a/.changes/2306-space-members-search.md b/.changes/2306-space-members-search.md deleted file mode 100644 index e5cb44216b4c..000000000000 --- a/.changes/2306-space-members-search.md +++ /dev/null @@ -1 +0,0 @@ -- [New] : Space members listing now contains search option to search members by name \ No newline at end of file diff --git a/.changes/2310-comment-on-boost.md b/.changes/2310-comment-on-boost.md deleted file mode 100644 index 8bc2a3605d94..000000000000 --- a/.changes/2310-comment-on-boost.md +++ /dev/null @@ -1 +0,0 @@ -- [New] : Boost getting even better with comments. \ No newline at end of file diff --git a/.changes/2323-bookmark-pins.md b/.changes/2323-bookmark-pins.md deleted file mode 100644 index 2ef0167d7dfd..000000000000 --- a/.changes/2323-bookmark-pins.md +++ /dev/null @@ -1 +0,0 @@ -- You can now bookmark pins to make it easier to find them again (which puts those at the beginning of all lists) \ No newline at end of file diff --git a/.changes/2333-calendar-device-sync.md b/.changes/2333-calendar-device-sync.md deleted file mode 100644 index 002676bb0d71..000000000000 --- a/.changes/2333-calendar-device-sync.md +++ /dev/null @@ -1 +0,0 @@ -- [Labs] calendar events should now show up in your device calendar (unless you have rejected them) by default. This is sill under labs, if you run into trouble you can disable it from the Settings -> labs and let us know what problem you had. \ No newline at end of file diff --git a/.changes/2339-comment-fixes.md b/.changes/2339-comment-fixes.md deleted file mode 100644 index 00f0f0e64198..000000000000 --- a/.changes/2339-comment-fixes.md +++ /dev/null @@ -1,2 +0,0 @@ -- [Fix] : Add comment issue in iOS device is fixed now -- [Improvement] : Comment list will show latest comments as default \ No newline at end of file From d9f1e97a987f914ac418ae0692eebc08daf4e5ac Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 25 Oct 2024 10:48:45 +0100 Subject: [PATCH 52/77] Remove left-over read-receipt-sync-attempt --- native/acter/src/api/client/sync.rs | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/native/acter/src/api/client/sync.rs b/native/acter/src/api/client/sync.rs index 1611172fb5db..8e2ecbdc526c 100644 --- a/native/acter/src/api/client/sync.rs +++ b/native/acter/src/api/client/sync.rs @@ -510,32 +510,6 @@ impl Client { } } - let room_read_receipts = response - .rooms - .join - .iter() - .flat_map(|(room_id, value)| { - value.ephemeral.iter().filter_map(|r| { - let room_id = room_id.clone(); - let Ok(AnySyncEphemeralRoomEvent::Receipt(r)) = r.deserialize() else { - return None; - }; - Some( - r.content - .keys() - .map(|k| format!("{room_id}:{k}:rr")) - .collect::>(), - ) - }) - }) - .flatten() - .collect::>(); - - if !room_read_receipts.is_empty() { - trace!(?room_read_receipts, "read_receipts"); - me.executor().notify(room_read_receipts); - } - let changed_rooms = response .rooms .join From f827014038d941f42a5c56bcbe20a12354ae7dfa Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 25 Oct 2024 16:48:21 +0100 Subject: [PATCH 53/77] Remove necessity for mock data for rust integration tests --- .../check-rust-integration-tests.yml | 6 - Makefile.toml | 4 - native/test/src/tests/auth.rs | 65 ++++------ native/test/src/tests/invitation.rs | 35 +----- native/test/src/tests/tasks.rs | 112 +----------------- native/test/src/tests/verification.rs | 37 +----- native/test/src/utils.rs | 72 ++--------- 7 files changed, 43 insertions(+), 288 deletions(-) diff --git a/.github/workflows/check-rust-integration-tests.yml b/.github/workflows/check-rust-integration-tests.yml index 857c4a52aa25..ddf1172839f3 100644 --- a/.github/workflows/check-rust-integration-tests.yml +++ b/.github/workflows/check-rust-integration-tests.yml @@ -67,12 +67,6 @@ jobs: - uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@nextest - - name: Initialize e2ee mock data - run: cargo run -p acter-cli -- mock - env: - DEFAULT_HOMESERVER_NAME: "localhost" - DEFAULT_HOMESERVER_URL: "http://localhost:8118" - - name: run cargo integration tests env: DEFAULT_HOMESERVER_URL: "http://localhost:8118" diff --git a/Makefile.toml b/Makefile.toml index 7ba4fddc556e..48ef7dfd7969 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -109,10 +109,6 @@ dependencies = [ "post-android", ] -[tasks.mock-data] -command = "cargo" -args = ["run", "-p", "acter-cli", "mock", "--homeserver-url", "http://localhost:8118", "--homeserver-name", "localhost"] - [tasks.fixup] dependencies = [ "format-fixup", diff --git a/native/test/src/tests/auth.rs b/native/test/src/tests/auth.rs index 5c9e5cf9a928..1fd3ccf8a8d5 100644 --- a/native/test/src/tests/auth.rs +++ b/native/test/src/tests/auth.rs @@ -46,23 +46,24 @@ async fn guest_can_login() -> Result<()> { } #[tokio::test] -async fn sisko_can_login() -> Result<()> { +async fn user_can_login() -> Result<()> { let _ = env_logger::try_init(); - let homeserver_name = option_env!("DEFAULT_HOMESERVER_NAME") - .unwrap_or("localhost") - .to_string(); - let homeserver_url = option_env!("DEFAULT_HOMESERVER_URL") - .unwrap_or("http://localhost:8118") - .to_string(); + let sisko = random_user("sisko").await?; + let user_id = sisko.user_id()?; + let username = user_id.localpart(); let tmp_dir = TempDir::new()?; let _client = login_new_client( tmp_dir.path().to_string_lossy().to_string(), tmp_dir.path().to_string_lossy().to_string(), - "@sisko".to_string(), - default_user_password("sisko"), - homeserver_name, - homeserver_url, + username.to_owned(), + default_user_password(username), + option_env!("DEFAULT_HOMESERVER_NAME") + .unwrap_or("localhost") + .to_string(), + option_env!("DEFAULT_HOMESERVER_URL") + .unwrap_or("http://localhost:8118") + .to_string(), Some("SISKO_DEV".to_string()), ) .await?; @@ -70,32 +71,13 @@ async fn sisko_can_login() -> Result<()> { } #[tokio::test] -async fn kyra_can_login() -> Result<()> { +async fn user_can_restore() -> Result<()> { let _ = env_logger::try_init(); - let tmp_dir = TempDir::new()?; - let homeserver_name = option_env!("DEFAULT_HOMESERVER_NAME") - .unwrap_or("localhost") - .to_string(); - let homeserver_url = option_env!("DEFAULT_HOMESERVER_URL") - .unwrap_or("http://localhost:8118") - .to_string(); - let _client = login_new_client( - tmp_dir.path().to_string_lossy().to_string(), - tmp_dir.path().to_string_lossy().to_string(), - "@kyra".to_string(), - default_user_password("kyra"), - homeserver_name, - homeserver_url, - Some("KYRA_DEV".to_string()), - ) - .await?; - Ok(()) -} + let kyra = random_user("kyra").await?; + let user_id = kyra.user_id()?; + let username = user_id.localpart(); -#[tokio::test] -async fn kyra_can_restore() -> Result<()> { - let _ = env_logger::try_init(); let homeserver_name = option_env!("DEFAULT_HOMESERVER_NAME") .unwrap_or("localhost") .to_string(); @@ -106,7 +88,7 @@ async fn kyra_can_restore() -> Result<()> { let media_dir = TempDir::new()?; let (config, user_id) = make_client_config( base_dir.path().to_string_lossy().to_string(), - "@kyra", + username, media_dir.path().to_string_lossy().to_string(), None, &homeserver_name, @@ -118,8 +100,8 @@ async fn kyra_can_restore() -> Result<()> { let (token, user_id) = { let client = login_new_client_under_config( config.clone(), - user_id, - default_user_password("kyra"), + user_id.to_owned(), + default_user_password(username), None, Some("KYRA_DEV".to_string()), ) @@ -142,6 +124,11 @@ async fn kyra_can_restore() -> Result<()> { #[tokio::test] async fn kyra_can_restore_with_db_passphrase() -> Result<()> { let _ = env_logger::try_init(); + + let kyra = random_user("kyra2").await?; + let user_id = kyra.user_id()?; + let username = user_id.localpart(); + let homeserver_name = option_env!("DEFAULT_HOMESERVER_NAME") .unwrap_or("localhost") .to_string(); @@ -153,7 +140,7 @@ async fn kyra_can_restore_with_db_passphrase() -> Result<()> { let db_passphrase = Uuid::new_v4().to_string(); let (config, user_id) = make_client_config( base_dir.path().to_string_lossy().to_string(), - "@kyra", + username, media_dir.path().to_string_lossy().to_string(), Some(db_passphrase.clone()), &homeserver_name, @@ -166,7 +153,7 @@ async fn kyra_can_restore_with_db_passphrase() -> Result<()> { let client = login_new_client_under_config( config.clone(), user_id, - default_user_password("kyra"), + default_user_password(username), Some(db_passphrase), Some("KYRA_DEV".to_string()), ) diff --git a/native/test/src/tests/invitation.rs b/native/test/src/tests/invitation.rs index 4e385037680c..395f88d82722 100644 --- a/native/test/src/tests/invitation.rs +++ b/native/test/src/tests/invitation.rs @@ -1,47 +1,18 @@ -use acter::api::login_new_client; use anyhow::Result; use futures::{pin_mut, stream::StreamExt}; use std::time::Duration; -use tempfile::TempDir; use tokio::time::sleep; -use crate::utils::default_user_password; +use crate::utils::random_user; #[tokio::test] async fn load_pending_invitation() -> Result<()> { let _ = env_logger::try_init(); - let homeserver_name = option_env!("DEFAULT_HOMESERVER_NAME") - .unwrap_or("localhost") - .to_string(); - let homeserver_url = option_env!("DEFAULT_HOMESERVER_URL") - .unwrap_or("http://localhost:8118") - .to_string(); - - let tmp_dir = TempDir::new()?; - let mut sisko = login_new_client( - tmp_dir.path().to_string_lossy().to_string(), - tmp_dir.path().to_string_lossy().to_string(), - "@sisko".to_string(), - default_user_password("sisko"), - homeserver_name.clone(), - homeserver_url.clone(), - Some("SISKO_DEV".to_string()), - ) - .await?; + let mut sisko = random_user("loading_pending_invitation_sisko").await?; let _sisko_syncer = sisko.start_sync(); - let tmp_dir = TempDir::new()?; - let mut kyra = login_new_client( - tmp_dir.path().to_string_lossy().to_string(), - tmp_dir.path().to_string_lossy().to_string(), - "@kyra".to_string(), - default_user_password("kyra"), - homeserver_name.clone(), - homeserver_url.clone(), - Some("KYRA_DEV".to_string()), - ) - .await?; + let mut kyra = random_user("loading_pending_invitation_kyra").await?; let _kyra_syncer = kyra.start_sync(); sleep(Duration::from_secs(3)).await; diff --git a/native/test/src/tests/tasks.rs b/native/test/src/tests/tasks.rs index e6de2c10b949..4031c70bed1c 100644 --- a/native/test/src/tests/tasks.rs +++ b/native/test/src/tests/tasks.rs @@ -1,8 +1,6 @@ -use acter::testing::{ensure_user, wait_for}; +use acter::testing::wait_for; use acter_core::models::ActerModel; use anyhow::{bail, Result}; -use matrix_sdk::config::StoreConfig; -use tokio::time::{sleep, Duration}; use tokio_retry::{ strategy::{jitter, FibonacciBackoff}, Retry, @@ -10,114 +8,6 @@ use tokio_retry::{ use crate::utils::random_user_with_random_space; -#[tokio::test] -async fn odos_tasks() -> Result<()> { - let _ = env_logger::try_init(); - let list_name = "Daily Security Brief".to_owned(); - let mut odo = ensure_user( - option_env!("DEFAULT_HOMESERVER_URL") - .unwrap_or("http://localhost:8118") - .to_string(), - option_env!("DEFAULT_HOMESERVER_NAME") - .unwrap_or("localhost") - .to_string(), - "odo".to_owned(), - option_env!("REGISTRATION_TOKEN").map(ToString::to_string), - "acter-integration-tests".to_owned(), - StoreConfig::default(), - ) - .await?; - - let state_sync = odo.start_sync(); - state_sync.await_has_synced_history().await?; - - let task_lists = odo.task_lists().await?; - let mut task_list = task_lists - .into_iter() - .find(|t| t.name() == list_name) - .expect("TaskList not found"); - - assert!( - task_list.tasks().await?.len() >= 3, - "Number of tasks too low", - ); - - let mut list_subscription = task_list.subscribe(); - - let new_task_event_id = task_list - .task_builder()? - .title("Integation Test Task".into()) - .description_text("Integration Test Task Description".into()) - .send() - .await?; - - let mut remaining = 3; - - let task = loop { - if remaining == 0 { - bail!("tried to find the new task 3 seconds"); - } - remaining -= 1; - - if list_subscription.try_recv().is_ok() { - task_list = odo - .task_lists() - .await? - .into_iter() - .find(|t| t.name() == list_name) - .expect("TaskList not found again"); - if let Some(task) = task_list - .tasks() - .await? - .into_iter() - .find(|t| t.event_id() == new_task_event_id) - { - break task; - } - } - - sleep(Duration::from_secs(1)).await; - }; - - assert_eq!(*task.title(), "Integation Test Task".to_string()); - assert!(!task.is_done(), "Task is already done"); - - let mut task_update = task.subscribe(); - let _update_id = task - .update_builder()? - .title("New Test title".to_owned()) - .mark_done() - .send() - .await?; - - let mut remaining = 4; - loop { - if remaining == 0 { - bail!("even after 3 seconds, no task update has been reported"); - } - remaining -= 1; - - if task_update.try_recv().is_ok() { - break; - } - - sleep(Duration::from_secs(1)).await; - } - - // we do not expect a signal on the list, as the order hasn’t changed - let task = task_list - .tasks() - .await? - .into_iter() - .find(|t| t.event_id() == new_task_event_id) - .expect("Task not found?!?"); - - assert_eq!(*task.title(), "New Test title".to_string()); - assert!(task.is_done(), "Task is not be marked as done"); - - Ok(()) -} - #[tokio::test] async fn task_smoketests() -> Result<()> { let _ = env_logger::try_init(); diff --git a/native/test/src/tests/verification.rs b/native/test/src/tests/verification.rs index ef1724adb49a..051b0c0104e4 100644 --- a/native/test/src/tests/verification.rs +++ b/native/test/src/tests/verification.rs @@ -1,10 +1,9 @@ -use acter::api::{login_new_client, VerificationEvent}; +use acter::api::VerificationEvent; use anyhow::Result; use futures::{channel::mpsc::Receiver, stream::StreamExt}; -use tempfile::TempDir; use tracing::info; -use crate::utils::default_user_password; +use crate::utils::random_user; fn wait_for_verification_event( rx: &mut Receiver, @@ -24,40 +23,12 @@ fn wait_for_verification_event( async fn interactive_verification_started_from_request() -> Result<()> { let _ = env_logger::try_init(); - let alice_dir = TempDir::new()?; - let mut alice = login_new_client( - alice_dir.path().to_string_lossy().to_string(), - alice_dir.path().to_string_lossy().to_string(), - "@sisko".to_string(), - default_user_password("sisko"), - option_env!("DEFAULT_HOMESERVER_NAME") - .unwrap_or("localhost") - .to_string(), - option_env!("DEFAULT_HOMESERVER_URL") - .unwrap_or("http://localhost:8118") - .to_string(), - Some("ALICE_DEV".to_string()), - ) - .await?; + let mut alice = random_user("interactive_verification_started_from_request_alice").await?; let alice_device_id = alice.device_id().expect("alice should get device id"); info!("alice device id: {}", alice_device_id); - let bob_dir = TempDir::new()?; - let mut bob = login_new_client( - bob_dir.path().to_string_lossy().to_string(), - bob_dir.path().to_string_lossy().to_string(), - "@sisko".to_string(), - default_user_password("sisko"), - option_env!("DEFAULT_HOMESERVER_NAME") - .unwrap_or("localhost") - .to_string(), - option_env!("DEFAULT_HOMESERVER_URL") - .unwrap_or("http://localhost:8118") - .to_string(), - Some("BOB_DEV".to_string()), - ) - .await?; + let mut bob = random_user("interactive_verification_started_from_request_bob").await?; let bob_device_id = bob.device_id().expect("bob should get device id"); info!("bob device id: {}", bob_device_id); diff --git a/native/test/src/utils.rs b/native/test/src/utils.rs index 78be02aaa484..11f03270533c 100644 --- a/native/test/src/utils.rs +++ b/native/test/src/utils.rs @@ -37,11 +37,11 @@ pub async fn accept_all_invites(client: &Client) -> Result> { } pub async fn random_user(prefix: &str) -> Result { - let (user, _uuid) = _random_user_with_uuid(prefix).await?; + let (user, _uuid) = random_user_with_uuid(prefix).await?; Ok(user) } -async fn _random_user_with_uuid(prefix: &str) -> Result<(Client, String)> { +async fn random_user_with_uuid(prefix: &str) -> Result<(Client, String)> { let uuid = Uuid::new_v4().to_string(); let user = ensure_user( option_env!("DEFAULT_HOMESERVER_URL") @@ -60,7 +60,7 @@ async fn _random_user_with_uuid(prefix: &str) -> Result<(Client, String)> { } pub async fn random_user_with_random_space(prefix: &str) -> Result<(Client, OwnedRoomId)> { - let (user, uuid) = _random_user_with_uuid(prefix).await?; + let (user, uuid) = random_user_with_uuid(prefix).await?; let settings = CreateSpaceSettingsBuilder::default() .name(format!("it-room-{prefix}-{uuid}")) @@ -73,13 +73,13 @@ pub async fn random_users_with_random_space( prefix: &str, user_count: u8, ) -> Result<(Vec, OwnedRoomId)> { - let (main_user, uuid) = _random_user_with_uuid(prefix).await?; + let (main_user, uuid) = random_user_with_uuid(prefix).await?; let mut settings = CreateSpaceSettingsBuilder::default(); settings.name(format!("it-room-{prefix}-{uuid}")); let mut users = vec![]; for _x in 0..user_count { - let (new_user, _uuid) = _random_user_with_uuid(prefix).await?; + let (new_user, _uuid) = random_user_with_uuid(prefix).await?; settings.add_invitee(new_user.user_id()?.to_string())?; users.push(new_user) } @@ -102,20 +102,7 @@ pub async fn random_users_with_random_space( } pub async fn random_user_with_random_convo(prefix: &str) -> Result<(Client, OwnedRoomId)> { - let uuid = Uuid::new_v4().to_string(); - let user = ensure_user( - option_env!("DEFAULT_HOMESERVER_URL") - .unwrap_or("http://localhost:8118") - .to_string(), - option_env!("DEFAULT_HOMESERVER_NAME") - .unwrap_or("localhost") - .to_string(), - format!("it-{prefix}-{uuid}"), - option_env!("REGISTRATION_TOKEN").map(ToString::to_string), - "acter-integration-tests".to_owned(), - StoreConfig::default(), - ) - .await?; + let (user, uuid) = random_user_with_uuid(prefix).await?; let settings = CreateConvoSettingsBuilder::default() .name(format!("it-room-{prefix}-{uuid}")) @@ -144,50 +131,9 @@ pub async fn random_user_under_token(prefix: &str, registration_token: &str) -> pub async fn random_users_with_random_convo( prefix: &str, ) -> Result<(Client, Client, Client, OwnedRoomId)> { - let uuid = Uuid::new_v4().to_string(); - let sisko = ensure_user( - option_env!("DEFAULT_HOMESERVER_URL") - .unwrap_or("http://localhost:8118") - .to_string(), - option_env!("DEFAULT_HOMESERVER_NAME") - .unwrap_or("localhost") - .to_string(), - format!("it-{prefix}-{uuid}"), - option_env!("REGISTRATION_TOKEN").map(ToString::to_string), - "acter-integration-tests".to_owned(), - StoreConfig::default(), - ) - .await?; - - let uuid = Uuid::new_v4().to_string(); - let kyra = ensure_user( - option_env!("DEFAULT_HOMESERVER_URL") - .unwrap_or("http://localhost:8118") - .to_string(), - option_env!("DEFAULT_HOMESERVER_NAME") - .unwrap_or("localhost") - .to_string(), - format!("it-{prefix}-{uuid}"), - option_env!("REGISTRATION_TOKEN").map(ToString::to_string), - "acter-integration-tests".to_owned(), - StoreConfig::default(), - ) - .await?; - - let uuid = Uuid::new_v4().to_string(); - let worf = ensure_user( - option_env!("DEFAULT_HOMESERVER_URL") - .unwrap_or("http://localhost:8118") - .to_string(), - option_env!("DEFAULT_HOMESERVER_NAME") - .unwrap_or("localhost") - .to_string(), - format!("it-{prefix}-{uuid}"), - option_env!("REGISTRATION_TOKEN").map(ToString::to_string), - "acter-integration-tests".to_owned(), - StoreConfig::default(), - ) - .await?; + let (sisko, _) = random_user_with_uuid(prefix).await?; + let (kyra, _) = random_user_with_uuid(prefix).await?; + let (worf, _) = random_user_with_uuid(prefix).await?; let uuid = Uuid::new_v4().to_string(); let settings = CreateConvoSettingsBuilder::default() From 0b8d30b6ab09a828861007948c9e215720a8bebd Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 25 Oct 2024 16:52:08 +0100 Subject: [PATCH 54/77] Remove mock cli command --- native/cli/src/action.rs | 5 - native/cli/src/action/mock.rs | 406 ---------------------------------- 2 files changed, 411 deletions(-) delete mode 100644 native/cli/src/action/mock.rs diff --git a/native/cli/src/action.rs b/native/cli/src/action.rs index 5d229e4e78a1..849da060988e 100644 --- a/native/cli/src/action.rs +++ b/native/cli/src/action.rs @@ -5,13 +5,11 @@ mod execute; mod history; mod list; mod manage; -mod mock; pub use execute::ExecuteOpts; pub use history::HistoryOpts; pub use list::List; pub use manage::Manage; -pub use mock::MockOpts; #[derive(Subcommand, Debug)] pub enum Action { @@ -21,8 +19,6 @@ pub enum Action { Manage(Manage), /// Reviewing the room history History(HistoryOpts), - /// Mock Data on fresh server - Mock(MockOpts), /// Template Execution Execute(ExecuteOpts), } @@ -31,7 +27,6 @@ impl Action { pub async fn run(&self) -> Result<()> { match self { Action::Manage(config) => config.run().await?, - Action::Mock(config) => config.run().await?, Action::List(config) => config.run().await?, Action::History(config) => config.run().await?, Action::Execute(config) => config.run().await?, diff --git a/native/cli/src/action/mock.rs b/native/cli/src/action/mock.rs deleted file mode 100644 index 8018916f7b24..000000000000 --- a/native/cli/src/action/mock.rs +++ /dev/null @@ -1,406 +0,0 @@ -use acter::{ - platform::sanitize, - testing::{ensure_user, wait_for}, - Client, CreateSpaceSettingsBuilder, -}; -use acter_core::models::ActerModel; -use anyhow::{bail, Context, Result}; -use clap::{crate_version, Parser, Subcommand}; -use futures::StreamExt; -use matrix_sdk::HttpError; -use matrix_sdk_base::{ - ruma::{api::client::room::Visibility, OwnedUserId}, - store::{MemoryStore, StoreConfig}, -}; -use matrix_sdk_sqlite::SqliteStateStore; -use std::collections::HashMap; -use tracing::{error, info, trace}; - -use crate::config::{ENV_DEFAULT_HOMESERVER_NAME, ENV_DEFAULT_HOMESERVER_URL, ENV_REG_TOKEN}; - -#[derive(Parser, Debug)] -pub struct MockOpts { - /// the URL to the homeserver are we running against - #[clap( - long = "homeserver-url", - env = ENV_DEFAULT_HOMESERVER_URL, - default_value = "http://localhost:8118" - )] - pub homeserver: String, - - /// name of that homeserver - #[clap( - long = "homeserver-name", - env = ENV_DEFAULT_HOMESERVER_NAME, - default_value = "localhost" - )] - pub server_name: String, - - /// name of that homeserver - #[clap( - long = "registration-token", - env = ENV_REG_TOKEN, - )] - pub reg_token: Option, - - /// Persist the store in .local/{user_id} - #[clap(long)] - pub persist: bool, - - //// export crypto database to .local for each known client - #[clap(long)] - pub export: bool, - - #[clap(subcommand)] - pub cmd: Option, -} - -#[derive(Debug, Subcommand)] -pub enum MockCmd { - All, - Users, - Spaces, - AcceptInvites, - Tasks, - // Convos, -} - -impl MockOpts { - pub async fn run(&self) -> Result<()> { - let mut m = Mock::new(self)?; - match self.cmd { - Some(MockCmd::Users) => { - m.everyone().await; - } - Some(MockCmd::Spaces) => m.spaces().await?, - Some(MockCmd::AcceptInvites) => m.accept_invitations().await?, - Some(MockCmd::Tasks) => m.tasks().await?, - Some(MockCmd::All) | None => { - m.spaces().await?; - m.accept_invitations().await?; - m.sync_up().await?; - m.tasks().await?; - } - }; - if self.export { - m.export().await?; - } - Ok(()) - } -} - -/// Posting a news item to a given room -#[derive(Debug, Clone)] -pub struct Mock<'a> { - users: HashMap, - opts: &'a MockOpts, -} - -impl<'a> Mock<'a> { - async fn client(&mut self, username: String) -> Result { - match self.users.get(&username) { - Some(c) => Ok(c.clone()), - None => { - trace!("client not found. creating for {:}", username); - - let store_config = if self.opts.persist { - let path = sanitize(".local", &username); - let store = SqliteStateStore::open(&path, Some(&username)).await?; - StoreConfig::new().state_store(store) - } else { - StoreConfig::new().state_store(MemoryStore::new()) - }; - - let user_agent = format!("acter-cli/{}", crate_version!()); - - let client = ensure_user( - self.opts.homeserver.clone(), - self.opts.server_name.clone(), - username.clone(), - self.opts.reg_token.clone(), - user_agent, - store_config, - ) - .await?; - self.users.insert(username, client.clone()); - Ok(client) - } - } - } - - pub fn new(opts: &'a MockOpts) -> Result { - Ok(Mock { - opts, - users: Default::default(), - }) - } - - async fn team(&mut self) -> [Client; 7] { - [ - self.client("sisko".to_owned()).await.unwrap(), - self.client("kyra".to_owned()).await.unwrap(), - self.client("worf".to_owned()).await.unwrap(), - self.client("bashir".to_owned()).await.unwrap(), - self.client("miles".to_owned()).await.unwrap(), - self.client("jadzia".to_owned()).await.unwrap(), - self.client("odo".to_owned()).await.unwrap(), - ] - } - - async fn civilians(&mut self) -> [Client; 4] { - [ - self.client("quark".to_owned()).await.unwrap(), - self.client("rom".to_owned()).await.unwrap(), - self.client("morn".to_owned()).await.unwrap(), - self.client("keiko".to_owned()).await.unwrap(), - ] - } - - async fn quark_customers(&mut self) -> [Client; 7] { - [ - self.client("quark".to_owned()).await.unwrap(), - self.client("rom".to_owned()).await.unwrap(), - self.client("morn".to_owned()).await.unwrap(), - self.client("jadzia".to_owned()).await.unwrap(), - self.client("kyra".to_owned()).await.unwrap(), - self.client("miles".to_owned()).await.unwrap(), - self.client("bashir".to_owned()).await.unwrap(), - ] - } - - async fn everyone(&mut self) -> Vec { - let mut everyone = Vec::new(); - everyone.extend_from_slice(&self.team().await); - everyone.extend_from_slice(&self.civilians().await); - everyone - } - - pub async fn spaces(&mut self) -> Result<()> { - let team = self.team().await; - let civilians = self.civilians().await; - let quark_customers = self.quark_customers().await; - - let team_ids = team - .iter() - .map(|a| a.user_id()) - .map(|a| a.expect("everyone here has an id")) - .collect(); - - let civilians_ids = civilians - .iter() - .map(|a| a.user_id()) - .map(|a| a.expect("everyone here has an id")) - .collect(); - - let quark_customer_ids = quark_customers - .iter() - .map(|a| a.user_id()) - .map(|a| a.expect("everyone here has an id")) - .collect(); - - let everyone = self.everyone().await; - - let _everyones_ids = everyone - .iter() - .map(|a| a.user_id()) - .map(|a| a.expect("everyone here has an id")) - .collect::>(); - - let ops_settings = CreateSpaceSettingsBuilder::default() - .name("Ops".to_owned()) - .alias("ops".to_owned()) - .invites(team_ids) - .build()?; - - let admin = self.client("admin".to_owned()).await.unwrap(); - - match admin.create_acter_space(Box::new(ops_settings)).await { - Ok(ops_id) => { - info!("Ops Room Id: {:?}", ops_id); - } - Err(x) if x.is::() => { - let inner = x.downcast::().expect("already checked"); - error!("Problem creating Ops Room: {:?}", inner); - } - Err(e) => { - error!("Creating Ops Room failed: {:?}", e); - } - } - - let promenade_settings = CreateSpaceSettingsBuilder::default() - .name("Promenade".to_owned()) - .alias("promenade".to_owned()) - .visibility(Visibility::Public) - .invites(civilians_ids) - .build()?; - - match admin.create_acter_space(Box::new(promenade_settings)).await { - Ok(promenade_id) => { - info!("Promenade Room Id: {:?}", promenade_id); - } - Err(x) if x.is::() => { - let inner = x.downcast::().expect("already checked"); - error!("Problem creating Promenade Room: {:?}", inner); - } - Err(e) => { - error!("Creating Promenade Room failed: {:?}", e); - } - } - - let quarks_settings = CreateSpaceSettingsBuilder::default() - .name("Quarks".to_owned()) - .alias("quarks".to_owned()) - .visibility(Visibility::Public) - .invites(quark_customer_ids) - .build()?; - - match admin.create_acter_space(Box::new(quarks_settings)).await { - Ok(quarks_id) => { - info!("Quarks Room Id: {:?}", quarks_id); - } - Err(x) if x.is::() => { - let inner = x.downcast::().expect("already checked"); - error!("Problem creating Quarks Room: {:?}", inner); - } - Err(e) => { - error!("Creating Quarks Room failed: {:?}", e); - } - } - - info!("Done creating spaces"); - Ok(()) - } - - pub async fn accept_invitations(&mut self) -> Result<()> { - for member in self.everyone().await.iter() { - info!("Accepting invites for {:}", member.user_id()?); - let mut sync_stream = Box::pin(member.sync_stream(Default::default()).await); - sync_stream.next().await; - for invited in member.invited_rooms().iter() { - info!(" - accepting {:?}", invited.room_id()); - invited.join().await?; - } - sync_stream.next().await; - } - info!("Done accepting invites"); - - Ok(()) - } - - pub async fn sync_up(&mut self) -> Result<()> { - for member in self.everyone().await.iter() { - member.sync_once(Default::default()).await?; - info!("Synced {:}", member.user_id()?); - } - Ok(()) - } - - fn local_alias(&self, name: &str) -> String { - format!("{name}:{0}", self.opts.server_name) - } - - pub async fn tasks(&mut self) -> Result<()> { - let list_name = "Daily Security Brief".to_owned(); - //let sisko = &self.sisko; - let mut odo = self.client("odo".to_owned()).await?; - //let kyra = &self.kyra; - //sisko.sync_once(Default::default()).await?; - let syncer = odo.start_sync(); - syncer.await_has_synced_history().await?; - - let task_lists = odo.task_lists().await?; - let alias = self.local_alias("#ops"); - let task_list = - if let Some(task_list) = task_lists.into_iter().find(|t| t.name() == list_name) { - task_list - } else { - //kyra.sync_once(Default::default()).await?; - - let cloned_odo = odo.clone(); - let Some(odo_ops) = wait_for(move || { - let cloned_odo = cloned_odo.clone(); - let alias = alias.clone(); - async move { - info!("tasks get_space {alias}"); - let space = cloned_odo.space(alias).await?; - Ok(Some(space)) - } - }) - .await? - else { - bail!("Odo couldn’t be found in Ops") - }; - let mut draft = odo_ops.task_list_draft()?; - - let task_list_id = draft - .name(list_name) - .description_text("The tops of the daily security briefing with kyra".into()) - .send() - .await?; - - let cloned_odo = odo.clone(); - wait_for(move || { - let cloned_odo = cloned_odo.clone(); - let task_list_id = task_list_id.clone(); - async move { - let task_list = cloned_odo - .task_lists() - .await? - .into_iter() - .find(|e| e.event_id() == task_list_id); - Ok(task_list) - } - }) - .await? - .context("Task list not found even after polling for 3 seconds")? - }; - - task_list - .task_builder()? - .title("Holding Cells review".into()) - .description_text( - "What is the occupancy rate? Who is in the holding cells, for how much longer?" - .into(), - ) - .send() - .await?; - - task_list - .task_builder()? - .title("Special guests".into()) - .description_text("Any special guests expected, needing special attention?".into()) - .send() - .await?; - - task_list - .task_builder()? - .title("Federation reports".into()) - .description_text("Daily status report from the federation".into()) - .send() - .await?; - - info!("Creating task lists and tasks done."); - - Ok(()) - } - - pub async fn export(&mut self) -> Result<()> { - std::fs::create_dir_all(".local")?; - - futures::future::try_join_all(self.users.values().map(|cl| async move { - let full_username = cl.user_id().expect("UserId needed"); - let user_export_file = sanitize(".local", &format!("mock_export_{full_username:}")); - - cl.sync_once(Default::default()).await?; - - cl.encryption() - .export_room_keys(user_export_file, "mock", |_| true) - .await - })) - .await?; - - info!("Encryption keys exported to .local"); - - Ok(()) - } -} From 144c16b20ccddd5583f7a7c388d83350d177cd4d Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Sun, 27 Oct 2024 18:25:13 +0000 Subject: [PATCH 55/77] Upgrade to latest rust sdk --- Cargo.lock | 830 ++++++++++++----------- Cargo.toml | 4 +- native/acter/src/api/attachments.rs | 8 +- native/acter/src/api/client/sync.rs | 2 +- native/acter/src/api/convo.rs | 7 +- native/acter/src/api/message.rs | 24 +- native/acter/src/api/reactions.rs | 6 +- native/acter/src/api/room.rs | 7 +- native/acter/src/api/spaces.rs | 19 +- native/acter/src/api/stream.rs | 26 +- native/acter/src/api/stream/msg_draft.rs | 8 +- native/cli/src/action/history.rs | 2 +- native/test/src/tests/redact.rs | 2 +- 13 files changed, 480 insertions(+), 465 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 61b4a76b9b0f..8a93dca5d099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -35,7 +35,7 @@ dependencies = [ "futures", "futures-signals", "icalendar", - "indexmap 2.5.0", + "indexmap 2.6.0", "infer", "lazy_static", "log", @@ -85,7 +85,7 @@ dependencies = [ "acter", "acter-core", "anyhow", - "clap 4.5.17", + "clap 4.5.20", "dialoguer", "env_logger 0.11.5", "futures", @@ -111,7 +111,7 @@ dependencies = [ "env_logger 0.11.5", "futures", "icalendar", - "indexmap 2.5.0", + "indexmap 2.6.0", "matrix-sdk", "matrix-sdk-base", "matrix-sdk-sqlite", @@ -161,7 +161,7 @@ dependencies = [ "acter-core", "anyhow", "app_dirs2", - "clap 4.5.17", + "clap 4.5.20", "crossterm", "dialoguer", "env_logger 0.10.2", @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -271,9 +271,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -286,43 +286,43 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "anymap2" @@ -344,16 +344,16 @@ dependencies = [ [[package]] name = "aquamarine" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21cc1548309245035eb18aa7f0967da6bc65587005170c56e6ef2788a4cf3f4e" +checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" dependencies = [ "include_dir", "itertools 0.10.5", - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -400,7 +400,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -448,9 +448,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.12" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" +checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" dependencies = [ "flate2", "futures-core", @@ -461,9 +461,9 @@ dependencies = [ [[package]] name = "async-once-cell" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9338790e78aa95a416786ec8389546c4b6a1dfc3dc36071ed9518a9413a542eb" +checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" [[package]] name = "async-recursion" @@ -473,7 +473,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -488,9 +488,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -499,24 +499,24 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -544,9 +544,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" @@ -561,7 +561,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "itoa", "matchit", "memchr", @@ -725,7 +725,7 @@ dependencies = [ "home", "http 1.1.0", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-named-pipe", "hyper-rustls", "hyper-util", @@ -783,9 +783,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "bytesize" @@ -861,9 +861,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.21" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "jobserver", "libc", @@ -983,9 +983,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -993,9 +993,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -1003,19 +1003,19 @@ dependencies = [ "strsim 0.11.1", "terminal_size", "unicase", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1035,9 +1035,9 @@ checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" @@ -1067,7 +1067,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width", + "unicode-width 0.1.14", "windows-sys 0.52.0", ] @@ -1247,7 +1247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1284,7 +1284,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1308,7 +1308,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1319,7 +1319,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1402,7 +1402,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1433,38 +1433,38 @@ checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "derive_builder" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "derive_builder_macro" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1491,27 +1491,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - [[package]] name = "discard" version = "1.0.4" @@ -1569,9 +1548,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -1585,7 +1564,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1640,6 +1619,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "5.3.1" @@ -1720,7 +1710,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1758,7 +1748,7 @@ dependencies = [ "ffi-gen", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1767,11 +1757,23 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -1828,9 +1830,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1843,9 +1845,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1853,15 +1855,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1870,19 +1872,19 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1903,21 +1905,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1968,7 +1970,7 @@ checksum = "553630feadf7b76442b0849fd25fdf89b860d933623aec9693fed19af0400c78" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1989,7 +1991,7 @@ checksum = "913dce4c5f06c2ea40fc178c06f777ac89fc6b1383e90c254fafb1abe4ba3c82" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", "uuid", ] @@ -2008,9 +2010,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git2" @@ -2071,9 +2073,9 @@ dependencies = [ [[package]] name = "growable-bloom-filter" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c669fa03050eb3445343f215d62fc1ab831e8098bc9a55f26e9724faff11075c" +checksum = "d174ccb4ba660d431329e7f0797870d0a4281e36353ec4b4a3c5eab6c2cfb6f1" dependencies = [ "serde", "serde_bytes", @@ -2093,7 +2095,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.5.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -2112,7 +2114,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.5.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -2134,6 +2136,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "hashlink" version = "0.9.1" @@ -2224,16 +2232,16 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ff6858c1f7e2a470c5403091866fa95b36fe0dbac5d771f932c15e5ff1ee501" +checksum = "2e15626aaf9c351bc696217cbe29cb9b5e86c43f8a46b5e2f5c6c5cf7cb904ce" dependencies = [ "log", "mac", "markup5ever", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -2294,9 +2302,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -2312,9 +2320,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -2336,9 +2344,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ "bytes", "futures-channel", @@ -2362,7 +2370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" dependencies = [ "hex", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "pin-project-lite", "tokio", @@ -2378,7 +2386,7 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "rustls", "rustls-native-certs 0.8.0", @@ -2395,7 +2403,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.30", + "hyper 0.14.31", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2409,7 +2417,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "native-tls", "tokio", @@ -2419,20 +2427,19 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.0", "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] @@ -2445,7 +2452,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "pin-project-lite", "tokio", @@ -2477,9 +2484,9 @@ dependencies = [ [[package]] name = "icalendar" -version = "0.16.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e8f1fe0a403db5e8f40accb3f8702eeb073de62e41fb9f70ca475dfb25f2f9" +checksum = "ade2740e54a5de285fcc5eb7f97c75b4fb8842d807f62ecbbe716006676d6181" dependencies = [ "chrono", "iso8601", @@ -2575,12 +2582,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.0", "serde", ] @@ -2614,9 +2621,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is-terminal" @@ -2710,9 +2717,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -2763,9 +2770,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libgit2-sys" @@ -2789,6 +2796,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", + "redox_syscall 0.5.7", ] [[package]] @@ -2885,7 +2893,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -2896,7 +2904,7 @@ checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -2909,7 +2917,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -2942,9 +2950,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d581ff8be69d08a2efa23a959d81aa22b739073f749f067348bd4f4ba4b69195" +checksum = "82c88c6129bd24319e62a0359cb6b958fa7e8be6e19bb1663bc396b90883aca5" dependencies = [ "log", "phf", @@ -2989,13 +2997,13 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "matrix-sdk" version = "0.7.1" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#2a03de3bd5ffd57dc63328964cb430be786d426e" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" dependencies = [ "anyhow", "anymap2", @@ -3015,7 +3023,7 @@ dependencies = [ "gloo-timers", "http 1.1.0", "imbl", - "indexmap 2.5.0", + "indexmap 2.6.0", "js_int", "matrix-sdk-base", "matrix-sdk-common", @@ -3043,7 +3051,7 @@ dependencies = [ [[package]] name = "matrix-sdk-base" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#2a03de3bd5ffd57dc63328964cb430be786d426e" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" dependencies = [ "as_variant", "async-trait", @@ -3067,7 +3075,7 @@ dependencies = [ [[package]] name = "matrix-sdk-common" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#2a03de3bd5ffd57dc63328964cb430be786d426e" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" dependencies = [ "async-trait", "futures-core", @@ -3088,14 +3096,13 @@ dependencies = [ [[package]] name = "matrix-sdk-crypto" version = "0.7.2" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#2a03de3bd5ffd57dc63328964cb430be786d426e" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" dependencies = [ "aes", "as_variant", "async-trait", "bs58", "byteorder", - "cbc", "cfg-if", "ctr", "eyeball", @@ -3128,7 +3135,7 @@ dependencies = [ [[package]] name = "matrix-sdk-indexeddb" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#2a03de3bd5ffd57dc63328964cb430be786d426e" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" dependencies = [ "anyhow", "async-trait", @@ -3156,7 +3163,7 @@ dependencies = [ [[package]] name = "matrix-sdk-sqlite" version = "0.7.1" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#2a03de3bd5ffd57dc63328964cb430be786d426e" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" dependencies = [ "async-trait", "deadpool-sqlite", @@ -3178,7 +3185,7 @@ dependencies = [ [[package]] name = "matrix-sdk-store-encryption" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#2a03de3bd5ffd57dc63328964cb430be786d426e" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" dependencies = [ "base64 0.22.1", "blake3", @@ -3220,7 +3227,7 @@ version = "0.1.3+dev" [[package]] name = "matrix-sdk-test" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#2a03de3bd5ffd57dc63328964cb430be786d426e" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" dependencies = [ "ctor", "getrandom", @@ -3239,16 +3246,16 @@ dependencies = [ [[package]] name = "matrix-sdk-test-macros" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#2a03de3bd5ffd57dc63328964cb430be786d426e" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" dependencies = [ "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "matrix-sdk-ui" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#2a03de3bd5ffd57dc63328964cb430be786d426e" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" dependencies = [ "as_variant", "async-once-cell", @@ -3264,7 +3271,7 @@ dependencies = [ "fuzzy-matcher", "growable-bloom-filter", "imbl", - "indexmap 2.5.0", + "indexmap 2.6.0", "itertools 0.12.1", "matrix-sdk", "matrix-sdk-base", @@ -3276,6 +3283,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "tokio-stream", "tracing", "unicode-normalization", ] @@ -3310,9 +3318,9 @@ dependencies = [ [[package]] name = "minicov" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" +checksum = "def6d99771d7c499c26ad4d40eb6645eafd3a1553b35fc26ea5a489a45e82d9a" dependencies = [ "cc", "walkdir", @@ -3320,9 +3328,9 @@ dependencies = [ [[package]] name = "minijinja" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1028b628753a7e1a88fc59c9ba4b02ecc3bc0bd3c7af23df667bc28df9b3310e" +checksum = "c9ca8daf4b0b4029777f1bc6e1aedd1aec7b74c276a43bc6f620a8e1a1c0a90e" dependencies = [ "serde", ] @@ -3371,7 +3379,7 @@ name = "mr_minutes" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.17", + "clap 4.5.20", "env_logger 0.10.2", "git2", "log", @@ -3459,18 +3467,18 @@ checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oneshot-uniffi" @@ -3486,9 +3494,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -3507,7 +3515,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -3518,18 +3526,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.2+3.3.2" +version = "300.4.0+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -3538,12 +3546,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "os_str_bytes" version = "6.6.1" @@ -3591,7 +3593,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.4", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] @@ -3604,7 +3606,7 @@ checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" dependencies = [ "parse-display-derive", "regex", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -3616,9 +3618,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "structmeta", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -3660,9 +3662,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.12" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c73c26c01b8c87956cea613c907c9d6ecffd8d18a2a5908e5de0adfaa185cea" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", "thiserror", @@ -3671,9 +3673,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.12" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664d22978e2815783adbdd2c588b455b1bd625299ce36b2a99881ac9627e6d8d" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" dependencies = [ "pest", "pest_generator", @@ -3681,22 +3683,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.12" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d5487022d5d33f4c30d91c22afa240ce2a644e87fe08caad974d4eab6badbe" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "pest_meta" -version = "2.7.12" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0091754bbd0ea592c4deb3a122ce8ecbb0753b738aa82bc055fcc2eccc8d8174" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" dependencies = [ "once_cell", "pest", @@ -3753,7 +3755,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -3776,29 +3778,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -3818,9 +3820,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plain" @@ -3869,30 +3871,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -3916,9 +3894,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -3935,12 +3913,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" dependencies = [ "bytes", - "prost-derive 0.13.2", + "prost-derive 0.13.3", ] [[package]] @@ -3953,20 +3931,20 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "prost-derive" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -3991,9 +3969,9 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666f0f59e259aea2d72e6012290c09877a780935cc3c18b1ceded41f3890d59c" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" dependencies = [ "bitflags 2.6.0", "memchr", @@ -4120,9 +4098,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.4" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] @@ -4133,27 +4120,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -4167,13 +4143,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -4184,9 +4160,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" @@ -4196,9 +4172,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.12.7" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" dependencies = [ "async-compression", "base64 0.22.1", @@ -4210,7 +4186,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-rustls", "hyper-tls", "hyper-util", @@ -4224,7 +4200,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", - "rustls-native-certs 0.7.3", + "rustls-native-certs 0.8.0", "rustls-pemfile", "rustls-pki-types", "serde", @@ -4286,7 +4262,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.10.1" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "assign", "js_int", @@ -4302,7 +4278,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.18.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "as_variant", "assign", @@ -4325,7 +4301,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "as_variant", "base64 0.22.1", @@ -4333,7 +4309,7 @@ dependencies = [ "form_urlencoded", "getrandom", "http 1.1.0", - "indexmap 2.5.0", + "indexmap 2.6.0", "js-sys", "js_int", "konst", @@ -4357,14 +4333,14 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.28.1" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "as_variant", - "indexmap 2.5.0", + "indexmap 2.6.0", "js_int", "js_option", "percent-encoding", - "pulldown-cmark 0.12.1", + "pulldown-cmark 0.12.2", "regex", "ruma-common", "ruma-html", @@ -4382,7 +4358,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.9.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "http 1.1.0", "js_int", @@ -4396,7 +4372,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.2.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "as_variant", "html5ever", @@ -4408,7 +4384,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.5" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "js_int", "thiserror", @@ -4417,7 +4393,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=1ae98db9c44f46a590f4c76baf5cef70ebb6970d#1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" dependencies = [ "cfg-if", "once_cell", @@ -4426,7 +4402,7 @@ dependencies = [ "quote", "ruma-identifiers-validation", "serde", - "syn 2.0.77", + "syn 2.0.85", "toml 0.8.19", ] @@ -4467,9 +4443,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags 2.6.0", "errno", @@ -4480,9 +4456,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" dependencies = [ "once_cell", "ring", @@ -4520,19 +4496,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" [[package]] name = "rustls-webpki" @@ -4547,9 +4522,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" @@ -4577,18 +4552,18 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.17" +version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c947adb109a8afce5fc9c7bf951f87f146e9147b3a6a58413105628fb1d1e66" +checksum = "d8d25269dd3a12467afe2e510f69fb0b46b698e5afb296b59f2145259deaf8e8" dependencies = [ "sdd", ] [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ "windows-sys 0.59.0", ] @@ -4622,14 +4597,14 @@ checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "sdd" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a7b59a5d9b0099720b417b6325d91a52cbf5b3dcb5041d864be53eefa58abc" +checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95" [[package]] name = "security-framework" @@ -4646,9 +4621,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -4665,9 +4640,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] @@ -4694,13 +4669,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -4710,7 +4685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" dependencies = [ "form_urlencoded", - "indexmap 2.5.0", + "indexmap 2.6.0", "itoa", "ryu", "serde", @@ -4718,9 +4693,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -4736,14 +4711,14 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -4762,15 +4737,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.5.0", + "indexmap 2.6.0", "serde", "serde_derive", "serde_json", @@ -4780,14 +4755,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -4967,7 +4942,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -4978,7 +4953,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -5000,7 +4975,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -5022,9 +4997,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -5069,9 +5044,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -5102,12 +5077,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -5124,17 +5099,17 @@ dependencies = [ [[package]] name = "testcontainers" -version = "0.22.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef8374cea2c164699681ecc39316c3e1d953831a7a5721e36c7736d974e15fa" +checksum = "5f40cc2bd72e17f328faf8ca7687fe337e61bccd8acf9674fa78dd3792b045e1" dependencies = [ "async-trait", "bollard", "bollard-stubs", "bytes", - "dirs", "docker_credential", "either", + "etcetera", "futures", "log", "memchr", @@ -5146,6 +5121,7 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "tokio-tar", "tokio-util", "url", ] @@ -5158,27 +5134,27 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -5239,9 +5215,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -5274,7 +5250,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -5321,6 +5297,21 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tar" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall 0.3.5", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "tokio-util" version = "0.7.12" @@ -5349,7 +5340,7 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ - "indexmap 2.5.0", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", @@ -5367,11 +5358,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.21" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.5.0", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", @@ -5392,7 +5383,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "hyper-timeout", "percent-encoding", "pin-project", @@ -5480,7 +5471,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -5550,7 +5541,7 @@ dependencies = [ "crossterm", "termion", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -5591,9 +5582,9 @@ checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "ulid" @@ -5608,18 +5599,15 @@ dependencies = [ [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" @@ -5650,15 +5638,21 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uniffi" @@ -5668,7 +5662,7 @@ checksum = "5ad0be8bba6c242d2d16922de4a9c8f167b9491729fda552e70f8626bf7302cb" dependencies = [ "anyhow", "camino", - "clap 4.5.17", + "clap 4.5.20", "uniffi_bindgen", "uniffi_build", "uniffi_core", @@ -5685,7 +5679,7 @@ dependencies = [ "askama", "camino", "cargo_metadata", - "clap 4.5.17", + "clap 4.5.20", "fs-err", "glob", "goblin", @@ -5718,7 +5712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72775b3afa6adb30e0c92b3107858d2fcb0ff1a417ac242db1f648b0e2dd0ef2" dependencies = [ "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -5750,7 +5744,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.77", + "syn 2.0.85", "toml 0.5.11", "uniffi_build", "uniffi_meta", @@ -5842,9 +5836,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "wasm-bindgen", @@ -5870,8 +5864,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vodozemac" -version = "0.7.0" -source = "git+https://github.com/matrix-org/vodozemac?rev=57cbf7e939d7b54d20207e8361b7135bd65c9cc2#57cbf7e939d7b54d20207e8361b7135bd65c9cc2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4b56780b7827dd72c3c6398c3048752bebf8d1d84ec19b606b15dbc3c850b8" dependencies = [ "aes", "arrayvec", @@ -5885,7 +5880,7 @@ dependencies = [ "hkdf", "hmac", "matrix-pickle", - "prost 0.13.2", + "prost 0.13.3", "rand", "serde", "serde_bytes", @@ -5924,9 +5919,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -5935,24 +5930,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -5962,9 +5957,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5972,28 +5967,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-bindgen-test" -version = "0.3.43" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9" +checksum = "d381749acb0943d357dcbd8f0b100640679883fcdeeef04def49daf8d33a5426" dependencies = [ "console_error_panic_hook", "js-sys", @@ -6006,20 +6001,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.43" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" +checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -6030,9 +6025,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -6068,9 +6063,9 @@ dependencies = [ [[package]] name = "wildmatch" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3928939971918220fed093266b809d1ee4ec6c1a2d72692ff6876898f3b16c19" +checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" [[package]] name = "winapi" @@ -6358,9 +6353,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -6378,7 +6373,7 @@ dependencies = [ "futures", "http 1.1.0", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "log", "once_cell", @@ -6401,6 +6396,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "xdg" version = "2.5.2" @@ -6431,7 +6437,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -6451,5 +6457,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] diff --git a/Cargo.toml b/Cargo.toml index 9e7c87747e41..70ecd5993357 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,11 +44,11 @@ default-features = false # ATTENTION: _MUST_ stay in sync with the ruma-rev used by matrix-sdk! [workspace.dependencies.ruma] git = "https://github.com/ruma/ruma" -rev = "1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a" # - see comment above [workspace.dependencies.ruma-common] git = "https://github.com/ruma/ruma" -rev = "1ae98db9c44f46a590f4c76baf5cef70ebb6970d" +rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a" [workspace.dependencies.url] version = "2.5.0" diff --git a/native/acter/src/api/attachments.rs b/native/acter/src/api/attachments.rs index 447bffb166de..b19620ba19d0 100644 --- a/native/acter/src/api/attachments.rs +++ b/native/acter/src/api/attachments.rs @@ -13,7 +13,7 @@ use matrix_sdk::{ }; use matrix_sdk_base::ruma::{ events::{room::message::RoomMessageEvent, MessageLikeEventType}, - EventId, OwnedEventId, OwnedTransactionId, + EventId, OwnedEventId, OwnedTransactionId, OwnedUserId, }; use std::{fs::exists, io::Write, ops::Deref, path::PathBuf, str::FromStr}; use tokio::sync::broadcast::Receiver; @@ -492,8 +492,10 @@ impl AttachmentsManager { RUNTIME .spawn(async move { let evt = room.event(&event_id, None).await?; - let event_content = evt.event.deserialize_as::()?; - let permitted = if event_content.sender() == my_id { + let Some(sender) = evt.kind.raw().get_field::("sender")? else { + bail!("Could not determine the sender of the previous event"); + }; + let permitted = if sender == my_id { room.can_user_redact_own(&my_id).await? } else { room.can_user_redact_other(&my_id).await? diff --git a/native/acter/src/api/client/sync.rs b/native/acter/src/api/client/sync.rs index 8e2ecbdc526c..02f3af1df77b 100644 --- a/native/acter/src/api/client/sync.rs +++ b/native/acter/src/api/client/sync.rs @@ -12,6 +12,7 @@ use matrix_sdk::{ config::SyncSettings, event_handler::EventHandlerHandle, room::Room as SdkRoom, RoomState, RumaApiError, }; +use matrix_sdk_base::ruma::events::AnySyncEphemeralRoomEvent; use matrix_sdk_base::ruma::{ api::client::{ error::{ErrorBody, ErrorKind}, @@ -19,7 +20,6 @@ use matrix_sdk_base::ruma::{ }, OwnedRoomId, }; -use ruma::events::AnySyncEphemeralRoomEvent; use std::{ collections::{BTreeMap, HashMap}, io::Write, diff --git a/native/acter/src/api/convo.rs b/native/acter/src/api/convo.rs index 0c77c7d3b310..ea0bf0f55d07 100644 --- a/native/acter/src/api/convo.rs +++ b/native/acter/src/api/convo.rs @@ -20,6 +20,7 @@ use matrix_sdk_base::ruma::{ RoomOrAliasId, ServerName, UserId, }; use matrix_sdk_ui::{timeline::RoomExt, Timeline}; +use ruma::OwnedTransactionId; use std::{ ops::Deref, path::PathBuf, @@ -163,11 +164,7 @@ impl Convo { }; let room_id = latest_msg_room.room_id(); - let full_event = RoomMessage::new_event_item( - user_id.clone(), - &msg, - format!("{room_id}:latest_msg"), - ); + let full_event = RoomMessage::new_event_item(user_id.clone(), &msg); set_latest_msg(&latest_msg_client, room_id, &last_msg_lock_tl, full_event).await; } warn!(room_id=?latest_msg_room.room_id(), "Timeline stopped") diff --git a/native/acter/src/api/message.rs b/native/acter/src/api/message.rs index 1ba3bc6009e6..47a3c9a24a1b 100644 --- a/native/acter/src/api/message.rs +++ b/native/acter/src/api/message.rs @@ -7,9 +7,10 @@ use matrix_sdk_base::ruma::{ OwnedEventId, OwnedTransactionId, OwnedUserId, }; use matrix_sdk_ui::timeline::{ - EventSendState as SdkEventSendState, EventTimelineItem, MembershipChange, TimelineItem, - TimelineItemContent, TimelineItemKind, VirtualTimelineItem, + EventSendState as SdkEventSendState, EventTimelineItem, MembershipChange, TimelineEventItemId, + TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem, }; +use ruma::TransactionId; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tracing::info; @@ -418,15 +419,14 @@ pub struct RoomMessage { } impl RoomMessage { - pub(crate) fn new_event_item( - my_id: OwnedUserId, - event: &EventTimelineItem, - unique_id: String, - ) -> Self { + pub(crate) fn new_event_item(my_id: OwnedUserId, event: &EventTimelineItem) -> Self { RoomMessage { item_type: "event".to_string(), event_item: Some(RoomEventItem::new(event, my_id)), - unique_id, + unique_id: match event.identifier() { + TimelineEventItemId::EventId(e) => e.to_string(), + TimelineEventItemId::TransactionId(t) => t.to_string(), + }, virtual_item: None, } } @@ -481,13 +481,11 @@ impl RoomMessage { impl From<(Arc, OwnedUserId)> for RoomMessage { fn from(v: (Arc, OwnedUserId)) -> RoomMessage { let (item, user_id) = v; - let unique_id = item.unique_id().to_owned(); + let unique_id = item.unique_id(); match item.kind() { - TimelineItemKind::Event(event_item) => { - RoomMessage::new_event_item(user_id, event_item, unique_id) - } + TimelineItemKind::Event(event_item) => RoomMessage::new_event_item(user_id, event_item), TimelineItemKind::Virtual(virtual_item) => { - RoomMessage::new_virtual_item(virtual_item, unique_id) + RoomMessage::new_virtual_item(virtual_item, unique_id.0.clone()) } } } diff --git a/native/acter/src/api/reactions.rs b/native/acter/src/api/reactions.rs index 16132241adb2..e68aae26dc15 100644 --- a/native/acter/src/api/reactions.rs +++ b/native/acter/src/api/reactions.rs @@ -161,8 +161,10 @@ impl ReactionManager { RUNTIME .spawn(async move { let evt = room.event(&event_id, None).await?; - let event_content = evt.event.deserialize_as::()?; - let permitted = if event_content.sender() == my_id { + let Some(sender) = evt.kind.raw().get_field::("sender")? else { + bail!("Could not determine the sender of the previous event"); + }; + let permitted = if sender == my_id { room.can_user_redact_own(&my_id).await? } else { room.can_user_redact_other(&my_id).await? diff --git a/native/acter/src/api/room.rs b/native/acter/src/api/room.rs index c927afec5312..1d897053a8da 100644 --- a/native/acter/src/api/room.rs +++ b/native/acter/src/api/room.rs @@ -1126,7 +1126,7 @@ impl Room { RUNTIME .spawn(async move { let evt = room.event(&event_id, None).await?; - let event_content = evt.event.deserialize_as::()?; + let event_content = evt.kind.raw().deserialize_as::()?; let original = event_content .as_original() .context("Couldn’t get original msg")?; @@ -1200,6 +1200,7 @@ impl Room { RoomState::Joined => "joined".to_string(), RoomState::Left => "left".to_string(), RoomState::Invited => "invited".to_string(), + RoomState::Knocked => "knocked".to_string(), } } @@ -1325,7 +1326,7 @@ impl Room { RUNTIME .spawn(async move { let evt = room.event(&evt_id, None).await?; - let event_content = evt.event.deserialize_as::()?; + let event_content = evt.kind.raw().deserialize_as::()?; let original = event_content .as_original() .context("Unable to get original msg")?; @@ -1531,7 +1532,7 @@ impl Room { RUNTIME .spawn(async move { let evt = room.event(&evt_id, None).await?; - let event_content = evt.event.deserialize_as::()?; + let event_content = evt.kind.raw().deserialize_as::()?; let original = event_content .as_original() .context("Couldn’t get original msg")?; diff --git a/native/acter/src/api/spaces.rs b/native/acter/src/api/spaces.rs index f19665825940..0dc3de78f023 100644 --- a/native/acter/src/api/spaces.rs +++ b/native/acter/src/api/spaces.rs @@ -18,6 +18,7 @@ use acter_core::{ SyncTaskEvent, SyncTaskListEvent, SyncTaskListUpdateEvent, SyncTaskSelfAssignEvent, SyncTaskSelfUnassignEvent, SyncTaskUpdateEvent, }, + AnyActerEvent, }, executor::Executor, models::{AnyActerModel, EventMeta}, @@ -430,16 +431,24 @@ impl Space { } loop { - trace!(name, ?msg_options, "fetching messages"); + trace!(?room_id, name, ?msg_options, "fetching messages"); let Messages { end, chunk, state, .. } = self.room.messages(msg_options).await?; - trace!(name, ?chunk, end, "messages received"); + trace!(?room_id, name, ?chunk, end, "messages received"); let has_chunks = !chunk.is_empty(); for msg in chunk { - match AnyActerModel::try_from(&msg.event) { + let event = match msg.kind.raw().deserialize_as::() { + Ok(e) => e, + Err(error) => { + error!(?error, ?room_id, "Not a proper acter event"); + continue; + } + }; + + match AnyActerModel::try_from(event) { Ok(model) => { trace!(?room_id, user_id=?client.user_id(), ?model, "handling timeline event"); if let Err(e) = self.client.executor().handle(model).await { @@ -465,11 +474,11 @@ impl Space { trace!(?room_id, user_id=?client.user_id(), ?inner, "ignoring event"); } Err(m) => { - if let Ok(state_key) = msg.event.get_field::("state_key") { + if let Ok(state_key) = msg.kind.raw().get_field::("state_key") { trace!(state_key=?state_key, "ignoring state event"); // ignore state keys } else { - error!(event=?msg.event, "Model didn’t parse {:}", m); + error!(event=?msg, "Model didn’t parse {:}", m); } } }; diff --git a/native/acter/src/api/stream.rs b/native/acter/src/api/stream.rs index e82c4bcf6f99..93f4a19f6875 100644 --- a/native/acter/src/api/stream.rs +++ b/native/acter/src/api/stream.rs @@ -20,7 +20,8 @@ use matrix_sdk_base::ruma::{ }, EventId, OwnedEventId, OwnedTransactionId, }; -use matrix_sdk_ui::timeline::Timeline; +use matrix_sdk_ui::timeline::{Timeline, TimelineEventItemId}; +use ruma::TransactionId; use std::{ops::Deref, sync::Arc}; use tracing::info; @@ -90,11 +91,7 @@ impl TimelineStream { let Some(tl) = timeline.item_by_event_id(&event_id).await else { bail!("Event not found") }; - Ok(RoomMessage::new_event_item( - user_id, - &tl, - event_id.to_string(), - )) + Ok(RoomMessage::new_event_item(user_id, &tl)) }) .await? } @@ -145,13 +142,11 @@ impl TimelineStream { bail!("No permissions to send message in this room"); } - let event_content = room - .event(&event_id, None) - .await? - .event - .deserialize_as::()?; + let Some(item) = timeline.item_by_event_id(&event_id).await else { + bail!("Unable to find event"); + }; - if event_content.sender() != my_id { + if item.is_editable() { bail!("Unable to edit an event not sent by own user"); } @@ -161,7 +156,7 @@ impl TimelineStream { .context("Not found which item would be edited")?; let event_content = draft.into_room_msg(&room).await?; let new_content = EditedContent::RoomMessage(event_content); - timeline.edit(&item, new_content).await?; + timeline.edit(&item.identifier(), new_content).await?; Ok(true) }) .await? @@ -306,6 +301,11 @@ impl TimelineStream { let room = self.room.clone(); let my_id = self.room.user_id()?; let timeline = self.timeline.clone(); + let unique_id = + match OwnedEventId::try_from(unique_id.clone()).map(TimelineEventItemId::EventId) { + Ok(o) => o, + Err(e) => TimelineEventItemId::TransactionId(OwnedTransactionId::from(unique_id)), + }; RUNTIME .spawn(async move { diff --git a/native/acter/src/api/stream/msg_draft.rs b/native/acter/src/api/stream/msg_draft.rs index 4208fb3edacb..b926bb9edce5 100644 --- a/native/acter/src/api/stream/msg_draft.rs +++ b/native/acter/src/api/stream/msg_draft.rs @@ -499,7 +499,7 @@ impl MsgDraft { let mut reader = std::fs::File::open(path.clone())?; let encrypted_file = room .client() - .prepare_encrypted_file(&content_type, &mut reader) + .upload_encrypted_file(&content_type, &mut reader) .await?; let body = path .file_name() @@ -538,7 +538,7 @@ impl MsgDraft { let mut reader = std::fs::File::open(path.clone())?; let encrypted_file = room .client() - .prepare_encrypted_file(&content_type, &mut reader) + .upload_encrypted_file(&content_type, &mut reader) .await?; let body = path .file_name() @@ -577,7 +577,7 @@ impl MsgDraft { let mut reader = std::fs::File::open(path.clone())?; let encrypted_file = room .client() - .prepare_encrypted_file(&content_type, &mut reader) + .upload_encrypted_file(&content_type, &mut reader) .await?; let body = path .file_name() @@ -616,7 +616,7 @@ impl MsgDraft { let mut reader = std::fs::File::open(path.clone())?; let encrypted_file = room .client() - .prepare_encrypted_file(&content_type, &mut reader) + .upload_encrypted_file(&content_type, &mut reader) .await?; let body = path .file_name() diff --git a/native/cli/src/action/history.rs b/native/cli/src/action/history.rs index b303a3d7857f..01d37e3f82fb 100644 --- a/native/cli/src/action/history.rs +++ b/native/cli/src/action/history.rs @@ -42,7 +42,7 @@ impl HistoryOpts { } = room.messages(msg_options).await?; for msg in chunk { - let evt = msg.event; + let evt = msg.kind.raw().clone(); println!("- {}", evt.into_json()); } diff --git a/native/test/src/tests/redact.rs b/native/test/src/tests/redact.rs index 16b938e178b3..d9361ccaa385 100644 --- a/native/test/src/tests/redact.rs +++ b/native/test/src/tests/redact.rs @@ -103,7 +103,7 @@ async fn message_redaction() -> Result<()> { // but it is possible to get redaction event by event id on convo let ev = convo.event(&redact_id, None).await?; - let event_content = ev.event.deserialize_as::()?; + let event_content = ev.kind.raw().deserialize_as::()?; let original = event_content .as_original() .context("Redaction event should get original event")?; From 74011b3ffb07d6f42825c7724390bdacac6c2958 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Thu, 31 Oct 2024 16:55:26 +0000 Subject: [PATCH 56/77] Update to latest matrix-sdk --- Cargo.lock | 61 +++++++++++++++------------ Cargo.toml | 6 +-- native/acter/src/api/push.rs | 7 +-- native/acter/src/api/verification.rs | 14 ++++-- native/file-event-cache/src/lib.rs | 13 ++++++ native/file-event-cache/src/queued.rs | 14 ++++++ 6 files changed, 76 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a93dca5d099..b28d98ba0420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3003,7 +3003,7 @@ dependencies = [ [[package]] name = "matrix-sdk" version = "0.7.1" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#d4b9145bc27f144b63b6830af2c037ca907b58be" dependencies = [ "anyhow", "anymap2", @@ -3021,6 +3021,7 @@ dependencies = [ "futures-core", "futures-util", "gloo-timers", + "growable-bloom-filter", "http 1.1.0", "imbl", "indexmap 2.6.0", @@ -3051,7 +3052,7 @@ dependencies = [ [[package]] name = "matrix-sdk-base" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#d4b9145bc27f144b63b6830af2c037ca907b58be" dependencies = [ "as_variant", "async-trait", @@ -3075,7 +3076,7 @@ dependencies = [ [[package]] name = "matrix-sdk-common" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#d4b9145bc27f144b63b6830af2c037ca907b58be" dependencies = [ "async-trait", "futures-core", @@ -3096,7 +3097,7 @@ dependencies = [ [[package]] name = "matrix-sdk-crypto" version = "0.7.2" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#d4b9145bc27f144b63b6830af2c037ca907b58be" dependencies = [ "aes", "as_variant", @@ -3135,7 +3136,7 @@ dependencies = [ [[package]] name = "matrix-sdk-indexeddb" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#d4b9145bc27f144b63b6830af2c037ca907b58be" dependencies = [ "anyhow", "async-trait", @@ -3163,7 +3164,7 @@ dependencies = [ [[package]] name = "matrix-sdk-sqlite" version = "0.7.1" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#d4b9145bc27f144b63b6830af2c037ca907b58be" dependencies = [ "async-trait", "deadpool-sqlite", @@ -3185,7 +3186,7 @@ dependencies = [ [[package]] name = "matrix-sdk-store-encryption" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#d4b9145bc27f144b63b6830af2c037ca907b58be" dependencies = [ "base64 0.22.1", "blake3", @@ -3227,7 +3228,7 @@ version = "0.1.3+dev" [[package]] name = "matrix-sdk-test" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#d4b9145bc27f144b63b6830af2c037ca907b58be" dependencies = [ "ctor", "getrandom", @@ -3246,7 +3247,7 @@ dependencies = [ [[package]] name = "matrix-sdk-test-macros" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#d4b9145bc27f144b63b6830af2c037ca907b58be" dependencies = [ "quote", "syn 2.0.85", @@ -3255,7 +3256,7 @@ dependencies = [ [[package]] name = "matrix-sdk-ui" version = "0.7.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk#40f4fc138b3b1f5ab764bd823be3b9a78a481896" +source = "git+https://github.com/matrix-org/matrix-rust-sdk#d4b9145bc27f144b63b6830af2c037ca907b58be" dependencies = [ "as_variant", "async-once-cell", @@ -4261,8 +4262,9 @@ dependencies = [ [[package]] name = "ruma" -version = "0.10.1" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d719b9e1ce5b34a1e0b6e2ba4707f7923ce7fb3474881d771466456d68f3e485" dependencies = [ "assign", "js_int", @@ -4277,8 +4279,9 @@ dependencies = [ [[package]] name = "ruma-client-api" -version = "0.18.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325e054db8d5545c00767d9868356d61e63f2c6cb8b54768346d66696ea4ad48" dependencies = [ "as_variant", "assign", @@ -4300,8 +4303,9 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4717eb215175df5087fdd79da2c9a4198c9a50fe747db0afbc23c8ac18a25da8" dependencies = [ "as_variant", "base64 0.22.1", @@ -4332,8 +4336,9 @@ dependencies = [ [[package]] name = "ruma-events" -version = "0.28.1" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969cfed397d22f0338d99457409aa9c9dd4def4a5ce8d6567e914a320bad30da" dependencies = [ "as_variant", "indexmap 2.6.0", @@ -4357,8 +4362,9 @@ dependencies = [ [[package]] name = "ruma-federation-api" -version = "0.9.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5a09ac22b3352bf7a350514dc9a87e1b56aba04c326ac9ce142740f7218afa" dependencies = [ "http 1.1.0", "js_int", @@ -4371,8 +4377,9 @@ dependencies = [ [[package]] name = "ruma-html" -version = "0.2.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7571886b6df90a4ed72e7481a5a39cc2a5b3a4e956e9366ad798e4e2e9fe8005" dependencies = [ "as_variant", "html5ever", @@ -4383,8 +4390,9 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" -version = "0.9.5" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7f9b534a65698d7db3c08d94bf91de0046fe6c7893a7b360502f65e7011ac4" dependencies = [ "js_int", "thiserror", @@ -4392,8 +4400,9 @@ dependencies = [ [[package]] name = "ruma-macros" -version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=26165b23fc2ae9928c5497a21db3d31f4b44cc2a#26165b23fc2ae9928c5497a21db3d31f4b44cc2a" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d57d3cb20e8e758e8f7c5e408ce831d46758003b615100099852e468631934" dependencies = [ "cfg-if", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 70ecd5993357..7acb11ace067 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,12 +43,10 @@ default-features = false # not used directly but needed to enable specific features from ruma # ATTENTION: _MUST_ stay in sync with the ruma-rev used by matrix-sdk! [workspace.dependencies.ruma] -git = "https://github.com/ruma/ruma" -rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a" + version = "0.11.0" # - see comment above [workspace.dependencies.ruma-common] -git = "https://github.com/ruma/ruma" -rev = "26165b23fc2ae9928c5497a21db3d31f4b44cc2a" + version = "0.14.0" [workspace.dependencies.url] version = "2.5.0" diff --git a/native/acter/src/api/push.rs b/native/acter/src/api/push.rs index 662650c6f81c..60305e6c7573 100644 --- a/native/acter/src/api/push.rs +++ b/native/acter/src/api/push.rs @@ -16,7 +16,7 @@ use matrix_sdk_base::ruma::{ device, push::{ get_pushers, get_pushrules_all, set_pusher, set_pushrule, EmailPusherData, - Pusher as RumaPusher, PusherIds, PusherInit, PusherKind, RuleScope, + Pusher as RumaPusher, PusherIds, PusherInit, PusherKind, }, }, assign, @@ -553,10 +553,7 @@ impl Client { .spawn(async move { for rule in default_rules() { let resp = client - .send( - set_pushrule::v3::Request::new(RuleScope::Global, rule), - None, - ) + .send(set_pushrule::v3::Request::new(rule), None) .await?; } Ok(true) diff --git a/native/acter/src/api/verification.rs b/native/acter/src/api/verification.rs index e92f5733030b..82fc72b33594 100644 --- a/native/acter/src/api/verification.rs +++ b/native/acter/src/api/verification.rs @@ -370,7 +370,7 @@ async fn request_verification_handler( } VerificationRequestState::Requested { their_methods, - other_device_id, + other_device_data, } => { let device_id = client.device_id()?; let event_type = "VerificationRequestState::Requested".to_string(); @@ -388,7 +388,10 @@ async fn request_verification_handler( .collect::>() .join(","); msg.set_content("their_methods".to_string(), methods); - msg.set_content("other_device_id".to_string(), other_device_id.to_string()); + msg.set_content( + "other_device_id".to_string(), + other_device_data.device_id().to_string(), + ); if let Err(e) = controller.event_tx.try_send(msg) { error!("Dropping flow for {}: {}", flow_id, e); } @@ -396,7 +399,7 @@ async fn request_verification_handler( VerificationRequestState::Ready { their_methods, our_methods, - other_device_id, + other_device_data, } => { let device_id = client.device_id()?; let event_type = "VerificationRequestState::Ready".to_string(); @@ -420,7 +423,10 @@ async fn request_verification_handler( .collect::>() .join(","); msg.set_content("our_methods".to_string(), methods); - msg.set_content("other_device_id".to_string(), other_device_id.to_string()); + msg.set_content( + "other_device_id".to_string(), + other_device_data.device_id().to_string(), + ); if let Err(e) = controller.event_tx.try_send(msg) { error!("Dropping flow for {}: {}", flow_id, e); } diff --git a/native/file-event-cache/src/lib.rs b/native/file-event-cache/src/lib.rs index 68259723a0b9..b99f52358926 100644 --- a/native/file-event-cache/src/lib.rs +++ b/native/file-event-cache/src/lib.rs @@ -104,6 +104,19 @@ impl EventCacheStore for FileEventCacheStore { .map_err(|e| EventCacheStoreError::Backend(Box::new(e)))?; Ok(()) } + + #[instrument(skip_all)] + async fn replace_media_key( + &self, + from: &MediaRequest, + to: &MediaRequest, + ) -> Result<(), Self::Error> { + let from_filename = self.encode_key(from.source.unique_key()); + let to_filename = self.encode_key(to.source.unique_key()); + fs::rename(from_filename, to_filename) + .map_err(|e| EventCacheStoreError::Backend(Box::new(e)))?; + Ok(()) + } } #[cfg(feature = "queued")] diff --git a/native/file-event-cache/src/queued.rs b/native/file-event-cache/src/queued.rs index 2115070e3926..22e11cef2882 100644 --- a/native/file-event-cache/src/queued.rs +++ b/native/file-event-cache/src/queued.rs @@ -79,4 +79,18 @@ where .expect("We never close the semaphore"); self.inner.remove_media_content_for_uri(uri).await } + + #[instrument(skip_all)] + async fn replace_media_key( + &self, + from: &MediaRequest, + to: &MediaRequest, + ) -> Result<(), Self::Error> { + let _handle = self + .queue + .acquire() + .await + .expect("We never close the semaphore"); + self.inner.replace_media_key(from, to).await + } } From d033a37a14fe141de2a5e61d02f9381f7fbf4b97 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 12:48:58 +0000 Subject: [PATCH 57/77] Upgrade to latest ruma with necessary fixes --- Cargo.lock | 12 ++++++------ Cargo.toml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b28d98ba0420..e7afe75c4df0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4262,9 +4262,9 @@ dependencies = [ [[package]] name = "ruma" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d719b9e1ce5b34a1e0b6e2ba4707f7923ce7fb3474881d771466456d68f3e485" +checksum = "e94984418ae8a5e1160e6c87608141330e9ae26330abf22e3d15416efa96d48a" dependencies = [ "assign", "js_int", @@ -4303,9 +4303,9 @@ dependencies = [ [[package]] name = "ruma-common" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4717eb215175df5087fdd79da2c9a4198c9a50fe747db0afbc23c8ac18a25da8" +checksum = "ad71c7f49abaa047ba228339d34f9aaefa4d8b50ebeb8e859d0340cc2138bda8" dependencies = [ "as_variant", "base64 0.22.1", @@ -4336,9 +4336,9 @@ dependencies = [ [[package]] name = "ruma-events" -version = "0.29.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969cfed397d22f0338d99457409aa9c9dd4def4a5ce8d6567e914a320bad30da" +checksum = "be86dccf3504588c1f4dc1bda4ce1f8bbd646fc6dda40c77cc7de6e203e62dad" dependencies = [ "as_variant", "indexmap 2.6.0", diff --git a/Cargo.toml b/Cargo.toml index 7acb11ace067..175c9e7c9dcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,10 +43,10 @@ default-features = false # not used directly but needed to enable specific features from ruma # ATTENTION: _MUST_ stay in sync with the ruma-rev used by matrix-sdk! [workspace.dependencies.ruma] - version = "0.11.0" + version = "0.11.1" # - see comment above [workspace.dependencies.ruma-common] - version = "0.14.0" + version = "0.14.1" [workspace.dependencies.url] version = "2.5.0" From 96f6249572de353c1c5d86f2a24f4e6466101758 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 12:49:41 +0000 Subject: [PATCH 58/77] Fixing some of the broken tests --- native/acter/src/api/convo.rs | 11 +++ native/acter/src/api/stream.rs | 2 +- native/test/src/tests/formatted_body.rs | 105 ++++++++---------------- 3 files changed, 45 insertions(+), 73 deletions(-) diff --git a/native/acter/src/api/convo.rs b/native/acter/src/api/convo.rs index ea0bf0f55d07..84e597d19db0 100644 --- a/native/acter/src/api/convo.rs +++ b/native/acter/src/api/convo.rs @@ -200,6 +200,17 @@ impl Convo { TimelineStream::new(self.inner.clone(), self.timeline.clone()) } + pub async fn items(&self) -> Vec { + let user_id = self.client.user_id().expect("User must be logged in"); + self.timeline + .items() + .await + .clone() + .into_iter() + .map(|x| RoomMessage::from((x, user_id.clone()))) + .collect() + } + pub fn num_unread_notification_count(&self) -> u64 { self.inner.unread_notification_counts().notification_count } diff --git a/native/acter/src/api/stream.rs b/native/acter/src/api/stream.rs index 93f4a19f6875..0a9361657c5c 100644 --- a/native/acter/src/api/stream.rs +++ b/native/acter/src/api/stream.rs @@ -146,7 +146,7 @@ impl TimelineStream { bail!("Unable to find event"); }; - if item.is_editable() { + if !item.is_editable() { bail!("Unable to edit an event not sent by own user"); } diff --git a/native/test/src/tests/formatted_body.rs b/native/test/src/tests/formatted_body.rs index 553e882af3bc..3df94560cad1 100644 --- a/native/test/src/tests/formatted_body.rs +++ b/native/test/src/tests/formatted_body.rs @@ -1,8 +1,5 @@ use acter::api::RoomMessage; -use anyhow::{Context, Result}; -use core::time::Duration; -use futures::{pin_mut, stream::StreamExt, FutureExt}; -use tokio::time::sleep; +use anyhow::{bail, Result}; use tokio_retry::{ strategy::{jitter, FibonacciBackoff}, Retry, @@ -45,7 +42,7 @@ async fn sisko_sends_rich_text_to_kyra() -> Result<()> { let retry_strategy = FibonacciBackoff::from_millis(100).map(jitter).take(10); let fetcher_client = kyra.clone(); let target_id = room_id.clone(); - Retry::spawn(retry_strategy, move || { + Retry::spawn(retry_strategy.clone(), move || { let client = fetcher_client.clone(); let room_id = target_id.clone(); async move { client.convo(room_id.to_string()).await } @@ -53,69 +50,24 @@ async fn sisko_sends_rich_text_to_kyra() -> Result<()> { .await?; let kyra_convo = kyra.convo(room_id.to_string()).await?; - let kyra_timeline = kyra_convo.timeline_stream(); - let kyra_stream = kyra_timeline.messages_stream(); - pin_mut!(kyra_stream); // sisko sends the formatted text message to kyra let draft = sisko.text_markdown_draft("**Hello**".to_string()); sisko_timeline.send_message(Box::new(draft)).await?; - // text msg may reach via pushback action or reset action - let mut i = 30; - let mut received = None; - while i > 0 { - info!("stream loop - {i}"); - if let Some(diff) = kyra_stream.next().now_or_never().flatten() { - info!("stream diff - {}", diff.action()); - match diff.action().as_str() { - "PushBack" => { - let value = diff - .value() - .expect("diff pushback action should have valid value"); - info!("diff pushback - {:?}", value); - if let Some(event_id) = - match_room_msg(&value, "

Hello

\n") - { - received = Some(event_id); - } - } - "Reset" => { - let values = diff - .values() - .expect("diff reset action should have valid values"); - info!("diff reset - {:?}", values); - for value in values.iter() { - if let Some(event_id) = - match_room_msg(value, "

Hello

\n") - { - received = Some(event_id); - break; - } - } - } - _ => {} - } - // yay - if received.is_some() { - break; + // wait for sync to catch up + let room_tl = kyra_convo.clone(); + Retry::spawn(retry_strategy.clone(), move || { + let timeline = room_tl.clone(); + async move { + for v in timeline.items().await { + let Some(event_id) = match_room_msg(&v, "Hello") else { + continue; + }; + return Ok(event_id); } + bail!("Event not found"); } - info!("continue loop"); - i -= 1; - sleep(Duration::from_secs(1)).await; - } - info!("loop finished"); - let received = received.context("Even after 30 seconds, text msg not received")?; - - // wait for sync to catch up - let retry_strategy = FibonacciBackoff::from_millis(100).map(jitter).take(10); - let fetcher_timeline = kyra_timeline.clone(); - let target_id = received.clone(); - Retry::spawn(retry_strategy, move || { - let timeline = fetcher_timeline.clone(); - let received = target_id.clone(); - async move { timeline.get_message(received.to_string()).await } }) .await?; @@ -124,17 +76,26 @@ async fn sisko_sends_rich_text_to_kyra() -> Result<()> { fn match_room_msg(msg: &RoomMessage, body: &str) -> Option { info!("match room msg - {:?}", msg.clone()); - if msg.item_type() == "event" { - let event_item = msg.event_item().expect("room msg should have event item"); - if let Some(msg_content) = event_item.msg_content() { - if let Some(formatted) = msg_content.formatted_body() { - if formatted == body { - // exclude the pending msg - if let Some(event_id) = event_item.event_id() { - return Some(event_id); - } - } - } + if msg.item_type() != "event" { + return None; + } + let Some(event_item) = msg.event_item() else { + return None; + }; + let Some(msg_content) = event_item.msg_content() else { + return None; + }; + + let _fresh_body = msg_content.body(); + + let Some(formatted) = msg_content.formatted_body() else { + return None; + }; + + if formatted == body { + // exclude the pending msg + if let Some(event_id) = event_item.event_id() { + return Some(event_id); } } None From 99901fdf1ff7fa4ecc7a240504aaa69dde754327 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 13:29:09 +0000 Subject: [PATCH 59/77] Parse regular matrix events in acter spaces, don't log them as error --- native/acter/src/api/spaces.rs | 6 +++- native/core/src/events.rs | 59 +++++++++++++++++++++------------- native/core/src/models.rs | 2 ++ 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/native/acter/src/api/spaces.rs b/native/acter/src/api/spaces.rs index 0dc3de78f023..2ebaaaa29162 100644 --- a/native/acter/src/api/spaces.rs +++ b/native/acter/src/api/spaces.rs @@ -49,7 +49,7 @@ use serde::{Deserialize, Serialize}; use std::{ops::Deref, sync::Arc}; use tokio::sync::broadcast::Receiver; use tokio_stream::{wrappers::BroadcastStream, Stream}; -use tracing::{error, trace, warn}; +use tracing::{error, info, trace, warn}; use crate::{Client, Room, TimelineStream, RUNTIME}; @@ -441,6 +441,10 @@ impl Space { for msg in chunk { let event = match msg.kind.raw().deserialize_as::() { + Ok(AnyActerEvent::RegularTimelineEvent(event)) => { + info!(?event, "Received regular event. Ignoring for now"); + continue; + } Ok(e) => e, Err(error) => { error!(?error, ?room_id, "Not a proper acter event"); diff --git a/native/core/src/events.rs b/native/core/src/events.rs index 480ca6f8a4fd..5e54316f3a1d 100644 --- a/native/core/src/events.rs +++ b/native/core/src/events.rs @@ -13,6 +13,7 @@ pub mod tasks; pub mod three_pid; pub use common::*; +use matrix_sdk::ruma::events::AnyTimelineEvent; use matrix_sdk_base::ruma::{ events::{ reaction::{ReactionEvent, ReactionEventContent}, @@ -51,6 +52,9 @@ pub enum AnyActerEvent { Reaction(ReactionEvent), ReadReceipt(ReadReceiptEvent), Rsvp(rsvp::RsvpEvent), + + // Regular Matrix / Ruma Event + RegularTimelineEvent(AnyTimelineEvent), } impl<'de> serde::Deserialize<'de> for AnyActerEvent { @@ -176,29 +180,38 @@ impl<'de> serde::Deserialize<'de> for AnyActerEvent { Ok(Self::Reaction(event)) } - _ => Err(SerdeDeError::unknown_variant( - &ev_type, - &[ - calendar::CalendarEventEventContent::TYPE, - calendar::CalendarEventUpdateEventContent::TYPE, - pins::PinEventContent::TYPE, - pins::PinUpdateEventContent::TYPE, - news::NewsEntryEventContent::TYPE, - news::NewsEntryUpdateEventContent::TYPE, - tasks::TaskListEventContent::TYPE, - tasks::TaskListUpdateEventContent::TYPE, - tasks::TaskEventContent::TYPE, - tasks::TaskUpdateEventContent::TYPE, - tasks::TaskSelfAssignEventContent::TYPE, - tasks::TaskSelfUnassignEventContent::TYPE, - comments::CommentEventContent::TYPE, - comments::CommentUpdateEventContent::TYPE, - attachments::AttachmentEventContent::TYPE, - attachments::AttachmentUpdateEventContent::TYPE, - rsvp::RsvpEventContent::TYPE, - ReactionEventContent::TYPE, - ], - )), + _ => { + if let Ok(event) = ::matrix_sdk_base::ruma::exports::serde_json::from_str::< + AnyTimelineEvent, + >(&json.get()) + { + Ok(Self::RegularTimelineEvent(event)) + } else { + Err(SerdeDeError::unknown_variant( + &ev_type, + &[ + calendar::CalendarEventEventContent::TYPE, + calendar::CalendarEventUpdateEventContent::TYPE, + pins::PinEventContent::TYPE, + pins::PinUpdateEventContent::TYPE, + news::NewsEntryEventContent::TYPE, + news::NewsEntryUpdateEventContent::TYPE, + tasks::TaskListEventContent::TYPE, + tasks::TaskListUpdateEventContent::TYPE, + tasks::TaskEventContent::TYPE, + tasks::TaskUpdateEventContent::TYPE, + tasks::TaskSelfAssignEventContent::TYPE, + tasks::TaskSelfUnassignEventContent::TYPE, + comments::CommentEventContent::TYPE, + comments::CommentUpdateEventContent::TYPE, + attachments::AttachmentEventContent::TYPE, + attachments::AttachmentUpdateEventContent::TYPE, + rsvp::RsvpEventContent::TYPE, + ReactionEventContent::TYPE, + ], + )) + } + } } } } diff --git a/native/core/src/models.rs b/native/core/src/models.rs index 9af341f7f2dd..d6ef48193972 100644 --- a/native/core/src/models.rs +++ b/native/core/src/models.rs @@ -659,6 +659,8 @@ impl TryFrom for AnyActerModel { reason: r.unsigned.redacted_because, }), }, + // should not really happen + AnyActerEvent::RegularTimelineEvent(_) => Err(Error::UnknownEvent), } } } From 434f1c35f196617a4100a5c0b05247a88a30bc78 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 13:30:03 +0000 Subject: [PATCH 60/77] Fix formatted body test for new improved format --- native/test/src/tests/news.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/native/test/src/tests/news.rs b/native/test/src/tests/news.rs index 9d44791dbf30..d88afea601ee 100644 --- a/native/test/src/tests/news.rs +++ b/native/test/src/tests/news.rs @@ -481,9 +481,10 @@ async fn news_multiple_slide_test() -> Result<()> { .expect("We have markdown text slide"); assert_eq!(second_slide.type_str(), "text"); let msg_content = second_slide.msg_content(); + let formatted_body = msg_content.formatted_body(); assert_eq!( - msg_content.formatted_body(), - Some("

This update is reallly important

\n".to_owned()) + formatted_body, + Some("This update is reallly important".to_owned()) ); let third_slide = final_entry.get_slide(2).expect("We have plain text slide"); assert_eq!(third_slide.type_str(), "text"); From d265d27759ae63d7b6b1b12cfc0a825143a44272 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 15:28:58 +0000 Subject: [PATCH 61/77] is_editable isn't sufficient for our test case --- native/acter/src/api/message.rs | 2 +- native/acter/src/api/stream.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/native/acter/src/api/message.rs b/native/acter/src/api/message.rs index 47a3c9a24a1b..a583fc088642 100644 --- a/native/acter/src/api/message.rs +++ b/native/acter/src/api/message.rs @@ -123,7 +123,7 @@ impl RoomEventItem { }) .collect(), ) - .editable(event.is_editable()); + .editable(event.is_editable()); // which means _images_ can't be edited right now ... but that is probably fine match event.content() { TimelineItemContent::Message(msg) => { diff --git a/native/acter/src/api/stream.rs b/native/acter/src/api/stream.rs index 0a9361657c5c..cbc3e0022eda 100644 --- a/native/acter/src/api/stream.rs +++ b/native/acter/src/api/stream.rs @@ -146,8 +146,9 @@ impl TimelineStream { bail!("Unable to find event"); }; - if !item.is_editable() { - bail!("Unable to edit an event not sent by own user"); + if !item.is_own() { + // !item.is_editable() { // FIXME: matrix-sdk is_editable doesn't allow us to post other things + bail!("You can't edit other peoples messages"); } let item = timeline From 6151c962559cb7649d74a41fb1cb0b47060cb76f Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 15:57:39 +0000 Subject: [PATCH 62/77] fixing clippy --- native/core/src/events.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/core/src/events.rs b/native/core/src/events.rs index 5e54316f3a1d..a92d2f9d1ede 100644 --- a/native/core/src/events.rs +++ b/native/core/src/events.rs @@ -183,7 +183,7 @@ impl<'de> serde::Deserialize<'de> for AnyActerEvent { _ => { if let Ok(event) = ::matrix_sdk_base::ruma::exports::serde_json::from_str::< AnyTimelineEvent, - >(&json.get()) + >(json.get()) { Ok(Self::RegularTimelineEvent(event)) } else { From a29ce458216d91e8b524cc26a76b82705bed16dc Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 16:36:21 +0000 Subject: [PATCH 63/77] Simplify task adding from inline --- .../tasks/sheets/create_update_task_item.dart | 6 +- .../tasks/widgets/task_items_list_widget.dart | 135 ++---------------- 2 files changed, 16 insertions(+), 125 deletions(-) diff --git a/app/lib/features/tasks/sheets/create_update_task_item.dart b/app/lib/features/tasks/sheets/create_update_task_item.dart index 7991b6aeb3f8..0bbb09c9f103 100644 --- a/app/lib/features/tasks/sheets/create_update_task_item.dart +++ b/app/lib/features/tasks/sheets/create_update_task_item.dart @@ -28,7 +28,7 @@ Future showCreateUpdateTaskItemBottomSheet( taskList: taskList, taskName: taskName, task: task, - cancel: cancel, + cancel: () => Navigator.of(context).pop(), ), ); } @@ -85,10 +85,6 @@ class _CreateUpdateItemListConsumerState @override Widget build(BuildContext context) { - return _buildBody(context); - } - - Widget _buildBody(BuildContext context) { final lang = L10n.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), diff --git a/app/lib/features/tasks/widgets/task_items_list_widget.dart b/app/lib/features/tasks/widgets/task_items_list_widget.dart index 349a04a2f196..ca6598615b92 100644 --- a/app/lib/features/tasks/widgets/task_items_list_widget.dart +++ b/app/lib/features/tasks/widgets/task_items_list_widget.dart @@ -76,27 +76,21 @@ class TaskItemsListWidgetState extends ConsumerState { Widget inlineAddTask() { final taskListId = widget.taskList.eventIdStr(); - return ValueListenableBuilder( - valueListenable: showInlineAddTask, - builder: (context, value, child) { - return value - ? _InlineTaskAdd( - taskList: widget.taskList, - cancel: () => showInlineAddTask.value = false, - ) - : Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 8, - ), - child: ActerInlineTextButton( - key: Key('task-list-$taskListId-add-task-inline'), - onPressed: () => showInlineAddTask.value = true, - child: Text(L10n.of(context).addTask), - ), - ); - }, + return Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8, + ), + child: ActerInlineTextButton( + key: Key('task-list-$taskListId-add-task-inline'), + onPressed: () => showCreateUpdateTaskItemBottomSheet( + context, + taskList: widget.taskList, + taskName: '', + ), + child: Text(L10n.of(context).addTask), + ), ); } @@ -137,102 +131,3 @@ class TaskItemsListWidgetState extends ConsumerState { ); } } - -class _InlineTaskAdd extends StatefulWidget { - final Function() cancel; - final TaskList taskList; - - const _InlineTaskAdd({ - required this.cancel, - required this.taskList, - }); - - @override - _InlineTaskAddState createState() => _InlineTaskAddState(); -} - -class _InlineTaskAddState extends State<_InlineTaskAdd> { - final _formKey = GlobalKey(debugLabel: 'inline task form'); - final _textCtrl = TextEditingController(); - final FocusNode focusNode = FocusNode(); - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final lang = L10n.of(context); - final tlId = widget.taskList.eventIdStr(); - return Form( - key: _formKey, - child: TextFormField( - key: Key('task-list-$tlId-add-task-inline-txt'), - focusNode: focusNode, - autofocus: true, - controller: _textCtrl, - textInputAction: TextInputAction.send, - decoration: InputDecoration( - prefixIcon: const Icon(Atlas.plus_circle_thin), - focusedBorder: InputBorder.none, - errorBorder: InputBorder.none, - enabledBorder: InputBorder.none, - hintText: lang.titleTheNewTask, - suffix: IconButton( - onPressed: () => showCreateUpdateTaskItemBottomSheet( - context, - taskList: widget.taskList, - taskName: _textCtrl.text, - cancel: widget.cancel, - ), - padding: EdgeInsets.zero, - icon: const Icon( - Atlas.arrows_up_right_down_left, - size: 18, - ), - ), - suffixIcon: IconButton( - key: Key('task-list-$tlId-add-task-inline-cancel'), - onPressed: widget.cancel, - icon: const Icon( - Atlas.xmark_circle_thin, - size: 24, - ), - ), - ), - onFieldSubmitted: (value) { - if (_formKey.currentState!.validate()) { - _formKey.currentState!.save(); - _handleSubmit(context); - } - }, - // required field, space not allowed - validator: (val) => - val == null || val.trim().isEmpty ? lang.aTaskMustHaveATitle : null, - ), - ); - } - - Future _handleSubmit(BuildContext context) async { - final taskDraft = widget.taskList.taskBuilder(); - taskDraft.title(_textCtrl.text); - try { - await taskDraft.send(); - } catch (e, s) { - _log.severe('Failed to change title of tasklist', e, s); - if (!context.mounted) return; - EasyLoading.showError( - L10n.of(context).updatingTaskFailed(e), - duration: const Duration(seconds: 3), - ); - return; - } - _textCtrl.text = ''; - if (_formKey.currentContext != null) { - Scrollable.ensureVisible(_formKey.currentContext!); - } - focusNode.requestFocus(); - } -} From 13b790b4a7f1c50749173a5ccb99bd3d451de8d8 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 17:02:47 +0000 Subject: [PATCH 64/77] Create Task test - simple --- .../tasks/sheets/create_update_task_item.dart | 4 ++ app/test/features/tasks/error_pages_test.dart | 4 +- .../features/tasks/tasks_adding_test.dart | 48 +++++++++++++++++++ app/test/helpers/mock_tasks_providers.dart | 14 ++++-- app/test/helpers/test_wrapper_widget.dart | 18 ++++--- 5 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 app/test/features/tasks/tasks_adding_test.dart diff --git a/app/lib/features/tasks/sheets/create_update_task_item.dart b/app/lib/features/tasks/sheets/create_update_task_item.dart index 0bbb09c9f103..4978e6ec5841 100644 --- a/app/lib/features/tasks/sheets/create_update_task_item.dart +++ b/app/lib/features/tasks/sheets/create_update_task_item.dart @@ -34,6 +34,8 @@ Future showCreateUpdateTaskItemBottomSheet( } class CreateUpdateTaskItemList extends ConsumerStatefulWidget { + static const submitBtn = Key('create-task-submit'); + static const titleField = Key('create-task-title-field'); final TaskList taskList; final String taskName; final Task? task; @@ -134,6 +136,7 @@ class _CreateUpdateItemListConsumerState ), const SizedBox(height: 5), TextFormField( + key: CreateUpdateTaskItemList.titleField, autofocus: true, decoration: InputDecoration(hintText: lang.name), autovalidateMode: AutovalidateMode.onUserInteraction, @@ -229,6 +232,7 @@ class _CreateUpdateItemListConsumerState Widget _widgetAddButton() { final lang = L10n.of(context); return ElevatedButton( + key: CreateUpdateTaskItemList.submitBtn, onPressed: widget.task == null ? addTask : updateTask, child: Text(widget.task == null ? lang.addTask : lang.updateTask), ); diff --git a/app/test/features/tasks/error_pages_test.dart b/app/test/features/tasks/error_pages_test.dart index 8f60eca7e5f9..8f9ee3fb54e2 100644 --- a/app/test/features/tasks/error_pages_test.dart +++ b/app/test/features/tasks/error_pages_test.dart @@ -76,7 +76,7 @@ void main() { }); group('TaskList Details Error Pages', () { testWidgets('body error page', (tester) async { - final mockedNotifier = MockTaskListItemNotifier(); + final mockedNotifier = FakeTaskListItemNotifier(); await tester.pumpProviderWidget( overrides: [ taskListItemProvider.overrideWith(() => mockedNotifier), @@ -90,7 +90,7 @@ void main() { }); group('Task Details Error Pages', () { testWidgets('body error page', (tester) async { - final mockedNotifier = MockTaskListItemNotifier(shouldFail: false); + final mockedNotifier = FakeTaskListItemNotifier(shouldFail: false); await tester.pumpProviderWidget( overrides: [ notifierTaskProvider.overrideWith(() => MockTaskItemNotifier()), diff --git a/app/test/features/tasks/tasks_adding_test.dart b/app/test/features/tasks/tasks_adding_test.dart new file mode 100644 index 000000000000..f789feb9b740 --- /dev/null +++ b/app/test/features/tasks/tasks_adding_test.dart @@ -0,0 +1,48 @@ +import 'package:acter/features/tasks/sheets/create_update_task_item.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../helpers/mock_a3sdk.dart'; +import '../../helpers/mock_tasks_providers.dart'; +import '../../helpers/test_util.dart'; + +void main() { + group('Adding Task Widget', () { + testWidgets('Create task', (tester) async { + final mockTaskList = MockTaskList(); + final mockTaskDraft = MockTaskDraft(); + when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); + when(() => mockTaskDraft.title('My new Task')).thenAnswer((_) => true); + when(() => mockTaskDraft.send()) + .thenAnswer((_) async => MockEventId(id: 'test')); + await tester.pumpProviderWidget( + overrides: [], + child: CreateUpdateTaskItemList( + taskName: '', + taskList: mockTaskList, + ), + ); + // try to submit without a title + + final submitBtn = find.byKey(CreateUpdateTaskItemList.submitBtn); + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + // not called + verifyNever(() => mockTaskList.taskBuilder()); + + // add the title + + final title = find.byKey(CreateUpdateTaskItemList.titleField); + expect(title, findsOneWidget); + await tester.enterText(title, 'My new Task'); + + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + verify(() => mockTaskList.taskBuilder()).called(1); + verify(() => mockTaskDraft.title('My new Task')).called(1); + verify(() => mockTaskDraft.send()).called(1); + }); + }); +} diff --git a/app/test/helpers/mock_tasks_providers.dart b/app/test/helpers/mock_tasks_providers.dart index 5d03d81aa067..9a3eaf0e38fd 100644 --- a/app/test/helpers/mock_tasks_providers.dart +++ b/app/test/helpers/mock_tasks_providers.dart @@ -24,22 +24,22 @@ class MockAsyncAllTaskListsNotifier extends AsyncNotifier> } } -class MockTaskListItemNotifier extends FamilyAsyncNotifier +class FakeTaskListItemNotifier extends FamilyAsyncNotifier with Mock implements TaskListItemNotifier { bool shouldFail; - MockTaskListItemNotifier({this.shouldFail = true}); + FakeTaskListItemNotifier({this.shouldFail = true}); @override - Future build(String arg) async { + Future build(String arg) async { if (shouldFail) { // toggle failure so the retry works shouldFail = !shouldFail; throw 'Expected fail'; } - return MockTaskList(); + return FakeTaskList(); } } @@ -52,7 +52,9 @@ class MockTaskItemNotifier extends FamilyAsyncNotifier } } -class MockTaskList extends Fake implements TaskList { +class MockTaskDraft extends Mock implements TaskDraft {} + +class FakeTaskList extends Fake implements TaskList { bool shouldFail = true; @override @@ -86,6 +88,8 @@ class MockTaskList extends Fake implements TaskList { } } +class MockTaskList extends FakeTaskList with Mock {} + class MockTask extends Fake implements Task { @override String title() => 'Test'; diff --git a/app/test/helpers/test_wrapper_widget.dart b/app/test/helpers/test_wrapper_widget.dart index 2e92c68bf97e..6252b76e8dd2 100644 --- a/app/test/helpers/test_wrapper_widget.dart +++ b/app/test/helpers/test_wrapper_widget.dart @@ -1,5 +1,6 @@ import 'package:acter/common/themes/acter_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:acter_trigger_auto_complete/acter_trigger_autocomplete.dart'; @@ -10,17 +11,22 @@ class InActerContextTestWrapper extends StatelessWidget { @override Widget build(BuildContext context) { + final easyLoadingBuilder = EasyLoading.init(); + EasyLoading.instance.toastPosition = EasyLoadingToastPosition.bottom; return Portal( child: MaterialApp( theme: ActerTheme.theme, title: 'Acter', home: child, - builder: (context, child) => Overlay( - initialEntries: [ - OverlayEntry( - builder: (context) => Scaffold(body: child), - ), - ], + builder: (context, child) => easyLoadingBuilder( + context, + Overlay( + initialEntries: [ + OverlayEntry( + builder: (context) => Scaffold(body: child), + ), + ], + ), ), locale: const Locale('en', 'US'), localizationsDelegates: L10n.localizationsDelegates, From 2b3e2dd5c09f5f450025469928fdae01d8c2a21c Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 17:07:22 +0000 Subject: [PATCH 65/77] Simplify AddTaskWidget --- .../tasks/sheets/create_update_task_item.dart | 87 ++++--------------- .../tasks/widgets/task_items_list_widget.dart | 3 +- .../features/tasks/tasks_adding_test.dart | 6 +- 3 files changed, 21 insertions(+), 75 deletions(-) diff --git a/app/lib/features/tasks/sheets/create_update_task_item.dart b/app/lib/features/tasks/sheets/create_update_task_item.dart index 4978e6ec5841..25e5f54a6b64 100644 --- a/app/lib/features/tasks/sheets/create_update_task_item.dart +++ b/app/lib/features/tasks/sheets/create_update_task_item.dart @@ -12,11 +12,10 @@ import 'package:logging/logging.dart'; final _log = Logger('a3::tasks::create_update_task_item'); -Future showCreateUpdateTaskItemBottomSheet( +Future showCreateTaskBottomSheet( BuildContext context, { required TaskList taskList, - required String taskName, - Task? task, + String? taskName, Function()? cancel, }) async { await showModalBottomSheet( @@ -24,40 +23,36 @@ Future showCreateUpdateTaskItemBottomSheet( showDragHandle: false, useSafeArea: true, isScrollControlled: true, - builder: (context) => CreateUpdateTaskItemList( + builder: (context) => CreateTaskWidget( taskList: taskList, taskName: taskName, - task: task, cancel: () => Navigator.of(context).pop(), ), ); } -class CreateUpdateTaskItemList extends ConsumerStatefulWidget { +class CreateTaskWidget extends ConsumerStatefulWidget { static const submitBtn = Key('create-task-submit'); static const titleField = Key('create-task-title-field'); final TaskList taskList; - final String taskName; - final Task? task; + final String? taskName; final Function()? cancel; - const CreateUpdateTaskItemList({ + const CreateTaskWidget({ super.key, required this.taskList, - required this.taskName, - this.task, + this.taskName, this.cancel, }); @override - ConsumerState createState() => - _CreateUpdateItemListConsumerState(); + ConsumerState createState() => + _CreateTaskWidgetConsumerState(); } -class _CreateUpdateItemListConsumerState - extends ConsumerState { +class _CreateTaskWidgetConsumerState extends ConsumerState { final GlobalKey _formKey = - GlobalKey(debugLabel: 'update task list form'); + GlobalKey(debugLabel: 'create task list form'); final TextEditingController _taskNameController = TextEditingController(); final TextEditingController _taskDescriptionController = TextEditingController(); @@ -67,22 +62,7 @@ class _CreateUpdateItemListConsumerState @override void initState() { super.initState(); - _taskNameController.text = widget.taskName; - setUpdateData(); - } - - void setUpdateData() { - widget.task.map((task) { - task.description().map((description) { - _taskDescriptionController.text = description.body(); - }); - task.dueDate().map((dueDate) { - DateTime.parse(dueDate).map((date) { - selectedDate = date; - _taskDueDateController.text = taskDueDateFormat(date); - }); - }); - }); + _taskNameController.text = widget.taskName ?? ''; } @override @@ -105,7 +85,7 @@ class _CreateUpdateItemListConsumerState ), const SizedBox(height: 20), Text( - widget.task == null ? lang.addTask : lang.updateTask, + lang.addTask, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), @@ -136,7 +116,7 @@ class _CreateUpdateItemListConsumerState ), const SizedBox(height: 5), TextFormField( - key: CreateUpdateTaskItemList.titleField, + key: CreateTaskWidget.titleField, autofocus: true, decoration: InputDecoration(hintText: lang.name), autovalidateMode: AutovalidateMode.onUserInteraction, @@ -232,9 +212,9 @@ class _CreateUpdateItemListConsumerState Widget _widgetAddButton() { final lang = L10n.of(context); return ElevatedButton( - key: CreateUpdateTaskItemList.submitBtn, - onPressed: widget.task == null ? addTask : updateTask, - child: Text(widget.task == null ? lang.addTask : lang.updateTask), + key: CreateTaskWidget.submitBtn, + onPressed: addTask, + child: Text(lang.addTask), ); } @@ -269,37 +249,4 @@ class _CreateUpdateItemListConsumerState ); } } - - Future updateTask() async { - final lang = L10n.of(context); - if (!_formKey.currentState!.validate()) return; - final task = widget.task; - if (task == null) return; - EasyLoading.show(status: lang.updatingTask); - final updater = task.updateBuilder(); - updater.title(_taskNameController.text); - if (_taskDescriptionController.text.isNotEmpty) { - updater.descriptionText(_taskDescriptionController.text); - } - final date = selectedDate; - if (date != null) { - updater.dueDate(date.year, date.month, date.day); - } - try { - await updater.send(); - EasyLoading.dismiss(); - if (!mounted) return; - Navigator.pop(context); - } catch (e, s) { - _log.severe('Failed to change task', e, s); - if (!mounted) { - EasyLoading.dismiss(); - return; - } - EasyLoading.showError( - lang.updatingTaskFailed(e), - duration: const Duration(seconds: 3), - ); - } - } } diff --git a/app/lib/features/tasks/widgets/task_items_list_widget.dart b/app/lib/features/tasks/widgets/task_items_list_widget.dart index ca6598615b92..aee32ade0478 100644 --- a/app/lib/features/tasks/widgets/task_items_list_widget.dart +++ b/app/lib/features/tasks/widgets/task_items_list_widget.dart @@ -84,10 +84,9 @@ class TaskItemsListWidgetState extends ConsumerState { ), child: ActerInlineTextButton( key: Key('task-list-$taskListId-add-task-inline'), - onPressed: () => showCreateUpdateTaskItemBottomSheet( + onPressed: () => showCreateTaskBottomSheet( context, taskList: widget.taskList, - taskName: '', ), child: Text(L10n.of(context).addTask), ), diff --git a/app/test/features/tasks/tasks_adding_test.dart b/app/test/features/tasks/tasks_adding_test.dart index f789feb9b740..7b14a0504f46 100644 --- a/app/test/features/tasks/tasks_adding_test.dart +++ b/app/test/features/tasks/tasks_adding_test.dart @@ -17,14 +17,14 @@ void main() { .thenAnswer((_) async => MockEventId(id: 'test')); await tester.pumpProviderWidget( overrides: [], - child: CreateUpdateTaskItemList( + child: CreateTaskWidget( taskName: '', taskList: mockTaskList, ), ); // try to submit without a title - final submitBtn = find.byKey(CreateUpdateTaskItemList.submitBtn); + final submitBtn = find.byKey(CreateTaskWidget.submitBtn); expect(submitBtn, findsOneWidget); await tester.tap(submitBtn); @@ -33,7 +33,7 @@ void main() { // add the title - final title = find.byKey(CreateUpdateTaskItemList.titleField); + final title = find.byKey(CreateTaskWidget.titleField); expect(title, findsOneWidget); await tester.enterText(title, 'My new Task'); From f0bd9f9bd8cfdc3768ef9e5508686df9977b7347 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 17:35:29 +0000 Subject: [PATCH 66/77] Add due date adding test --- .../tasks/sheets/create_update_task_item.dart | 134 +++++++++++++++--- .../features/tasks/tasks_adding_test.dart | 70 ++++++++- 2 files changed, 180 insertions(+), 24 deletions(-) diff --git a/app/lib/features/tasks/sheets/create_update_task_item.dart b/app/lib/features/tasks/sheets/create_update_task_item.dart index 25e5f54a6b64..eadbd1a8cc17 100644 --- a/app/lib/features/tasks/sheets/create_update_task_item.dart +++ b/app/lib/features/tasks/sheets/create_update_task_item.dart @@ -1,5 +1,5 @@ -import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/toolkit/buttons/inline_text_button.dart'; +import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/utils/utils.dart'; import 'package:acter/features/tasks/widgets/due_picker.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; @@ -16,7 +16,6 @@ Future showCreateTaskBottomSheet( BuildContext context, { required TaskList taskList, String? taskName, - Function()? cancel, }) async { await showModalBottomSheet( context: context, @@ -26,7 +25,6 @@ Future showCreateTaskBottomSheet( builder: (context) => CreateTaskWidget( taskList: taskList, taskName: taskName, - cancel: () => Navigator.of(context).pop(), ), ); } @@ -34,15 +32,19 @@ Future showCreateTaskBottomSheet( class CreateTaskWidget extends ConsumerStatefulWidget { static const submitBtn = Key('create-task-submit'); static const titleField = Key('create-task-title-field'); + static const addDueDateAction = Key('create-task-actions-add-due'); + static const addDescAction = Key('create-task-actions-add-desc'); + static const dueDateField = Key('create-task-due-field'); + static const dueDateTodayBtn = Key('create-task-due-today-btn'); + static const dueDateTomorrowBtn = Key('create-task-due-tomorrow-btn'); + static const descField = Key('create-task-desc-field'); final TaskList taskList; final String? taskName; - final Function()? cancel; const CreateTaskWidget({ super.key, required this.taskList, this.taskName, - this.cancel, }); @override @@ -59,15 +61,40 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { final TextEditingController _taskDueDateController = TextEditingController(); DateTime? selectedDate; + bool showDescriptionField = false; + bool showDueDate = false; + @override void initState() { super.initState(); - _taskNameController.text = widget.taskName ?? ''; + widget.taskName.map((text) { + _taskNameController.text = text; + }); } @override Widget build(BuildContext context) { final lang = L10n.of(context); + + final fields = [ + const SizedBox(height: 20), + _widgetTaskName(), + ]; + + if (showDescriptionField) { + fields.addAll([ + const SizedBox(height: 20), + _widgetDescriptionName(), + ]); + } + + if (showDueDate) { + fields.addAll([ + const SizedBox(height: 20), + _widgetDueDate(), + ]); + } + return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: SingleChildScrollView( @@ -89,14 +116,11 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), + ...fields, const SizedBox(height: 20), - _widgetTaskName(), + _addFields(), const SizedBox(height: 20), - _widgetDescriptionName(), - const SizedBox(height: 20), - _widgetDueDate(), - const SizedBox(height: 20), - _widgetAddButton(), + _widgetCreateButton(), const SizedBox(height: 20), ], ), @@ -134,9 +158,22 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - lang.description, - style: Theme.of(context).textTheme.bodySmall, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + lang.description, + style: Theme.of(context).textTheme.bodySmall, + ), + IconButton( + onPressed: () { + setState(() { + showDescriptionField = false; + }); + }, + icon: const Icon(Icons.close), + ), + ], ), const SizedBox(height: 5), TextFormField( @@ -154,13 +191,27 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - lang.dueDate, - style: Theme.of(context).textTheme.bodySmall, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + lang.dueDate, + style: Theme.of(context).textTheme.bodySmall, + ), + IconButton( + onPressed: () { + setState(() { + showDueDate = false; + }); + }, + icon: const Icon(Icons.close), + ), + ], ), const SizedBox(height: 5), TextFormField( readOnly: true, + key: CreateTaskWidget.dueDateField, decoration: InputDecoration( hintText: lang.dueDate, suffixIcon: IconButton( @@ -175,6 +226,7 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.end, children: [ ActerInlineTextButton( + key: CreateTaskWidget.dueDateTodayBtn, onPressed: () => setState(() { final date = DateTime.now(); selectedDate = date; @@ -183,6 +235,7 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { child: Text(lang.today), ), ActerInlineTextButton( + key: CreateTaskWidget.dueDateTomorrowBtn, onPressed: () => setState(() { final date = DateTime.now().addDays(1); selectedDate = date; @@ -209,7 +262,45 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { }); } - Widget _widgetAddButton() { + Widget _addFields() { + final lang = L10n.of(context); + final actions = []; + if (!showDescriptionField) { + actions.add( + ActerInlineTextButton( + key: CreateTaskWidget.addDescAction, + onPressed: () => setState(() { + showDescriptionField = true; + }), + child: Text(lang.description), + ), + ); + } + if (!showDueDate) { + actions.add( + ActerInlineTextButton( + key: CreateTaskWidget.addDueDateAction, + onPressed: () => setState(() { + showDueDate = true; + }), + child: Text(lang.dueDate), + ), + ); + } + if (actions.isEmpty) { + return const SizedBox.shrink(); + } + + return Row( + children: [ + Text(lang.add), + const SizedBox(width: 5), + ...actions, + ], + ); + } + + Widget _widgetCreateButton() { final lang = L10n.of(context); return ElevatedButton( key: CreateTaskWidget.submitBtn, @@ -224,18 +315,17 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { EasyLoading.show(status: lang.addingTask); final taskDraft = widget.taskList.taskBuilder(); taskDraft.title(_taskNameController.text); - if (_taskDescriptionController.text.isNotEmpty) { + if (showDescriptionField && _taskDescriptionController.text.isNotEmpty) { taskDraft.descriptionText(_taskDescriptionController.text); } final date = selectedDate; - if (date != null) { + if (showDueDate && date != null) { taskDraft.dueDate(date.year, date.month, date.day); } try { await taskDraft.send(); EasyLoading.dismiss(); if (!mounted) return; - widget.cancel.map((cb) => cb()); Navigator.pop(context); } catch (e, s) { _log.severe('Failed to create task', e, s); diff --git a/app/test/features/tasks/tasks_adding_test.dart b/app/test/features/tasks/tasks_adding_test.dart index 7b14a0504f46..79e2fba1064d 100644 --- a/app/test/features/tasks/tasks_adding_test.dart +++ b/app/test/features/tasks/tasks_adding_test.dart @@ -7,8 +7,8 @@ import '../../helpers/mock_tasks_providers.dart'; import '../../helpers/test_util.dart'; void main() { - group('Adding Task Widget', () { - testWidgets('Create task', (tester) async { + group('Create Task Widget', () { + testWidgets('Simple only title', (tester) async { final mockTaskList = MockTaskList(); final mockTaskDraft = MockTaskDraft(); when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); @@ -44,5 +44,71 @@ void main() { verify(() => mockTaskDraft.title('My new Task')).called(1); verify(() => mockTaskDraft.send()).called(1); }); + testWidgets('with due date', (tester) async { + final mockTaskList = MockTaskList(); + final mockTaskDraft = MockTaskDraft(); + when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); + when(() => mockTaskDraft.title('My new Task')).thenAnswer((_) => true); + when(() => mockTaskDraft.dueDate(any(), any(), any())) + .thenAnswer((_) => true); + when(() => mockTaskDraft.send()) + .thenAnswer((_) async => MockEventId(id: 'test')); + await tester.pumpProviderWidget( + overrides: [], + child: CreateTaskWidget( + taskName: '', + taskList: mockTaskList, + ), + ); + // try to submit without a title + + final submitBtn = find.byKey(CreateTaskWidget.submitBtn); + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + // not called + verifyNever(() => mockTaskList.taskBuilder()); + + // add the title + final title = find.byKey(CreateTaskWidget.titleField); + expect(title, findsOneWidget); + await tester.enterText(title, 'Another Task'); + + // add due date + + final addDueDateAction = find.byKey(CreateTaskWidget.addDueDateAction); + final dueDateField = find.byKey(CreateTaskWidget.dueDateField); + final dueTomorrow = find.byKey(CreateTaskWidget.dueDateTomorrowBtn); + + // not yet visible + expect(dueDateField, findsNothing); + expect(dueTomorrow, findsNothing); + + expect(addDueDateAction, findsOneWidget); + await tester.tap(addDueDateAction); + await tester.pump(); + + // now visible + expect(dueDateField, findsOneWidget); + expect(dueTomorrow, findsOneWidget); + + await tester.tap(dueTomorrow); + + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + final expectedDate = DateTime.now().add(const Duration(days: 1)); + + verify(() => mockTaskList.taskBuilder()).called(1); + verify(() => mockTaskDraft.title('Another Task')).called(1); + verify( + () => mockTaskDraft.dueDate( + expectedDate.year, + expectedDate.month, + expectedDate.day, + ), + ).called(1); + verify(() => mockTaskDraft.send()).called(1); + }); }); } From df0d065ddfdcc04e0d4362d689abe15fa0a603da Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 18:00:42 +0000 Subject: [PATCH 67/77] Move sheet, extended tests --- .../create_task.dart} | 7 +- .../tasks/widgets/task_items_list_widget.dart | 2 +- .../features/tasks/tasks_adding_test.dart | 197 +++++++++++++++++- 3 files changed, 203 insertions(+), 3 deletions(-) rename app/lib/features/tasks/{sheets/create_update_task_item.dart => actions/create_task.dart} (96%) diff --git a/app/lib/features/tasks/sheets/create_update_task_item.dart b/app/lib/features/tasks/actions/create_task.dart similarity index 96% rename from app/lib/features/tasks/sheets/create_update_task_item.dart rename to app/lib/features/tasks/actions/create_task.dart index eadbd1a8cc17..4773e0b7ba17 100644 --- a/app/lib/features/tasks/sheets/create_update_task_item.dart +++ b/app/lib/features/tasks/actions/create_task.dart @@ -38,6 +38,8 @@ class CreateTaskWidget extends ConsumerStatefulWidget { static const dueDateTodayBtn = Key('create-task-due-today-btn'); static const dueDateTomorrowBtn = Key('create-task-due-tomorrow-btn'); static const descField = Key('create-task-desc-field'); + static const closeDescAction = Key('create-task-actions-close-desc'); + static const closeDueDateAction = Key('create-task-actions-close-due-date'); final TaskList taskList; final String? taskName; @@ -166,6 +168,7 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { style: Theme.of(context).textTheme.bodySmall, ), IconButton( + key: CreateTaskWidget.closeDescAction, onPressed: () { setState(() { showDescriptionField = false; @@ -177,8 +180,9 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { ), const SizedBox(height: 5), TextFormField( + key: CreateTaskWidget.descField, decoration: InputDecoration(hintText: lang.description), - minLines: 4, + minLines: 2, maxLines: 4, controller: _taskDescriptionController, ), @@ -199,6 +203,7 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { style: Theme.of(context).textTheme.bodySmall, ), IconButton( + key: CreateTaskWidget.closeDueDateAction, onPressed: () { setState(() { showDueDate = false; diff --git a/app/lib/features/tasks/widgets/task_items_list_widget.dart b/app/lib/features/tasks/widgets/task_items_list_widget.dart index aee32ade0478..36d3694c3fb1 100644 --- a/app/lib/features/tasks/widgets/task_items_list_widget.dart +++ b/app/lib/features/tasks/widgets/task_items_list_widget.dart @@ -1,7 +1,7 @@ import 'package:acter/common/toolkit/buttons/inline_text_button.dart'; import 'package:acter/features/tasks/models/tasks.dart'; import 'package:acter/features/tasks/providers/task_items_providers.dart'; -import 'package:acter/features/tasks/sheets/create_update_task_item.dart'; +import 'package:acter/features/tasks/actions/create_task.dart'; import 'package:acter/features/tasks/widgets/skeleton/task_items_skeleton.dart'; import 'package:acter/features/tasks/widgets/task_item.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; diff --git a/app/test/features/tasks/tasks_adding_test.dart b/app/test/features/tasks/tasks_adding_test.dart index 79e2fba1064d..49893d9457ef 100644 --- a/app/test/features/tasks/tasks_adding_test.dart +++ b/app/test/features/tasks/tasks_adding_test.dart @@ -1,4 +1,4 @@ -import 'package:acter/features/tasks/sheets/create_update_task_item.dart'; +import 'package:acter/features/tasks/actions/create_task.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -44,6 +44,128 @@ void main() { verify(() => mockTaskDraft.title('My new Task')).called(1); verify(() => mockTaskDraft.send()).called(1); }); + testWidgets('with description', (tester) async { + final mockTaskList = MockTaskList(); + final mockTaskDraft = MockTaskDraft(); + when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); + when(() => mockTaskDraft.title(any())).thenAnswer((_) => true); + when(() => mockTaskDraft.descriptionText(any())).thenAnswer((_) => true); + when(() => mockTaskDraft.send()) + .thenAnswer((_) async => MockEventId(id: 'test')); + await tester.pumpProviderWidget( + overrides: [], + child: CreateTaskWidget( + taskName: '', + taskList: mockTaskList, + ), + ); + // try to submit without a title + + final submitBtn = find.byKey(CreateTaskWidget.submitBtn); + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + // not called + verifyNever(() => mockTaskList.taskBuilder()); + + // add the title + final title = find.byKey(CreateTaskWidget.titleField); + expect(title, findsOneWidget); + await tester.enterText(title, 'Task with Description'); + + // add due date + + final addDescAction = find.byKey(CreateTaskWidget.addDescAction); + final descField = find.byKey(CreateTaskWidget.descField); + + // not yet visible + expect(descField, findsNothing); + + expect(addDescAction, findsOneWidget); + await tester.tap(addDescAction); + await tester.pump(); + + // now visible + expect(descField, findsOneWidget); + + await tester.enterText(descField, 'This is the description'); + + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + verify(() => mockTaskList.taskBuilder()).called(1); + verify(() => mockTaskDraft.title('Task with Description')).called(1); + verify( + () => mockTaskDraft.descriptionText('This is the description'), + ).called(1); + verify(() => mockTaskDraft.send()).called(1); + verifyNever(() => mockTaskDraft.dueDate(any(), any(), any())); + }); + + testWidgets('toggle description, not added', (tester) async { + final mockTaskList = MockTaskList(); + final mockTaskDraft = MockTaskDraft(); + when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); + when(() => mockTaskDraft.title(any())).thenAnswer((_) => true); + when(() => mockTaskDraft.descriptionText(any())).thenAnswer((_) => true); + when(() => mockTaskDraft.send()) + .thenAnswer((_) async => MockEventId(id: 'test')); + await tester.pumpProviderWidget( + overrides: [], + child: CreateTaskWidget( + taskName: '', + taskList: mockTaskList, + ), + ); + // try to submit without a title + + final submitBtn = find.byKey(CreateTaskWidget.submitBtn); + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + // not called + verifyNever(() => mockTaskList.taskBuilder()); + + // add the title + final title = find.byKey(CreateTaskWidget.titleField); + expect(title, findsOneWidget); + await tester.enterText(title, 'Task with Description'); + + // add due date + + final addDescAction = find.byKey(CreateTaskWidget.addDescAction); + final descField = find.byKey(CreateTaskWidget.descField); + + // not yet visible + expect(descField, findsNothing); + + expect(addDescAction, findsOneWidget); + await tester.tap(addDescAction); + await tester.pump(); + + // now visible + expect(descField, findsOneWidget); + + await tester.enterText(descField, 'This is the description'); + + // but we decided against it again after + final closeDescAction = find.byKey(CreateTaskWidget.closeDescAction); + expect(closeDescAction, findsOneWidget); + await tester.tap(closeDescAction); + + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + verify(() => mockTaskList.taskBuilder()).called(1); + verify(() => mockTaskDraft.title('Task with Description')).called(1); + verify(() => mockTaskDraft.send()).called(1); + + verifyNever( + () => mockTaskDraft.descriptionText('This is the description'), + ); + verifyNever(() => mockTaskDraft.dueDate(any(), any(), any())); + }); + testWidgets('with due date', (tester) async { final mockTaskList = MockTaskList(); final mockTaskDraft = MockTaskDraft(); @@ -110,5 +232,78 @@ void main() { ).called(1); verify(() => mockTaskDraft.send()).called(1); }); + + testWidgets('with due date toggled, not added', (tester) async { + final mockTaskList = MockTaskList(); + final mockTaskDraft = MockTaskDraft(); + when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); + when(() => mockTaskDraft.title('My new Task')).thenAnswer((_) => true); + when(() => mockTaskDraft.dueDate(any(), any(), any())) + .thenAnswer((_) => true); + when(() => mockTaskDraft.send()) + .thenAnswer((_) async => MockEventId(id: 'test')); + await tester.pumpProviderWidget( + overrides: [], + child: CreateTaskWidget( + taskName: '', + taskList: mockTaskList, + ), + ); + // try to submit without a title + + final submitBtn = find.byKey(CreateTaskWidget.submitBtn); + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + // not called + verifyNever(() => mockTaskList.taskBuilder()); + + // add the title + final title = find.byKey(CreateTaskWidget.titleField); + expect(title, findsOneWidget); + await tester.enterText(title, 'Another Task'); + + // add due date + + final addDueDateAction = find.byKey(CreateTaskWidget.addDueDateAction); + final dueDateField = find.byKey(CreateTaskWidget.dueDateField); + final dueTomorrow = find.byKey(CreateTaskWidget.dueDateTomorrowBtn); + + // not yet visible + expect(dueDateField, findsNothing); + expect(dueTomorrow, findsNothing); + + expect(addDueDateAction, findsOneWidget); + await tester.tap(addDueDateAction); + await tester.pump(); + + // now visible + expect(dueDateField, findsOneWidget); + expect(dueTomorrow, findsOneWidget); + + await tester.tap(dueTomorrow); + + // we closed the due again + final closeDueDateAction = + find.byKey(CreateTaskWidget.closeDueDateAction); + expect(closeDueDateAction, findsOneWidget); + await tester.tap(closeDueDateAction); + + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + final expectedDate = DateTime.now().add(const Duration(days: 1)); + + verify(() => mockTaskList.taskBuilder()).called(1); + verify(() => mockTaskDraft.title('Another Task')).called(1); + verifyNever( + () => mockTaskDraft.dueDate( + expectedDate.year, + expectedDate.month, + expectedDate.day, + ), + ); + verify(() => mockTaskDraft.send()).called(1); + }); }); } From 02553ab38493977037d016e813177b66160d405d Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 18:06:05 +0000 Subject: [PATCH 68/77] Prettify task adding dialog --- app/lib/features/tasks/actions/create_task.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/lib/features/tasks/actions/create_task.dart b/app/lib/features/tasks/actions/create_task.dart index 4773e0b7ba17..08ef8c05abc8 100644 --- a/app/lib/features/tasks/actions/create_task.dart +++ b/app/lib/features/tasks/actions/create_task.dart @@ -1,3 +1,4 @@ +import 'package:acter/common/drag_handle_widget.dart'; import 'package:acter/common/toolkit/buttons/inline_text_button.dart'; import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/utils/utils.dart'; @@ -106,12 +107,8 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox(height: 20), - const Divider( - indent: 150, - endIndent: 150, - thickness: 2, - ), + const SizedBox(height: 10), + const Center(child: DragHandleWidget()), const SizedBox(height: 20), Text( lang.addTask, From ee0f080abea1d2d02e52e75acfa7d49d983a4b6f Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 19:33:08 +0000 Subject: [PATCH 69/77] Implement TaskList selector for Task Creation --- app/lib/common/actions/select_space.dart | 22 +++++ .../events/pages/create_event_page.dart | 19 ++-- .../features/tasks/actions/create_task.dart | 97 ++++++++++++++++++- .../tasks/actions/select_tasklist.dart | 92 ++++++++++++++++++ .../features/tasks/pages/tasks_list_page.dart | 26 +---- .../tasks/widgets/task_items_list_widget.dart | 2 - .../tasks/widgets/task_list_item_card.dart | 88 +++++++++++------ .../tasks/widgets/task_lists_empty.dart | 42 ++++++++ app/lib/l10n/app_en.arb | 1 + .../features/tasks/tasks_adding_test.dart | 65 ++++++++++--- 10 files changed, 373 insertions(+), 81 deletions(-) create mode 100644 app/lib/common/actions/select_space.dart create mode 100644 app/lib/features/tasks/actions/select_tasklist.dart create mode 100644 app/lib/features/tasks/widgets/task_lists_empty.dart diff --git a/app/lib/common/actions/select_space.dart b/app/lib/common/actions/select_space.dart new file mode 100644 index 000000000000..09a00fed32d9 --- /dev/null +++ b/app/lib/common/actions/select_space.dart @@ -0,0 +1,22 @@ +import 'package:acter/common/providers/space_providers.dart'; +import 'package:acter/common/widgets/spaces/space_selector_drawer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +const Key selectSpaceDrawerKey = Key('space-widgets-select-space-drawer'); + +Future selectSpace({ + required BuildContext context, + required WidgetRef ref, + required String canCheck, +}) async { + final newSelectedSpaceId = await selectSpaceDrawer( + context: context, + currentSpaceId: ref.read(selectedSpaceIdProvider), + canCheck: canCheck, + title: Text(L10n.of(context).selectSpace), + ); + ref.read(selectedSpaceIdProvider.notifier).state = newSelectedSpaceId; + return newSelectedSpaceId; +} diff --git a/app/lib/features/events/pages/create_event_page.dart b/app/lib/features/events/pages/create_event_page.dart index cd070b7fe37f..bf7b8f605030 100644 --- a/app/lib/features/events/pages/create_event_page.dart +++ b/app/lib/features/events/pages/create_event_page.dart @@ -1,3 +1,4 @@ +import 'package:acter/common/actions/select_space.dart'; import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/providers/space_providers.dart'; import 'package:acter/common/toolkit/buttons/primary_action_button.dart'; @@ -5,7 +6,6 @@ import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/utils/utils.dart'; import 'package:acter/common/widgets/html_editor.dart'; import 'package:acter/common/widgets/spaces/select_space_form_field.dart'; -import 'package:acter/common/widgets/spaces/space_selector_drawer.dart'; import 'package:acter/features/events/model/keys.dart'; import 'package:acter/features/events/utils/events_utils.dart'; import 'package:acter/features/home/providers/client_providers.dart'; @@ -417,7 +417,11 @@ class CreateEventPageConsumerState extends ConsumerState { Future _handleCreateEvent() async { final lang = L10n.of(context); String? spaceId = ref.read(selectedSpaceIdProvider); - spaceId ??= await selectSpace(); + spaceId ??= await selectSpace( + context: context, + ref: ref, + canCheck: 'CanPostEvent', + ); if (!mounted) return; if (spaceId == null) { @@ -487,15 +491,4 @@ class CreateEventPageConsumerState extends ConsumerState { ); } } - - Future selectSpace() async { - final newSelectedSpaceId = await selectSpaceDrawer( - context: context, - currentSpaceId: ref.read(selectedSpaceIdProvider), - canCheck: 'CanPostEvent', - title: Text(L10n.of(context).selectSpace), - ); - ref.read(selectedSpaceIdProvider.notifier).state = newSelectedSpaceId; - return newSelectedSpaceId; - } } diff --git a/app/lib/features/tasks/actions/create_task.dart b/app/lib/features/tasks/actions/create_task.dart index 08ef8c05abc8..c07b779081d3 100644 --- a/app/lib/features/tasks/actions/create_task.dart +++ b/app/lib/features/tasks/actions/create_task.dart @@ -1,7 +1,12 @@ +import 'package:acter/common/actions/select_space.dart'; import 'package:acter/common/drag_handle_widget.dart'; +import 'package:acter/common/providers/space_providers.dart'; import 'package:acter/common/toolkit/buttons/inline_text_button.dart'; import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/utils/utils.dart'; +import 'package:acter/common/widgets/spaces/select_space_form_field.dart'; +import 'package:acter/features/tasks/actions/select_tasklist.dart'; +import 'package:acter/features/tasks/providers/tasklists_providers.dart'; import 'package:acter/features/tasks/widgets/due_picker.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:dart_date/dart_date.dart'; @@ -15,7 +20,7 @@ final _log = Logger('a3::tasks::create_update_task_item'); Future showCreateTaskBottomSheet( BuildContext context, { - required TaskList taskList, + TaskList? taskList, String? taskName, }) async { await showModalBottomSheet( @@ -41,12 +46,12 @@ class CreateTaskWidget extends ConsumerStatefulWidget { static const descField = Key('create-task-desc-field'); static const closeDescAction = Key('create-task-actions-close-desc'); static const closeDueDateAction = Key('create-task-actions-close-due-date'); - final TaskList taskList; + final TaskList? taskList; final String? taskName; const CreateTaskWidget({ super.key, - required this.taskList, + this.taskList, this.taskName, }); @@ -67,12 +72,31 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { bool showDescriptionField = false; bool showDueDate = false; + TaskList? taskList; + @override void initState() { super.initState(); + widget.taskList.map((tl) { + WidgetsBinding.instance.addPostFrameCallback((d) { + setState(() { + taskList = tl; + ref.read(selectedSpaceIdProvider.notifier).state = tl.spaceIdStr(); + }); + }); + }); widget.taskName.map((text) { _taskNameController.text = text; }); + ref.listenManual(selectedSpaceIdProvider, (prev, next) { + // if the space changed and this isn't our list now, we + // need to reset + if (next != taskList?.spaceIdStr()) { + setState(() { + taskList = null; + }); + } + }); } @override @@ -115,6 +139,28 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 5), + child: Row( + children: [ + const SelectSpaceFormField( + canCheck: 'CanPostTask', + useCompatView: true, + ), + const Text(' > '), + if (taskList == null) + ActerInlineTextButton( + child: Text(lang.selectTaskList), + onPressed: () => _selectTaskList(), + ) + else + InkWell( + onTap: () => _selectTaskList(), + child: Text(taskList?.name() ?? ''), + ), + ], + ), + ), ...fields, const SizedBox(height: 20), _addFields(), @@ -311,11 +357,54 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { ); } + Future _selectTaskList() async { + final lang = L10n.of(context); + String? spaceId = ref.read(selectedSpaceIdProvider); + spaceId ??= await selectSpace( + context: context, + ref: ref, + canCheck: 'CanPostTask', + ); + if (!mounted) return; + + if (spaceId == null) { + EasyLoading.showError( + lang.pleaseSelectSpace, + duration: const Duration(seconds: 2), + ); + return; + } + + final taskListId = await selectTaskList(context: context, spaceId: spaceId); + if (!mounted) return; + if (taskListId == null) { + return; + } + + final newTaskList = await ref.read(taskListItemProvider(taskListId).future); + setState(() { + taskList = newTaskList; + }); + } + Future addTask() async { final lang = L10n.of(context); if (!_formKey.currentState!.validate()) return; + + if (taskList == null) { + await _selectTaskList(); + } + final tl = taskList; + if (tl == null) { + EasyLoading.showError( + lang.selectTaskList, + duration: const Duration(seconds: 2), + ); + return; + } + EasyLoading.show(status: lang.addingTask); - final taskDraft = widget.taskList.taskBuilder(); + final taskDraft = tl.taskBuilder(); taskDraft.title(_taskNameController.text); if (showDescriptionField && _taskDescriptionController.text.isNotEmpty) { taskDraft.descriptionText(_taskDescriptionController.text); diff --git a/app/lib/features/tasks/actions/select_tasklist.dart b/app/lib/features/tasks/actions/select_tasklist.dart new file mode 100644 index 000000000000..5a407ef86165 --- /dev/null +++ b/app/lib/features/tasks/actions/select_tasklist.dart @@ -0,0 +1,92 @@ +import 'package:acter/common/providers/room_providers.dart'; +import 'package:acter/common/toolkit/errors/error_page.dart'; +import 'package:acter/features/tasks/providers/tasklists_providers.dart'; +import 'package:acter/features/tasks/widgets/skeleton/tasks_list_skeleton.dart'; +import 'package:acter/features/tasks/widgets/task_list_item_card.dart'; +import 'package:acter/features/tasks/widgets/task_lists_empty.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +const Key selectSpaceDrawerKey = Key('space-widgets-select-space-drawer'); + +Future selectTaskList({ + required BuildContext context, + required String spaceId, +}) async { + return await showModalBottomSheet( + showDragHandle: true, + enableDrag: true, + context: context, + isDismissible: true, + builder: (context) => _SelectTaskList(spaceId: spaceId), + ); +} + +class _SelectTaskList extends ConsumerWidget { + final String spaceId; + const _SelectTaskList({required this.spaceId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final lang = L10n.of(context); + final tasklistsLoader = ref.watch( + tasksListSearchProvider( + (spaceId: spaceId, searchText: ''), + ), + ); + + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Row( + children: [Expanded(child: Text(lang.selectTaskList))], + ), + ), + Expanded( + child: tasklistsLoader.when( + data: (tasklists) { + if (tasklists.isEmpty) { + final canAdd = ref + .watch(roomMembershipProvider(spaceId)) + .valueOrNull + ?.canString('CanPostTaskList') == + true; + return TaskListsEmptyState( + canAdd: canAdd, + inSearch: false, + spaceId: spaceId, + ); + } + + return SingleChildScrollView( + child: ListView.builder( + itemCount: tasklists.length, + shrinkWrap: true, + itemBuilder: (context, idx) => TaskListItemCard( + onTitleTap: () => Navigator.of(context).pop(tasklists[idx]), + taskListId: tasklists[idx], + canExpand: false, + ), + ), + ); + }, + error: (error, stack) { + return ErrorPage( + background: const TasksListSkeleton(), + error: error, + stack: stack, + onRetryTap: () => ref.invalidate(allTasksListsProvider), + ); + }, + loading: () => const TasksListSkeleton(), + ), + ), + ], + ); + } +} diff --git a/app/lib/features/tasks/pages/tasks_list_page.dart b/app/lib/features/tasks/pages/tasks_list_page.dart index 77a247f35603..ea667db24633 100644 --- a/app/lib/features/tasks/pages/tasks_list_page.dart +++ b/app/lib/features/tasks/pages/tasks_list_page.dart @@ -2,16 +2,15 @@ import 'dart:math'; import 'package:acter/common/providers/common_providers.dart'; import 'package:acter/common/providers/space_providers.dart'; -import 'package:acter/common/toolkit/buttons/primary_action_button.dart'; import 'package:acter/common/toolkit/errors/error_page.dart'; import 'package:acter/common/widgets/acter_search_widget.dart'; import 'package:acter/common/widgets/add_button_with_can_permission.dart'; -import 'package:acter/common/widgets/empty_state_widget.dart'; import 'package:acter/common/widgets/space_name_widget.dart'; import 'package:acter/features/tasks/providers/tasklists_providers.dart'; import 'package:acter/features/tasks/sheets/create_update_task_list.dart'; import 'package:acter/features/tasks/widgets/skeleton/tasks_list_skeleton.dart'; import 'package:acter/features/tasks/widgets/task_list_item_card.dart'; +import 'package:acter/features/tasks/widgets/task_lists_empty.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -158,31 +157,16 @@ class _TasksListPageConsumerState extends ConsumerState { } Widget _buildTasklistsEmptyState() { - final lang = L10n.of(context); var canAdd = false; if (searchValue.isEmpty) { final canPostLoader = ref.watch(hasSpaceWithPermissionProvider('CanPostTaskList')); if (canPostLoader.valueOrNull == true) canAdd = true; } - return Center( - heightFactor: 1, - child: EmptyState( - title: searchValue.isNotEmpty - ? lang.noMatchingTasksListFound - : lang.noTasksListAvailableYet, - subtitle: lang.noTasksListAvailableDescription, - image: 'assets/images/tasks.svg', - primaryButton: canAdd - ? ActerPrimaryActionButton( - onPressed: () => showCreateUpdateTaskListBottomSheet( - context, - initialSelectedSpace: widget.spaceId, - ), - child: Text(lang.createTaskList), - ) - : null, - ), + return TaskListsEmptyState( + canAdd: canAdd, + inSearch: searchValue.isNotEmpty, + spaceId: widget.spaceId, ); } } diff --git a/app/lib/features/tasks/widgets/task_items_list_widget.dart b/app/lib/features/tasks/widgets/task_items_list_widget.dart index 36d3694c3fb1..97d488567c8a 100644 --- a/app/lib/features/tasks/widgets/task_items_list_widget.dart +++ b/app/lib/features/tasks/widgets/task_items_list_widget.dart @@ -5,9 +5,7 @@ import 'package:acter/features/tasks/actions/create_task.dart'; import 'package:acter/features/tasks/widgets/skeleton/task_items_skeleton.dart'; import 'package:acter/features/tasks/widgets/task_item.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; -import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; diff --git a/app/lib/features/tasks/widgets/task_list_item_card.dart b/app/lib/features/tasks/widgets/task_list_item_card.dart index ecc967a0b9d1..eec47b8c8428 100644 --- a/app/lib/features/tasks/widgets/task_list_item_card.dart +++ b/app/lib/features/tasks/widgets/task_list_item_card.dart @@ -20,6 +20,8 @@ class TaskListItemCard extends ConsumerWidget { final bool showSpace; final bool showCompletedTask; final bool initiallyExpanded; + final bool canExpand; + final GestureTapCallback? onTitleTap; const TaskListItemCard({ super.key, @@ -27,6 +29,8 @@ class TaskListItemCard extends ConsumerWidget { this.showSpace = false, this.showCompletedTask = false, this.initiallyExpanded = true, + this.canExpand = true, + this.onTitleTap, }); @override @@ -36,30 +40,9 @@ class TaskListItemCard extends ConsumerWidget { return tasklistLoader.when( data: (taskList) => Card( key: Key('task-list-card-$taskListId'), - child: ExpansionTile( - initiallyExpanded: initiallyExpanded, - leading: ActerIconWidget( - iconSize: 30, - color: convertColor( - taskList.display()?.color(), - iconPickerColors[0], - ), - icon: ActerIcon.iconForTask(taskList.display()?.iconStr()), - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - iconColor: Theme.of(context).colorScheme.onSurface, - childrenPadding: const EdgeInsets.symmetric(horizontal: 10), - title: title(context, taskList), - subtitle: subtitle(ref, taskList), - children: [ - TaskItemsListWidget( - taskList: taskList, - showCompletedTask: showCompletedTask, - ), - ], - ), + child: canExpand + ? expandable(context, ref, taskList) + : simple(context, ref, taskList), ), error: (e, s) { _log.severe('Failed to load tasklist', e, s); @@ -73,14 +56,59 @@ class TaskListItemCard extends ConsumerWidget { ); } + Widget expandable(BuildContext context, WidgetRef ref, TaskList taskList) => + ExpansionTile( + initiallyExpanded: initiallyExpanded, + leading: ActerIconWidget( + iconSize: 30, + color: convertColor( + taskList.display()?.color(), + iconPickerColors[0], + ), + icon: ActerIcon.iconForTask(taskList.display()?.iconStr()), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + iconColor: Theme.of(context).colorScheme.onSurface, + childrenPadding: const EdgeInsets.symmetric(horizontal: 10), + title: title(context, taskList), + subtitle: subtitle(ref, taskList), + children: [ + TaskItemsListWidget( + taskList: taskList, + showCompletedTask: showCompletedTask, + ), + ], + ); + + Widget simple(BuildContext context, WidgetRef ref, TaskList taskList) => + ListTile( + leading: ActerIconWidget( + iconSize: 30, + color: convertColor( + taskList.display()?.color(), + iconPickerColors[0], + ), + icon: ActerIcon.iconForTask(taskList.display()?.iconStr()), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + iconColor: Theme.of(context).colorScheme.onSurface, + title: title(context, taskList), + subtitle: subtitle(ref, taskList), + ); + Widget title(BuildContext context, TaskList taskList) { return InkWell( - onTap: () { - context.pushNamed( - Routes.taskListDetails.name, - pathParameters: {'taskListId': taskListId}, - ); - }, + onTap: onTitleTap ?? + () { + context.pushNamed( + Routes.taskListDetails.name, + pathParameters: {'taskListId': taskListId}, + ); + }, child: Text( key: Key('task-list-title-$taskListId'), taskList.name(), diff --git a/app/lib/features/tasks/widgets/task_lists_empty.dart b/app/lib/features/tasks/widgets/task_lists_empty.dart new file mode 100644 index 000000000000..a73cb8455907 --- /dev/null +++ b/app/lib/features/tasks/widgets/task_lists_empty.dart @@ -0,0 +1,42 @@ +import 'package:acter/common/toolkit/buttons/primary_action_button.dart'; +import 'package:acter/common/widgets/empty_state_widget.dart'; +import 'package:acter/features/tasks/sheets/create_update_task_list.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class TaskListsEmptyState extends ConsumerWidget { + final bool canAdd; + final bool inSearch; + final String? spaceId; + const TaskListsEmptyState({ + super.key, + required this.canAdd, + required this.inSearch, + this.spaceId, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final lang = L10n.of(context); + return Center( + heightFactor: 1, + child: EmptyState( + title: inSearch + ? lang.noMatchingTasksListFound + : lang.noTasksListAvailableYet, + subtitle: lang.noTasksListAvailableDescription, + image: 'assets/images/tasks.svg', + primaryButton: canAdd + ? ActerPrimaryActionButton( + onPressed: () => showCreateUpdateTaskListBottomSheet( + context, + initialSelectedSpace: spaceId, + ), + child: Text(lang.createTaskList), + ) + : null, + ), + ); + } +} diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 3fa53be67033..1ce7118e2ad6 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -882,6 +882,7 @@ "pleaseProvideYourUserPassword": "Please provide your user password to confirm you want to end that session.", "@pleaseProvideYourUserPassword": {}, "pleaseSelectSpace": "Please select space", + "selectTaskList": "Select Task List", "@pleaseSelectSpace": {}, "pleaseWait": "Please wait…", "@pleaseWait": {}, diff --git a/app/test/features/tasks/tasks_adding_test.dart b/app/test/features/tasks/tasks_adding_test.dart index 49893d9457ef..2d5b9639e233 100644 --- a/app/test/features/tasks/tasks_adding_test.dart +++ b/app/test/features/tasks/tasks_adding_test.dart @@ -1,3 +1,4 @@ +import 'package:acter/common/providers/space_providers.dart'; import 'package:acter/features/tasks/actions/create_task.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -7,7 +8,7 @@ import '../../helpers/mock_tasks_providers.dart'; import '../../helpers/test_util.dart'; void main() { - group('Create Task Widget', () { + group('Create Task Widget on TaskList', () { testWidgets('Simple only title', (tester) async { final mockTaskList = MockTaskList(); final mockTaskDraft = MockTaskDraft(); @@ -16,9 +17,10 @@ void main() { when(() => mockTaskDraft.send()) .thenAnswer((_) async => MockEventId(id: 'test')); await tester.pumpProviderWidget( - overrides: [], + overrides: [ + selectedSpaceDetailsProvider.overrideWith((_) => null), + ], child: CreateTaskWidget( - taskName: '', taskList: mockTaskList, ), ); @@ -53,9 +55,10 @@ void main() { when(() => mockTaskDraft.send()) .thenAnswer((_) async => MockEventId(id: 'test')); await tester.pumpProviderWidget( - overrides: [], + overrides: [ + selectedSpaceDetailsProvider.overrideWith((_) => null), + ], child: CreateTaskWidget( - taskName: '', taskList: mockTaskList, ), ); @@ -111,9 +114,10 @@ void main() { when(() => mockTaskDraft.send()) .thenAnswer((_) async => MockEventId(id: 'test')); await tester.pumpProviderWidget( - overrides: [], + overrides: [ + selectedSpaceDetailsProvider.overrideWith((_) => null), + ], child: CreateTaskWidget( - taskName: '', taskList: mockTaskList, ), ); @@ -176,9 +180,10 @@ void main() { when(() => mockTaskDraft.send()) .thenAnswer((_) async => MockEventId(id: 'test')); await tester.pumpProviderWidget( - overrides: [], + overrides: [ + selectedSpaceDetailsProvider.overrideWith((_) => null), + ], child: CreateTaskWidget( - taskName: '', taskList: mockTaskList, ), ); @@ -243,9 +248,10 @@ void main() { when(() => mockTaskDraft.send()) .thenAnswer((_) async => MockEventId(id: 'test')); await tester.pumpProviderWidget( - overrides: [], + overrides: [ + selectedSpaceDetailsProvider.overrideWith((_) => null), + ], child: CreateTaskWidget( - taskName: '', taskList: mockTaskList, ), ); @@ -306,4 +312,41 @@ void main() { verify(() => mockTaskDraft.send()).called(1); }); }); + + group('Create Task no Tasklist', () { + testWidgets('Simple only title no tasklist', (tester) async { + final mockTaskList = MockTaskList(); + final mockTaskDraft = MockTaskDraft(); + when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); + when(() => mockTaskDraft.title('My new Task')).thenAnswer((_) => true); + when(() => mockTaskDraft.send()) + .thenAnswer((_) async => MockEventId(id: 'test')); + await tester.pumpProviderWidget( + overrides: [ + selectedSpaceDetailsProvider.overrideWith((_) => null), + ], + child: const CreateTaskWidget(), + ); + // try to submit without a title + + final submitBtn = find.byKey(CreateTaskWidget.submitBtn); + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + // not called + verifyNever(() => mockTaskList.taskBuilder()); + + // add the title + + final title = find.byKey(CreateTaskWidget.titleField); + expect(title, findsOneWidget); + await tester.enterText(title, 'My new Task'); + + expect(submitBtn, findsOneWidget); + await tester.tap(submitBtn); + + // blocks because we have no task list yet + verifyNever(() => mockTaskList.taskBuilder()); + }); + }); } From ada5316617d685829e9a3ad18f5b5309ba28459e Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 19:37:13 +0000 Subject: [PATCH 70/77] link up new task creation in quick actions --- .../features/home/widgets/quick_action_buttons.dart | 12 ++++++------ app/lib/features/home/widgets/sidebar_widget.dart | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/lib/features/home/widgets/quick_action_buttons.dart b/app/lib/features/home/widgets/quick_action_buttons.dart index 6466da75adc3..0f53cacddd3c 100644 --- a/app/lib/features/home/widgets/quick_action_buttons.dart +++ b/app/lib/features/home/widgets/quick_action_buttons.dart @@ -3,6 +3,7 @@ import 'package:acter/common/themes/colors/color_scheme.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/action_button_widget.dart'; import 'package:acter/features/main/providers/main_providers.dart'; +import 'package:acter/features/tasks/actions/create_task.dart'; import 'package:acter/features/tasks/sheets/create_update_task_list.dart'; import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; @@ -42,10 +43,9 @@ class QuickActionButtons extends ConsumerWidget { final canAddEvent = ref.watch(hasSpaceWithPermissionProvider('CanPostEvent')).valueOrNull ?? false; - final canAddTask = ref - .watch(hasSpaceWithPermissionProvider('CanPostTaskList')) - .valueOrNull ?? - false; + final canAddTask = + ref.watch(hasSpaceWithPermissionProvider('CanPostTask')).valueOrNull ?? + false; final canAddBoost = ref.watch(hasSpaceWithPermissionProvider('CanPostNews')).valueOrNull ?? false; @@ -64,12 +64,12 @@ class QuickActionButtons extends ConsumerWidget { if (canAddTask) ActionButtonWidget( iconData: Atlas.list, - title: lang.addTaskList, + title: lang.addTask, color: taskFeatureColor, padding: const EdgeInsets.symmetric(vertical: 6), onPressed: () { ref.read(quickActionVisibilityProvider.notifier).state = false; - showCreateUpdateTaskListBottomSheet(context); + showCreateTaskBottomSheet(context); }, ), if (canAddEvent) diff --git a/app/lib/features/home/widgets/sidebar_widget.dart b/app/lib/features/home/widgets/sidebar_widget.dart index a0d6ba663685..8d0a905af3e8 100644 --- a/app/lib/features/home/widgets/sidebar_widget.dart +++ b/app/lib/features/home/widgets/sidebar_widget.dart @@ -12,6 +12,7 @@ import 'package:acter/features/bug_report/providers/bug_report_providers.dart'; import 'package:acter/features/home/data/keys.dart'; import 'package:acter/features/home/widgets/activities_icon.dart'; import 'package:acter/features/home/widgets/chats_icon.dart'; +import 'package:acter/features/tasks/actions/create_task.dart'; import 'package:acter/features/tasks/sheets/create_update_task_list.dart'; import 'package:acter/router/providers/router_providers.dart'; import 'package:acter/router/utils.dart'; @@ -231,10 +232,9 @@ class SidebarWidget extends ConsumerWidget { final canAddEvent = ref.watch(hasSpaceWithPermissionProvider('CanPostEvent')).valueOrNull ?? false; - final canAddTask = ref - .watch(hasSpaceWithPermissionProvider('CanPostTaskList')) - .valueOrNull ?? - false; + final canAddTask = + ref.watch(hasSpaceWithPermissionProvider('CanPostTask')).valueOrNull ?? + false; final canAddBoost = ref.watch(hasSpaceWithPermissionProvider('CanPostNews')).valueOrNull ?? false; @@ -258,11 +258,11 @@ class SidebarWidget extends ConsumerWidget { PopupMenuItem( child: ActionButtonWidget( iconData: Atlas.list, - title: lang.addTaskList, + title: lang.addTask, color: taskFeatureColor, onPressed: () { if (context.canPop()) Navigator.pop(context); - showCreateUpdateTaskListBottomSheet(context); + showCreateTaskBottomSheet(context); }, ), ), From be3bb9edebe2ebf076950b41bcb02f7666637506 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 19:48:24 +0000 Subject: [PATCH 71/77] Add Changelog --- .changes/2434-task-quick-actions.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/2434-task-quick-actions.md diff --git a/.changes/2434-task-quick-actions.md b/.changes/2434-task-quick-actions.md new file mode 100644 index 000000000000..f317ee752bf6 --- /dev/null +++ b/.changes/2434-task-quick-actions.md @@ -0,0 +1 @@ +- Create Tasks right from the quick actions, including to select the target space and list. \ No newline at end of file From e77b115c3f96f309d649edd9fb6d09fcede9050c Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Fri, 1 Nov 2024 19:54:39 +0000 Subject: [PATCH 72/77] Always allow to create new list if we can --- .../home/widgets/quick_action_buttons.dart | 1 - .../features/home/widgets/sidebar_widget.dart | 1 - .../tasks/actions/select_tasklist.dart | 28 +++++++++++++++---- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/lib/features/home/widgets/quick_action_buttons.dart b/app/lib/features/home/widgets/quick_action_buttons.dart index 0f53cacddd3c..fb8f134cfe1a 100644 --- a/app/lib/features/home/widgets/quick_action_buttons.dart +++ b/app/lib/features/home/widgets/quick_action_buttons.dart @@ -4,7 +4,6 @@ import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/action_button_widget.dart'; import 'package:acter/features/main/providers/main_providers.dart'; import 'package:acter/features/tasks/actions/create_task.dart'; -import 'package:acter/features/tasks/sheets/create_update_task_list.dart'; import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/app/lib/features/home/widgets/sidebar_widget.dart b/app/lib/features/home/widgets/sidebar_widget.dart index 8d0a905af3e8..0cc44ee7bc86 100644 --- a/app/lib/features/home/widgets/sidebar_widget.dart +++ b/app/lib/features/home/widgets/sidebar_widget.dart @@ -13,7 +13,6 @@ import 'package:acter/features/home/data/keys.dart'; import 'package:acter/features/home/widgets/activities_icon.dart'; import 'package:acter/features/home/widgets/chats_icon.dart'; import 'package:acter/features/tasks/actions/create_task.dart'; -import 'package:acter/features/tasks/sheets/create_update_task_list.dart'; import 'package:acter/router/providers/router_providers.dart'; import 'package:acter/router/utils.dart'; import 'package:acter_avatar/acter_avatar.dart'; diff --git a/app/lib/features/tasks/actions/select_tasklist.dart b/app/lib/features/tasks/actions/select_tasklist.dart index 5a407ef86165..757cca004c51 100644 --- a/app/lib/features/tasks/actions/select_tasklist.dart +++ b/app/lib/features/tasks/actions/select_tasklist.dart @@ -1,12 +1,15 @@ import 'package:acter/common/providers/room_providers.dart'; +import 'package:acter/common/toolkit/buttons/inline_text_button.dart'; import 'package:acter/common/toolkit/errors/error_page.dart'; import 'package:acter/features/tasks/providers/tasklists_providers.dart'; +import 'package:acter/features/tasks/sheets/create_update_task_list.dart'; import 'package:acter/features/tasks/widgets/skeleton/tasks_list_skeleton.dart'; import 'package:acter/features/tasks/widgets/task_list_item_card.dart'; import 'package:acter/features/tasks/widgets/task_lists_empty.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; const Key selectSpaceDrawerKey = Key('space-widgets-select-space-drawer'); @@ -35,6 +38,11 @@ class _SelectTaskList extends ConsumerWidget { (spaceId: spaceId, searchText: ''), ), ); + final canAdd = ref + .watch(roomMembershipProvider(spaceId)) + .valueOrNull + ?.canString('CanPostTaskList') == + true; return Column( mainAxisAlignment: MainAxisAlignment.start, @@ -44,18 +52,26 @@ class _SelectTaskList extends ConsumerWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Row( - children: [Expanded(child: Text(lang.selectTaskList))], + children: [ + Expanded(child: Text(lang.selectTaskList)), + if (canAdd) + ActerInlineTextButton.icon( + icon: Icon(PhosphorIcons.plus()), + onPressed: () { + showCreateUpdateTaskListBottomSheet( + context, + initialSelectedSpace: spaceId, + ); + }, + label: Text(lang.addTaskList), + ), + ], ), ), Expanded( child: tasklistsLoader.when( data: (tasklists) { if (tasklists.isEmpty) { - final canAdd = ref - .watch(roomMembershipProvider(spaceId)) - .valueOrNull - ?.canString('CanPostTaskList') == - true; return TaskListsEmptyState( canAdd: canAdd, inSearch: false, From 79722f8385997b509bd218867bdfb8cf90bcd2f4 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Sun, 3 Nov 2024 11:32:24 +0000 Subject: [PATCH 73/77] Refactor spacechip, rename compatview -> compactView --- .../spaces/select_space_form_field.dart | 10 ++-- app/lib/features/home/widgets/space_chip.dart | 50 +++++++++++++------ .../features/pins/pages/create_pin_page.dart | 2 +- .../features/pins/pages/pin_details_page.dart | 2 +- .../features/tasks/actions/create_task.dart | 2 +- .../tasks/widgets/task_list_item_card.dart | 4 +- app/lib/l10n/app_de.arb | 2 +- app/lib/l10n/app_en.arb | 2 +- app/lib/l10n/app_sw.arb | 2 +- app/lib/l10n/app_ur.arb | 2 +- 10 files changed, 47 insertions(+), 31 deletions(-) diff --git a/app/lib/common/widgets/spaces/select_space_form_field.dart b/app/lib/common/widgets/spaces/select_space_form_field.dart index 443c9616aa37..ffb0bde09e21 100644 --- a/app/lib/common/widgets/spaces/select_space_form_field.dart +++ b/app/lib/common/widgets/spaces/select_space_form_field.dart @@ -14,7 +14,7 @@ class SelectSpaceFormField extends ConsumerWidget { final String? emptyText; final String canCheck; final bool mandatory; - final bool useCompatView; + final bool useCompactView; const SelectSpaceFormField({ super.key, @@ -23,7 +23,7 @@ class SelectSpaceFormField extends ConsumerWidget { this.emptyText, this.mandatory = true, required this.canCheck, - this.useCompatView = false, + this.useCompactView = false, }); void selectSpace(BuildContext context, WidgetRef ref) async { @@ -55,7 +55,7 @@ class SelectSpaceFormField extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (!useCompatView) + if (!useCompactView) Text( title ?? lang.space, style: Theme.of(context).textTheme.bodyMedium, @@ -105,9 +105,9 @@ class SelectSpaceFormField extends ConsumerWidget { (p0) => SpaceChip( spaceId: p0.roomId, onTapOpenSpaceDetail: false, - useCompatView: useCompatView, + useCompactView: useCompactView, onTapSelectSpace: () { - if (useCompatView) selectSpace(context, ref); + if (useCompactView) selectSpace(context, ref); }, ), ) ?? diff --git a/app/lib/features/home/widgets/space_chip.dart b/app/lib/features/home/widgets/space_chip.dart index 265f1932bdfa..1ecea164fc14 100644 --- a/app/lib/features/home/widgets/space_chip.dart +++ b/app/lib/features/home/widgets/space_chip.dart @@ -10,47 +10,65 @@ import 'package:skeletonizer/skeletonizer.dart'; class SpaceChip extends ConsumerWidget { final String spaceId; final bool onTapOpenSpaceDetail; - final bool useCompatView; + final bool useCompactView; final VoidCallback? onTapSelectSpace; const SpaceChip({ super.key, required this.spaceId, this.onTapOpenSpaceDetail = true, - this.useCompatView = false, + this.useCompactView = false, this.onTapSelectSpace, }); @override Widget build(BuildContext context, WidgetRef ref) { - if (useCompatView) { + if (useCompactView) { return renderCompactView(context, ref); } return renderFullChip(context, ref); } - static Widget loading() { - return Skeletonizer( - child: Chip( - avatar: ActerAvatar( - options: const AvatarOptions( - AvatarInfo(uniqueId: 'unique Id'), - size: 24, + static Widget loading({useCompactView = false}) => + useCompactView ? loadingCompact() : loadingFull(); + + static Widget loadingFull() => Skeletonizer( + child: Chip( + avatar: ActerAvatar( + options: const AvatarOptions( + AvatarInfo(uniqueId: 'unique Id'), + size: 24, + ), ), + label: const Text('unique name'), ), - label: const Text('unique name'), - ), - ); - } + ); + + static Widget loadingCompact() => const Skeletonizer( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('In: '), + SizedBox(width: 4), + Text('displayName'), + ], + ), + ); Widget renderCompactView(BuildContext context, WidgetRef ref) { final lang = L10n.of(context); final displayName = ref.watch(roomDisplayNameProvider(spaceId)).valueOrNull ?? spaceId; return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(lang.inSpaceLabelInline), - Text(lang.colonCharacter), + Text( + lang.inSpaceLabelInline, + style: Theme.of(context).textTheme.labelLarge, + ), + const SizedBox(width: 4), InkWell( onTap: () { if (onTapOpenSpaceDetail) { diff --git a/app/lib/features/pins/pages/create_pin_page.dart b/app/lib/features/pins/pages/create_pin_page.dart index 6e51182a62b6..987ac095c640 100644 --- a/app/lib/features/pins/pages/create_pin_page.dart +++ b/app/lib/features/pins/pages/create_pin_page.dart @@ -118,7 +118,7 @@ class _CreatePinConsumerState extends ConsumerState { alignment: Alignment.centerLeft, child: SelectSpaceFormField( canCheck: 'CanPostPin', - useCompatView: true, + useCompactView: true, ), ), const SizedBox(height: 14), diff --git a/app/lib/features/pins/pages/pin_details_page.dart b/app/lib/features/pins/pages/pin_details_page.dart index 327352c9f0df..0e706da0899a 100644 --- a/app/lib/features/pins/pages/pin_details_page.dart +++ b/app/lib/features/pins/pages/pin_details_page.dart @@ -307,7 +307,7 @@ class _PinDetailsPageState extends ConsumerState { Widget pinSpaceNameUI(ActerPin pin) { return SpaceChip( spaceId: pin.roomIdStr(), - useCompatView: true, + useCompactView: true, ); } diff --git a/app/lib/features/tasks/actions/create_task.dart b/app/lib/features/tasks/actions/create_task.dart index c07b779081d3..8b8c80afc1d9 100644 --- a/app/lib/features/tasks/actions/create_task.dart +++ b/app/lib/features/tasks/actions/create_task.dart @@ -145,7 +145,7 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { children: [ const SelectSpaceFormField( canCheck: 'CanPostTask', - useCompatView: true, + useCompactView: true, ), const Text(' > '), if (taskList == null) diff --git a/app/lib/features/tasks/widgets/task_list_item_card.dart b/app/lib/features/tasks/widgets/task_list_item_card.dart index eec47b8c8428..6486d059566c 100644 --- a/app/lib/features/tasks/widgets/task_list_item_card.dart +++ b/app/lib/features/tasks/widgets/task_list_item_card.dart @@ -118,8 +118,6 @@ class TaskListItemCard extends ConsumerWidget { Widget? subtitle(WidgetRef ref, TaskList taskList) { if (!showSpace) return null; - final spaceId = taskList.spaceIdStr(); - final spaceProfile = ref.watch(roomAvatarInfoProvider(spaceId)); - return Text(spaceProfile.displayName ?? ''); + return SpaceChip(spaceId: taskList.spaceIdStr(), useCompactView: true); } } diff --git a/app/lib/l10n/app_de.arb b/app/lib/l10n/app_de.arb index ce0d807d4374..21f5b682beac 100644 --- a/app/lib/l10n/app_de.arb +++ b/app/lib/l10n/app_de.arb @@ -2068,7 +2068,7 @@ "@text": {}, "audio": "Audio", "@audio": {}, - "inSpaceLabelInline": "In", + "inSpaceLabelInline": "In:", "@inSpaceLabelInline": {}, "comingSoon": "Noch nicht unterstützt. kommt bald!", "@comingSoon": {}, diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 1ce7118e2ad6..675d6de0c781 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -2187,7 +2187,7 @@ "@audio": {}, "pinDetails": "Pin Details", "@pinDetails": {}, - "inSpaceLabelInline": "In", + "inSpaceLabelInline": "In:", "@inSpaceLabelInline": {}, "comingSoon": "Not supported yet, coming soon!", "@comingSoon": {}, diff --git a/app/lib/l10n/app_sw.arb b/app/lib/l10n/app_sw.arb index 44e2525b50d2..279bfaf7408d 100644 --- a/app/lib/l10n/app_sw.arb +++ b/app/lib/l10n/app_sw.arb @@ -1203,7 +1203,7 @@ "@live": {}, "happeningNow": "Inatokea Sasa", "@happeningNow": {}, - "inSpaceLabelInline": "Katika", + "inSpaceLabelInline": "Katika:", "@inSpaceLabelInline": {}, "attachmentEmptyStateTitle": "Hakuna viambatisho vilivyopatikana.", "@attachmentEmptyStateTitle": {}, diff --git a/app/lib/l10n/app_ur.arb b/app/lib/l10n/app_ur.arb index 78115ec6cded..a5c6db014f2f 100644 --- a/app/lib/l10n/app_ur.arb +++ b/app/lib/l10n/app_ur.arb @@ -2144,7 +2144,7 @@ "@audio": {}, "pinDetails": "پن کی تفصیلات", "@pinDetails": {}, - "inSpaceLabelInline": "میں", + "inSpaceLabelInline": ":میں", "@inSpaceLabelInline": {}, "comingSoon": "ابھی تک تعاون یافتہ نہیں، جلد آرہا ہے!", "@comingSoon": {}, From 7785dc78e8febead5fceff8a6bda8410b5c655e8 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Sun, 3 Nov 2024 11:34:12 +0000 Subject: [PATCH 74/77] Improve TaskList Detail Page: - more space for the separated header - less loading states - Unified titles design for consistency - Show space in title - Proper loading skeleton --- .../widgets/comments_section_widget.dart | 3 + .../features/tasks/actions/create_task.dart | 2 +- .../tasks/pages/task_list_details_page.dart | 306 ++++++++++-------- .../tasks/widgets/task_items_list_widget.dart | 2 + .../tasks/widgets/task_list_item_card.dart | 1 + 5 files changed, 176 insertions(+), 138 deletions(-) diff --git a/app/lib/features/comments/widgets/comments_section_widget.dart b/app/lib/features/comments/widgets/comments_section_widget.dart index e9b5623ec3ee..af88ae3853a9 100644 --- a/app/lib/features/comments/widgets/comments_section_widget.dart +++ b/app/lib/features/comments/widgets/comments_section_widget.dart @@ -36,6 +36,9 @@ class CommentsSectionWidget extends ConsumerWidget { ); } + static CommentListSkeletonWidget loading() => + const CommentListSkeletonWidget(); + Widget buildCommentSectionUI( BuildContext context, CommentsManager commentManager, diff --git a/app/lib/features/tasks/actions/create_task.dart b/app/lib/features/tasks/actions/create_task.dart index 8b8c80afc1d9..2f078eea8ba2 100644 --- a/app/lib/features/tasks/actions/create_task.dart +++ b/app/lib/features/tasks/actions/create_task.dart @@ -385,7 +385,7 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { setState(() { taskList = newTaskList; }); - } + } Future addTask() async { final lang = L10n.of(context); diff --git a/app/lib/features/tasks/pages/task_list_details_page.dart b/app/lib/features/tasks/pages/task_list_details_page.dart index 17c094f7cd64..8430b086bcbb 100644 --- a/app/lib/features/tasks/pages/task_list_details_page.dart +++ b/app/lib/features/tasks/pages/task_list_details_page.dart @@ -10,6 +10,7 @@ import 'package:acter/common/widgets/edit_title_sheet.dart'; import 'package:acter/common/widgets/render_html.dart'; import 'package:acter/features/attachments/widgets/attachment_section.dart'; import 'package:acter/features/comments/widgets/comments_section_widget.dart'; +import 'package:acter/features/home/widgets/space_chip.dart'; import 'package:acter/features/tasks/actions/update_tasklist.dart'; import 'package:acter/features/tasks/providers/tasklists_providers.dart'; import 'package:acter/features/tasks/widgets/task_items_list_widget.dart'; @@ -20,6 +21,7 @@ import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; +import 'package:skeletonizer/skeletonizer.dart'; final _log = Logger('a3::tasks::tasklist_details'); @@ -52,98 +54,45 @@ class _TaskListPageState extends ConsumerState { AppBar _buildAppbar() { final lang = L10n.of(context); final textTheme = Theme.of(context).textTheme; - final tasklistLoader = ref.watch(taskListItemProvider(widget.taskListId)); - return tasklistLoader.when( - data: (tasklist) { - final membership = ref - .watch(roomMembershipProvider(tasklist.spaceIdStr())) - .valueOrNull; - bool canPost = membership?.canString('CanPostTaskList') == true; - return AppBar( - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ActerIconWidget( - iconSize: 40, - color: convertColor( - tasklist.display()?.color(), - iconPickerColors[0], - ), - icon: ActerIcon.iconForTask( - tasklist.display()?.iconStr(), + final tasklist = + ref.watch(taskListItemProvider(widget.taskListId)).valueOrNull; + final actions = List.empty(growable: true); + if (tasklist != null) { + actions.addAll( + [ + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + // FIXME: check permissions for all theses + PopupMenuItem( + onTap: () => showEditDescriptionSheet(tasklist), + child: Text( + lang.editDescription, + style: textTheme.bodyMedium, + ), ), - onIconSelection: canPost - ? (color, acterIcon) { - updateTaskListIcon( - context, - ref, - tasklist, - color, - acterIcon, - ); - } - : null, - ), - const SizedBox(width: 10), - SelectionArea( - child: GestureDetector( - onTap: () => showEditTaskListNameBottomSheet( - context: context, - ref: ref, - taskList: tasklist, - titleValue: tasklist.name(), + PopupMenuItem( + onTap: () => showRedactDialog(taskList: tasklist), + child: Text( + lang.delete, + style: textTheme.bodyMedium, ), + ), + PopupMenuItem( + onTap: () => showReportDialog(tasklist), child: Text( - key: TaskListDetailPage.taskListTitleKey, - tasklist.name(), - style: textTheme.titleMedium, + lang.report, + style: textTheme.bodyMedium, ), ), - ), - ], + ]; + }, ), - actions: [ - PopupMenuButton( - icon: const Icon(Icons.more_vert), - itemBuilder: (context) { - return [ - PopupMenuItem( - onTap: () => showEditDescriptionSheet(tasklist), - child: Text( - lang.editDescription, - style: textTheme.bodyMedium, - ), - ), - PopupMenuItem( - onTap: () => showRedactDialog(taskList: tasklist), - child: Text( - lang.delete, - style: textTheme.bodyMedium, - ), - ), - PopupMenuItem( - onTap: () => showReportDialog(tasklist), - child: Text( - lang.report, - style: textTheme.bodyMedium, - ), - ), - ]; - }, - ), - ], - ); - }, - error: (e, s) { - _log.severe('Failed to load tasklist', e, s); - return AppBar( - title: Text(lang.loadingFailed(e)), - ); - }, - loading: () => AppBar( - title: Text(lang.loading), - ), - ); + ], + ); + } + return AppBar(actions: actions); } // Redact Task List Dialog @@ -173,14 +122,13 @@ class _TaskListPageState extends ConsumerState { } Widget _buildBody() { - final lang = L10n.of(context); final tasklistLoader = ref.watch(taskListItemProvider(widget.taskListId)); return tasklistLoader.when( data: (tasklist) => _buildTaskListData(tasklist), error: (error, stack) { _log.severe('Failed to load tasklist', error, stack); return ErrorPage( - background: Text(lang.loading), + background: _loadingSkeleton(), error: error, stack: stack, onRetryTap: () { @@ -188,7 +136,8 @@ class _TaskListPageState extends ConsumerState { }, ); }, - loading: () => Text(lang.loading), + loading: () => _loadingSkeleton(), + skipLoadingOnReload: true, // don't refresh to weirdly ); } @@ -197,48 +146,101 @@ class _TaskListPageState extends ConsumerState { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), + _taskListHeader(taskListData), + const SizedBox(height: 20), _widgetDescription(taskListData), - _widgetTasksList(taskListData), + const SizedBox(height: 30), + _widgetTasksListHeader(), + ValueListenableBuilder( + valueListenable: showCompletedTask, + builder: (context, value, child) => TaskItemsListWidget( + taskList: taskListData, + showCompletedTask: value, + ), + ), + const SizedBox(height: 20), + AttachmentSectionWidget(manager: taskListData.attachments()), + const SizedBox(height: 20), + CommentsSectionWidget(manager: taskListData.comments()), + const SizedBox(height: 20), ], ), ), ); } + Widget _taskListHeader(TaskList tasklist) { + final textTheme = Theme.of(context).textTheme; + final canPost = ref + .watch(roomMembershipProvider(tasklist.spaceIdStr())) + .valueOrNull + ?.canString('CanPostTaskList') == + true; + return ListTile( + leading: ActerIconWidget( + iconSize: 40, + color: convertColor( + tasklist.display()?.color(), + iconPickerColors[0], + ), + icon: ActerIcon.iconForTask( + tasklist.display()?.iconStr(), + ), + onIconSelection: canPost + ? (color, acterIcon) { + updateTaskListIcon( + context, + ref, + tasklist, + color, + acterIcon, + ); + } + : null, + ), + title: SelectionArea( + child: GestureDetector( + onTap: () => showEditTaskListNameBottomSheet( + context: context, + ref: ref, + taskList: tasklist, + titleValue: tasklist.name(), + ), + child: Text( + key: TaskListDetailPage.taskListTitleKey, + tasklist.name(), + style: textTheme.titleMedium, + ), + ), + ), + subtitle: SpaceChip(spaceId: tasklist.spaceIdStr(), useCompactView: true), + ); + } + Widget _widgetDescription(TaskList taskListData) { final description = taskListData.description(); if (description == null) return const SizedBox.shrink(); final formattedBody = description.formattedBody(); final textTheme = Theme.of(context).textTheme; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectionArea( - child: GestureDetector( - onTap: () { - showEditDescriptionSheet(taskListData); - }, - child: formattedBody != null - ? RenderHtml( - text: formattedBody, - defaultTextStyle: textTheme.labelLarge, - ) - : Text( - description.body(), - style: textTheme.labelLarge, - ), - ), - ), - const SizedBox(height: 10), - const Divider( - indent: 10, - endIndent: 18, - ), - const SizedBox(height: 10), - ], + return SelectionArea( + child: GestureDetector( + onTap: () { + showEditDescriptionSheet(taskListData); + }, + child: formattedBody != null + ? RenderHtml( + text: formattedBody, + defaultTextStyle: textTheme.labelLarge, + ) + : Text( + description.body(), + style: textTheme.labelLarge, + ), + ), ); } @@ -279,32 +281,15 @@ class _TaskListPageState extends ConsumerState { } } - Widget _widgetTasksList(TaskList taskListData) { - return Column( - children: [ - _widgetTasksListHeader(), - ValueListenableBuilder( - valueListenable: showCompletedTask, - builder: (context, value, child) => TaskItemsListWidget( - taskList: taskListData, - showCompletedTask: value, - ), - ), - const SizedBox(height: 20), - AttachmentSectionWidget(manager: taskListData.attachments()), - const SizedBox(height: 20), - CommentsSectionWidget(manager: taskListData.comments()), - const SizedBox(height: 20), - ], - ); - } - Widget _widgetTasksListHeader() { final lang = L10n.of(context); return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(lang.tasks), + Text( + lang.tasks, + style: Theme.of(context).textTheme.titleSmall, + ), ValueListenableBuilder( valueListenable: showCompletedTask, builder: (context, value, child) { @@ -340,4 +325,51 @@ class _TaskListPageState extends ConsumerState { onSave: (newName) => updateTaskListTitle(context, taskList, newName), ); } + + Widget _loadingSkeleton() => Skeletonizer.zone( + child: Column( + children: [ + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Bone.icon(size: 40), + const SizedBox(width: 10), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Task List Title', + style: Theme.of(context).textTheme.titleMedium, + ), + SpaceChip.loadingCompact(), + ], + ), + ], + ), + const SizedBox(height: 20), + const Text('Task description'), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + L10n.of(context).tasks, + style: Theme.of(context).textTheme.titleSmall, + ), + const Bone.iconButton( + size: 18, + ), + ], + ), + TaskItemsListWidget.loading(), + const SizedBox(height: 20), + AttachmentSectionWidget.loading(), + const SizedBox(height: 20), + CommentsSectionWidget.loading(), + const SizedBox(height: 20), + ], + ), + ); } diff --git a/app/lib/features/tasks/widgets/task_items_list_widget.dart b/app/lib/features/tasks/widgets/task_items_list_widget.dart index 97d488567c8a..04c08a3e8803 100644 --- a/app/lib/features/tasks/widgets/task_items_list_widget.dart +++ b/app/lib/features/tasks/widgets/task_items_list_widget.dart @@ -22,6 +22,8 @@ class TaskItemsListWidget extends ConsumerStatefulWidget { this.showCompletedTask = false, }); + static TaskItemsSkeleton loading() => const TaskItemsSkeleton(); + @override ConsumerState createState() => TaskItemsListWidgetState(); diff --git a/app/lib/features/tasks/widgets/task_list_item_card.dart b/app/lib/features/tasks/widgets/task_list_item_card.dart index 6486d059566c..33a399901920 100644 --- a/app/lib/features/tasks/widgets/task_list_item_card.dart +++ b/app/lib/features/tasks/widgets/task_list_item_card.dart @@ -3,6 +3,7 @@ import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/acter_icon_picker/acter_icon_widget.dart'; import 'package:acter/common/widgets/acter_icon_picker/model/acter_icons.dart'; import 'package:acter/common/widgets/acter_icon_picker/model/color_data.dart'; +import 'package:acter/features/home/widgets/space_chip.dart'; import 'package:acter/features/tasks/providers/tasklists_providers.dart'; import 'package:acter/features/tasks/widgets/task_items_list_widget.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; From 99ca5b348b4404b31e548f094a7b45d0779790e0 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Sun, 3 Nov 2024 12:22:17 +0000 Subject: [PATCH 75/77] Clean up the Task Item Page: - Show the List and Space a task belongs to - Show status and let the user check the item off - Refactor for clarity and overview - Adapting design to be more aligned --- .../providers/space_navbar_provider.dart | 2 +- .../widgets/space_sections/tasks_section.dart | 2 +- .../tasks/actions/update_tasklist.dart | 2 +- .../tasks/pages/task_item_detail_page.dart | 305 ++++++++++-------- .../tasks/providers/tasklists_providers.dart | 10 +- app/lib/features/tasks/widgets/task_item.dart | 23 +- .../tasks/widgets/task_status_widget.dart | 49 +++ 7 files changed, 233 insertions(+), 160 deletions(-) create mode 100644 app/lib/features/tasks/widgets/task_status_widget.dart diff --git a/app/lib/features/space/providers/space_navbar_provider.dart b/app/lib/features/space/providers/space_navbar_provider.dart index 0592f85fe9b2..372fa8aa4c36 100644 --- a/app/lib/features/space/providers/space_navbar_provider.dart +++ b/app/lib/features/space/providers/space_navbar_provider.dart @@ -47,7 +47,7 @@ final tabsProvider = } if (appSettings.tasks().active()) { - final taskList = await ref.watch(taskListProvider(spaceId).future); + final taskList = await ref.watch(taskListsProvider(spaceId).future); if (taskList.isNotEmpty) { tabs.add(TabEntry.tasks); } diff --git a/app/lib/features/space/widgets/space_sections/tasks_section.dart b/app/lib/features/space/widgets/space_sections/tasks_section.dart index c9a1965ef44d..010bc8ea70ad 100644 --- a/app/lib/features/space/widgets/space_sections/tasks_section.dart +++ b/app/lib/features/space/widgets/space_sections/tasks_section.dart @@ -23,7 +23,7 @@ class TasksSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final lang = L10n.of(context); - final tasksLoader = ref.watch(taskListProvider(spaceId)); + final tasksLoader = ref.watch(taskListsProvider(spaceId)); return tasksLoader.when( data: (tasks) => buildTasksSectionUI(context, tasks), error: (e, s) { diff --git a/app/lib/features/tasks/actions/update_tasklist.dart b/app/lib/features/tasks/actions/update_tasklist.dart index ee0a36c872fc..9d728676017b 100644 --- a/app/lib/features/tasks/actions/update_tasklist.dart +++ b/app/lib/features/tasks/actions/update_tasklist.dart @@ -32,7 +32,7 @@ Future updateTaskListIcon( try { await updateBuilder.send(); EasyLoading.dismiss(); - ref.invalidate(taskListProvider); + ref.invalidate(taskListsProvider); if (!context.mounted) return; } catch (e, s) { _log.severe('Failed to rename tasklist', e, s); diff --git a/app/lib/features/tasks/pages/task_item_detail_page.dart b/app/lib/features/tasks/pages/task_item_detail_page.dart index 4b181e78bba8..8ea4e4f6ad35 100644 --- a/app/lib/features/tasks/pages/task_item_detail_page.dart +++ b/app/lib/features/tasks/pages/task_item_detail_page.dart @@ -6,21 +6,30 @@ import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/providers/room_providers.dart'; import 'package:acter/common/toolkit/buttons/inline_text_button.dart'; import 'package:acter/common/toolkit/errors/error_page.dart'; +import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/utils/utils.dart'; +import 'package:acter/common/widgets/acter_icon_picker/acter_icon_widget.dart'; +import 'package:acter/common/widgets/acter_icon_picker/model/acter_icons.dart'; +import 'package:acter/common/widgets/acter_icon_picker/model/color_data.dart'; import 'package:acter/common/widgets/edit_html_description_sheet.dart'; import 'package:acter/common/widgets/edit_title_sheet.dart'; import 'package:acter/common/widgets/render_html.dart'; import 'package:acter/features/attachments/widgets/attachment_section.dart'; import 'package:acter/features/comments/widgets/comments_section_widget.dart'; +import 'package:acter/features/home/widgets/space_chip.dart'; import 'package:acter/features/tasks/providers/task_items_providers.dart'; +import 'package:acter/features/tasks/providers/tasklists_providers.dart'; import 'package:acter/features/tasks/widgets/due_picker.dart'; import 'package:acter/features/tasks/widgets/skeleton/task_item_detail_page_skeleton.dart'; +import 'package:acter/features/tasks/widgets/task_status_widget.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; final _log = Logger('a3::tasks::task_item_details'); @@ -37,120 +46,78 @@ class TaskItemDetailPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final taskLoader = - ref.watch(taskItemProvider((taskListId: taskListId, taskId: taskId))); return Scaffold( - appBar: _buildAppBar(context, ref, taskLoader), - body: _buildBody(context, ref, taskLoader), + appBar: _buildAppBar(context, ref), + body: _buildBody(context, ref), ); } AppBar _buildAppBar( BuildContext context, WidgetRef ref, - AsyncValue taskLoader, ) { + final task = ref + .watch(taskItemProvider((taskListId: taskListId, taskId: taskId))) + .valueOrNull; + final lang = L10n.of(context); final textTheme = Theme.of(context).textTheme; - return taskLoader.when( - data: (task) => AppBar( - title: SelectionArea( - child: GestureDetector( - onTap: () => showEditTaskItemNameBottomSheet( - context: context, - ref: ref, - task: task, - titleValue: task.title(), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - task.title(), - style: textTheme.titleMedium, + final actions = List.empty(growable: true); + if (task != null) { + actions.addAll([ + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + onTap: () { + showEditTitleBottomSheet( + context: context, + bottomSheetTitle: lang.editName, + titleValue: task.title(), + onSave: (newName) => saveTitle(context, ref, task, newName), + ); + }, + child: Text( + lang.editTitle, + style: textTheme.bodyMedium, ), - Row( - children: [ - const Icon( - Icons.list, - color: Colors.white54, - size: 22, - ), - const SizedBox(width: 6), - Text( - lang.taskList, - style: textTheme.labelMedium, - ), - ], + ), + PopupMenuItem( + onTap: () => showEditDescriptionSheet(context, ref, task), + child: Text( + lang.editDescription, + style: textTheme.bodyMedium, ), - ], - ), - ), - ), - actions: [ - PopupMenuButton( - icon: const Icon(Icons.more_vert), - itemBuilder: (context) { - return [ - PopupMenuItem( - onTap: () { - showEditTitleBottomSheet( - context: context, - bottomSheetTitle: lang.editName, - titleValue: task.title(), - onSave: (newName) => - saveTitle(context, ref, task, newName), - ); - }, - child: Text( - lang.editTitle, - style: textTheme.bodyMedium, - ), + ), + PopupMenuItem( + onTap: () => showRedactDialog( + context: context, + ref: ref, + task: task, ), - PopupMenuItem( - onTap: () => showEditDescriptionSheet(context, ref, task), - child: Text( - lang.editDescription, - style: textTheme.bodyMedium, - ), + child: Text( + lang.delete, + style: textTheme.bodyMedium, ), - PopupMenuItem( - onTap: () => showRedactDialog( - context: context, - ref: ref, - task: task, - ), - child: Text( - lang.delete, - style: textTheme.bodyMedium, - ), + ), + PopupMenuItem( + onTap: () => showReportDialog( + context: context, + task: task, ), - PopupMenuItem( - onTap: () => showReportDialog( - context: context, - task: task, - ), - child: Text( - lang.report, - style: textTheme.bodyMedium, - ), + child: Text( + lang.report, + style: textTheme.bodyMedium, ), - ]; - }, - ), - ], - ), - error: (e, s) { - _log.severe('Failed to load task', e, s); - return AppBar( - title: Text(lang.loadingFailed(e)), - ); - }, - loading: () => AppBar( - title: Text(lang.loading), - ), - ); + ), + ]; + }, + ), + ]); + } + + return AppBar(actions: actions); } // Redact Task Item Dialog @@ -189,39 +156,42 @@ class TaskItemDetailPage extends ConsumerWidget { Widget _buildBody( BuildContext context, WidgetRef ref, - AsyncValue taskLoader, - ) { - return taskLoader.when( - data: (task) => taskData(context, task, ref), - error: (error, stack) { - _log.severe('Failed to load task', error, stack); - return ErrorPage( - background: const TaskItemDetailPageSkeleton(), - error: error, - stack: stack, - onRetryTap: () { - ref.invalidate( - taskItemProvider((taskListId: taskListId, taskId: taskId)), - ); - }, - ); - }, - loading: () => const TaskItemDetailPageSkeleton(), - ); - } + ) => + ref + .watch(taskItemProvider((taskListId: taskListId, taskId: taskId))) + .when( + data: (task) => taskData(context, task, ref), + error: (error, stack) { + _log.severe('Failed to load task', error, stack); + return ErrorPage( + background: const TaskItemDetailPageSkeleton(), + error: error, + stack: stack, + onRetryTap: () { + ref.invalidate( + taskItemProvider((taskListId: taskListId, taskId: taskId)), + ); + }, + ); + }, + loading: () => const TaskItemDetailPageSkeleton(), + skipLoadingOnReload: true, + ); Widget taskData(BuildContext context, Task task, WidgetRef ref) { return SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), - _widgetDescription(context, task, ref), + _taskHeader(context, task, ref), const SizedBox(height: 10), _widgetTaskDate(context, task), _widgetTaskAssignment(context, task, ref), - const SizedBox(height: 20), + ..._widgetDescription(context, task, ref), + const SizedBox(height: 40), AttachmentSectionWidget(manager: task.attachments()), const SizedBox(height: 20), CommentsSectionWidget(manager: task.comments()), @@ -232,20 +202,80 @@ class TaskItemDetailPage extends ConsumerWidget { ); } - Widget _widgetDescription(BuildContext context, Task task, WidgetRef ref) { + Widget _taskHeader(BuildContext context, Task task, WidgetRef ref) { + final textTheme = Theme.of(context).textTheme; + final taskList = ref.watch(taskListProvider(taskListId)).valueOrNull; + return ListTile( + dense: true, + leading: TaskStatusWidget(task: task, size: 40), + title: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () => showEditTaskItemNameBottomSheet( + context: context, + ref: ref, + task: task, + titleValue: task.title(), + ), + child: Text( + task.title(), + style: textTheme.titleMedium, + ), + ), + if (taskList != null) + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SpaceChip(spaceId: taskList.spaceIdStr(), useCompactView: true), + const SizedBox(width: 5), + ActerIconWidget( + iconSize: 22, + color: convertColor( + taskList.display()?.color(), + iconPickerColors[0], + ), + icon: ActerIcon.iconForTask( + taskList.display()?.iconStr(), + ), + ), + InkWell( + onTap: () => context.pushNamed( + Routes.taskListDetails.name, + pathParameters: {'taskListId': taskListId}, + ), + child: Text( + taskList.name(), + style: textTheme.labelMedium, + ), + ), + ], + ), + ], + ), + ); + } + + List _widgetDescription( + BuildContext context, + Task task, + WidgetRef ref, + ) { final description = task.description(); - if (description == null) return const SizedBox.shrink(); + if (description == null) return []; final formattedBody = description.formattedBody(); final textTheme = Theme.of(context).textTheme; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectionArea( - child: GestureDetector( - onTap: () { - showEditDescriptionSheet(context, ref, task); - }, + return [ + const SizedBox(height: 20), + SelectionArea( + child: GestureDetector( + onTap: () { + showEditDescriptionSheet(context, ref, task); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), child: formattedBody != null ? RenderHtml( text: formattedBody, @@ -257,9 +287,8 @@ class TaskItemDetailPage extends ConsumerWidget { ), ), ), - const SizedBox(height: 10), - ], - ); + ), + ]; } void showEditDescriptionSheet( @@ -319,7 +348,10 @@ class TaskItemDetailPage extends ConsumerWidget { lang.noDueDate; return ListTile( dense: true, - leading: const Icon(Atlas.calendar_date_thin), + leading: const Padding( + padding: EdgeInsets.only(left: 15), + child: Icon(Atlas.calendar_date_thin), + ), title: Text( lang.dueDate, style: textTheme.bodyMedium, @@ -382,7 +414,10 @@ class TaskItemDetailPage extends ConsumerWidget { final textTheme = Theme.of(context).textTheme; return ListTile( dense: true, - leading: const Icon(Atlas.business_man_thin), + leading: const Padding( + padding: EdgeInsets.only(left: 15), + child: Icon(Atlas.business_man_thin), + ), title: Row( children: [ Text( diff --git a/app/lib/features/tasks/providers/tasklists_providers.dart b/app/lib/features/tasks/providers/tasklists_providers.dart index 8faa93857511..a0cbd6821938 100644 --- a/app/lib/features/tasks/providers/tasklists_providers.dart +++ b/app/lib/features/tasks/providers/tasklists_providers.dart @@ -15,7 +15,7 @@ final allTasksListsProvider = () => AsyncAllTaskListsNotifier(), ); -final taskListProvider = +final taskListsProvider = FutureProvider.family, String?>((ref, spaceId) async { final allTaskLists = await ref.watch(allTasksListsProvider.future); if (spaceId == null) { @@ -28,12 +28,18 @@ final taskListProvider = } }); +final taskListProvider = + FutureProvider.family((ref, eventId) async { + final allTaskLists = await ref.watch(allTasksListsProvider.future); + return allTaskLists.where((e) => e.eventIdStr() == eventId).firstOrNull; +}); + //Search any tasks list typedef TasksListSearchParams = ({String? spaceId, String searchText}); final tasksListSearchProvider = FutureProvider.autoDispose .family, TasksListSearchParams>((ref, params) async { - final tasksList = await ref.watch(taskListProvider(params.spaceId).future); + final tasksList = await ref.watch(taskListsProvider(params.spaceId).future); //Return all task list if search text is empty if (params.searchText.isEmpty) return tasksList; diff --git a/app/lib/features/tasks/widgets/task_item.dart b/app/lib/features/tasks/widgets/task_item.dart index c01d13c9145d..388006c16290 100644 --- a/app/lib/features/tasks/widgets/task_item.dart +++ b/app/lib/features/tasks/widgets/task_item.dart @@ -5,9 +5,9 @@ import 'package:acter/common/utils/utils.dart'; import 'package:acter/common/widgets/room/room_avatar_builder.dart'; import 'package:acter/features/tasks/providers/task_items_providers.dart'; import 'package:acter/features/tasks/providers/tasklists_providers.dart'; +import 'package:acter/features/tasks/widgets/task_status_widget.dart'; import 'package:acter_avatar/acter_avatar.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; -import 'package:atlas_icons/atlas_icons.dart'; import 'package:dart_date/dart_date.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -89,25 +89,8 @@ class TaskItem extends ConsumerWidget { ); } - Widget leadingWidget(Task task) { - final isDone = task.isDone(); - return InkWell( - key: isDone ? doneKey() : notDoneKey(), - child: Icon( - isDone ? Atlas.check_circle_thin : Icons.radio_button_off_outlined, - ), - onTap: () async { - final updater = task.updateBuilder(); - if (!isDone) { - updater.markDone(); - } else { - updater.markUndone(); - } - await updater.send(); - onDone.map((cb) => cb()); - }, - ); - } + Widget leadingWidget(Task task) => + TaskStatusWidget(task: task, onDone: onDone); Widget takeItemSubTitle(WidgetRef ref, BuildContext context, Task task) { final lang = L10n.of(context); diff --git a/app/lib/features/tasks/widgets/task_status_widget.dart b/app/lib/features/tasks/widgets/task_status_widget.dart new file mode 100644 index 000000000000..8438f3dec023 --- /dev/null +++ b/app/lib/features/tasks/widgets/task_status_widget.dart @@ -0,0 +1,49 @@ +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:acter/common/extensions/options.dart'; +import 'package:atlas_icons/atlas_icons.dart'; +import 'package:flutter/material.dart'; + +class TaskStatusWidget extends StatelessWidget { + final Task task; + final double? size; + final Function()? onDone; + + const TaskStatusWidget({ + super.key, + required this.task, + this.size, + this.onDone, + }); + + static Key doneKey(String taskId) { + return Key('task-entry-$taskId-status-btn-done'); + } + + static Key notDoneKey(String taskId) { + return Key('task-entry-$taskId-status-btn-not-done'); + } + + @override + Widget build(BuildContext context) { + final isDone = task.isDone(); + final taskId = task.eventIdStr(); + + return InkWell( + key: isDone ? doneKey(taskId) : notDoneKey(taskId), + child: Icon( + isDone ? Atlas.check_circle_thin : Icons.radio_button_off_outlined, + size: size, + ), + onTap: () async { + final updater = task.updateBuilder(); + if (!isDone) { + updater.markDone(); + } else { + updater.markUndone(); + } + await updater.send(); + onDone.map((cb) => cb()); + }, + ); + } +} From f7e3ebd224e2dc199ef7ea6d42f3352eae88508d Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Sun, 3 Nov 2024 12:31:55 +0000 Subject: [PATCH 76/77] Fix subtitle row overflow issue --- app/lib/features/home/widgets/space_chip.dart | 9 ++--- .../tasks/pages/task_item_detail_page.dart | 35 +++++++++++-------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/lib/features/home/widgets/space_chip.dart b/app/lib/features/home/widgets/space_chip.dart index 1ecea164fc14..b2e421fe3f3f 100644 --- a/app/lib/features/home/widgets/space_chip.dart +++ b/app/lib/features/home/widgets/space_chip.dart @@ -45,9 +45,7 @@ class SpaceChip extends ConsumerWidget { ); static Widget loadingCompact() => const Skeletonizer( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + child: Wrap( children: [ Text('In: '), SizedBox(width: 4), @@ -60,9 +58,8 @@ class SpaceChip extends ConsumerWidget { final lang = L10n.of(context); final displayName = ref.watch(roomDisplayNameProvider(spaceId)).valueOrNull ?? spaceId; - return Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: [ Text( lang.inSpaceLabelInline, diff --git a/app/lib/features/tasks/pages/task_item_detail_page.dart b/app/lib/features/tasks/pages/task_item_detail_page.dart index 8ea4e4f6ad35..a45ecd2070fd 100644 --- a/app/lib/features/tasks/pages/task_item_detail_page.dart +++ b/app/lib/features/tasks/pages/task_item_detail_page.dart @@ -225,29 +225,34 @@ class TaskItemDetailPage extends ConsumerWidget { ), ), if (taskList != null) - Row( - mainAxisAlignment: MainAxisAlignment.start, + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: [ SpaceChip(spaceId: taskList.spaceIdStr(), useCompactView: true), const SizedBox(width: 5), - ActerIconWidget( - iconSize: 22, - color: convertColor( - taskList.display()?.color(), - iconPickerColors[0], - ), - icon: ActerIcon.iconForTask( - taskList.display()?.iconStr(), - ), - ), InkWell( onTap: () => context.pushNamed( Routes.taskListDetails.name, pathParameters: {'taskListId': taskListId}, ), - child: Text( - taskList.name(), - style: textTheme.labelMedium, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ActerIconWidget( + iconSize: 22, + color: convertColor( + taskList.display()?.color(), + iconPickerColors[0], + ), + icon: ActerIcon.iconForTask( + taskList.display()?.iconStr(), + ), + ), + Text( + taskList.name(), + style: textTheme.labelMedium, + ), + ], ), ), ], From 51e0d620d2886677bfa284e78c117d88e2c9319e Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Sun, 3 Nov 2024 17:51:47 +0000 Subject: [PATCH 77/77] Fixing and Cleaning up for Tests --- .../features/tasks/actions/create_task.dart | 2 +- .../tasks/pages/task_list_details_page.dart | 82 ++++++++++--------- .../tasks/providers/task_items_providers.dart | 7 +- .../tasks/providers/tasklists_providers.dart | 11 +-- app/lib/features/tasks/widgets/task_item.dart | 2 +- .../tasks/widgets/task_list_item_card.dart | 3 +- app/test/features/tasks/error_pages_test.dart | 6 +- app/test/helpers/mock_tasks_providers.dart | 6 ++ 8 files changed, 61 insertions(+), 58 deletions(-) diff --git a/app/lib/features/tasks/actions/create_task.dart b/app/lib/features/tasks/actions/create_task.dart index 2f078eea8ba2..b4191fcc70f7 100644 --- a/app/lib/features/tasks/actions/create_task.dart +++ b/app/lib/features/tasks/actions/create_task.dart @@ -381,7 +381,7 @@ class _CreateTaskWidgetConsumerState extends ConsumerState { return; } - final newTaskList = await ref.read(taskListItemProvider(taskListId).future); + final newTaskList = await ref.read(taskListProvider(taskListId).future); setState(() { taskList = newTaskList; }); diff --git a/app/lib/features/tasks/pages/task_list_details_page.dart b/app/lib/features/tasks/pages/task_list_details_page.dart index 8430b086bcbb..34ef45f4b5ec 100644 --- a/app/lib/features/tasks/pages/task_list_details_page.dart +++ b/app/lib/features/tasks/pages/task_list_details_page.dart @@ -54,8 +54,7 @@ class _TaskListPageState extends ConsumerState { AppBar _buildAppbar() { final lang = L10n.of(context); final textTheme = Theme.of(context).textTheme; - final tasklist = - ref.watch(taskListItemProvider(widget.taskListId)).valueOrNull; + final tasklist = ref.watch(taskListProvider(widget.taskListId)).valueOrNull; final actions = List.empty(growable: true); if (tasklist != null) { actions.addAll( @@ -122,7 +121,7 @@ class _TaskListPageState extends ConsumerState { } Widget _buildBody() { - final tasklistLoader = ref.watch(taskListItemProvider(widget.taskListId)); + final tasklistLoader = ref.watch(taskListProvider(widget.taskListId)); return tasklistLoader.when( data: (tasklist) => _buildTaskListData(tasklist), error: (error, stack) { @@ -132,7 +131,7 @@ class _TaskListPageState extends ConsumerState { error: error, stack: stack, onRetryTap: () { - ref.invalidate(taskListItemProvider(widget.taskListId)); + ref.invalidate(taskListProvider(widget.taskListId)); }, ); }, @@ -326,50 +325,55 @@ class _TaskListPageState extends ConsumerState { ); } - Widget _loadingSkeleton() => Skeletonizer.zone( - child: Column( - children: [ - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.start, + Widget _loadingSkeleton() => SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Skeletonizer.zone( + child: Column( children: [ - const Bone.icon(size: 40), - const SizedBox(width: 10), - Column( + const SizedBox(height: 10), + Row( mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Task List Title', - style: Theme.of(context).textTheme.titleMedium, + const Bone.icon(size: 40), + const SizedBox(width: 10), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Task List Title', + style: Theme.of(context).textTheme.titleMedium, + ), + SpaceChip.loadingCompact(), + ], ), - SpaceChip.loadingCompact(), ], ), - ], - ), - const SizedBox(height: 20), - const Text('Task description'), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - L10n.of(context).tasks, - style: Theme.of(context).textTheme.titleSmall, - ), - const Bone.iconButton( - size: 18, + const SizedBox(height: 20), + const Text('Task description'), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + L10n.of(context).tasks, + style: Theme.of(context).textTheme.titleSmall, + ), + const Bone.iconButton( + size: 18, + ), + ], ), + TaskItemsListWidget.loading(), + const SizedBox(height: 20), + AttachmentSectionWidget.loading(), + const SizedBox(height: 20), + CommentsSectionWidget.loading(), + const SizedBox(height: 20), ], ), - TaskItemsListWidget.loading(), - const SizedBox(height: 20), - AttachmentSectionWidget.loading(), - const SizedBox(height: 20), - CommentsSectionWidget.loading(), - const SizedBox(height: 20), - ], + ), ), ); } diff --git a/app/lib/features/tasks/providers/task_items_providers.dart b/app/lib/features/tasks/providers/task_items_providers.dart index bc2ddc23cb71..65b67b59caad 100644 --- a/app/lib/features/tasks/providers/task_items_providers.dart +++ b/app/lib/features/tasks/providers/task_items_providers.dart @@ -5,8 +5,8 @@ import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:riverpod/riverpod.dart'; //List of task items based on the specified task list -final taskItemsListProvider = - AsyncNotifierProvider.family(() { +final taskItemsListProvider = AsyncNotifierProvider.family< + TaskItemsListNotifier, TasksOverview, TaskList>(() { return TaskItemsListNotifier(); }); @@ -22,8 +22,7 @@ final notifierTaskProvider = //Single Task Item Details Provider based on the TaskList Id and Task Item Id final taskItemProvider = FutureProvider.autoDispose.family((ref, query) async { - final taskList = - await ref.watch(taskListItemProvider(query.taskListId).future); + final taskList = await ref.watch(taskListProvider(query.taskListId).future); final task = await taskList.task(query.taskId); return await ref .watch(notifierTaskProvider(task).future); // ensure we stay updated diff --git a/app/lib/features/tasks/providers/tasklists_providers.dart b/app/lib/features/tasks/providers/tasklists_providers.dart index a0cbd6821938..09fab1eb06cb 100644 --- a/app/lib/features/tasks/providers/tasklists_providers.dart +++ b/app/lib/features/tasks/providers/tasklists_providers.dart @@ -4,7 +4,7 @@ import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:riverpod/riverpod.dart'; //Single Task List Item based on the task list id -final taskListItemProvider = +final taskListProvider = AsyncNotifierProvider.family( () => TaskListItemNotifier(), ); @@ -28,12 +28,6 @@ final taskListsProvider = } }); -final taskListProvider = - FutureProvider.family((ref, eventId) async { - final allTaskLists = await ref.watch(allTasksListsProvider.future); - return allTaskLists.where((e) => e.eventIdStr() == eventId).firstOrNull; -}); - //Search any tasks list typedef TasksListSearchParams = ({String? spaceId, String searchText}); @@ -49,8 +43,7 @@ final tasksListSearchProvider = FutureProvider.autoDispose List filteredTaskList = []; for (final taskListId in tasksList) { //Check search param in task list - final taskListItem = - await ref.watch(taskListItemProvider(taskListId).future); + final taskListItem = await ref.watch(taskListProvider(taskListId).future); if (taskListItem.name().toLowerCase().contains(search)) { filteredTaskList.add(taskListId); continue; diff --git a/app/lib/features/tasks/widgets/task_item.dart b/app/lib/features/tasks/widgets/task_item.dart index 388006c16290..5d1204a3ba57 100644 --- a/app/lib/features/tasks/widgets/task_item.dart +++ b/app/lib/features/tasks/widgets/task_item.dart @@ -97,7 +97,7 @@ class TaskItem extends ConsumerWidget { final textTheme = Theme.of(context).textTheme; final description = task.description()?.body(); final tasklistId = task.taskListIdStr(); - final tasklistLoader = ref.watch(taskListItemProvider(tasklistId)); + final tasklistLoader = ref.watch(taskListProvider(tasklistId)); return Padding( padding: const EdgeInsets.only(right: 12), child: Column( diff --git a/app/lib/features/tasks/widgets/task_list_item_card.dart b/app/lib/features/tasks/widgets/task_list_item_card.dart index 33a399901920..7ae954bc22a3 100644 --- a/app/lib/features/tasks/widgets/task_list_item_card.dart +++ b/app/lib/features/tasks/widgets/task_list_item_card.dart @@ -1,4 +1,3 @@ -import 'package:acter/common/providers/room_providers.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/acter_icon_picker/acter_icon_widget.dart'; import 'package:acter/common/widgets/acter_icon_picker/model/acter_icons.dart'; @@ -37,7 +36,7 @@ class TaskListItemCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final lang = L10n.of(context); - final tasklistLoader = ref.watch(taskListItemProvider(taskListId)); + final tasklistLoader = ref.watch(taskListProvider(taskListId)); return tasklistLoader.when( data: (taskList) => Card( key: Key('task-list-card-$taskListId'), diff --git a/app/test/features/tasks/error_pages_test.dart b/app/test/features/tasks/error_pages_test.dart index 8f9ee3fb54e2..b9aba75008ce 100644 --- a/app/test/features/tasks/error_pages_test.dart +++ b/app/test/features/tasks/error_pages_test.dart @@ -79,9 +79,10 @@ void main() { final mockedNotifier = FakeTaskListItemNotifier(); await tester.pumpProviderWidget( overrides: [ - taskListItemProvider.overrideWith(() => mockedNotifier), + taskListProvider.overrideWith(() => mockedNotifier), hasSpaceWithPermissionProvider.overrideWith((_, ref) => false), roomMembershipProvider.overrideWith((a, b) => null), + roomDisplayNameProvider.overrideWith((a, b) async => 'Space'), ], child: const TaskListDetailPage(taskListId: 'taskListId'), ); @@ -94,8 +95,9 @@ void main() { await tester.pumpProviderWidget( overrides: [ notifierTaskProvider.overrideWith(() => MockTaskItemNotifier()), - taskListItemProvider.overrideWith(() => mockedNotifier), + taskListProvider.overrideWith(() => mockedNotifier), hasSpaceWithPermissionProvider.overrideWith((_, ref) => false), + roomDisplayNameProvider.overrideWith((a, b) async => 'Space'), ], child: const TaskItemDetailPage( taskListId: 'taskListId', diff --git a/app/test/helpers/mock_tasks_providers.dart b/app/test/helpers/mock_tasks_providers.dart index 9a3eaf0e38fd..3df8d841a350 100644 --- a/app/test/helpers/mock_tasks_providers.dart +++ b/app/test/helpers/mock_tasks_providers.dart @@ -91,9 +91,15 @@ class FakeTaskList extends Fake implements TaskList { class MockTaskList extends FakeTaskList with Mock {} class MockTask extends Fake implements Task { + @override + bool isDone() => false; + @override String title() => 'Test'; + @override + String eventIdStr() => 'eventId'; + @override MsgContent? description() => null;