diff --git a/.changes/2612-autosubscribe.md b/.changes/2612-autosubscribe.md new file mode 100644 index 000000000000..cbcf36707204 --- /dev/null +++ b/.changes/2612-autosubscribe.md @@ -0,0 +1 @@ +- Automatically subscribe to all objects you created or interact with to receive push notifications on updates about it. You can disable that in the notification settings if you want to. \ No newline at end of file diff --git a/app/lib/common/widgets/edit_html_description_sheet.dart b/app/lib/common/widgets/edit_html_description_sheet.dart index 052daadccdfe..05b83b59717d 100644 --- a/app/lib/common/widgets/edit_html_description_sheet.dart +++ b/app/lib/common/widgets/edit_html_description_sheet.dart @@ -11,7 +11,7 @@ void showEditHtmlDescriptionBottomSheet({ String? bottomSheetTitle, String? descriptionHtmlValue, String? descriptionMarkdownValue, - required Function(String, String) onSave, + required Function(WidgetRef, String, String) onSave, }) { showModalBottomSheet( showDragHandle: true, @@ -34,7 +34,7 @@ class EditHtmlDescriptionSheet extends ConsumerStatefulWidget { final String? bottomSheetTitle; final String? descriptionHtmlValue; final String? descriptionMarkdownValue; - final Function(String, String) onSave; + final Function(WidgetRef, String, String) onSave; const EditHtmlDescriptionSheet({ super.key, @@ -107,7 +107,7 @@ class _EditHtmlDescriptionSheetState return; } - widget.onSave(htmlBodyDescription, plainDescription); + widget.onSave(ref, htmlBodyDescription, plainDescription); }, child: Text(lang.save), ), diff --git a/app/lib/common/widgets/edit_title_sheet.dart b/app/lib/common/widgets/edit_title_sheet.dart index 393ba3307a88..409b64b6694d 100644 --- a/app/lib/common/widgets/edit_title_sheet.dart +++ b/app/lib/common/widgets/edit_title_sheet.dart @@ -7,7 +7,7 @@ void showEditTitleBottomSheet({ required BuildContext context, String? bottomSheetTitle, required String titleValue, - required Function(String) onSave, + required Function(WidgetRef, String) onSave, }) { showModalBottomSheet( showDragHandle: true, @@ -27,7 +27,7 @@ void showEditTitleBottomSheet({ class EditTitleSheet extends ConsumerStatefulWidget { final String? bottomSheetTitle; final String titleValue; - final Function(String) onSave; + final Function(WidgetRef, String) onSave; const EditTitleSheet({ super.key, @@ -91,7 +91,7 @@ class _EditTitleSheetState extends ConsumerState { } // Need to update change of tile - widget.onSave(_titleController.text.trim()); + widget.onSave(ref, _titleController.text.trim()); }, child: Text(lang.save), ), diff --git a/app/lib/features/attachments/actions/handle_selected_attachments.dart b/app/lib/features/attachments/actions/handle_selected_attachments.dart index dbb92bc42b16..09fa6e7135f8 100644 --- a/app/lib/features/attachments/actions/handle_selected_attachments.dart +++ b/app/lib/features/attachments/actions/handle_selected_attachments.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:acter/common/models/types.dart'; import 'package:acter/features/home/providers/client_providers.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' show AttachmentDraft, AttachmentsManager, RefDetails; import 'package:flutter/material.dart'; @@ -112,6 +113,12 @@ Future addRefDetailAttachment({ final res = await draft.send(); _log.info('attachment sent: $res'); } + + await autosubscribe( + ref: ref, + objectId: manager.objectIdStr(), + lang: lang, + ); EasyLoading.dismiss(); } catch (e, s) { _log.severe('Failed to create attachments', e, s); diff --git a/app/lib/features/chat/pages/room_profile_page.dart b/app/lib/features/chat/pages/room_profile_page.dart index 4be2c3980e03..9c727ac0653c 100644 --- a/app/lib/features/chat/pages/room_profile_page.dart +++ b/app/lib/features/chat/pages/room_profile_page.dart @@ -211,7 +211,7 @@ class _RoomProfilePageState extends ConsumerState { context: context, bottomSheetTitle: L10n.of(context).editName, titleValue: roomAvatarInfo.displayName ?? '', - onSave: (newName) => _saveName(newName), + onSave: (ref, newName) => _saveName(newName), ); } diff --git a/app/lib/features/comments/actions/submit_comment.dart b/app/lib/features/comments/actions/submit_comment.dart index 8a70ed5e3660..17571f13e023 100644 --- a/app/lib/features/comments/actions/submit_comment.dart +++ b/app/lib/features/comments/actions/submit_comment.dart @@ -6,7 +6,7 @@ import 'package:logging/logging.dart'; final _log = Logger('a3::submit::comment'); -Future submitComment( +Future submitComment( L10n lang, String plainDescription, String htmlBodyDescription, @@ -15,22 +15,22 @@ Future submitComment( final trimmedPlainText = plainDescription.trim(); if (trimmedPlainText.isEmpty) { EasyLoading.showToast(lang.youNeedToEnterAComment); - return false; + return null; } EasyLoading.show(status: lang.submittingComment); try { final draft = manager.commentDraft(); draft.contentFormatted(trimmedPlainText, htmlBodyDescription); - await draft.send(); + final id = await draft.send(); FocusManager.instance.primaryFocus?.unfocus(); EasyLoading.showToast(lang.commentSubmitted); - return true; + return id.toString(); } catch (e, s) { _log.severe('Failed to submit comment', e, s); EasyLoading.showError( lang.errorSubmittingComment(e), duration: const Duration(seconds: 3), ); - return false; + return null; } } diff --git a/app/lib/features/comments/types.dart b/app/lib/features/comments/types.dart index e25fd767f68e..f4407e41b888 100644 --- a/app/lib/features/comments/types.dart +++ b/app/lib/features/comments/types.dart @@ -1,8 +1,6 @@ import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' show ActerPin, CalendarEvent, CommentsManager, NewsEntry, Task, TaskList; -typedef PostCreateComment = Function(); - /// This is the actual input type for the providers and widget of this feature /// the way to get this is through implementing a "wrapper" type for the getter /// and make sure that the manager hash-id and equal is bound to the inner diff --git a/app/lib/features/comments/widgets/add_comment_widget.dart b/app/lib/features/comments/widgets/add_comment_widget.dart index fb49c9a42a08..56ebbbb0fd92 100644 --- a/app/lib/features/comments/widgets/add_comment_widget.dart +++ b/app/lib/features/comments/widgets/add_comment_widget.dart @@ -1,7 +1,8 @@ import 'package:acter/common/providers/common_providers.dart'; import 'package:acter/common/widgets/edit_html_description_sheet.dart'; import 'package:acter/features/comments/actions/submit_comment.dart'; -import 'package:acter/features/comments/types.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; +import 'package:acter/features/notifications/types.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'; @@ -14,12 +15,12 @@ class AddCommentWidget extends ConsumerStatefulWidget { static const addCommentButton = Key('add-comment-button'); final CommentsManager manager; - final PostCreateComment? postCreateComment; + final SubscriptionSubType? autoSubscribeSection; const AddCommentWidget({ super.key, required this.manager, - this.postCreateComment, + this.autoSubscribeSection, }); @override @@ -61,8 +62,9 @@ class _AddCommentWidgetState extends ConsumerState { context: context, bottomSheetTitle: L10n.of(context).addComment, descriptionHtmlValue: _commentController.text, - onSave: (htmlBodyDescription, plainDescription) async { + onSave: (ref, htmlBodyDescription, plainDescription) async { final success = await addComment( + ref: ref, plainDescription: plainDescription, htmlBodyDescription: htmlBodyDescription, ); @@ -91,8 +93,10 @@ class _AddCommentWidgetState extends ConsumerState { ), child: IconButton( key: AddCommentWidget.addCommentButton, - onPressed: () => - addComment(plainDescription: _commentController.text), + onPressed: () => addComment( + ref: ref, + plainDescription: _commentController.text, + ), icon: Icon(PhosphorIcons.paperPlaneTilt()), ), ) @@ -102,23 +106,27 @@ class _AddCommentWidgetState extends ConsumerState { } Future addComment({ + required WidgetRef ref, required String plainDescription, String? htmlBodyDescription, }) async { - final success = await submitComment( - L10n.of(context), + final lang = L10n.of(context); + final objectId = await submitComment( + lang, plainDescription, htmlBodyDescription ?? plainDescription, widget.manager, ); - if (success) { + if (objectId != null) { _commentController.clear(); showSendButton.value = false; - final postCommentFn = widget.postCreateComment; - if (postCommentFn != null) { - postCommentFn(); - } + await autosubscribe( + ref: ref, + objectId: widget.manager.objectIdStr(), + lang: lang, + subType: widget.autoSubscribeSection, + ); } - return success; + return objectId != null; } } diff --git a/app/lib/features/comments/widgets/comments_section_widget.dart b/app/lib/features/comments/widgets/comments_section_widget.dart index 3223516b77a3..0dbb7a2a019d 100644 --- a/app/lib/features/comments/widgets/comments_section_widget.dart +++ b/app/lib/features/comments/widgets/comments_section_widget.dart @@ -5,6 +5,7 @@ import 'package:acter/features/comments/types.dart'; import 'package:acter/features/comments/widgets/add_comment_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/features/notifications/types.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -19,7 +20,7 @@ class CommentsSectionWidget extends ConsumerWidget { final bool centerTitle; final bool useCompactEmptyState; final List? actions; - final PostCreateComment? postCreateComment; + final SubscriptionSubType? autoSubscribeSection; const CommentsSectionWidget({ super.key, @@ -27,7 +28,7 @@ class CommentsSectionWidget extends ConsumerWidget { this.centerTitle = false, this.useCompactEmptyState = true, required this.managerProvider, - this.postCreateComment, + this.autoSubscribeSection, // null means all this.actions, }); @@ -71,7 +72,7 @@ class CommentsSectionWidget extends ConsumerWidget { commentListUI(commentManager), AddCommentWidget( manager: commentManager, - postCreateComment: postCreateComment, + autoSubscribeSection: autoSubscribeSection, ), ], ); diff --git a/app/lib/features/events/pages/create_event_page.dart b/app/lib/features/events/pages/create_event_page.dart index 9eba33511f9b..6160f16e316e 100644 --- a/app/lib/features/events/pages/create_event_page.dart +++ b/app/lib/features/events/pages/create_event_page.dart @@ -9,6 +9,7 @@ import 'package:acter/common/widgets/spaces/select_space_form_field.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'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -462,6 +463,7 @@ class CreateEventPageConsumerState extends ConsumerState { final eventId = (await draft.send()).toString(); final client = await ref.read(alwaysClientProvider.future); final calendarEvent = await client.waitForCalendarEvent(eventId, null); + await autosubscribe(ref: ref, objectId: eventId, lang: lang); /// Event is created, set RSVP status to `Yes` by default for host. final rsvpManager = await calendarEvent.rsvps(); diff --git a/app/lib/features/events/pages/event_details_page.dart b/app/lib/features/events/pages/event_details_page.dart index f8edc13ad356..4c27c6f17eb2 100644 --- a/app/lib/features/events/pages/event_details_page.dart +++ b/app/lib/features/events/pages/event_details_page.dart @@ -25,6 +25,8 @@ import 'package:acter/features/events/widgets/participants_list.dart'; import 'package:acter/features/events/widgets/skeletons/event_details_skeleton_widget.dart'; import 'package:acter/features/home/providers/client_providers.dart'; import 'package:acter/features/home/widgets/space_chip.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; +import 'package:acter/features/notifications/widgets/object_notification_status.dart'; import 'package:acter/features/share/action/share_space_object_action.dart'; import 'package:acter/features/space/widgets/member_avatar.dart'; import 'package:acter_avatar/acter_avatar.dart'; @@ -103,6 +105,7 @@ class _EventDetailPageConsumerState extends ConsumerState { BookmarkAction( bookmarker: BookmarkType.forEvent(widget.calendarId), ), + ObjectNotificationStatus(objectId: widget.calendarId), _buildActionMenu(calendarEvent), ] : [], @@ -354,9 +357,10 @@ class _EventDetailPageConsumerState extends ConsumerState { showEditTitleBottomSheet( context: context, titleValue: calendarEvent.title(), - onSave: (newName) { + onSave: (ref, newName) { saveEventTitle( context: context, + ref: ref, calendarEvent: calendarEvent, newName: newName, ); @@ -380,6 +384,12 @@ class _EventDetailPageConsumerState extends ConsumerState { draft.status(statusStr); final rsvpId = await draft.send(); _log.info('new rsvp id: $rsvpId'); + + await autosubscribe( + ref: ref, + objectId: widget.calendarId, + lang: lang, + ); // refresh cache final client = await ref.read(alwaysClientProvider.future); await client.waitForRsvp(rsvpId.toString(), null); @@ -684,10 +694,11 @@ class _EventDetailPageConsumerState extends ConsumerState { context: context, descriptionHtmlValue: content?.formatted(), descriptionMarkdownValue: content?.body(), - onSave: (htmlBodyDescription, plainDescription) { + onSave: (ref, htmlBodyDescription, plainDescription) { saveEventDescription( context: context, calendarEvent: ev, + ref: ref, htmlBodyDescription: htmlBodyDescription, plainDescription: plainDescription, ); diff --git a/app/lib/features/events/utils/events_utils.dart b/app/lib/features/events/utils/events_utils.dart index fad1917400cb..947e42c50e4d 100644 --- a/app/lib/features/events/utils/events_utils.dart +++ b/app/lib/features/events/utils/events_utils.dart @@ -1,8 +1,10 @@ +import 'package:acter/features/notifications/actions/autosubscribe.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.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_gen/gen_l10n/l10n.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; @@ -10,6 +12,7 @@ final _log = Logger('a3::cal_event::utils'); Future saveEventTitle({ required BuildContext context, + required WidgetRef ref, required CalendarEvent calendarEvent, required String newName, }) async { @@ -20,6 +23,11 @@ Future saveEventTitle({ updateBuilder.title(newName); final eventId = await updateBuilder.send(); _log.info('Calendar Event Title Updated $eventId'); + await autosubscribe( + ref: ref, + objectId: calendarEvent.eventId().toString(), + lang: lang, + ); EasyLoading.dismiss(); if (context.mounted) Navigator.pop(context); @@ -38,6 +46,7 @@ Future saveEventTitle({ Future saveEventDescription({ required BuildContext context, + required WidgetRef ref, required CalendarEvent calendarEvent, required String htmlBodyDescription, required String plainDescription, @@ -48,6 +57,11 @@ Future saveEventDescription({ final updateBuilder = calendarEvent.updateBuilder(); updateBuilder.descriptionHtml(plainDescription, htmlBodyDescription); await updateBuilder.send(); + await autosubscribe( + ref: ref, + objectId: calendarEvent.eventId().toString(), + lang: lang, + ); EasyLoading.dismiss(); if (context.mounted) Navigator.pop(context); } catch (e, s) { diff --git a/app/lib/features/events/widgets/change_date_sheet.dart b/app/lib/features/events/widgets/change_date_sheet.dart index 0c0d936a7db3..66eb338e640d 100644 --- a/app/lib/features/events/widgets/change_date_sheet.dart +++ b/app/lib/features/events/widgets/change_date_sheet.dart @@ -2,6 +2,7 @@ import 'package:acter/common/toolkit/buttons/primary_action_button.dart'; import 'package:acter/common/utils/utils.dart'; import 'package:acter/features/events/providers/event_providers.dart'; import 'package:acter/features/events/utils/events_utils.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; import 'package:dart_date/dart_date.dart'; import 'package:flutter/material.dart'; @@ -328,6 +329,12 @@ class _ChangeDateSheetState extends ConsumerState { final eventId = await updateBuilder.send(); _log.info('Calendar Event updated $eventId'); + await autosubscribe( + ref: ref, + objectId: eventId.toString(), + lang: lang, + ); + EasyLoading.dismiss(); if (mounted) Navigator.pop(context); diff --git a/app/lib/features/labs/model/labs_features.dart b/app/lib/features/labs/model/labs_features.dart index c71253b8e3a5..c6b05e673579 100644 --- a/app/lib/features/labs/model/labs_features.dart +++ b/app/lib/features/labs/model/labs_features.dart @@ -14,7 +14,6 @@ enum LabsFeature { // system features encryptionBackup, - autoSubscribe, // in labs until we have this ready for all types // candidates for always on comments, @@ -25,6 +24,7 @@ enum LabsFeature { tasks, events, pins, + autoSubscribe, showNotifications; // old name for desktop notifications static List get defaults => 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 84b1df5ff3c6..8935e28f01b4 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 @@ -11,7 +11,6 @@ import 'package:acter/features/comments/types.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/features/notifications/actions/autosubscribe.dart'; import 'package:acter/features/notifications/types.dart'; import 'package:acter/features/notifications/widgets/object_notification_status.dart'; import 'package:acter/features/read_receipts/widgets/read_counter.dart'; @@ -95,15 +94,8 @@ class NewsSideBar extends ConsumerWidget { shrinkWrap: false, centerTitle: true, useCompactEmptyState: false, - postCreateComment: () async { - // when user create a comment, let's autosubscribe to comments - await autosubscribe( - ref: ref, - objectId: objectId, - subType: SubscriptionSubType.comments, - lang: L10n.of(context), - ); - }, + autoSubscribeSection: SubscriptionSubType + .comments, // we want to be using the comments only on boosts actions: [ ObjectNotificationStatus( objectId: objectId, @@ -115,9 +107,13 @@ class NewsSideBar extends ConsumerWidget { }, icon: Column( children: [ - ShadowEffectWidget(child: Icon(Atlas.comment_blank),), + ShadowEffectWidget( + child: Icon(Atlas.comment_blank), + ), const SizedBox(height: 4), - ShadowEffectWidget(child:Text(commentCount.toString(), style: style),), + ShadowEffectWidget( + child: Text(commentCount.toString(), style: style), + ), ], ), ), @@ -313,4 +309,4 @@ class ActionBox extends ConsumerWidget { ), ); } -} \ No newline at end of file +} diff --git a/app/lib/features/notifications/actions/autosubscribe.dart b/app/lib/features/notifications/actions/autosubscribe.dart index c669301c91eb..521dfdef9d60 100644 --- a/app/lib/features/notifications/actions/autosubscribe.dart +++ b/app/lib/features/notifications/actions/autosubscribe.dart @@ -1,5 +1,3 @@ -import 'package:acter/features/labs/model/labs_features.dart'; -import 'package:acter/features/labs/providers/labs_providers.dart'; import 'package:acter/features/notifications/actions/subscribe_object_push.dart'; import 'package:acter/features/notifications/providers/notification_settings_providers.dart'; import 'package:acter/features/notifications/providers/object_notifications_settings_provider.dart'; @@ -21,11 +19,6 @@ Future autosubscribe({ required L10n lang, SubscriptionSubType? subType, }) async { - if (!ref.read(isActiveProvider(LabsFeature.autoSubscribe))) { - _log.info('AutoSubscribe Labs not activated'); - return false; - } - if (!await ref.read(autoSubscribeProvider.future)) { _log.info('AutoSubscribe deactivated'); return false; diff --git a/app/lib/features/notifications/pages/notifications_page.dart b/app/lib/features/notifications/pages/notifications_page.dart index f21f37c10850..370195422ff0 100644 --- a/app/lib/features/notifications/pages/notifications_page.dart +++ b/app/lib/features/notifications/pages/notifications_page.dart @@ -5,8 +5,6 @@ import 'package:acter/common/toolkit/buttons/primary_action_button.dart'; import 'package:acter/common/widgets/with_sidebar.dart'; import 'package:acter/config/notifications/init.dart'; import 'package:acter/features/home/providers/client_providers.dart'; -import 'package:acter/features/labs/model/labs_features.dart'; -import 'package:acter/features/labs/providers/labs_providers.dart'; import 'package:acter/features/notifications/actions/update_autosubscribe.dart'; import 'package:acter/features/notifications/providers/notification_settings_providers.dart'; import 'package:acter/features/room/widgets/notifications_settings_tile.dart'; @@ -87,8 +85,6 @@ class NotificationsSettingsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final lang = L10n.of(context); - final subscribeIsOn = - ref.watch(isActiveProvider(LabsFeature.autoSubscribe)); return WithSidebar( sidebar: const SettingsPage(), child: Scaffold( @@ -108,31 +104,25 @@ class NotificationsSettingsPage extends ConsumerWidget { description: lang.notifyAboutSpaceUpdates, appKey: 'global.acter.dev.news', ), + SettingsTile.switchTile( + title: Text(lang.autoSubscribeSettingsTitle), + description: Text(lang.autoSubscribeFeatureDesc), + initialValue: ref + .watch( + autoSubscribeProvider, + ) + .value == + true, + onToggle: (newVal) async { + await updateAutoSubscribe( + ref, + lang, + newVal, + ); + }, + ), ], ), - if (subscribeIsOn) - SettingsSection( - title: Text(lang.autoSubscribeSettingsTitle), - tiles: [ - SettingsTile.switchTile( - title: Text(lang.boosts), - // description: Text(lang.autoSubscribeFeatureDesc), - initialValue: ref - .watch( - autoSubscribeProvider, - ) - .value == - true, - onToggle: (newVal) async { - await updateAutoSubscribe( - ref, - lang, - newVal, - ); - }, - ), - ], - ), SettingsSection( title: Text(lang.defaultModes), tiles: [ diff --git a/app/lib/features/notifications/providers/notifiers/notification_settings_notifier.dart b/app/lib/features/notifications/providers/notifiers/notification_settings_notifier.dart index 1fe45373c6d8..fdd79e9e7a28 100644 --- a/app/lib/features/notifications/providers/notifiers/notification_settings_notifier.dart +++ b/app/lib/features/notifications/providers/notifiers/notification_settings_notifier.dart @@ -10,21 +10,26 @@ final _log = Logger('a3::common::notification_settings_notifier'); class AsyncNotificationSettingsNotifier extends AsyncNotifier { - late Stream _listener; - late StreamSubscription _poller; + // ignore: unused_field + Stream? _listener; + StreamSubscription? _poller; @override Future build() async { + ref.onDispose(() => _poller?.cancel()); + return await reset(); + } + + Future reset() async { + _poller?.cancel(); final client = await ref.watch(alwaysClientProvider.future); final settings = await client.notificationSettings(); - _listener = settings.changesStream(); - _poller = _listener.listen( + final listener = _listener = settings.changesStream(); + _poller = listener.listen( (data) async { // reset the state of this to trigger the notification // cascade - state = await AsyncValue.guard( - () async => await client.notificationSettings(), - ); + state = AsyncValue.data(await reset()); }, onError: (e, s) { _log.severe('stream errored', e, s); @@ -33,7 +38,6 @@ class AsyncNotificationSettingsNotifier _log.info('stream ended'); }, ); - ref.onDispose(() => _poller.cancel()); return settings; } } diff --git a/app/lib/features/pins/actions/edit_pin_actions.dart b/app/lib/features/pins/actions/edit_pin_actions.dart index 10f1b760afdc..6067745b99b7 100644 --- a/app/lib/features/pins/actions/edit_pin_actions.dart +++ b/app/lib/features/pins/actions/edit_pin_actions.dart @@ -1,5 +1,6 @@ import 'package:acter/common/widgets/edit_html_description_sheet.dart'; import 'package:acter/common/widgets/edit_title_sheet.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; import 'package:acter/features/pins/actions/pin_update_actions.dart'; import 'package:acter/features/pins/providers/pins_provider.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; @@ -12,14 +13,16 @@ void showEditPintTitleDialog( WidgetRef ref, ActerPin pin, ) { + final lang = L10n.of(context); showEditTitleBottomSheet( context: context, bottomSheetTitle: L10n.of(context).editName, titleValue: pin.title(), - onSave: (newTitle) async { + onSave: (ref, newTitle) async { final pinEditNotifier = ref.read(pinEditProvider(pin).notifier); pinEditNotifier.setTitle(newTitle); - updatePinTitle(context, pin, newTitle); + updatePinTitle(context, ref, pin, newTitle); + await autosubscribe(ref: ref, objectId: pin.eventIdStr(), lang: lang); }, ); } @@ -29,17 +32,21 @@ void showEditPintDescriptionDialog( WidgetRef ref, ActerPin pin, ) { + final lang = L10n.of(context); showEditHtmlDescriptionBottomSheet( context: context, descriptionHtmlValue: pin.content()?.formattedBody(), descriptionMarkdownValue: pin.content()?.body(), - onSave: (htmlBodyDescription, plainDescription) async { + onSave: (ref, htmlBodyDescription, plainDescription) async { updatePinDescription( context, + ref, htmlBodyDescription, plainDescription, pin, ); + + await autosubscribe(ref: ref, objectId: pin.eventIdStr(), lang: lang); }, ); } diff --git a/app/lib/features/pins/actions/pin_update_actions.dart b/app/lib/features/pins/actions/pin_update_actions.dart index 2a6f7b9655e1..cb33b902a6b6 100644 --- a/app/lib/features/pins/actions/pin_update_actions.dart +++ b/app/lib/features/pins/actions/pin_update_actions.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:acter/common/providers/sdk_provider.dart'; import 'package:acter/common/utils/utils.dart'; import 'package:acter/common/widgets/acter_icon_picker/model/acter_icons.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; import 'package:acter/features/pins/providers/pins_provider.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; @@ -15,6 +16,7 @@ final _log = Logger('a3::pins::update_actions'); Future updatePinTitle( BuildContext context, + WidgetRef ref, ActerPin pin, String newTitle, ) async { @@ -24,6 +26,12 @@ Future updatePinTitle( final updateBuilder = pin.updateBuilder(); updateBuilder.title(newTitle); await updateBuilder.send(); + + await autosubscribe( + ref: ref, + objectId: pin.eventIdStr(), + lang: lang, + ); EasyLoading.dismiss(); if (!context.mounted) return; Navigator.pop(context); @@ -67,6 +75,7 @@ Future updatePinLink( Future updatePinDescription( BuildContext context, + WidgetRef ref, String htmlBodyDescription, String plainDescription, ActerPin pin, @@ -78,6 +87,12 @@ Future updatePinDescription( updateBuilder.contentText(plainDescription); updateBuilder.contentHtml(plainDescription, htmlBodyDescription); await updateBuilder.send(); + + await autosubscribe( + ref: ref, + objectId: pin.eventIdStr(), + lang: lang, + ); EasyLoading.dismiss(); if (!context.mounted) return; Navigator.pop(context); @@ -130,4 +145,4 @@ Future updatePinIcon( duration: const Duration(seconds: 3), ); } -} \ No newline at end of file +} diff --git a/app/lib/features/pins/actions/set_pin_description.dart b/app/lib/features/pins/actions/set_pin_description.dart index 21041b583a38..a50c69e3e449 100644 --- a/app/lib/features/pins/actions/set_pin_description.dart +++ b/app/lib/features/pins/actions/set_pin_description.dart @@ -1,12 +1,10 @@ import 'package:acter/common/widgets/edit_html_description_sheet.dart'; import 'package:acter/features/pins/providers/pins_provider.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; void showEditPinDescriptionBottomSheet({ required BuildContext context, - required WidgetRef ref, bool isBottomSheetOpen = false, String? htmlBodyDescription, String? plainDescription, @@ -16,7 +14,7 @@ void showEditPinDescriptionBottomSheet({ context: context, descriptionHtmlValue: htmlBodyDescription, descriptionMarkdownValue: plainDescription, - onSave: (htmlBodyDescription, plainDescription) { + onSave: (ref, htmlBodyDescription, plainDescription) { if (isBottomSheetOpen) Navigator.pop(context); Navigator.pop(context); ref.read(createPinStateProvider.notifier).setDescriptionValue( diff --git a/app/lib/features/pins/pages/create_pin_page.dart b/app/lib/features/pins/pages/create_pin_page.dart index 15c4221615a8..f9e1a9c7108d 100644 --- a/app/lib/features/pins/pages/create_pin_page.dart +++ b/app/lib/features/pins/pages/create_pin_page.dart @@ -16,6 +16,7 @@ import 'package:acter/common/widgets/render_html.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/attachments/actions/handle_selected_attachments.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; import 'package:acter/features/pins/actions/attachment_leading_icon.dart'; import 'package:acter/features/pins/actions/set_pin_description.dart'; import 'package:acter/features/pins/actions/set_pin_links.dart'; @@ -260,7 +261,7 @@ class _CreatePinConsumerState extends ConsumerState { showEditTitleBottomSheet( context: context, titleValue: attachmentData.title, - onSave: (newTitle) { + onSave: (ref, newTitle) { Navigator.pop(context); final pinAttachment = attachmentData.copyWith(title: newTitle); final notifier = ref.read(createPinStateProvider.notifier); @@ -288,7 +289,6 @@ class _CreatePinConsumerState extends ConsumerState { onTap: () { showEditPinDescriptionBottomSheet( context: context, - ref: ref, htmlBodyDescription: params.htmlBodyDescription, plainDescription: params.plainDescription, ); @@ -364,6 +364,7 @@ class _CreatePinConsumerState extends ConsumerState { // Add Attachments await addAttachment(pinId, pinState); + await autosubscribe(ref: ref, objectId: pinId.toString(), lang: lang); EasyLoading.dismiss(); if (!mounted) return; diff --git a/app/lib/features/pins/pages/pin_details_page.dart b/app/lib/features/pins/pages/pin_details_page.dart index a46d6007f35a..f4573ba3d5b0 100644 --- a/app/lib/features/pins/pages/pin_details_page.dart +++ b/app/lib/features/pins/pages/pin_details_page.dart @@ -15,6 +15,7 @@ import 'package:acter/features/comments/types.dart'; import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/comments/widgets/skeletons/comment_list_skeleton_widget.dart'; import 'package:acter/features/home/widgets/space_chip.dart'; +import 'package:acter/features/notifications/widgets/object_notification_status.dart'; import 'package:acter/features/pins/actions/edit_pin_actions.dart'; import 'package:acter/features/pins/actions/pin_update_actions.dart'; import 'package:acter/features/pins/actions/reduct_pin_action.dart'; @@ -85,6 +86,7 @@ class _PinDetailsPageState extends ConsumerState { }, ), BookmarkAction(bookmarker: BookmarkType.forPins(widget.pinId)), + ObjectNotificationStatus(objectId: widget.pinId), _buildActionMenu(), ], ); @@ -312,10 +314,10 @@ class _PinDetailsPageState extends ConsumerState { context: context, bottomSheetTitle: L10n.of(context).editName, titleValue: pin.title(), - onSave: (newTitle) async { + onSave: (ref, newTitle) async { final notifier = ref.read(pinEditProvider(pin).notifier); notifier.setTitle(newTitle); - updatePinTitle(context, pin, newTitle); + updatePinTitle(context, ref, pin, newTitle); }, ); } @@ -356,9 +358,10 @@ class _PinDetailsPageState extends ConsumerState { context: context, descriptionHtmlValue: description.formattedBody(), descriptionMarkdownValue: plainBody, - onSave: (htmlBodyDescription, plainDescription) async { + onSave: (ref, htmlBodyDescription, plainDescription) async { await updatePinDescription( context, + ref, htmlBodyDescription, plainDescription, pin, diff --git a/app/lib/features/pins/widgets/pin_attachment_options.dart b/app/lib/features/pins/widgets/pin_attachment_options.dart index 6c959773cb02..749597f33249 100644 --- a/app/lib/features/pins/widgets/pin_attachment_options.dart +++ b/app/lib/features/pins/widgets/pin_attachment_options.dart @@ -38,7 +38,6 @@ class PinAttachmentOptions extends ConsumerWidget { onTap: () { showEditPinDescriptionBottomSheet( context: context, - ref: ref, isBottomSheetOpen: isBottomSheetOpen, htmlBodyDescription: pinState.pinDescriptionParams?.htmlBodyDescription, diff --git a/app/lib/features/settings/pages/labs_page.dart b/app/lib/features/settings/pages/labs_page.dart index e293bfca342b..687dc4730f82 100644 --- a/app/lib/features/settings/pages/labs_page.dart +++ b/app/lib/features/settings/pages/labs_page.dart @@ -108,25 +108,6 @@ class SettingsLabsPage extends ConsumerWidget { ), ], ), - SettingsSection( - title: Text(lang.notifications), - tiles: [ - SettingsTile.switchTile( - title: Text(lang.autoSubscribeLabsFeature), - description: Text(lang.autoSubscribeFeatureDesc), - initialValue: ref.watch( - isActiveProvider(LabsFeature.autoSubscribe), - ), - onToggle: (newVal) async { - await updateFeatureState( - ref, - LabsFeature.autoSubscribe, - newVal, - ); - }, - ), - ], - ), SettingsSection( title: Text(lang.apps), tiles: [ diff --git a/app/lib/features/settings/providers/notifiers/app_settings_notifier.dart b/app/lib/features/settings/providers/notifiers/app_settings_notifier.dart index cbfa6c4b0d44..f697298801b7 100644 --- a/app/lib/features/settings/providers/notifiers/app_settings_notifier.dart +++ b/app/lib/features/settings/providers/notifiers/app_settings_notifier.dart @@ -24,7 +24,7 @@ class UserAppSettingsNotifier _poller = _listener.listen( (data) async { // refresh on update - state = await AsyncValue.guard(() async => await _getSettings(account)); + state = AsyncValue.data(await _getSettings(account)); }, onError: (e, s) { _log.severe('stream errored', e, s); diff --git a/app/lib/features/space/actions/set_space_title.dart b/app/lib/features/space/actions/set_space_title.dart index 494bac6ae82a..a612ff475f11 100644 --- a/app/lib/features/space/actions/set_space_title.dart +++ b/app/lib/features/space/actions/set_space_title.dart @@ -22,7 +22,7 @@ void showEditSpaceNameBottomSheet({ context: context, bottomSheetTitle: lang.editName, titleValue: spaceAvatarInfo.displayName ?? '', - onSave: (newName) async { + onSave: (ref, newName) async { try { EasyLoading.show(status: lang.updateName); final space = await ref.read(spaceProvider(spaceId).future); diff --git a/app/lib/features/tasks/actions/add_task.dart b/app/lib/features/tasks/actions/add_task.dart index e2d6a8790405..f11baa62ac8c 100644 --- a/app/lib/features/tasks/actions/add_task.dart +++ b/app/lib/features/tasks/actions/add_task.dart @@ -1,3 +1,4 @@ +import 'package:acter/features/notifications/actions/autosubscribe.dart'; import 'package:acter/features/tasks/actions/select_tasklist.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; @@ -38,8 +39,20 @@ Future<(String, String)?> addTask({ } try { final eventId = await taskDraft.send(); + final tlId = taskList.eventIdStr(); + + await autosubscribe( + ref: ref, + objectId: eventId.toString(), + lang: lang, + ); + await autosubscribe( + ref: ref, + objectId: tlId.toString(), + lang: lang, + ); EasyLoading.dismiss(); - return (taskList.eventIdStr(), eventId.toString()); + return (tlId, eventId.toString()); } catch (e, s) { _log.severe('Failed to create task', e, s); if (!context.mounted) { diff --git a/app/lib/features/tasks/actions/update_tasklist.dart b/app/lib/features/tasks/actions/update_tasklist.dart index b977d54ef642..57f024e450fc 100644 --- a/app/lib/features/tasks/actions/update_tasklist.dart +++ b/app/lib/features/tasks/actions/update_tasklist.dart @@ -1,6 +1,7 @@ import 'package:acter/common/providers/sdk_provider.dart'; import 'package:acter/common/utils/utils.dart'; import 'package:acter/common/widgets/acter_icon_picker/model/acter_icons.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; import 'package:acter/features/tasks/providers/tasklists_providers.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/cupertino.dart'; @@ -32,6 +33,12 @@ Future updateTaskListIcon( try { await updateBuilder.send(); + + await autosubscribe( + ref: ref, + objectId: taskList.eventIdStr(), + lang: lang, + ); EasyLoading.dismiss(); ref.invalidate(taskListsProvider); if (!context.mounted) return; @@ -50,6 +57,7 @@ Future updateTaskListIcon( Future updateTaskListTitle( BuildContext context, + WidgetRef ref, TaskList taskList, String newName, ) async { @@ -59,6 +67,11 @@ Future updateTaskListTitle( updater.name(newName); try { await updater.send(); + await autosubscribe( + ref: ref, + objectId: taskList.eventIdStr(), + lang: lang, + ); EasyLoading.dismiss(); if (!context.mounted) return; Navigator.pop(context); 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 df1606603c49..b07595ad8cf8 100644 --- a/app/lib/features/tasks/pages/task_item_detail_page.dart +++ b/app/lib/features/tasks/pages/task_item_detail_page.dart @@ -19,6 +19,8 @@ import 'package:acter/features/attachments/types.dart'; import 'package:acter/features/comments/types.dart'; import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/home/widgets/space_chip.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; +import 'package:acter/features/notifications/widgets/object_notification_status.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'; @@ -61,12 +63,15 @@ class TaskItemDetailPage extends ConsumerWidget { final task = ref .watch(taskItemProvider((taskListId: taskListId, taskId: taskId))) .valueOrNull; + if (task == null) { + return AppBar(); + } final lang = L10n.of(context); final textTheme = Theme.of(context).textTheme; - final actions = List.empty(growable: true); - if (task != null) { - actions.addAll([ + return AppBar( + actions: [ + ObjectNotificationStatus(objectId: taskId), PopupMenuButton( icon: const Icon(Icons.more_vert), itemBuilder: (context) { @@ -77,7 +82,8 @@ class TaskItemDetailPage extends ConsumerWidget { context: context, bottomSheetTitle: lang.editName, titleValue: task.title(), - onSave: (newName) => saveTitle(context, ref, task, newName), + onSave: (ref, newName) => + saveTitle(context, ref, task, newName), ); }, child: Text( @@ -86,7 +92,7 @@ class TaskItemDetailPage extends ConsumerWidget { ), ), PopupMenuItem( - onTap: () => showEditDescriptionSheet(context, ref, task), + onTap: () => showEditDescriptionSheet(context, task), child: Text( lang.editDescription, style: textTheme.bodyMedium, @@ -116,10 +122,8 @@ class TaskItemDetailPage extends ConsumerWidget { ]; }, ), - ]); - } - - return AppBar(actions: actions); + ], + ); } // Redact Task Item Dialog @@ -190,9 +194,9 @@ class TaskItemDetailPage extends ConsumerWidget { const SizedBox(height: 10), _taskHeader(context, task, ref), const SizedBox(height: 10), - _widgetTaskDate(context, task), + _widgetTaskDate(context, ref, task), _widgetTaskAssignment(context, task, ref), - ..._widgetDescription(context, task, ref), + ..._widgetDescription(context, task), const SizedBox(height: 40), ] else const TaskItemDetailPageSkeleton(), @@ -223,7 +227,6 @@ class TaskItemDetailPage extends ConsumerWidget { InkWell( onTap: () => showEditTaskItemNameBottomSheet( context: context, - ref: ref, task: task, titleValue: task.title(), ), @@ -273,7 +276,6 @@ class TaskItemDetailPage extends ConsumerWidget { List _widgetDescription( BuildContext context, Task task, - WidgetRef ref, ) { final description = task.description(); if (description == null) return []; @@ -285,7 +287,7 @@ class TaskItemDetailPage extends ConsumerWidget { SelectionArea( child: GestureDetector( onTap: () { - showEditDescriptionSheet(context, ref, task); + showEditDescriptionSheet(context, task); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 30), @@ -306,14 +308,13 @@ class TaskItemDetailPage extends ConsumerWidget { void showEditDescriptionSheet( BuildContext context, - WidgetRef ref, Task task, ) { showEditHtmlDescriptionBottomSheet( context: context, descriptionHtmlValue: task.description()?.formattedBody(), descriptionMarkdownValue: task.description()?.body(), - onSave: (htmlBodyDescription, plainDescription) { + onSave: (ref, htmlBodyDescription, plainDescription) { _saveDescription( context, ref, @@ -338,6 +339,11 @@ class TaskItemDetailPage extends ConsumerWidget { final updater = task.updateBuilder(); updater.descriptionHtml(plainDescription, htmlBodyDescription); await updater.send(); + await autosubscribe( + ref: ref, + objectId: task.eventIdStr(), + lang: lang, + ); EasyLoading.dismiss(); if (context.mounted) Navigator.pop(context); } catch (e, s) { @@ -353,7 +359,7 @@ class TaskItemDetailPage extends ConsumerWidget { } } - Widget _widgetTaskDate(BuildContext context, Task task) { + Widget _widgetTaskDate(BuildContext context, WidgetRef ref, Task task) { final lang = L10n.of(context); final textTheme = Theme.of(context).textTheme; final dateText = @@ -376,11 +382,15 @@ class TaskItemDetailPage extends ConsumerWidget { style: textTheme.bodyMedium, ), ), - onTap: () => duePickerAction(context, task), + onTap: () => duePickerAction(context, ref, task), ); } - Future duePickerAction(BuildContext context, Task task) async { + Future duePickerAction( + BuildContext context, + WidgetRef ref, + Task task, + ) async { final lang = L10n.of(context); final newDue = await showDuePicker( context: context, @@ -404,6 +414,12 @@ class TaskItemDetailPage extends ConsumerWidget { updater.unsetUtcDueTimeOfDay(); } await updater.send(); + + await autosubscribe( + ref: ref, + objectId: task.eventIdStr(), + lang: lang, + ); if (!context.mounted) { EasyLoading.dismiss(); return; @@ -440,8 +456,8 @@ class TaskItemDetailPage extends ConsumerWidget { const Spacer(), ActerInlineTextButton( onPressed: () => task.isAssignedToMe() - ? onUnAssign(context, task) - : onAssign(context, task), + ? onUnAssign(context, ref, task) + : onAssign(context, ref, task), child: Text( task.isAssignedToMe() ? lang.removeMyself : lang.assignMyself, ), @@ -481,11 +497,17 @@ class TaskItemDetailPage extends ConsumerWidget { ); } - Future onAssign(BuildContext context, Task task) async { + Future onAssign(BuildContext context, WidgetRef ref, Task task) async { final lang = L10n.of(context); EasyLoading.show(status: lang.assigningSelf); try { await task.assignSelf(); + + await autosubscribe( + ref: ref, + objectId: task.eventIdStr(), + lang: lang, + ); if (!context.mounted) return; EasyLoading.showToast(lang.assignedYourself); } catch (e, s) { @@ -501,11 +523,21 @@ class TaskItemDetailPage extends ConsumerWidget { } } - Future onUnAssign(BuildContext context, Task task) async { + Future onUnAssign( + BuildContext context, + WidgetRef ref, + Task task, + ) async { final lang = L10n.of(context); EasyLoading.show(status: lang.unassigningSelf); try { await task.unassignSelf(); + + await autosubscribe( + ref: ref, + objectId: task.eventIdStr(), + lang: lang, + ); if (!context.mounted) return; EasyLoading.showToast(lang.assignmentWithdrawn); } catch (e, s) { @@ -525,13 +557,12 @@ class TaskItemDetailPage extends ConsumerWidget { required BuildContext context, required String titleValue, required Task task, - required WidgetRef ref, }) { showEditTitleBottomSheet( context: context, bottomSheetTitle: L10n.of(context).editName, titleValue: titleValue, - onSave: (newName) => saveTitle(context, ref, task, newName), + onSave: (ref, newName) => saveTitle(context, ref, task, newName), ); } @@ -547,6 +578,12 @@ class TaskItemDetailPage extends ConsumerWidget { updater.title(newName); try { await updater.send(); + + await autosubscribe( + ref: ref, + objectId: task.eventIdStr(), + lang: lang, + ); EasyLoading.dismiss(); if (!context.mounted) return; Navigator.pop(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 f7ec80197214..fb261312eac3 100644 --- a/app/lib/features/tasks/pages/task_list_details_page.dart +++ b/app/lib/features/tasks/pages/task_list_details_page.dart @@ -15,6 +15,8 @@ import 'package:acter/features/bookmarks/widgets/bookmark_action.dart'; import 'package:acter/features/comments/types.dart'; import 'package:acter/features/comments/widgets/comments_section_widget.dart'; import 'package:acter/features/home/widgets/space_chip.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; +import 'package:acter/features/notifications/widgets/object_notification_status.dart'; import 'package:acter/features/share/action/share_space_object_action.dart'; import 'package:acter/features/tasks/actions/update_tasklist.dart'; import 'package:acter/features/tasks/providers/tasklists_providers.dart'; @@ -81,6 +83,7 @@ class _TaskListPageState extends ConsumerState { }, ), BookmarkAction(bookmarker: BookmarkType.forTaskList(widget.taskListId)), + ObjectNotificationStatus(objectId: widget.taskListId), ]; if (tasklist != null) { actions.addAll( @@ -212,9 +215,9 @@ class _TaskListPageState extends ConsumerState { Widget _taskListHeader(TaskList tasklist) { final textTheme = Theme.of(context).textTheme; final canPost = ref - .watch(roomMembershipProvider(tasklist.spaceIdStr())) - .valueOrNull - ?.canString('CanPostTaskList') == + .watch(roomMembershipProvider(tasklist.spaceIdStr())) + .valueOrNull + ?.canString('CanPostTaskList') == true; return ListTile( contentPadding: EdgeInsets.zero, @@ -229,21 +232,20 @@ class _TaskListPageState extends ConsumerState { ), onIconSelection: canPost ? (color, acterIcon) { - updateTaskListIcon( - context, - ref, - tasklist, - color, - acterIcon, - ); - } + updateTaskListIcon( + context, + ref, + tasklist, + color, + acterIcon, + ); + } : null, ), title: SelectionArea( child: GestureDetector( onTap: () => showEditTaskListNameBottomSheet( context: context, - ref: ref, taskList: tasklist, titleValue: tasklist.name(), ), @@ -287,7 +289,7 @@ class _TaskListPageState extends ConsumerState { context: context, descriptionHtmlValue: taskListData.description()?.formattedBody(), descriptionMarkdownValue: taskListData.description()?.body(), - onSave: (htmlBodyDescription, plainDescription) { + onSave: (ref, htmlBodyDescription, plainDescription) { _saveDescription(taskListData, htmlBodyDescription, plainDescription); }, ); @@ -304,6 +306,12 @@ class _TaskListPageState extends ConsumerState { final updater = taskListData.updateBuilder(); updater.descriptionHtml(plainDescription, htmlBodyDescription); await updater.send(); + + await autosubscribe( + ref: ref, + objectId: taskListData.eventIdStr(), + lang: lang, + ); EasyLoading.dismiss(); if (mounted) Navigator.pop(context); } catch (e, s) { @@ -354,13 +362,13 @@ class _TaskListPageState extends ConsumerState { required BuildContext context, required String titleValue, required TaskList taskList, - required WidgetRef ref, }) { showEditTitleBottomSheet( context: context, bottomSheetTitle: L10n.of(context).editName, titleValue: titleValue, - onSave: (newName) => updateTaskListTitle(context, taskList, newName), + onSave: (ref, newName) => + updateTaskListTitle(context, ref, taskList, newName), ); } @@ -415,4 +423,4 @@ class _TaskListPageState extends ConsumerState { ), ), ); -} \ No newline at end of file +} diff --git a/app/lib/features/tasks/sheets/create_update_task_list.dart b/app/lib/features/tasks/sheets/create_update_task_list.dart index aab6ddc41567..425cd267aa5a 100644 --- a/app/lib/features/tasks/sheets/create_update_task_list.dart +++ b/app/lib/features/tasks/sheets/create_update_task_list.dart @@ -7,6 +7,7 @@ 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/html_editor/html_editor.dart'; import 'package:acter/common/widgets/spaces/select_space_form_field.dart'; +import 'package:acter/features/notifications/actions/autosubscribe.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; @@ -230,7 +231,8 @@ class _CreateUpdateTaskListConsumerState final plainDescription = textEditorState.intoMarkdown(); final htmlBodyDescription = textEditorState.intoHtml(); taskListDraft.descriptionHtml(plainDescription, htmlBodyDescription); - await taskListDraft.send(); + final objectId = await taskListDraft.send(); + await autosubscribe(ref: ref, objectId: objectId.toString(), lang: lang); EasyLoading.dismiss(); if (!mounted) return; diff --git a/app/lib/l10n/app_de.arb b/app/lib/l10n/app_de.arb index 0dde20572476..1d50f70be7f9 100644 --- a/app/lib/l10n/app_de.arb +++ b/app/lib/l10n/app_de.arb @@ -2362,11 +2362,9 @@ "@subscribeAction": {}, "unsubscribeAction": "Benachrichtigungen deaktivieren", "@unsubscribeAction": {}, - "autoSubscribeLabsFeature": "Automatische abbonieren", - "@autoSubscribeLabsFeature": {}, - "autoSubscribeFeatureDesc": "bei deinen Boosts", + "autoSubscribeFeatureDesc": "bei Erstellen oder Interaktion mit Objekten", "@autoSubscribeFeatureDesc": {}, - "autoSubscribeSettingsTitle": "Bei Interaktion automatisch abonnieren", + "autoSubscribeSettingsTitle": "Automatische abbonieren", "@autoSubscribeSettingsTitle": {}, "toAccess": "Für den Zugriff auf", "@toAccess": {}, @@ -2384,4 +2382,4 @@ "@syncThisCalendarTitle": {}, "syncThisCalendarDesc": "Synchronisiere diese Events im Kalender des Geräts", "@syncThisCalendarDesc": {} -} +} \ No newline at end of file diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 8c08164043c2..7633905831b1 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1962,9 +1962,8 @@ "@saveUsernameDescription3": {}, "acterUsername": "Your Acter Username", "@acterUsername": {}, - "autoSubscribeLabsFeature": "Automatically subscribe", - "autoSubscribeFeatureDesc": "to boosts you created", - "autoSubscribeSettingsTitle": "Automatically subscribe upon interaction", + "autoSubscribeFeatureDesc": "upon creation or interaction with objects", + "autoSubscribeSettingsTitle": "Automatically subscribe ", "copyToClip": "Copy to Clipboard", "@copyToClip": {}, "wizzardContinue": "Continue", diff --git a/app/test/features/tasks/tasks_adding_test.dart b/app/test/features/tasks/tasks_adding_test.dart index 833f5736f9ba..3fcbaf9ac879 100644 --- a/app/test/features/tasks/tasks_adding_test.dart +++ b/app/test/features/tasks/tasks_adding_test.dart @@ -1,4 +1,7 @@ import 'package:acter/common/providers/space_providers.dart'; +import 'package:acter/features/notifications/providers/notification_settings_providers.dart'; +import 'package:acter/features/notifications/providers/object_notifications_settings_provider.dart'; +import 'package:acter/features/notifications/types.dart'; import 'package:acter/features/tasks/actions/create_task.dart'; import 'package:acter/features/tasks/widgets/due_picker.dart'; import 'package:flutter/material.dart'; @@ -14,22 +17,21 @@ class FakeBottomModal extends Fake implements ModalBottomSheetRoute {} void main() { group('Create Task Widget on TaskList', () { - late MockGoRouter mockedGoRouter; - late MockNavigator navigator; - setUpAll(() { registerFallbackValue(FakeBottomModal()); }); - setUp(() { - mockedGoRouter = MockGoRouter(); - navigator = MockNavigator(); + (MockGoRouter, MockNavigator) setUpRouters() { + final mockedGoRouter = MockGoRouter(); + final navigator = MockNavigator(); when(navigator.canPop).thenReturn(true); when(() => navigator.pop(any())).thenAnswer((_) async {}); when(() => navigator.push(any())).thenAnswer((_) async {}); - }); + return (mockedGoRouter, navigator); + } testWidgets('Simple only title', (tester) async { + final (mockedGoRouter, navigator) = setUpRouters(); final mockTaskList = MockTaskList(); final mockTaskDraft = MockTaskDraft(); when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); @@ -42,6 +44,7 @@ void main() { goRouter: mockedGoRouter, overrides: [ selectedSpaceDetailsProvider.overrideWith((_) => null), + autoSubscribeProvider.overrideWith((a) async => false), ], child: CreateTaskWidget( taskList: mockTaskList, @@ -70,6 +73,7 @@ void main() { verify(() => mockTaskDraft.send()).called(1); }); testWidgets('with description', (tester) async { + final (mockedGoRouter, navigator) = setUpRouters(); final mockTaskList = MockTaskList(); final mockTaskDraft = MockTaskDraft(); when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); @@ -82,6 +86,9 @@ void main() { goRouter: mockedGoRouter, overrides: [ selectedSpaceDetailsProvider.overrideWith((_) => null), + autoSubscribeProvider.overrideWith((a) async => false), + pushNotificationSubscribedStatusProvider + .overrideWith((ref, arg) => SubscriptionStatus.none), ], child: CreateTaskWidget( taskList: mockTaskList, @@ -89,6 +96,8 @@ void main() { ); // try to submit without a title + debugDumpApp(); + final submitBtn = find.byKey(CreateTaskWidget.submitBtn); expect(submitBtn, findsOneWidget); await tester.tap(submitBtn); @@ -131,6 +140,7 @@ void main() { }); testWidgets('toggle description, not added', (tester) async { + final (mockedGoRouter, navigator) = setUpRouters(); final mockTaskList = MockTaskList(); final mockTaskDraft = MockTaskDraft(); when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); @@ -143,6 +153,9 @@ void main() { goRouter: mockedGoRouter, overrides: [ selectedSpaceDetailsProvider.overrideWith((_) => null), + autoSubscribeProvider.overrideWith((a) async => false), + pushNotificationSubscribedStatusProvider + .overrideWith((ref, arg) => SubscriptionStatus.none), ], child: CreateTaskWidget( taskList: mockTaskList, @@ -198,6 +211,7 @@ void main() { }); testWidgets('with due date', (tester) async { + final (mockedGoRouter, navigator) = setUpRouters(); final mockTaskList = MockTaskList(); final mockTaskDraft = MockTaskDraft(); when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); @@ -214,6 +228,9 @@ void main() { goRouter: mockedGoRouter, overrides: [ selectedSpaceDetailsProvider.overrideWith((_) => null), + autoSubscribeProvider.overrideWith((a) async => false), + pushNotificationSubscribedStatusProvider + .overrideWith((ref, arg) => SubscriptionStatus.none), ], child: CreateTaskWidget( taskList: mockTaskList, @@ -277,6 +294,7 @@ void main() { }); testWidgets('with due date toggled, not added', (tester) async { + final (mockedGoRouter, navigator) = setUpRouters(); final mockTaskList = MockTaskList(); final mockTaskDraft = MockTaskDraft(); when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); @@ -294,6 +312,9 @@ void main() { goRouter: mockedGoRouter, overrides: [ selectedSpaceDetailsProvider.overrideWith((_) => null), + autoSubscribeProvider.overrideWith((a) async => false), + pushNotificationSubscribedStatusProvider + .overrideWith((ref, arg) => SubscriptionStatus.none), ], child: CreateTaskWidget( taskList: mockTaskList, @@ -360,6 +381,7 @@ void main() { }); testWidgets('with due date from immediate dialog', (tester) async { + final (mockedGoRouter, navigator) = setUpRouters(); final mockTaskList = MockTaskList(); final mockTaskDraft = MockTaskDraft(); when(() => mockTaskList.taskBuilder()).thenAnswer((_) => mockTaskDraft); @@ -381,6 +403,9 @@ void main() { goRouter: mockedGoRouter, overrides: [ selectedSpaceDetailsProvider.overrideWith((_) => null), + autoSubscribeProvider.overrideWith((a) async => false), + pushNotificationSubscribedStatusProvider + .overrideWith((ref, arg) => SubscriptionStatus.none), ], child: CreateTaskWidget( taskList: mockTaskList, diff --git a/native/acter/api.rsh b/native/acter/api.rsh index 942134998089..cf3e4ee8188e 100644 --- a/native/acter/api.rsh +++ b/native/acter/api.rsh @@ -1565,6 +1565,9 @@ object CommentsManager { /// String representation of the room id this comments manager is in fn room_id_str() -> string; + /// String of the id of the object the comments are managed for + fn object_id_str() -> string; + /// Does this item have any comments? fn has_comments() -> bool; @@ -1638,6 +1641,9 @@ object AttachmentsManager { /// the room this attachments manager lives in fn room_id_str() -> string; + /// the id of the object whose attachments are managed + fn object_id_str() -> string; + /// Whether or not the current user can post, edit and delete /// attachments in this manager fn can_edit_attachments() -> bool; diff --git a/native/acter/src/api/attachments.rs b/native/acter/src/api/attachments.rs index be58a37d173d..7b8f9b4deafa 100644 --- a/native/acter/src/api/attachments.rs +++ b/native/acter/src/api/attachments.rs @@ -78,7 +78,6 @@ impl Attachment { pub fn room_id_str(&self) -> String { self.room.room_id().to_string() } - pub fn type_str(&self) -> String { self.inner.content().type_str() } @@ -388,6 +387,10 @@ impl AttachmentsManager { self.room.room_id().to_string() } + pub fn object_id_str(&self) -> String { + self.inner.event_id().to_string() + } + pub fn can_edit_attachments(&self) -> bool { // FIXME: this requires an actual configurable option. true diff --git a/native/acter/src/api/comments.rs b/native/acter/src/api/comments.rs index 2ff3e7545515..d7017240c579 100644 --- a/native/acter/src/api/comments.rs +++ b/native/acter/src/api/comments.rs @@ -164,6 +164,10 @@ impl CommentsManager { .await? } + pub fn object_id_str(&self) -> String { + self.inner.event_id().to_string() + } + pub fn room_id_str(&self) -> String { self.room.room_id().to_string() } diff --git a/packages/rust_sdk/lib/acter_flutter_sdk_ffi.dart b/packages/rust_sdk/lib/acter_flutter_sdk_ffi.dart index fb4af3308227..26218c6fb953 100644 --- a/packages/rust_sdk/lib/acter_flutter_sdk_ffi.dart +++ b/packages/rust_sdk/lib/acter_flutter_sdk_ffi.dart @@ -22964,6 +22964,17 @@ class Api { _CommentsManagerRoomIdStrReturn Function( int, )>(); + late final _commentsManagerObjectIdStrPtr = _lookup< + ffi.NativeFunction< + _CommentsManagerObjectIdStrReturn Function( + ffi.IntPtr, + )>>("__CommentsManager_object_id_str"); + + late final _commentsManagerObjectIdStr = + _commentsManagerObjectIdStrPtr.asFunction< + _CommentsManagerObjectIdStrReturn Function( + int, + )>(); late final _commentsManagerHasCommentsPtr = _lookup< ffi.NativeFunction< ffi.Uint8 Function( @@ -23173,6 +23184,17 @@ class Api { _AttachmentsManagerRoomIdStrReturn Function( int, )>(); + late final _attachmentsManagerObjectIdStrPtr = _lookup< + ffi.NativeFunction< + _AttachmentsManagerObjectIdStrReturn Function( + ffi.IntPtr, + )>>("__AttachmentsManager_object_id_str"); + + late final _attachmentsManagerObjectIdStr = + _attachmentsManagerObjectIdStrPtr.asFunction< + _AttachmentsManagerObjectIdStrReturn Function( + int, + )>(); late final _attachmentsManagerCanEditAttachmentsPtr = _lookup< ffi.NativeFunction< ffi.Uint8 Function( @@ -48480,6 +48502,36 @@ class CommentsManager { return tmp2; } + /// String of the id of the object the comments are managed for + String objectIdStr() { + var tmp0 = 0; + tmp0 = _box.borrow(); + final tmp1 = _api._commentsManagerObjectIdStr( + tmp0, + ); + final tmp3 = tmp1.arg0; + final tmp4 = tmp1.arg1; + final tmp5 = tmp1.arg2; + if (tmp4 == 0) { + print("returning empty string"); + return ""; + } + final ffi.Pointer tmp3_ptr = ffi.Pointer.fromAddress(tmp3); + List tmp3_buf = []; + final tmp3_precast = tmp3_ptr.cast(); + for (int i = 0; i < tmp4; i++) { + int char = tmp3_precast.elementAt(i).value; + tmp3_buf.add(char); + } + final tmp2 = utf8.decode(tmp3_buf, allowMalformed: true); + if (tmp5 > 0) { + final ffi.Pointer tmp3_0; + tmp3_0 = ffi.Pointer.fromAddress(tmp3); + _api.__deallocate(tmp3_0, tmp5 * 1, 1); + } + return tmp2; + } + /// Does this item have any comments? bool hasComments() { var tmp0 = 0; @@ -48959,6 +49011,36 @@ class AttachmentsManager { return tmp2; } + /// the id of the object whose attachments are managed + String objectIdStr() { + var tmp0 = 0; + tmp0 = _box.borrow(); + final tmp1 = _api._attachmentsManagerObjectIdStr( + tmp0, + ); + final tmp3 = tmp1.arg0; + final tmp4 = tmp1.arg1; + final tmp5 = tmp1.arg2; + if (tmp4 == 0) { + print("returning empty string"); + return ""; + } + final ffi.Pointer tmp3_ptr = ffi.Pointer.fromAddress(tmp3); + List tmp3_buf = []; + final tmp3_precast = tmp3_ptr.cast(); + for (int i = 0; i < tmp4; i++) { + int char = tmp3_precast.elementAt(i).value; + tmp3_buf.add(char); + } + final tmp2 = utf8.decode(tmp3_buf, allowMalformed: true); + if (tmp5 > 0) { + final ffi.Pointer tmp3_0; + tmp3_0 = ffi.Pointer.fromAddress(tmp3); + _api.__deallocate(tmp3_0, tmp5 * 1, 1); + } + return tmp2; + } + /// Whether or not the current user can post, edit and delete /// attachments in this manager bool canEditAttachments() { @@ -65854,6 +65936,15 @@ class _CommentsManagerRoomIdStrReturn extends ffi.Struct { external int arg2; } +class _CommentsManagerObjectIdStrReturn extends ffi.Struct { + @ffi.IntPtr() + external int arg0; + @ffi.UintPtr() + external int arg1; + @ffi.UintPtr() + external int arg2; +} + class _AttachmentNameReturn extends ffi.Struct { @ffi.Uint8() external int arg0; @@ -65935,6 +66026,15 @@ class _AttachmentsManagerRoomIdStrReturn extends ffi.Struct { external int arg2; } +class _AttachmentsManagerObjectIdStrReturn extends ffi.Struct { + @ffi.IntPtr() + external int arg0; + @ffi.UintPtr() + external int arg1; + @ffi.UintPtr() + external int arg2; +} + class _TaskTitleReturn extends ffi.Struct { @ffi.IntPtr() external int arg0;