diff --git a/app/lib/common/actions/show_limited_space_list.dart b/app/lib/common/actions/show_limited_space_list.dart index f828563c056d..cdfb2fac5162 100644 --- a/app/lib/common/actions/show_limited_space_list.dart +++ b/app/lib/common/actions/show_limited_space_list.dart @@ -43,8 +43,9 @@ class LimitedSpaceList extends ConsumerWidget { _log.severe('Failed to load pin', e, s); return Text(L10n.of(context).errorLoadingSpaces(e)); }, - loading: () => - const Skeletonizer(child: SizedBox(height: 100, width: 100)), + loading: () => const Skeletonizer( + child: SizedBox(height: 100, width: 100), + ), ); } diff --git a/app/lib/common/providers/common_providers.dart b/app/lib/common/providers/common_providers.dart index 293d2b69282d..fb4d85976a2c 100644 --- a/app/lib/common/providers/common_providers.dart +++ b/app/lib/common/providers/common_providers.dart @@ -105,9 +105,8 @@ final notificationSettingsProvider = AsyncNotifierProvider< final appContentNotificationSetting = FutureProvider.family((ref, appKey) async { - final notificationsSettings = - await ref.watch(notificationSettingsProvider.future); - return await notificationsSettings.globalContentSetting(appKey); + final settings = await ref.watch(notificationSettingsProvider.future); + return await settings.globalContentSetting(appKey); }); // Email addresses that registered by user diff --git a/app/lib/common/providers/notifiers/chat_notifiers.dart b/app/lib/common/providers/notifiers/chat_notifiers.dart index e9678a06ba42..c5cecb1fda09 100644 --- a/app/lib/common/providers/notifiers/chat_notifiers.dart +++ b/app/lib/common/providers/notifiers/chat_notifiers.dart @@ -20,23 +20,20 @@ class AsyncConvoNotifier extends FamilyAsyncNotifier { final client = ref.watch(alwaysClientProvider); _listener = client.subscribeStream(convoId); // keep it resident in memory _poller = _listener.listen( - (e) async { + (data) async { final newConvo = await client.convo(convoId); state = AsyncValue.data(newConvo); }, - onError: (e, stack) { - state = AsyncValue.error(e, stack); - _log.severe('stream errored.', e, stack); + onError: (e, s) { + _log.severe('convo stream errored', e, s); + state = AsyncValue.error(e, s); }, onDone: () { - _log.info('stream ended'); + _log.info('convo stream ended'); }, ); ref.onDispose(() => _poller.cancel()); - return await client.convoWithRetry( - convoId, - 120, - ); + return await client.convoWithRetry(convoId, 120); } } @@ -50,17 +47,18 @@ class AsyncLatestMsgNotifier extends FamilyAsyncNotifier { final client = ref.watch(alwaysClientProvider); _listener = client.subscribeStream('$roomId::latest_message'); _poller = _listener.listen( - (e) { + (data) { _log.info('received new latest message call for $roomId'); state = ref .watch(chatProvider(roomId)) .whenData((cb) => cb?.latestMessage()); }, - onError: (e, stack) { - _log.severe('stream errored', e, stack); + onError: (e, s) { + _log.severe('latest msg stream errored', e, s); + state = AsyncValue.error(e, s); }, onDone: () { - _log.info('stream ended'); + _log.info('latest msg stream ended'); }, ); ref.onDispose(() => _poller.cancel()); @@ -82,7 +80,15 @@ class ChatRoomsListNotifier extends StateNotifier> { void _init(Ref ref) { _listener = client.convosStream(); // keep it resident in memory - _poller = _listener.listen(_handleDiff); + _poller = _listener.listen( + _handleDiff, + onError: (e, s) { + _log.severe('convo list stream errored', e, s); + }, + onDone: () { + _log.info('convo list stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); } diff --git a/app/lib/common/providers/notifiers/notification_settings_notifier.dart b/app/lib/common/providers/notifiers/notification_settings_notifier.dart index 9b678cf064b5..f82f8e055c9b 100644 --- a/app/lib/common/providers/notifiers/notification_settings_notifier.dart +++ b/app/lib/common/providers/notifiers/notification_settings_notifier.dart @@ -18,13 +18,13 @@ class AsyncNotificationSettingsNotifier final settings = await client.notificationSettings(); _listener = settings.changesStream(); _poller = _listener.listen( - (e) { + (data) { // reset the state of this to trigger the notification // cascade state = AsyncValue.data(settings); }, - onError: (e, stack) { - _log.severe('stream errored', e, stack); + onError: (e, s) { + _log.severe('stream errored', e, s); }, onDone: () { _log.info('stream ended'); diff --git a/app/lib/common/providers/notifiers/reactions_notifiers.dart b/app/lib/common/providers/notifiers/reactions_notifiers.dart index 11e6d9000a54..d68cb5f550aa 100644 --- a/app/lib/common/providers/notifiers/reactions_notifiers.dart +++ b/app/lib/common/providers/notifiers/reactions_notifiers.dart @@ -15,14 +15,14 @@ class ReactionManagerNotifier ReactionManager build(ReactionManager arg) { _listener = arg.subscribeStream(); // keep it resident in memory _poller = _listener.listen( - (e) async { + (data) async { _log.info('attempting to reload'); final newManager = await arg.reload(); _log.info('manager updated. likes: ${newManager.likesCount()}'); state = newManager; }, - onError: (e, stack) { - _log.severe('stream errored.', e, stack); + onError: (e, s) { + _log.severe('stream errored', e, s); }, onDone: () { _log.info('stream ended'); diff --git a/app/lib/common/providers/notifiers/room_notifiers.dart b/app/lib/common/providers/notifiers/room_notifiers.dart index a2143d35ef74..f092c3d30bce 100644 --- a/app/lib/common/providers/notifiers/room_notifiers.dart +++ b/app/lib/common/providers/notifiers/room_notifiers.dart @@ -29,15 +29,15 @@ class AsyncMaybeRoomNotifier extends FamilyAsyncNotifier { final client = ref.watch(alwaysClientProvider); _listener = client.subscribeStream(arg); // keep it resident in memory _poller = _listener.listen( - (e) async { + (data) async { _log.info('seen update for room $arg'); state = await AsyncValue.guard(_getRoom); }, - onError: (e, stack) { - _log.severe('stream errored', e, stack); + onError: (e, s) { + _log.severe('room stream errored', e, s); }, onDone: () { - _log.info('stream ended'); + _log.info('room stream ended'); }, ); ref.onDispose(() => _poller.cancel()); diff --git a/app/lib/common/providers/notifiers/space_notifiers.dart b/app/lib/common/providers/notifiers/space_notifiers.dart index 56aa9ee4cfb7..f63b1519ee6d 100644 --- a/app/lib/common/providers/notifiers/space_notifiers.dart +++ b/app/lib/common/providers/notifiers/space_notifiers.dart @@ -21,15 +21,15 @@ class AsyncMaybeSpaceNotifier extends FamilyAsyncNotifier { final client = ref.watch(alwaysClientProvider); _listener = client.subscribeStream(arg); // keep it resident in memory _poller = _listener.listen( - (e) async { + (data) async { _log.info('seen update $arg'); state = await AsyncValue.guard(_getSpace); }, - onError: (e, stack) { - _log.severe('stream errored', e, stack); + onError: (e, s) { + _log.severe('space stream errored', e, s); }, onDone: () { - _log.info('stream ended'); + _log.info('space stream ended'); }, ); ref.onDispose(() => _poller.cancel()); @@ -52,7 +52,15 @@ class SpaceListNotifier extends StateNotifier> { void _init() async { _listener = client.spacesStream(); // keep it resident in memory - _poller = _listener.listen(_handleDiff); + _poller = _listener.listen( + _handleDiff, + onError: (e, s) { + _log.severe('space list stream errored', e, s); + }, + onDone: () { + _log.info('space list stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); } diff --git a/app/lib/common/widgets/add_button_with_can_permission.dart b/app/lib/common/widgets/add_button_with_can_permission.dart index a7801ad07157..a2cf126b8863 100644 --- a/app/lib/common/widgets/add_button_with_can_permission.dart +++ b/app/lib/common/widgets/add_button_with_can_permission.dart @@ -15,10 +15,8 @@ class AddButtonWithCanPermission extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { if (canString == null) return _buildIconButton(context); - final canAdd = - ref.watch(hasSpaceWithPermissionProvider(canString!)).valueOrNull ?? - false; - + final canDoLoader = ref.watch(hasSpaceWithPermissionProvider(canString!)); + final canAdd = canDoLoader.valueOrNull ?? false; return canAdd ? _buildIconButton(context) : const SizedBox.shrink(); } diff --git a/app/lib/common/widgets/event/event_selector_drawer.dart b/app/lib/common/widgets/event/event_selector_drawer.dart index 854c4883d337..7f07699f6cef 100644 --- a/app/lib/common/widgets/event/event_selector_drawer.dart +++ b/app/lib/common/widgets/event/event_selector_drawer.dart @@ -24,58 +24,56 @@ Future selectEventDrawer({ isDismissible: true, builder: (context) => Consumer( builder: (context, ref, child) { - final events = ref.watch(allEventListProvider(spaceId)); - return events.when( - data: (eventsList) { - return Column( - key: key, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Row( - children: [ - Expanded( - child: title ?? const Text('Select event'), - ), - OutlinedButton.icon( - icon: const Icon(Atlas.minus_circle_thin), - onPressed: () { - Navigator.pop(context, null); - }, - label: const Text('Clear'), - ), - ], - ), + final calEventsLoader = ref.watch(allEventListProvider(spaceId)); + return calEventsLoader.when( + data: (calEvents) => Column( + key: key, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Row( + children: [ + Expanded( + child: title ?? const Text('Select event'), + ), + OutlinedButton.icon( + icon: const Icon(Atlas.minus_circle_thin), + onPressed: () { + Navigator.pop(context, null); + }, + label: const Text('Clear'), + ), + ], ), - Flexible( - child: eventsList.isEmpty - ? Container( - height: 200, - alignment: Alignment.center, - child: const Text('No events found'), - ) - : ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: eventsList.length, - itemBuilder: (context, index) => EventItem( - event: eventsList[index], - isShowRsvp: false, - onTapEventItem: (event) { - Navigator.pop(context, event); - }, - ), + ), + Flexible( + child: calEvents.isEmpty + ? Container( + height: 200, + alignment: Alignment.center, + child: const Text('No events found'), + ) + : ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: calEvents.length, + itemBuilder: (context, index) => EventItem( + event: calEvents[index], + isShowRsvp: false, + onTapEventItem: (event) { + Navigator.pop(context, event); + }, ), - ), - ], - ); - }, - error: (error, stack) { - _log.severe('Failed to load all cal events', error, stack); + ), + ), + ], + ), + error: (e, s) { + _log.severe('Failed to load all cal events', e, s); return Center( - child: Text(L10n.of(context).failedToLoadEventsDueTo(error)), + child: Text(L10n.of(context).failedToLoadEventsDueTo(e)), ); }, loading: () => const SizedBox( diff --git a/app/lib/common/widgets/html_editor.dart b/app/lib/common/widgets/html_editor.dart index c2dc30231366..bdf050df13f2 100644 --- a/app/lib/common/widgets/html_editor.dart +++ b/app/lib/common/widgets/html_editor.dart @@ -1,9 +1,13 @@ import 'dart:async'; + import 'package:acter/common/toolkit/buttons/primary_action_button.dart'; import 'package:acter/common/utils/constants.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; -import 'package:flutter/material.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('a3::common::html_editor'); AppFlowyEditorHTMLCodec defaultHtmlCodec = const AppFlowyEditorHTMLCodec( encodeParsers: [ @@ -140,9 +144,17 @@ class HtmlEditorState extends State { _changeListener?.cancel(); if (widget.onChanged != null) { - _changeListener = editorState.transactionStream.listen((event) { - _triggerExport(widget.onChanged!); - }); + _changeListener = editorState.transactionStream.listen( + (data) { + _triggerExport(widget.onChanged!); + }, + onError: (e, s) { + _log.severe('tx stream errored', e, s); + }, + onDone: () { + _log.info('tx stream ended'); + }, + ); } }); } diff --git a/app/lib/common/widgets/room/select_room_drawer.dart b/app/lib/common/widgets/room/select_room_drawer.dart index 6789735a2eb5..3062d41de0e2 100644 --- a/app/lib/common/widgets/room/select_room_drawer.dart +++ b/app/lib/common/widgets/room/select_room_drawer.dart @@ -38,10 +38,12 @@ class SelectRoomDrawer extends ConsumerStatefulWidget { ConsumerState createState() => _SelectRoomDrawerState(); - DisplayMode get avatarDisplayMode => switch (roomType) { - RoomType.space => DisplayMode.Space, - RoomType.groupChat => DisplayMode.GroupChat - }; + DisplayMode get avatarDisplayMode { + return switch (roomType) { + RoomType.space => DisplayMode.Space, + RoomType.groupChat => DisplayMode.GroupChat + }; + } } class _SelectRoomDrawerState extends ConsumerState { @@ -145,14 +147,11 @@ class _SelectRoomDrawerState extends ConsumerState { //Show space list based on the search term Widget searchedRoomsList(BuildContext context) { - final searched = ref.watch( - switch (widget.roomType) { - RoomType.space => searchedSpacesProvider, - RoomType.groupChat => roomSearchedChatsProvider, - }, - ); - - return searched.when( + final roomsLoader = switch (widget.roomType) { + RoomType.space => ref.watch(searchedSpacesProvider), + RoomType.groupChat => ref.watch(roomSearchedChatsProvider), + }; + return roomsLoader.when( data: (rooms) { if (rooms.isEmpty) { return Center( @@ -179,19 +178,16 @@ class _SelectRoomDrawerState extends ConsumerState { return ListView.builder( padding: const EdgeInsets.all(8), itemCount: rooms.length, - itemBuilder: (context, index) { - final roomId = rooms[index]; - return BriefRoomEntry( - roomId: roomId, - avatarDisplayMode: widget.avatarDisplayMode, - keyPrefix: widget.keyPrefix, - selectedValue: current, - canCheck: widget.canCheck, - onSelect: (roomId) { - Navigator.pop(context, roomId); - }, - ); - }, + itemBuilder: (context, index) => BriefRoomEntry( + roomId: rooms[index], + avatarDisplayMode: widget.avatarDisplayMode, + keyPrefix: widget.keyPrefix, + selectedValue: current, + canCheck: widget.canCheck, + onSelect: (roomId) { + Navigator.pop(context, roomId); + }, + ), ); } } diff --git a/app/lib/common/widgets/spaces/has_space_permission.dart b/app/lib/common/widgets/spaces/has_space_permission.dart index a8f29140c15e..0cb7af183b64 100644 --- a/app/lib/common/widgets/spaces/has_space_permission.dart +++ b/app/lib/common/widgets/spaces/has_space_permission.dart @@ -22,14 +22,17 @@ class HasSpacePermission extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final otherwise = fallback ?? const SizedBox.shrink(); - return ref.watch(roomMembershipProvider(spaceId)).when( - data: (membership) => - membership?.canString(permission) == true ? child : otherwise, - error: (e, s) { - _log.severe('Failed to load membership', e, s); - return otherwise; - }, - loading: () => otherwise, - ); + final membershipLoader = ref.watch(roomMembershipProvider(spaceId)); + return membershipLoader.when( + data: (membership) { + if (membership?.canString(permission) == true) return child; + return otherwise; + }, + error: (e, s) { + _log.severe('Failed to load membership', e, s); + return otherwise; + }, + loading: () => otherwise, + ); } } 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 a3f2bee7a873..29677d4d2c30 100644 --- a/app/lib/common/widgets/spaces/select_space_form_field.dart +++ b/app/lib/common/widgets/spaces/select_space_form_field.dart @@ -46,10 +46,10 @@ class SelectSpaceFormField extends ConsumerWidget { final selectedSpace = currentSelectedSpace != null; final emptyButton = OutlinedButton( - key: openKey, - onPressed: () => selectSpace(context, ref), - child: Text(emptyText ?? L10n.of(context).pleaseSelectSpace), - ); + key: openKey, + onPressed: () => selectSpace(context, ref), + child: Text(emptyText ?? L10n.of(context).pleaseSelectSpace), + ); return FormField( builder: (state) => selectedSpace @@ -94,18 +94,20 @@ class SelectSpaceFormField extends ConsumerWidget { } Widget spaceBuilder(BuildContext context, WidgetRef ref, Widget? child) { - final spaceDetails = ref.watch(selectedSpaceDetailsProvider); - final currentSelectedSpace = ref.watch(selectedSpaceIdProvider); - return spaceDetails.when( - data: (space) => space != null - ? SpaceChip( - space: space, - onTapOpenSpaceDetail: false, - useCompatView: useCompatView, - onTapSelectSpace: () => - useCompatView ? selectSpace(context, ref) : null, - ) - : Text(currentSelectedSpace!), + final spaceLoader = ref.watch(selectedSpaceDetailsProvider); + final currentId = ref.watch(selectedSpaceIdProvider); + return spaceLoader.when( + data: (space) { + if (space == null) return Text(currentId!); + return SpaceChip( + space: space, + onTapOpenSpaceDetail: false, + useCompatView: useCompatView, + onTapSelectSpace: () { + if (useCompatView) selectSpace(context, ref); + }, + ); + }, error: (e, s) { _log.severe('Failed to load the details of selected space', e, s); return Text(L10n.of(context).loadingFailed(e)); @@ -114,9 +116,7 @@ class SelectSpaceFormField extends ConsumerWidget { child: Chip( avatar: ActerAvatar( options: AvatarOptions( - AvatarInfo( - uniqueId: L10n.of(context).loading, - ), + AvatarInfo(uniqueId: L10n.of(context).loading), size: 24, ), ), diff --git a/app/lib/common/widgets/spaces/space_info.dart b/app/lib/common/widgets/spaces/space_info.dart index a168e8018b2e..ee75cdd2dd87 100644 --- a/app/lib/common/widgets/spaces/space_info.dart +++ b/app/lib/common/widgets/spaces/space_info.dart @@ -19,23 +19,25 @@ class SpaceInfo extends ConsumerWidget { final String spaceId; final double size; - const SpaceInfo({super.key, this.size = 16, required this.spaceId}); + const SpaceInfo({ + super.key, + this.size = 16, + required this.spaceId, + }); @override Widget build(BuildContext context, WidgetRef ref) { - final space = ref.watch(spaceProvider(spaceId)); - return space.when( - data: (space) { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - VisibilityChip(roomId: spaceId), - const SizedBox(width: 5), - acterSpaceInfoUI(context, ref, space), - const Spacer(), - ], - ); - }, + final spaceLoader = ref.watch(spaceProvider(spaceId)); + return spaceLoader.when( + data: (space) => Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + VisibilityChip(roomId: spaceId), + const SizedBox(width: 5), + acterSpaceInfoUI(context, ref, space), + const Spacer(), + ], + ), error: (e, s) { _log.severe('Failed to load space', e, s); return Text(L10n.of(context).loadingFailed(e)); @@ -75,24 +77,21 @@ class SpaceInfo extends ConsumerWidget { } Widget acterSpaceInfoUI(BuildContext context, WidgetRef ref, Space space) { - final isActerSpace = ref.watch(isActerSpaceForSpace(space)); - return isActerSpace - .whenData( - (isProper) => isProper - ? const SizedBox.shrink() - : Padding( - padding: const EdgeInsets.only(right: 3), - child: Tooltip( - message: L10n.of(context).thisIsNotAProperActerSpace, - child: Icon( - Atlas.triangle_exclamation_thin, - size: size, - color: Theme.of(context).colorScheme.error, - ), - ), - ), - ) - .valueOrNull ?? - const SizedBox.shrink(); + final isActerLoader = ref.watch(isActerSpaceForSpace(space)); + final widgetLoader = isActerLoader.whenData((isActer) { + if (isActer) return const SizedBox.shrink(); + return Padding( + padding: const EdgeInsets.only(right: 3), + child: Tooltip( + message: L10n.of(context).thisIsNotAProperActerSpace, + child: Icon( + Atlas.triangle_exclamation_thin, + size: size, + color: Theme.of(context).colorScheme.error, + ), + ), + ); + }); + return widgetLoader.valueOrNull ?? const SizedBox.shrink(); } } diff --git a/app/lib/common/widgets/visibility/visibility_chip.dart b/app/lib/common/widgets/visibility/visibility_chip.dart index e62d67716b50..5bcc3d02e2fd 100644 --- a/app/lib/common/widgets/visibility/visibility_chip.dart +++ b/app/lib/common/widgets/visibility/visibility_chip.dart @@ -13,25 +13,22 @@ final _log = Logger('a3::common::visibility::chip'); class VisibilityChip extends ConsumerWidget { final String roomId; - const VisibilityChip({super.key, required this.roomId}); + const VisibilityChip({ + super.key, + required this.roomId, + }); @override Widget build(BuildContext context, WidgetRef ref) { - final spaceVisibility = ref.watch(roomVisibilityProvider(roomId)); - return spaceVisibility.when( - data: (visibility) { - return GestureDetector( - onTap: () { - if (visibility != RoomVisibility.SpaceVisible) return; - showLimitedSpaceList( - context, - ref, - roomId, - ); - }, - child: renderSpaceChip(context, visibility), - ); - }, + final visibilityLoader = ref.watch(roomVisibilityProvider(roomId)); + return visibilityLoader.when( + data: (visibility) => GestureDetector( + onTap: () { + if (visibility != RoomVisibility.SpaceVisible) return; + showLimitedSpaceList(context, ref, roomId); + }, + child: renderSpaceChip(context, visibility), + ), error: (e, s) { _log.severe('Failed to load room visibility', e, s); return Chip( diff --git a/app/lib/features/activities/providers/notifiers/invitation_list_notifier.dart b/app/lib/features/activities/providers/notifiers/invitation_list_notifier.dart index 212b0a1629b7..d63cdfa9648e 100644 --- a/app/lib/features/activities/providers/notifiers/invitation_list_notifier.dart +++ b/app/lib/features/activities/providers/notifiers/invitation_list_notifier.dart @@ -2,8 +2,11 @@ import 'dart:async'; import 'package:acter/features/home/providers/client_providers.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod/riverpod.dart'; +final _log = Logger('a3::activities::invitation_list'); + class InvitationListNotifier extends Notifier> { late Stream _listener; late StreamSubscription _poller; @@ -15,10 +18,18 @@ class InvitationListNotifier extends Notifier> { return []; } _listener = client.invitationsRx(); // keep it resident in memory - _poller = _listener.listen((ev) { - final asList = ev.toList(); - state = asList; - }); + _poller = _listener.listen( + (data) { + final asList = data.toList(); + state = asList; + }, + onError: (e, s) { + _log.severe('stream errored', e, s); + }, + onDone: () { + _log.info('stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return []; } diff --git a/app/lib/features/attachments/providers/notifiers/attachments_notifiers.dart b/app/lib/features/attachments/providers/notifiers/attachments_notifiers.dart index b60e285677cd..d7497300a80a 100644 --- a/app/lib/features/attachments/providers/notifiers/attachments_notifiers.dart +++ b/app/lib/features/attachments/providers/notifiers/attachments_notifiers.dart @@ -22,13 +22,12 @@ class AttachmentsManagerNotifier extends AutoDisposeFamilyAsyncNotifier< (e) async { _log.info('attempting to reload'); final newManager = await manager.reload(); - _log.info( - 'manager updated. attachments: ${newManager.attachmentsCount()}', - ); + final count = newManager.attachmentsCount(); + _log.info('manager updated. attachments: $count'); state = AsyncValue.data(newManager); }, - onError: (e, stack) { - _log.severe('stream errored.', e, stack); + onError: (e, s) { + _log.severe('stream errored', e, s); }, onDone: () { _log.info('stream ended'); diff --git a/app/lib/features/attachments/widgets/attachment_section.dart b/app/lib/features/attachments/widgets/attachment_section.dart index 7cf341449baf..960c9b9a0265 100644 --- a/app/lib/features/attachments/widgets/attachment_section.dart +++ b/app/lib/features/attachments/widgets/attachment_section.dart @@ -29,17 +29,18 @@ class AttachmentSectionWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ref.watch(attachmentsManagerProvider(manager)).when( - data: (manager) => FoundAttachmentSectionWidget( - attachmentManager: manager, - key: attachmentsKey, - ), - error: (e, s) { - _log.severe('Failed to load attachment manager', e, s); - return onError(context, e); - }, - loading: () => loading(context), - ); + final managerLoader = ref.watch(attachmentsManagerProvider(manager)); + return managerLoader.when( + data: (manager) => FoundAttachmentSectionWidget( + attachmentManager: manager, + key: attachmentsKey, + ), + error: (e, s) { + _log.severe('Failed to load attachment manager', e, s); + return onError(context, e); + }, + loading: () => loading(context), + ); } Widget onError(BuildContext context, Object error) { @@ -66,16 +67,15 @@ class FoundAttachmentSectionWidget extends ConsumerWidget { final AttachmentsManager attachmentManager; const FoundAttachmentSectionWidget({ - required this.attachmentManager, super.key, + required this.attachmentManager, }); @override Widget build(BuildContext context, WidgetRef ref) { - final attachments = ref.watch(attachmentsProvider(attachmentManager)); - - return attachments.when( - data: (list) => attachmentData(list, context, ref), + final attachmentsLoader = ref.watch(attachmentsProvider(attachmentManager)); + return attachmentsLoader.when( + data: (attachments) => attachmentData(attachments, context, ref), error: (e, s) { _log.severe('Failed to load attachments', e, s); return Text(L10n.of(context).errorLoadingAttachments(e)); diff --git a/app/lib/features/backups/providers/notifiers/backup_state_notifier.dart b/app/lib/features/backups/providers/notifiers/backup_state_notifier.dart index 805f7819e889..fcee5763e970 100644 --- a/app/lib/features/backups/providers/notifiers/backup_state_notifier.dart +++ b/app/lib/features/backups/providers/notifiers/backup_state_notifier.dart @@ -3,10 +3,12 @@ import 'dart:async'; import 'package:acter/features/backups/providers/backup_manager_provider.dart'; import 'package:acter/features/backups/types.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('a3::backups::backup_state'); class RecoveryStateNotifier extends Notifier { late Stream _listener; - // ignore: unused_field late StreamSubscription _poller; @override @@ -14,9 +16,17 @@ class RecoveryStateNotifier extends Notifier { // Load initial todo list from the remote repository final backup = ref.watch(backupManagerProvider); _listener = backup.stateStream(); // keep it resident in memory - _poller = _listener.listen((element) async { - state = stringToState(element); - }); + _poller = _listener.listen( + (data) async { + state = stringToState(data); + }, + onError: (e, s) { + _log.severe('stream errored', e, s); + }, + onDone: () { + _log.info('stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return stringToState(backup.stateStr()); } diff --git a/app/lib/features/chat/pages/room_page.dart b/app/lib/features/chat/pages/room_page.dart index 36480702e13a..469c6f4287e2 100644 --- a/app/lib/features/chat/pages/room_page.dart +++ b/app/lib/features/chat/pages/room_page.dart @@ -39,13 +39,13 @@ class RoomPage extends ConsumerWidget { final String roomId; const RoomPage({ - required this.roomId, super.key = roomPageKey, + required this.roomId, }); Widget appBar(BuildContext context, WidgetRef ref) { final roomAvatarInfo = ref.watch(roomAvatarInfoProvider(roomId)); - final activeMembers = ref.watch(membersIdsProvider(roomId)); + final membersLoader = ref.watch(membersIdsProvider(roomId)); return AppBar( elevation: 0, automaticallyImplyLeading: !context.isLargeScreen, @@ -69,14 +69,11 @@ class RoomPage extends ConsumerWidget { style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 5), - activeMembers.when( - data: (members) { - int count = members.length; - return Text( - L10n.of(context).membersCount(count), - style: Theme.of(context).textTheme.bodySmall, - ); - }, + membersLoader.when( + data: (members) => Text( + L10n.of(context).membersCount(members.length), + style: Theme.of(context).textTheme.bodySmall, + ), skipLoadingOnReload: false, error: (e, s) { _log.severe('Failed to load active members', e, s); @@ -146,8 +143,8 @@ class ChatRoom extends ConsumerStatefulWidget { final String roomId; const ChatRoom({ - required this.roomId, super.key, + required this.roomId, }); @override @@ -182,10 +179,7 @@ class _ChatRoomConsumerState extends ConsumerState { }); } - void showMessageOptions( - BuildContext context, - types.Message message, - ) async { + void showMessageOptions(BuildContext context, types.Message message) { if (message is types.CustomMessage) { if (message.metadata!.containsKey('eventType') && message.metadata!['eventType'] == 'm.room.redaction') { @@ -231,6 +225,8 @@ class _ChatRoomConsumerState extends ConsumerState { final userId = ref.watch(myUserIdStrProvider); final isDirectChat = ref.watch(isDirectChatProvider(roomId)).valueOrNull ?? false; + final typingUsers = + ref.watch(chatTypingEventProvider(roomId)).valueOrNull ?? []; return Expanded( child: Chat( @@ -243,12 +239,13 @@ class _ChatRoomConsumerState extends ConsumerState { types.TextMessage m, { required int messageWidth, required bool showName, - }) => - TextMessageBuilder( - roomId: widget.roomId, - message: m, - messageWidth: messageWidth, - ), + }) { + return TextMessageBuilder( + roomId: widget.roomId, + message: m, + messageWidth: messageWidth, + ); + }, l10n: ChatL10nEn( emptyChatPlaceholder: '', attachmentButtonAccessibilityLabel: '', @@ -263,42 +260,47 @@ class _ChatRoomConsumerState extends ConsumerState { // disable image preview disableImageGallery: true, // custom avatar builder - avatarBuilder: (types.User user) => - AvatarBuilder(userId: user.id, roomId: roomId), + avatarBuilder: (types.User user) => AvatarBuilder( + userId: user.id, + roomId: roomId, + ), isLastPage: endReached, bubbleBuilder: ( Widget child, { required types.Message message, required bool nextMessageInGroup, - }) => - GestureDetector( - onSecondaryTap: () => showMessageOptions(context, message), - child: BubbleBuilder( - roomId: widget.roomId, - message: message, - nextMessageInGroup: nextMessageInGroup, - enlargeEmoji: message.metadata!['enlargeEmoji'] ?? false, - child: child, - ), - ), + }) { + return GestureDetector( + onSecondaryTap: () => showMessageOptions(context, message), + child: BubbleBuilder( + roomId: widget.roomId, + message: message, + nextMessageInGroup: nextMessageInGroup, + enlargeEmoji: message.metadata!['enlargeEmoji'] ?? false, + child: child, + ), + ); + }, imageMessageBuilder: ( types.ImageMessage message, { required int messageWidth, - }) => - ImageMessageBuilder( - roomId: widget.roomId, - message: message, - messageWidth: messageWidth, - ), + }) { + return ImageMessageBuilder( + roomId: widget.roomId, + message: message, + messageWidth: messageWidth, + ); + }, videoMessageBuilder: ( types.VideoMessage message, { required int messageWidth, - }) => - VideoMessageBuilder( - roomId: widget.roomId, - message: message, - messageWidth: messageWidth, - ), + }) { + return VideoMessageBuilder( + roomId: widget.roomId, + message: message, + messageWidth: messageWidth, + ); + }, fileMessageBuilder: ( types.FileMessage message, { required messageWidth, @@ -312,29 +314,23 @@ class _ChatRoomConsumerState extends ConsumerState { customMessageBuilder: ( types.CustomMessage message, { required int messageWidth, - }) => - CustomMessageBuilder( - message: message, - messageWidth: messageWidth, - ), - systemMessageBuilder: (msg) => renderSystemMessage(context, msg), + }) { + return CustomMessageBuilder( + message: message, + messageWidth: messageWidth, + ); + }, + systemMessageBuilder: (msg) => renderSysMessage(context, msg), showUserAvatars: !isDirectChat, - onMessageLongPress: ( - BuildContext context, - types.Message message, - ) async => - showMessageOptions(context, message), - + onMessageLongPress: showMessageOptions, onEndReached: ref .read(chatStateProvider(widget.roomId).notifier) .handleEndReached, onEndReachedThreshold: 0.75, - onBackgroundTap: () => - ref.read(chatInputProvider.notifier).unsetActions(), + onBackgroundTap: ref.read(chatInputProvider.notifier).unsetActions, typingIndicatorOptions: TypingIndicatorOptions( typingMode: TypingIndicatorMode.name, - typingUsers: - ref.watch(chatTypingEventProvider(roomId)).valueOrNull ?? [], + typingUsers: typingUsers, ), //Custom Theme class, see lib/common/store/chatTheme.dart theme: Theme.of(context).chatTheme, @@ -342,10 +338,7 @@ class _ChatRoomConsumerState extends ConsumerState { ); } - Widget renderSystemMessage( - BuildContext context, - types.SystemMessage message, - ) { + Widget renderSysMessage(BuildContext context, types.SystemMessage message) { return switch (message.metadata?['type']) { '_invite' => InviteSystemMessageWidget( message: message, diff --git a/app/lib/features/chat/pages/room_profile_page.dart b/app/lib/features/chat/pages/room_profile_page.dart index c8c0b22374d0..965e0e4f0451 100644 --- a/app/lib/features/chat/pages/room_profile_page.dart +++ b/app/lib/features/chat/pages/room_profile_page.dart @@ -32,8 +32,8 @@ class RoomProfilePage extends ConsumerStatefulWidget { final String roomId; const RoomProfilePage({ - required this.roomId, super.key, + required this.roomId, }); @override @@ -86,13 +86,11 @@ class _RoomProfilePageState extends ConsumerState { if (membership?.canString('CanSetTopic') == true) { menuListItems.add( PopupMenuItem( - onTap: () { - showEditDescriptionBottomSheet( - context: context, - convo: convo, - descriptionValue: convo?.topic() ?? '', - ); - }, + onTap: () => showEditDescriptionBottomSheet( + context: context, + convo: convo, + descriptionValue: convo?.topic() ?? '', + ), child: Text(L10n.of(context).editDescription), ), ); @@ -187,9 +185,9 @@ class _RoomProfilePageState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ InkWell( - onTap: isDirectChat - ? null - : () => openAvatar(context, ref, widget.roomId), + onTap: () { + if (!isDirectChat) openAvatar(context, ref, widget.roomId); + }, child: RoomAvatar( roomId: widget.roomId, avatarSize: 75, @@ -278,7 +276,7 @@ class _RoomProfilePageState extends ConsumerState { } Widget _actions(BuildContext context, Convo? convo, bool isDirectChat) { - final myMembership = ref.watch(roomMembershipProvider(widget.roomId)); + final membershipLoader = ref.watch(roomMembershipProvider(widget.roomId)); final isBookmarked = ref.watch(isConvoBookmarked(widget.roomId)).valueOrNull ?? false; @@ -297,11 +295,9 @@ class _RoomProfilePageState extends ConsumerState { ), // Invite - myMembership.when( + membershipLoader.when( data: (membership) { - if (membership == null || (isDirectChat)) { - return const SizedBox(); - } + if (membership == null || isDirectChat) return const SizedBox(); return _actionItem( context: context, iconData: Atlas.user_plus_thin, @@ -380,11 +376,7 @@ class _RoomProfilePageState extends ConsumerState { ); } - Widget _optionsBody( - BuildContext context, - Convo? convo, - bool isDirectChat, - ) { + Widget _optionsBody(BuildContext context, Convo? convo, bool isDirectChat) { return Column( children: [ // Notification section @@ -430,7 +422,7 @@ class _RoomProfilePageState extends ConsumerState { } Widget _convoMembersList() { - final members = ref.watch(membersIdsProvider(widget.roomId)); + final membersLoader = ref.watch(membersIdsProvider(widget.roomId)); return Container( width: double.infinity, @@ -442,19 +434,17 @@ class _RoomProfilePageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), - members.when( - data: (list) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: Text( - L10n.of(context).membersCount(list.length), - style: Theme.of(context).textTheme.titleSmall, - ), - ); - }, + membersLoader.when( + data: (members) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + L10n.of(context).membersCount(members.length), + style: Theme.of(context).textTheme.titleSmall, + ), + ), loading: () => Skeletonizer( child: Text(L10n.of(context).membersCount(0)), ), diff --git a/app/lib/features/chat/providers/notifiers/chat_room_notifier.dart b/app/lib/features/chat/providers/notifiers/chat_room_notifier.dart index 9791fe9c1366..ce5e366b4387 100644 --- a/app/lib/features/chat/providers/notifiers/chat_room_notifier.dart +++ b/app/lib/features/chat/providers/notifiers/chat_room_notifier.dart @@ -40,7 +40,15 @@ class ChatRoomNotifier extends StateNotifier { try { timeline = await ref.read(timelineStreamProvider(roomId).future); _listener = timeline.messagesStream(); // keep it resident in memory - _poller = _listener.listen(handleDiff); + _poller = _listener.listen( + handleDiff, + onError: (e, s) { + _log.severe('msg stream errored', e, s); + }, + onDone: () { + _log.info('msg stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); do { await loadMore(failOnError: true); diff --git a/app/lib/features/chat/widgets/chats_list.dart b/app/lib/features/chat/widgets/chats_list.dart index fa25dc0e39f0..06cfbf26894b 100644 --- a/app/lib/features/chat/widgets/chats_list.dart +++ b/app/lib/features/chat/widgets/chats_list.dart @@ -18,7 +18,10 @@ final _log = Logger('a3::chat::chats_list'); class ChatsList extends ConsumerWidget { final Function(String)? onSelected; - const ChatsList({this.onSelected, super.key}); + const ChatsList({ + super.key, + this.onSelected, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -48,10 +51,7 @@ class ChatsList extends ConsumerWidget { ), ); } - return _renderList( - context, - chatsIds, - ); + return _renderList(context, chatsIds); }, loading: () => const SliverToBoxAdapter( child: Center( @@ -95,9 +95,7 @@ class ChatsList extends ConsumerWidget { subtitle: L10n.of(context).getInTouchWithOtherChangeMakers, image: 'assets/images/empty_chat.svg', primaryButton: ActerPrimaryActionButton( - onPressed: () async => context.pushNamed( - Routes.createChat.name, - ), + onPressed: () => context.pushNamed(Routes.createChat.name), child: Text(L10n.of(context).sendDM), ), ), @@ -214,8 +212,10 @@ class __AnimatedChatsListState extends State<_AnimatedChatsList> { animation: animation, key: Key('convo-card-$roomId-removed'), roomId: roomId, - onTap: () => - widget.onSelected != null ? widget.onSelected!(roomId) : null, + onTap: () { + final onSelected = widget.onSelected; + if (onSelected != null) onSelected(roomId); + }, ); } @@ -235,8 +235,10 @@ class __AnimatedChatsListState extends State<_AnimatedChatsList> { animation: animation, key: Key('convo-card-$roomId'), roomId: roomId, - onTap: () => - widget.onSelected != null ? widget.onSelected!(roomId) : null, + onTap: () { + final onSelected = widget.onSelected; + if (onSelected != null) onSelected(roomId); + }, ); } diff --git a/app/lib/features/chat/widgets/create_chat.dart b/app/lib/features/chat/widgets/create_chat.dart index 7438ff0657f1..fe24c4b326b3 100644 --- a/app/lib/features/chat/widgets/create_chat.dart +++ b/app/lib/features/chat/widgets/create_chat.dart @@ -346,7 +346,7 @@ class _CreateChatWidgetConsumerState extends ConsumerState<_CreateChatWidget> { Widget renderFoundUsers(BuildContext context) { final searchCtrl = ref.watch(searchController); - final foundUsers = ref.watch(searchResultProvider); + final usersLoader = ref.watch(searchResultProvider); return Visibility( visible: searchCtrl.text.isNotEmpty, child: Column( @@ -356,24 +356,27 @@ class _CreateChatWidgetConsumerState extends ConsumerState<_CreateChatWidget> { L10n.of(context).foundUsers, style: Theme.of(context).textTheme.bodyMedium, ), - foundUsers.when( - data: (data) => data.isEmpty - ? Center( - heightFactor: 10, - child: Text( - L10n.of(context).noUsersFoundWithSpecifiedSearchTerm, - style: Theme.of(context).textTheme.bodySmall, - ), - ) - : ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: data.length, - itemBuilder: (context, index) => _UserWidget( - profile: data[index], - onUp: _onUp, - ), + usersLoader.when( + data: (users) { + if (users.isEmpty) { + return Center( + heightFactor: 10, + child: Text( + L10n.of(context).noUsersFoundWithSpecifiedSearchTerm, + style: Theme.of(context).textTheme.bodySmall, ), + ); + } + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: users.length, + itemBuilder: (context, index) => _UserWidget( + profile: users[index], + onUp: _onUp, + ), + ); + }, error: (e, s) { _log.severe('Failed to search users', e, s); return Text(L10n.of(context).errorLoadingUsers(e)); @@ -687,7 +690,7 @@ class _UserWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final avatarProv = ref.watch(userAvatarProvider(profile)); + final avatarLoader = ref.watch(userAvatarProvider(profile)); final displayName = profile.getDisplayName(); final userId = profile.userId().toString(); return ListTile( @@ -702,19 +705,17 @@ class _UserWidget extends ConsumerWidget { userId, style: Theme.of(context).textTheme.labelMedium, ), - leading: avatarProv.when( - data: (data) { - return ActerAvatar( - options: AvatarOptions.DM( - AvatarInfo( - uniqueId: userId, - displayName: displayName, - avatar: data, - ), - size: 18, + leading: avatarLoader.when( + data: (avatar) => ActerAvatar( + options: AvatarOptions.DM( + AvatarInfo( + uniqueId: userId, + displayName: displayName, + avatar: avatar, ), - ); - }, + size: 18, + ), + ), error: (e, s) { _log.severe('Failed to load binary data of avatar', e, s); return Text(L10n.of(context).errorLoadingAvatar(e)); diff --git a/app/lib/features/chat/widgets/member_list.dart b/app/lib/features/chat/widgets/member_list.dart index c3acd0e3c2bd..671b125bd532 100644 --- a/app/lib/features/chat/widgets/member_list.dart +++ b/app/lib/features/chat/widgets/member_list.dart @@ -12,15 +12,14 @@ class MemberList extends ConsumerWidget { final String roomId; const MemberList({ - required this.roomId, super.key, + required this.roomId, }); @override Widget build(BuildContext context, WidgetRef ref) { - final members = ref.watch(membersIdsProvider(roomId)); - - return members.when( + final membersLoader = ref.watch(membersIdsProvider(roomId)); + return membersLoader.when( data: (members) { if (members.isEmpty) { return Center( @@ -32,15 +31,13 @@ class MemberList extends ConsumerWidget { physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.symmetric(vertical: 5), itemCount: members.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: MemberListEntry( - memberId: members[index], - roomId: roomId, - ), - ); - }, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.all(8), + child: MemberListEntry( + memberId: members[index], + roomId: roomId, + ), + ), ); }, error: (e, s) { diff --git a/app/lib/features/chat/widgets/mention_profile_builder.dart b/app/lib/features/chat/widgets/mention_profile_builder.dart index 6d40f3e20871..baf2138b94dc 100644 --- a/app/lib/features/chat/widgets/mention_profile_builder.dart +++ b/app/lib/features/chat/widgets/mention_profile_builder.dart @@ -26,8 +26,8 @@ class MentionProfileBuilder extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final client = ref.watch(alwaysClientProvider); final userId = client.userId().toString(); - var memberIds = ref.watch(membersIdsProvider(roomQuery.roomId)); - return memberIds.when( + final membersLoader = ref.watch(membersIdsProvider(roomQuery.roomId)); + return membersLoader.when( loading: () => Skeletonizer( child: SizedBox( height: MediaQuery.of(context).size.height * 0.3, @@ -38,19 +38,17 @@ class MentionProfileBuilder extends ConsumerWidget { _log.severe('Failed to load room members', e, s); return ErrorWidget(L10n.of(context).loadingFailed(e)); }, - data: (data) { - final users = data.fold>({}, (map, uId) { + data: (members) { + final users = members.fold>({}, (map, uId) { if (uId != userId) { - final displayName = ref - .watch( - memberDisplayNameProvider( - (roomId: roomQuery.roomId, userId: uId), - ), - ) - .valueOrNull; + final displayName = ref.watch( + memberDisplayNameProvider( + (roomId: roomQuery.roomId, userId: uId), + ), + ); final normalizedId = uId.toLowerCase(); - final normalizedName = displayName ?? ''; + final normalizedName = displayName.valueOrNull ?? ''; final normalizedQuery = roomQuery.query.toLowerCase(); if (normalizedId.contains(normalizedQuery) || @@ -73,25 +71,17 @@ class MentionProfileBuilder extends ConsumerWidget { shrinkWrap: true, padding: const EdgeInsets.all(0), itemCount: users.length, - itemBuilder: (_, index) { - final id = users.keys.elementAt(index); + itemBuilder: (context, index) { + final userId = users.keys.elementAt(index); final displayName = users.values.elementAt(index); return ListTile( dense: true, - onTap: () { - final autocomplete = MultiTriggerAutocomplete.of(context); - ref - .read(chatInputProvider.notifier) - .addMention(displayName, id); - return autocomplete.acceptAutocompleteOption( - displayName.isNotEmpty ? displayName : id.substring(1), - ); - }, + onTap: () => onComplete(ref, userId, displayName), leading: Consumer( builder: (context, ref, child) { final avatarInfo = ref.watch( memberAvatarInfoProvider( - (roomId: roomQuery.roomId, userId: id), + (roomId: roomQuery.roomId, userId: userId), ), ); return ActerAvatar( @@ -105,7 +95,7 @@ class MentionProfileBuilder extends ConsumerWidget { title: Text(displayName), titleTextStyle: Theme.of(context).textTheme.bodyMedium, subtitleTextStyle: Theme.of(context).textTheme.labelMedium, - subtitle: displayName.isNotEmpty ? Text(id) : null, + subtitle: displayName.isNotEmpty ? Text(userId) : null, ); }, ), @@ -114,4 +104,11 @@ class MentionProfileBuilder extends ConsumerWidget { }, ); } + + void onComplete(WidgetRef ref, String userId, String displayName) { + final autocomplete = MultiTriggerAutocomplete.of(context); + ref.read(chatInputProvider.notifier).addMention(displayName, userId); + final option = displayName.isNotEmpty ? displayName : userId.substring(1); + return autocomplete.acceptAutocompleteOption(option); + } } diff --git a/app/lib/features/chat/widgets/message_metadata_builder.dart b/app/lib/features/chat/widgets/message_metadata_builder.dart index 5d6737086b2d..a2ac731ca6e8 100644 --- a/app/lib/features/chat/widgets/message_metadata_builder.dart +++ b/app/lib/features/chat/widgets/message_metadata_builder.dart @@ -5,13 +5,14 @@ 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_chat_types/flutter_chat_types.dart' as types; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quds_popup_menu/quds_popup_menu.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; class MessageMetadataBuilder extends ConsumerWidget { final String roomId; final types.Message message; + const MessageMetadataBuilder({ super.key, required this.roomId, @@ -60,22 +61,24 @@ class MessageMetadataBuilder extends ConsumerWidget { class _UserReceiptsWidget extends ConsumerWidget { final String roomId; final List seenList; - const _UserReceiptsWidget({required this.roomId, required this.seenList}); + static int limit = 5; + + const _UserReceiptsWidget({ + required this.roomId, + required this.seenList, + }); @override Widget build(BuildContext context, WidgetRef ref) { - final limit = seenList.length > 5 ? 5 : seenList.length; - final subList = - limit == seenList.length ? seenList : seenList.sublist(0, limit); return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: QudsPopupButton( items: showDetails(), child: Wrap( spacing: -16, - children: limit != seenList.length + children: seenList.length > limit ? [ - for (var userId in subList) + for (var userId in seenList.sublist(0, limit)) Consumer( builder: (context, ref, child) { final memberProfile = ref.watch( @@ -97,14 +100,13 @@ class _UserReceiptsWidget extends ConsumerWidget { CircleAvatar( radius: 8, child: Text( - '+${seenList.length - subList.length}', + '+${seenList.length - limit}', textScaler: const TextScaler.linear(0.4), ), ), ] - : List.generate( - seenList.length, - (idx) => Consumer( + : List.generate(seenList.length, (idx) { + return Consumer( builder: (context, ref, child) { final memberProfile = ref.watch( memberAvatarInfoProvider( @@ -121,8 +123,8 @@ class _UserReceiptsWidget extends ConsumerWidget { ), ); }, - ), - ), + ); + }), ), ), ); diff --git a/app/lib/features/chat/widgets/room_avatar.dart b/app/lib/features/chat/widgets/room_avatar.dart index 445009d64d22..ca3f48a311c8 100644 --- a/app/lib/features/chat/widgets/room_avatar.dart +++ b/app/lib/features/chat/widgets/room_avatar.dart @@ -87,8 +87,8 @@ class RoomAvatar extends ConsumerWidget { Widget dmAvatar(WidgetRef ref, BuildContext context) { final client = ref.watch(alwaysClientProvider); - final convoMembers = ref.watch(membersIdsProvider(roomId)); - return convoMembers.when( + final membersLoader = ref.watch(membersIdsProvider(roomId)); + return membersLoader.when( data: (members) { int count = members.length; @@ -128,20 +128,20 @@ class RoomAvatar extends ConsumerWidget { } Widget groupAvatarDM(List members, WidgetRef ref) { - final profile = ref - .watch(memberAvatarInfoProvider((userId: members[0], roomId: roomId))); - final secondaryProfile = ref - .watch(memberAvatarInfoProvider((userId: members[1], roomId: roomId))); + final profile = ref.watch( + memberAvatarInfoProvider((userId: members[0], roomId: roomId)), + ); + final secondaryProfile = ref.watch( + memberAvatarInfoProvider((userId: members[1], roomId: roomId)), + ); return ActerAvatar( options: AvatarOptions.GroupDM( profile, groupAvatars: [ secondaryProfile, - for (int i = 2; i < members.length; i++) - AvatarInfo( - uniqueId: members[i], - ), + for (var i = 2; i < members.length; i++) + AvatarInfo(uniqueId: members[i]), ], size: avatarSize / 2, ), diff --git a/app/lib/features/comments/providers/comments.dart b/app/lib/features/comments/providers/comments.dart index 26ef44137553..3a564408a726 100644 --- a/app/lib/features/comments/providers/comments.dart +++ b/app/lib/features/comments/providers/comments.dart @@ -2,6 +2,9 @@ import 'dart:async'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('a3::comments::manager'); final commentsManagerProvider = AsyncNotifierProvider.autoDispose.family< AsyncCommentsManagerNotifier, CommentsManager, Future>( @@ -17,10 +20,18 @@ class AsyncCommentsManagerNotifier extends AutoDisposeFamilyAsyncNotifier< FutureOr build(Future arg) async { final manager = await arg; _listener = manager.subscribeStream(); // keep it resident in memory - _poller = _listener.listen((e) async { - // reset - state = await AsyncValue.guard(() => manager.reload()); - }); + _poller = _listener.listen( + (data) async { + // reset + state = await AsyncValue.guard(() => manager.reload()); + }, + onError: (e, s) { + _log.severe('msg stream errored', e, s); + }, + onDone: () { + _log.info('msg stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return manager; } diff --git a/app/lib/features/comments/widgets/comments_list.dart b/app/lib/features/comments/widgets/comments_list.dart index e460998789dd..e34217e5c7c0 100644 --- a/app/lib/features/comments/widgets/comments_list.dart +++ b/app/lib/features/comments/widgets/comments_list.dart @@ -27,20 +27,21 @@ class _CommentsListState extends ConsumerState { @override Widget build(BuildContext context) { - return ref.watch(commentsListProvider(widget.manager)).when( - data: (manager) { - if (manager.isEmpty) { - return commentEmptyState(context); - } else { - return commentListUI(context, manager); - } - }, - error: (e, s) { - _log.severe('Failed to load comments', e, s); - return onError(context, e); - }, - loading: () => loading(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() { @@ -58,12 +59,7 @@ class _CommentsListState extends ConsumerState { children: [ Column( children: comments - .map( - (c) => CommentWidget( - comment: c, - manager: widget.manager, - ), - ) + .map((c) => CommentWidget(comment: c, manager: widget.manager)) .toList(), ), if (editorOpened) diff --git a/app/lib/features/comments/widgets/comments_section.dart b/app/lib/features/comments/widgets/comments_section.dart index 0929528a52d5..8e8a70193a84 100644 --- a/app/lib/features/comments/widgets/comments_section.dart +++ b/app/lib/features/comments/widgets/comments_section.dart @@ -25,14 +25,15 @@ class CommentsSection extends ConsumerWidget { if (!provider.isActive(LabsFeature.comments)) { return const SizedBox.shrink(); } - return ref.watch(commentsManagerProvider(manager)).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), - ); + 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), + ); } Widget inBox(BuildContext context, Widget child) { @@ -43,7 +44,10 @@ class CommentsSection extends ConsumerWidget { children: [ Row( children: [ - const Icon(Atlas.comment_blank_thin, size: 14), + const Icon( + Atlas.comment_blank_thin, + size: 14, + ), const SizedBox(width: 5), Text( L10n.of(context).comments, diff --git a/app/lib/features/cross_signing/providers/notifiers/verification_notifiers.dart b/app/lib/features/cross_signing/providers/notifiers/verification_notifiers.dart index e16f9fa57270..d4d1bab9a973 100644 --- a/app/lib/features/cross_signing/providers/notifiers/verification_notifiers.dart +++ b/app/lib/features/cross_signing/providers/notifiers/verification_notifiers.dart @@ -29,7 +29,15 @@ class VerificationNotifier extends StateNotifier { void _init() { _listener = client.verificationEventRx(); // keep it resident in memory - _poller = _listener?.listen(_handleEvent); + _poller = _listener?.listen( + _handleEvent, + onError: (e, s) { + _log.severe('stream errored', e, s); + }, + onDone: () { + _log.info('stream ended'); + }, + ); ref.onDispose(() => _poller?.cancel()); } diff --git a/app/lib/features/events/pages/event_details_page.dart b/app/lib/features/events/pages/event_details_page.dart index 98f1bc5567eb..6c807cc55126 100644 --- a/app/lib/features/events/pages/event_details_page.dart +++ b/app/lib/features/events/pages/event_details_page.dart @@ -43,7 +43,10 @@ final _log = Logger('a3::cal_event::details'); class EventDetailPage extends ConsumerStatefulWidget { final String calendarId; - const EventDetailPage({super.key, required this.calendarId}); + const EventDetailPage({ + super.key, + required this.calendarId, + }); @override ConsumerState createState() => @@ -55,17 +58,17 @@ class _EventDetailPageConsumerState extends ConsumerState { @override Widget build(BuildContext context) { - final event = ref.watch(calendarEventProvider(widget.calendarId)); + final calEventLoader = ref.watch(calendarEventProvider(widget.calendarId)); return Scaffold( - body: event.when( - data: (calendarEvent) { + body: calEventLoader.when( + data: (calEvent) { // Update event participants list - updateEventParticipantsList(calendarEvent); + updateEventParticipantsList(calEvent); return CustomScrollView( slivers: [ - _buildEventAppBar(calendarEvent), - _buildEventBody(calendarEvent), + _buildEventAppBar(calEvent), + _buildEventBody(calEvent), ], ); }, diff --git a/app/lib/features/events/pages/event_list_page.dart b/app/lib/features/events/pages/event_list_page.dart index ee026b397348..1878480733f6 100644 --- a/app/lib/features/events/pages/event_list_page.dart +++ b/app/lib/features/events/pages/event_list_page.dart @@ -71,7 +71,7 @@ class _EventListPageState extends ConsumerState { } Widget _buildBody() { - final eventList = ref.watch( + final calEventsLoader = ref.watch( eventListSearchFilterProvider( (spaceId: widget.spaceId, searchText: searchValue), ), @@ -83,8 +83,8 @@ class _EventListPageState extends ConsumerState { ActerSearchWidget(searchTextController: searchTextController), filterChipsButtons(), Expanded( - child: eventList.when( - data: (events) => _buildEventList(events), + child: calEventsLoader.when( + data: (calEvents) => _buildEventList(calEvents), error: (e, s) { _log.severe('Failed to search events in space', e, s); return Center( @@ -172,12 +172,12 @@ class _EventListPageState extends ConsumerState { } Widget _buildEventsEmptyState() { - bool canAdd = false; + var canAdd = false; if (searchValue.isEmpty) { - canAdd = ref - .watch(hasSpaceWithPermissionProvider('CanPostEvent')) - .valueOrNull ?? - false; + final canPostLoader = ref.watch( + hasSpaceWithPermissionProvider('CanPostEvent'), + ); + if (canPostLoader.valueOrNull == true) canAdd = true; } return Center( heightFactor: 1, diff --git a/app/lib/features/events/widgets/event_item.dart b/app/lib/features/events/widgets/event_item.dart index 41d9c14745bc..ce307002afbc 100644 --- a/app/lib/features/events/widgets/event_item.dart +++ b/app/lib/features/events/widgets/event_item.dart @@ -108,15 +108,12 @@ class EventItem extends StatelessWidget { return Consumer( builder: (context, ref, child) { final eventId = event.eventId().toString(); - final myRsvpStatus = ref.watch(myRsvpStatusProvider(eventId)); - return myRsvpStatus.when( - data: (data) { - final status = data.statusStr(); // kebab-case - final rsvpStatusWidget = - _getRsvpStatus(context, status); // kebab-case - return (rsvpStatusWidget != null) - ? rsvpStatusWidget - : const SizedBox.shrink(); + final rsvpLoader = ref.watch(myRsvpStatusProvider(eventId)); + return rsvpLoader.when( + data: (rsvp) { + final status = rsvp.statusStr(); // kebab-case + final widget = _getRsvpStatus(context, status); // kebab-case + return widget ?? const SizedBox.shrink(); }, error: (e, s) { _log.severe('Failed to load RSVP status', e, s); diff --git a/app/lib/features/home/providers/task_providers.dart b/app/lib/features/home/providers/task_providers.dart index 8c498548b198..c1ff3a0bf82f 100644 --- a/app/lib/features/home/providers/task_providers.dart +++ b/app/lib/features/home/providers/task_providers.dart @@ -2,8 +2,11 @@ import 'dart:async'; import 'package:acter/features/home/providers/client_providers.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod/riverpod.dart'; +final _log = Logger('a3::home::task'); + class MyOpenTasksNotifier extends AsyncNotifier> { late Stream _listener; late StreamSubscription _poller; @@ -14,9 +17,19 @@ class MyOpenTasksNotifier extends AsyncNotifier> { final client = ref.watch(alwaysClientProvider); _listener = client.subscribeMyOpenTasksStream(); // keep it resident in memory - _poller = _listener.listen((element) async { - state = await AsyncValue.guard(() async => await fetchMyOpenTask(client)); - }); + _poller = _listener.listen( + (data) async { + state = await AsyncValue.guard( + () async => await fetchMyOpenTask(client), + ); + }, + onError: (e, s) { + _log.severe('stream errored', e, s); + }, + onDone: () { + _log.info('stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return await fetchMyOpenTask(client); } diff --git a/app/lib/features/home/widgets/my_events.dart b/app/lib/features/home/widgets/my_events.dart index 0d8d1e1d4f36..4d7c3b351f40 100644 --- a/app/lib/features/home/widgets/my_events.dart +++ b/app/lib/features/home/widgets/my_events.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:acter/common/toolkit/buttons/inline_text_button.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/features/events/providers/event_providers.dart'; @@ -25,27 +27,25 @@ class MyEventsSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { //Get my events data - AsyncValue> myEvents; - String sectionTitle = ''; - if (eventFilters == EventFilters.ongoing) { - myEvents = ref.watch(myOngoingEventListProvider(null)); - sectionTitle = L10n.of(context).happeningNow; - } else { - myEvents = ref.watch(myUpcomingEventListProvider(null)); - sectionTitle = L10n.of(context).myUpcomingEvents; - } + final calEventsLoader = switch (eventFilters) { + EventFilters.ongoing => ref.watch(myOngoingEventListProvider(null)), + _ => ref.watch(myUpcomingEventListProvider(null)), + }; + final sectionTitle = switch (eventFilters) { + EventFilters.ongoing => L10n.of(context).happeningNow, + _ => L10n.of(context).myUpcomingEvents, + }; - return myEvents.when( - data: (events) { - return events.isNotEmpty - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - sectionHeader(context, sectionTitle), - eventListUI(context, events), - ], - ) - : const SizedBox.shrink(); + return calEventsLoader.when( + data: (calEvents) { + if (calEvents.isEmpty) return const SizedBox.shrink(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + sectionHeader(context, sectionTitle), + eventListUI(context, calEvents), + ], + ); }, error: (e, s) { _log.severe('Failed to load cal events', e, s); @@ -71,20 +71,16 @@ class MyEventsSection extends ConsumerWidget { ); } - Widget eventListUI( - BuildContext context, - List events, - ) { - int eventsLimit = - (limit != null && events.length > limit!) ? limit! : events.length; + Widget eventListUI(BuildContext context, List events) { + final count = limit == null ? events.length : min(events.length, limit!); return ListView.builder( shrinkWrap: true, - itemCount: eventsLimit, + itemCount: count, physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, idx) => EventItem( + itemBuilder: (context, index) => EventItem( isShowSpaceName: true, margin: const EdgeInsets.only(bottom: 14), - event: events[idx], + event: events[index], ), ); } diff --git a/app/lib/features/home/widgets/my_spaces_section.dart b/app/lib/features/home/widgets/my_spaces_section.dart index 2b464e1f9fe7..f1d9c5cf9d20 100644 --- a/app/lib/features/home/widgets/my_spaces_section.dart +++ b/app/lib/features/home/widgets/my_spaces_section.dart @@ -1,7 +1,8 @@ +import 'dart:math'; + import 'package:acter/common/providers/space_providers.dart'; import 'package:acter/common/themes/app_theme.dart'; import 'package:acter/common/toolkit/buttons/inline_text_button.dart'; - import 'package:acter/common/toolkit/buttons/primary_action_button.dart'; import 'package:acter/common/tutorial_dialogs/space_overview_tutorials/create_or_join_space_tutorials.dart'; import 'package:acter/common/utils/routes.dart'; @@ -9,14 +10,17 @@ import 'package:acter/common/widgets/spaces/space_card.dart'; import 'package:acter/features/home/data/keys.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:go_router/go_router.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; class MySpacesSection extends ConsumerWidget { final int? limit; - const MySpacesSection({super.key, this.limit}); + const MySpacesSection({ + super.key, + this.limit, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -40,13 +44,12 @@ class MySpacesSection extends ConsumerWidget { return const _NoSpacesWidget(); } - int spacesLimit = - (limit != null && spaces.length > limit!) ? limit! : spaces.length; + final count = limit == null ? spaces.length : min(spaces.length, limit!); return _RenderSpacesSection( spaces: spaces, - limit: spacesLimit, - showAll: spacesLimit != spaces.length, - showActions: spacesLimit == spaces.length, + limit: count, + showAll: count != spaces.length, + showActions: count == spaces.length, showAllCounter: spaces.length, title: InkWell( key: DashboardKeys.widgetMySpacesHeader, diff --git a/app/lib/features/home/widgets/my_tasks.dart b/app/lib/features/home/widgets/my_tasks.dart index 33abea0d1a5e..f8f800fe5533 100644 --- a/app/lib/features/home/widgets/my_tasks.dart +++ b/app/lib/features/home/widgets/my_tasks.dart @@ -14,39 +14,43 @@ final _log = Logger('a3::home::my_tasks'); class MyTasksSection extends ConsumerWidget { final int limit; - const MyTasksSection({super.key, required this.limit}); + const MyTasksSection({ + super.key, + required this.limit, + }); @override Widget build(BuildContext context, WidgetRef ref) { - final tasks = ref.watch(myOpenTasksProvider); - return tasks.when( - data: (tasks) => tasks.isEmpty - ? const SizedBox.shrink() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - myTaskHeader(context), - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (context, index) => const Divider( - color: Colors.white24, - indent: 30, + final tasksLoader = ref.watch(myOpenTasksProvider); + return tasksLoader.when( + data: (tasks) { + if (tasks.isEmpty) return const SizedBox.shrink(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + myTaskHeader(context), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => const Divider( + color: Colors.white24, + indent: 30, + ), + itemCount: tasks.length, + itemBuilder: (context, index) { + return TaskItem( + taskListId: tasks[index].taskListIdStr(), + taskId: tasks[index].eventIdStr(), + showBreadCrumb: true, + onDone: () => EasyLoading.showToast( + L10n.of(context).markedAsDone, ), - itemCount: tasks.length, - itemBuilder: (context, index) { - return TaskItem( - taskListId: tasks[index].taskListIdStr(), - taskId: tasks[index].eventIdStr(), - showBreadCrumb: true, - onDone: () => EasyLoading.showToast( - L10n.of(context).markedAsDone, - ), - ); - }, - ), - ], + ); + }, ), + ], + ); + }, error: (e, s) { _log.severe('Failed to load open tasks', e, s); return Text(L10n.of(context).loadingTasksFailed(e)); diff --git a/app/lib/features/home/widgets/space_chip.dart b/app/lib/features/home/widgets/space_chip.dart index de9e77420248..dda8703f511e 100644 --- a/app/lib/features/home/widgets/space_chip.dart +++ b/app/lib/features/home/widgets/space_chip.dart @@ -31,11 +31,9 @@ class SpaceChip extends ConsumerWidget { if (spaceId == null) { throw L10n.of(context).spaceOrSpaceIdMustBeProvided; } - final brief = ref.watch(briefSpaceItemProvider(spaceId!)); - return brief.when( - data: (space) { - return renderSpace(context, space); - }, + final spaceLoader = ref.watch(briefSpaceItemProvider(spaceId!)); + return spaceLoader.when( + data: (space) => renderSpace(context, space), error: (e, s) { _log.severe('Failed to load brief of space', e, s); return Chip( @@ -70,9 +68,15 @@ class SpaceChip extends ConsumerWidget { Text(L10n.of(context).inSpaceLabelInline), Text(L10n.of(context).colonCharacter), InkWell( - onTap: () => (onTapSelectSpace != null && !onTapOpenSpaceDetail) - ? onTapSelectSpace!() - : goToSpace(context, space.roomId), + onTap: () { + if (!onTapOpenSpaceDetail) { + if (onTapSelectSpace != null) { + onTapSelectSpace!(); + return; + } + } + goToSpace(context, space.roomId); + }, child: Text( spaceName, style: Theme.of(context).textTheme.labelLarge!.copyWith( @@ -83,9 +87,9 @@ class SpaceChip extends ConsumerWidget { ], ) : InkWell( - onTap: onTapOpenSpaceDetail - ? () => goToSpace(context, space.roomId) - : null, + onTap: () { + if (onTapOpenSpaceDetail) goToSpace(context, space.roomId); + }, child: Chip( avatar: ActerAvatar( options: AvatarOptions( diff --git a/app/lib/features/invite_members/pages/invite_individual_users.dart b/app/lib/features/invite_members/pages/invite_individual_users.dart index 9ce0150114f5..47d3ca5364df 100644 --- a/app/lib/features/invite_members/pages/invite_individual_users.dart +++ b/app/lib/features/invite_members/pages/invite_individual_users.dart @@ -8,10 +8,6 @@ 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:logging/logging.dart'; -import 'package:skeletonizer/skeletonizer.dart'; - -final _log = Logger('a3::invite::individual_users'); class InviteIndividualUsers extends ConsumerWidget { final String roomId; @@ -151,47 +147,38 @@ class InviteIndividualUsers extends ConsumerWidget { } Widget _buildFoundUserList(BuildContext context, WidgetRef ref) { - final foundUsers = ref.watch(searchResultProvider); - if (foundUsers.hasValue && foundUsers.value!.isNotEmpty) { - return Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Text( - L10n.of(context).usersfoundDirectory, - style: Theme.of(context).textTheme.titleSmall, - ), - ), - const SizedBox(height: 5), - Expanded( - child: ListView.builder( - itemCount: foundUsers.value!.length, - shrinkWrap: true, - itemBuilder: (context, index) { - return foundUsers.when( - data: (data) => UserBuilder( - userId: data[index].userId().toString(), + final usersLoader = ref.watch(searchResultProvider); + if (usersLoader.hasValue) { + final value = usersLoader.value; + if (value != null) { + if (value.isNotEmpty) { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Text( + L10n.of(context).usersfoundDirectory, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + const SizedBox(height: 5), + Expanded( + child: ListView.builder( + itemCount: value.length, + shrinkWrap: true, + itemBuilder: (context, index) => UserBuilder( + userId: value[index].userId().toString(), roomId: roomId, ), - error: (e, s) { - _log.severe('Failed to search users', e, s); - return Text(L10n.of(context).searchingFailed(e)); - }, - loading: () => Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Skeletonizer( - child: Text(L10n.of(context).loading), - ), - ), - ); - }, - ), + ), + ), + ], ), - ], - ), - ); + ); + } + } } return EmptyState( title: L10n.of(context).noUserFoundTitle, diff --git a/app/lib/features/invite_members/pages/invite_space_members.dart b/app/lib/features/invite_members/pages/invite_space_members.dart index 05be89885d5e..f4f440f71783 100644 --- a/app/lib/features/invite_members/pages/invite_space_members.dart +++ b/app/lib/features/invite_members/pages/invite_space_members.dart @@ -15,7 +15,10 @@ final _log = Logger('a3::invite::space_members'); class InviteSpaceMembers extends ConsumerStatefulWidget { final String roomId; - const InviteSpaceMembers({super.key, required this.roomId}); + const InviteSpaceMembers({ + super.key, + required this.roomId, + }); @override ConsumerState createState() => @@ -102,9 +105,9 @@ class _InviteSpaceMembersConsumerState } Widget _buildOtherSpace() { - final otherSpaces = + final spacesLoader = ref.watch(otherSpacesForInviteMembersProvider(widget.roomId)); - return otherSpaces.when( + return spacesLoader.when( data: _buildOtherSpaceData, error: (e, s) { _log.severe('Failed to load other spaces', e, s); @@ -116,13 +119,13 @@ class _InviteSpaceMembersConsumerState ); } - Widget _buildOtherSpaceData(List data) { + Widget _buildOtherSpaceData(List spaces) { return Expanded( child: ListView.builder( - itemCount: data.length, + itemCount: spaces.length, shrinkWrap: true, itemBuilder: (context, index) { - final roomId = data[index].getRoomIdStr(); + final roomId = spaces[index].getRoomIdStr(); return SpaceMemberInviteCard( roomId: roomId, isSelected: selectedSpaces.contains(roomId), diff --git a/app/lib/features/member/widgets/member_info_drawer.dart b/app/lib/features/member/widgets/member_info_drawer.dart index d56ce6aff3df..cdd2281a9317 100644 --- a/app/lib/features/member/widgets/member_info_drawer.dart +++ b/app/lib/features/member/widgets/member_info_drawer.dart @@ -164,88 +164,81 @@ class _MemberInfoDrawerInner extends ConsumerWidget { } List _roomMenu(BuildContext context, WidgetRef ref) { - return ref.watch(roomMembershipProvider(member.roomIdStr())).when( - data: (myMembership) { - if (myMembership == null) { - // showing just the power level - return [_roomTitle(context, ref), _showPowerLevel(context, null)]; - } + final roomId = member.roomIdStr(); + final membershipLoader = ref.watch(roomMembershipProvider(roomId)); + return membershipLoader.when( + data: (membership) { + if (membership == null) { + // showing just the power level + return [_roomTitle(context, ref), _showPowerLevel(context, null)]; + } - final menu = [_roomTitle(context, ref)]; + final menu = [_roomTitle(context, ref)]; - if (myMembership.canString('CanUpdatePowerLevels')) { - menu.add( - _showPowerLevel( - context, - () async { - await changePowerLevel(context, ref); - if (context.mounted) { - Navigator.pop(context); - } - }, - ), - ); - } else { - menu.add(_showPowerLevel(context, null)); - } + if (membership.canString('CanUpdatePowerLevels')) { + menu.add( + _showPowerLevel(context, () async { + await changePowerLevel(context, ref); + if (context.mounted) Navigator.pop(context); + }), + ); + } else { + menu.add(_showPowerLevel(context, null)); + } - if (myMembership.canString('CanKick')) { - menu.add( - MenuItemWidget( - iconData: Icons.eject_outlined, - title: L10n.of(context).kickUser, - withMenu: false, - onTap: () async { - await showKickUserDialog(context, member); - if (context.mounted) { - Navigator.pop(context); - } - }, - ), - ); + if (membership.canString('CanKick')) { + menu.add( + MenuItemWidget( + iconData: Icons.eject_outlined, + title: L10n.of(context).kickUser, + withMenu: false, + onTap: () async { + await showKickUserDialog(context, member); + if (context.mounted) Navigator.pop(context); + }, + ), + ); - if (myMembership.canString('CanBan')) { - menu.add( - MenuItemWidget( - iconData: Icons.gpp_bad_outlined, - title: L10n.of(context).kickAndBanUser, - withMenu: false, - onTap: () async { - await showKickAndBanUserDialog(context, member); - if (context.mounted) { - Navigator.pop(context); - } - }, - ), - ); - } - } - return menu; - }, - error: (e, s) { - _log.severe('Failed to load room membership', e, s); - return [ - _roomTitle(context, ref), + if (membership.canString('CanBan')) { + menu.add( MenuItemWidget( - iconData: Atlas.triangle_exclamation_thin, - title: L10n.of(context).loadingFailed(e), - withMenu: false, - onTap: () {}, - ), - ]; - }, - loading: () => [ - _roomTitle(context, ref), - Skeletonizer( - child: MenuItemWidget( - iconData: Atlas.medal_badge_award_thin, - title: L10n.of(context).changePowerLevel, + iconData: Icons.gpp_bad_outlined, + title: L10n.of(context).kickAndBanUser, withMenu: false, - onTap: () {}, + onTap: () async { + await showKickAndBanUserDialog(context, member); + if (context.mounted) Navigator.pop(context); + }, ), - ), - ], - ); + ); + } + } + return menu; + }, + error: (e, s) { + _log.severe('Failed to load room membership', e, s); + return [ + _roomTitle(context, ref), + MenuItemWidget( + iconData: Atlas.triangle_exclamation_thin, + title: L10n.of(context).loadingFailed(e), + withMenu: false, + onTap: () {}, + ), + ]; + }, + loading: () => [ + _roomTitle(context, ref), + Skeletonizer( + child: MenuItemWidget( + iconData: Atlas.medal_badge_award_thin, + title: L10n.of(context).changePowerLevel, + withMenu: false, + onTap: () {}, + ), + ), + ], + ); } Widget _roomTitle(BuildContext context, WidgetRef ref) { @@ -315,20 +308,23 @@ class MemberInfoDrawer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ref.watch(memberProvider((roomId: roomId, userId: memberId))).when( - data: (data) => _MemberInfoDrawerInner( - member: data, - memberId: memberId, - isShowActions: isShowActions, - ), - error: (e, s) { - _log.severe('Failed to load room member', e, s); - return Padding( - padding: const EdgeInsets.all(20.0), - child: Text(L10n.of(context).errorLoadingProfile(e)), - ); - }, - loading: () => const MemberInfoSkeleton(), + final memberLoader = ref.watch( + memberProvider((roomId: roomId, userId: memberId)), + ); + return memberLoader.when( + data: (member) => _MemberInfoDrawerInner( + member: member, + memberId: memberId, + isShowActions: isShowActions, + ), + error: (e, s) { + _log.severe('Failed to load room member', e, s); + return Padding( + padding: const EdgeInsets.all(20.0), + child: Text(L10n.of(context).errorLoadingProfile(e)), ); + }, + loading: () => const MemberInfoSkeleton(), + ); } } diff --git a/app/lib/features/news/pages/add_news_page.dart b/app/lib/features/news/pages/add_news_page.dart index 30849171ba3a..15b43ee487fb 100644 --- a/app/lib/features/news/pages/add_news_page.dart +++ b/app/lib/features/news/pages/add_news_page.dart @@ -206,16 +206,16 @@ class AddNewsState extends ConsumerState { //Show selected Action Buttons Widget selectedActionButtonsUI() { final newsReferences = selectedNewsPost?.newsReferencesModel; - if (newsReferences == null) return const SizedBox(); + final calEventId = newsReferences.id; return Positioned( bottom: 10, left: 10, child: Row( children: [ if (newsReferences.type == NewsReferencesType.shareEvent && - newsReferences.id != null) - ref.watch(calendarEventProvider(newsReferences.id!)).when( + calEventId != null) + ref.watch(calendarEventProvider(calEventId)).when( data: (calendarEvent) { return SizedBox( width: 300, diff --git a/app/lib/features/news/pages/news_page.dart b/app/lib/features/news/pages/news_page.dart index 4a115662fbef..e208cca198d3 100644 --- a/app/lib/features/news/pages/news_page.dart +++ b/app/lib/features/news/pages/news_page.dart @@ -11,9 +11,10 @@ class NewsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final canPostNews = - ref.watch(hasSpaceWithPermissionProvider('CanPostNews')).valueOrNull ?? - false; + final canPostLoader = ref.watch( + hasSpaceWithPermissionProvider('CanPostNews'), + ); + final canPostNews = canPostLoader.valueOrNull ?? false; return Scaffold( extendBodyBehindAppBar: true, diff --git a/app/lib/features/news/providers/notifiers/news_list_notifier.dart b/app/lib/features/news/providers/notifiers/news_list_notifier.dart index 6157d90d1df9..7ec4fabc0628 100644 --- a/app/lib/features/news/providers/notifiers/news_list_notifier.dart +++ b/app/lib/features/news/providers/notifiers/news_list_notifier.dart @@ -15,10 +15,18 @@ class AsyncNewsListNotifier extends AutoDisposeAsyncNotifier> { Future> build() async { final client = ref.watch(alwaysClientProvider); _listener = client.subscribeStream('news'); // keep it resident in memory - _poller = _listener.listen((e) async { - _log.info('new subscribe received'); - state = await AsyncValue.guard(_fetchNews); - }); + _poller = _listener.listen( + (data) async { + _log.info('news subscribe received'); + state = await AsyncValue.guard(_fetchNews); + }, + onError: (e, s) { + _log.severe('stream errored', e, s); + }, + onDone: () { + _log.info('stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return await _fetchNews(); } diff --git a/app/lib/features/news/widgets/news_item.dart b/app/lib/features/news/widgets/news_item.dart index 14e538037f63..02ba053429ad 100644 --- a/app/lib/features/news/widgets/news_item.dart +++ b/app/lib/features/news/widgets/news_item.dart @@ -43,7 +43,7 @@ class _NewsItemState extends ConsumerState { @override Widget build(BuildContext context) { final roomId = widget.news.roomId().toString(); - final space = ref.watch(briefSpaceItemProvider(roomId)); + final spaceLoader = ref.watch(briefSpaceItemProvider(roomId)); final slides = widget.news.slides().toList(); return Stack( @@ -54,39 +54,36 @@ class _NewsItemState extends ConsumerState { onPageChanged: (page) { currentSlideIndex.value = page; }, - itemBuilder: (context, idx) { - final slideType = slides[idx].typeStr(); - final bgColor = getBackgroundColor(slides[idx]); - final fgColor = getForegroundColor(slides[idx]); + itemBuilder: (context, index) { + final slide = slides[index]; + final slideType = slide.typeStr(); + final bgColor = getBackgroundColor(slide); + final fgColor = getForegroundColor(slide); switch (slideType) { case 'image': return ImageSlide( - slide: slides[idx], + slide: slide, bgColor: bgColor, fgColor: fgColor, ); - case 'video': return VideoSlide( - slide: slides[idx], + slide: slide, bgColor: bgColor, fgColor: fgColor, ); - case 'text': return TextSlide( - slide: slides[idx], + slide: slide, bgColor: bgColor, fgColor: fgColor, pageController: widget.pageController, ); - default: return Expanded( child: Center( - child: Text( - L10n.of(context).slidesNotYetSupported(slideType), - ), + child: + Text(L10n.of(context).slidesNotYetSupported(slideType)), ), ); } @@ -105,7 +102,7 @@ class _NewsItemState extends ConsumerState { onTap: () => goToSpace(context, roomId), child: Padding( padding: const EdgeInsets.all(16), - child: space.when( + child: spaceLoader.when( data: (space) => Text(space.avatarInfo.displayName ?? roomId), error: (e, s) { _log.severe('Failed to load brief of space', e, s); @@ -168,9 +165,7 @@ class _NewsItemState extends ConsumerState { ); } - Widget newsActionButtons({ - required NewsSlide newsSlide, - }) { + Widget newsActionButtons({required NewsSlide newsSlide}) { final newsReferencesList = newsSlide.references().toList(); if (newsReferencesList.isEmpty) return const SizedBox(); @@ -179,20 +174,17 @@ class _NewsItemState extends ConsumerState { final title = referenceDetails.title() ?? ''; if (title == NewsReferencesType.shareEvent.name) { - return ref.watch(calendarEventProvider(uriId)).when( - data: (calendarEvent) { - return EventItem( - event: calendarEvent, - ); - }, - loading: () => const EventItemSkeleton(), - error: (e, s) { - _log.severe('Failed to load cal event', e, s); - return Center( - child: Text(L10n.of(context).failedToLoadEvent(e)), - ); - }, + final calEventLoader = ref.watch(calendarEventProvider(uriId)); + return calEventLoader.when( + data: (calEvent) => EventItem(event: calEvent), + loading: () => const EventItemSkeleton(), + error: (e, s) { + _log.severe('Failed to load cal event', e, s); + return Center( + child: Text(L10n.of(context).failedToLoadEvent(e)), ); + }, + ); } else { return Card( child: Padding( diff --git a/app/lib/features/news/widgets/news_side_bar.dart b/app/lib/features/news/widgets/news_side_bar.dart index e98cca9090e9..127d4593746e 100644 --- a/app/lib/features/news/widgets/news_side_bar.dart +++ b/app/lib/features/news/widgets/news_side_bar.dart @@ -36,7 +36,7 @@ class NewsSideBar extends ConsumerWidget { final userId = ref.watch(myUserIdStrProvider); final isLikedByMe = ref.watch(likedByMeProvider(news)); final likesCount = ref.watch(totalLikesForNewsProvider(news)); - final space = ref.watch(briefSpaceItemProvider(roomId)); + final spaceLoader = ref.watch(briefSpaceItemProvider(roomId)); final style = Theme.of(context).textTheme.bodyLarge!.copyWith(fontSize: 13); return Column( @@ -61,7 +61,7 @@ class NewsSideBar extends ConsumerWidget { }, ), const SizedBox(height: 10), - space.maybeWhen( + spaceLoader.maybeWhen( data: (space) => InkWell( key: NewsUpdateKeys.newsSidebarActionBottomSheet, onTap: () => showModalBottomSheet( @@ -87,7 +87,7 @@ class NewsSideBar extends ConsumerWidget { ), ), const SizedBox(height: 10), - space.when( + spaceLoader.when( data: (space) => ActerAvatar( options: AvatarOptions( AvatarInfo( diff --git a/app/lib/features/news/widgets/news_widget.dart b/app/lib/features/news/widgets/news_widget.dart index a792a3147f64..326d91410da0 100644 --- a/app/lib/features/news/widgets/news_widget.dart +++ b/app/lib/features/news/widgets/news_widget.dart @@ -38,10 +38,10 @@ class _NewsWidgetState extends ConsumerState { @override Widget build(BuildContext context) { final client = ref.watch(alwaysClientProvider); - final newsList = ref.watch(newsListProvider); - return newsList.when( - data: (data) { - if (data.isEmpty) { + final newsListLoader = ref.watch(newsListProvider); + return newsListLoader.when( + data: (newsList) { + if (newsList.isEmpty) { return Center( child: EmptyState( title: L10n.of(context).youHaveNoUpdates, @@ -56,12 +56,12 @@ class _NewsWidgetState extends ConsumerState { } return PageView.builder( controller: _pageController, - itemCount: data.length, + itemCount: newsList.length, scrollDirection: Axis.vertical, itemBuilder: (context, index) => InkWell( onDoubleTap: () async { LikeAnimation.run(index); - final news = data[index]; + final news = newsList[index]; final manager = await ref.read(newsReactionsProvider(news).future); final status = manager.likedByMe(); @@ -71,7 +71,7 @@ class _NewsWidgetState extends ConsumerState { }, child: NewsItem( client: client, - news: data[index], + news: newsList[index], index: index, pageController: _pageController, ), diff --git a/app/lib/features/pins/pages/pin_details_page.dart b/app/lib/features/pins/pages/pin_details_page.dart index eabf2cf2edb7..d9f1662d1346 100644 --- a/app/lib/features/pins/pages/pin_details_page.dart +++ b/app/lib/features/pins/pages/pin_details_page.dart @@ -78,10 +78,10 @@ class _PinDetailsPageState extends ConsumerState { loading: () => Skeletonizer( child: Text(L10n.of(context).loadingPin), ), - error: (err, st) { - _log.severe('Error loading pin', err, st); + error: (e, s) { + _log.severe('Error loading pin', e, s); return Text( - L10n.of(context).errorLoadingPin(err), + L10n.of(context).errorLoadingPin(e), ); }, ); @@ -106,7 +106,10 @@ class _PinDetailsPageState extends ConsumerState { ); }, loading: () => Skeletonizer(child: Text(L10n.of(context).loadingPin)), - error: (err, st) => const SizedBox.shrink(), + error: (e, s) { + _log.severe('Error loading pin', e, s); + return const SizedBox.shrink(); + }, ); } diff --git a/app/lib/features/pins/pages/pins_list_page.dart b/app/lib/features/pins/pages/pins_list_page.dart index 7d1e581c1ae8..c6dfee883a2b 100644 --- a/app/lib/features/pins/pages/pins_list_page.dart +++ b/app/lib/features/pins/pages/pins_list_page.dart @@ -23,7 +23,10 @@ final _log = Logger('a3::pins::list'); class PinsListPage extends ConsumerStatefulWidget { final String? spaceId; - const PinsListPage({super.key, this.spaceId}); + const PinsListPage({ + super.key, + this.spaceId, + }); @override ConsumerState createState() => _AllPinsPageConsumerState(); @@ -43,6 +46,7 @@ class _AllPinsPageConsumerState extends ConsumerState { } AppBar _buildAppBar() { + final spaceId = widget.spaceId; return AppBar( centerTitle: false, title: Column( @@ -50,10 +54,7 @@ class _AllPinsPageConsumerState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(L10n.of(context).pins), - if (widget.spaceId != null) - SpaceNameWidget( - spaceId: widget.spaceId!, - ), + if (spaceId != null) SpaceNameWidget(spaceId: spaceId), ], ), actions: [ @@ -69,26 +70,23 @@ class _AllPinsPageConsumerState extends ConsumerState { } Widget _buildBody() { - AsyncValue> pinList; - + AsyncValue> pinsLoader; if (searchValue.isNotEmpty) { - pinList = ref.watch( + pinsLoader = ref.watch( pinListSearchProvider( (spaceId: widget.spaceId, searchText: searchValue), ), ); } else { - pinList = ref.watch(pinListProvider(widget.spaceId)); + pinsLoader = ref.watch(pinListProvider(widget.spaceId)); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ActerSearchWidget( - searchTextController: searchTextController, - ), + ActerSearchWidget(searchTextController: searchTextController), Expanded( - child: pinList.when( + child: pinsLoader.when( data: (pins) => _buildPinsList(pins), error: (e, s) { _log.severe('Failed to load pins', e, s); @@ -125,11 +123,12 @@ class _AllPinsPageConsumerState extends ConsumerState { } Widget _buildPinsEmptyState() { - bool canAdd = false; + var canAdd = false; if (searchValue.isEmpty) { - canAdd = - ref.watch(hasSpaceWithPermissionProvider('CanPostPin')).valueOrNull ?? - false; + final canPostLoader = ref.watch( + hasSpaceWithPermissionProvider('CanPostPin'), + ); + if (canPostLoader.valueOrNull == true) canAdd = true; } return Center( heightFactor: 1, @@ -139,7 +138,7 @@ class _AllPinsPageConsumerState extends ConsumerState { : L10n.of(context).noPinsAvailableYet, subtitle: L10n.of(context).noPinsAvailableDescription, image: 'assets/images/empty_pin.svg', - primaryButton: canAdd && searchValue.isEmpty + primaryButton: canAdd ? ActerPrimaryActionButton( onPressed: () => context.pushNamed( Routes.createPin.name, diff --git a/app/lib/features/pins/providers/notifiers/pins_notifiers.dart b/app/lib/features/pins/providers/notifiers/pins_notifiers.dart index 0464e83e6401..9f27c59362bd 100644 --- a/app/lib/features/pins/providers/notifiers/pins_notifiers.dart +++ b/app/lib/features/pins/providers/notifiers/pins_notifiers.dart @@ -1,8 +1,12 @@ import 'dart:async'; + import 'package:acter/features/home/providers/client_providers.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod/riverpod.dart'; +final _log = Logger('a3::pins::pins_notifier'); + //Get single pin details class AsyncPinNotifier extends AutoDisposeFamilyAsyncNotifier { @@ -18,9 +22,17 @@ class AsyncPinNotifier Future build(String arg) async { final client = ref.watch(alwaysClientProvider); _listener = client.subscribeStream(arg); // keep it resident in memory - _poller = _listener.listen((e) async { - state = await AsyncValue.guard(_getPin); - }); // stay up to date + _poller = _listener.listen( + (data) async { + state = await AsyncValue.guard(_getPin); + }, + onError: (e, s) { + _log.severe('stream errored', e, s); + }, + onDone: () { + _log.info('stream ended'); + }, + ); // stay up to date ref.onDispose(() => _poller.cancel()); return await _getPin(); } @@ -44,9 +56,17 @@ class AsyncPinListNotifier _listener = client.subscribeStream('$arg::pins'); } - _poller = _listener.listen((e) async { - state = await AsyncValue.guard(() => _getPinList(client)); - }); + _poller = _listener.listen( + (data) async { + state = await AsyncValue.guard(() => _getPinList(client)); + }, + onError: (e, s) { + _log.severe('stream errored', e, s); + }, + onDone: () { + _log.info('stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return await _getPinList(client); } diff --git a/app/lib/features/pins/widgets/fake_link_attachment_item.dart b/app/lib/features/pins/widgets/fake_link_attachment_item.dart index a5fc042a3f8f..6d2c086a5043 100644 --- a/app/lib/features/pins/widgets/fake_link_attachment_item.dart +++ b/app/lib/features/pins/widgets/fake_link_attachment_item.dart @@ -28,20 +28,14 @@ class FakeLinkAttachmentItem extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final pinData = ref.watch(pinProvider(pinId)); return pinData.when( - data: (pin) { - return fakeLinkAttachmentItemUI( - context, - ref, - pin, - ); - }, + data: (pin) => fakeLinkAttachmentItemUI(context, ref, pin), loading: () => Skeletonizer( child: Text(L10n.of(context).loadingPin), ), - error: (err, st) { - _log.severe('Error loading pin', err, st); + error: (e, s) { + _log.severe('Error loading pin', e, s); return Text( - L10n.of(context).errorLoadingPin(err), + L10n.of(context).errorLoadingPin(e), ); }, ); diff --git a/app/lib/features/public_room_search/widgets/public_room_item.dart b/app/lib/features/public_room_search/widgets/public_room_item.dart index 3587f76aad3e..a95c85ca6658 100644 --- a/app/lib/features/public_room_search/widgets/public_room_item.dart +++ b/app/lib/features/public_room_search/widgets/public_room_item.dart @@ -22,20 +22,22 @@ class _JoinBtn extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ref.watch(roomMembershipProvider(item.roomIdStr())).when( - data: (data) => - data == null ? noMember(context) : alreadyMember(context), - error: (e, s) { - _log.severe('Failed to load room membership', e, s); - return Text(L10n.of(context).loadingFailed(e)); - }, - loading: () => Skeletonizer( - child: OutlinedButton( - onPressed: () => onSelected(item), - child: Text(L10n.of(context).requestToJoin), - ), - ), - ); + final roomId = item.roomIdStr(); + final membershipLoader = ref.watch(roomMembershipProvider(roomId)); + return membershipLoader.when( + data: (membership) => + membership == null ? noMember(context) : alreadyMember(context), + error: (e, s) { + _log.severe('Failed to load room membership', e, s); + return Text(L10n.of(context).loadingFailed(e)); + }, + loading: () => Skeletonizer( + child: OutlinedButton( + onPressed: () => onSelected(item), + child: Text(L10n.of(context).requestToJoin), + ), + ), + ); } Widget alreadyMember(BuildContext context) { @@ -72,7 +74,7 @@ class PublicRoomItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final profileInfo = ref.watch(searchItemProfileData(item)); + final avatarLoader = ref.watch(searchItemProfileData(item)); final topic = item.topic(); return Card( @@ -86,9 +88,9 @@ class PublicRoomItem extends ConsumerWidget { padding: const EdgeInsets.symmetric(vertical: 5), child: ListTile( onTap: () => onSelected(item), - leading: profileInfo.when( - data: (profile) => ActerAvatar( - options: AvatarOptions(profile), + leading: avatarLoader.when( + data: (avatar) => ActerAvatar( + options: AvatarOptions(avatar), ), error: (e, s) { _log.severe('Failed to load avatar info', e, s); diff --git a/app/lib/features/search/widgets/pins_builder.dart b/app/lib/features/search/widgets/pins_builder.dart index c75a06ee9b46..4be68a8717ff 100644 --- a/app/lib/features/search/widgets/pins_builder.dart +++ b/app/lib/features/search/widgets/pins_builder.dart @@ -10,6 +10,7 @@ final _log = Logger('a3::search::pins_builder'); class PinsBuilder extends ConsumerWidget { final bool popBeforeRoute; + const PinsBuilder({ super.key, required this.popBeforeRoute, @@ -17,60 +18,52 @@ class PinsBuilder extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final foundPins = ref.watch(pinsFoundProvider); - return foundPins.when( + final pinsLoader = ref.watch(pinsFoundProvider); + return pinsLoader.when( loading: () => Text(L10n.of(context).loading), error: (e, s) { _log.severe('Failed to search pins', e, s); return Text(L10n.of(context).searchingFailed(e)); }, - data: (data) { - final Widget body; - if (data.isEmpty) { - body = Text(L10n.of(context).noMatchingPinsFound); - } else { - final List children = data - .map( - (e) => InkWell( - child: Padding( - padding: const EdgeInsets.all(10), - child: Row( - children: [ - e.icon, - const SizedBox(width: 5), - Text(e.name), - ], - ), - ), - onTap: () async { - if (popBeforeRoute) { - Navigator.pop(context); - } - context.pushNamed( - Routes.pin.name, - pathParameters: {'pinId': e.navigationTargetId}, + data: (pins) => Padding( + padding: const EdgeInsets.only(top: 10), + child: Column( + children: [ + Text(L10n.of(context).pins), + const SizedBox(height: 15), + if (pins.isEmpty) + Text(L10n.of(context).noMatchingPinsFound) + else + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: pins.map((pin) { + return InkWell( + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + pin.icon, + const SizedBox(width: 5), + Text(pin.name), + ], + ), + ), + onTap: () { + if (popBeforeRoute) Navigator.pop(context); + context.pushNamed( + Routes.pin.name, + pathParameters: {'pinId': pin.navigationTargetId}, + ); + }, ); - }, + }).toList(), ), - ) - .toList(); - body = SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row(children: children), - ); - } - return Padding( - padding: const EdgeInsets.only(top: 10), - child: Column( - children: [ - Text(L10n.of(context).pins), - const SizedBox(height: 15), - body, - const SizedBox(height: 10), - ], - ), - ); - }, + ), + const SizedBox(height: 10), + ], + ), + ), ); } } diff --git a/app/lib/features/search/widgets/quick_actions_builder.dart b/app/lib/features/search/widgets/quick_actions_builder.dart index 3247e7f334ca..38a57eeb1a06 100644 --- a/app/lib/features/search/widgets/quick_actions_builder.dart +++ b/app/lib/features/search/widgets/quick_actions_builder.dart @@ -28,24 +28,27 @@ class QuickActionsBuilder extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final features = ref.watch(featuresProvider); bool isActive(f) => features.isActive(f); - final canPostNewsProvider = ref.watch( + + final canPostNewsLoader = ref.watch( hasSpaceWithPermissionProvider('CanPostNews'), ); - final canPostNews = canPostNewsProvider.valueOrNull ?? false; + final canPostNews = canPostNewsLoader.valueOrNull ?? false; - final canPostPinProvider = ref.watch( + final canPostPinLoader = ref.watch( hasSpaceWithPermissionProvider('CanPostPin'), ); - final canPostPin = canPostPinProvider.valueOrNull ?? false; + final canPostPin = canPostPinLoader.valueOrNull ?? false; - final canPostEventProvider = ref.watch( + final canPostEventLoader = ref.watch( hasSpaceWithPermissionProvider('CanPostEvent'), ); - final canCreateTaskListProvider = ref.watch( + final canPostEvent = canPostEventLoader.valueOrNull ?? false; + + final canPostTasklistLoader = ref.watch( hasSpaceWithPermissionProvider('CanPostTaskList'), ); - final canPostEvent = (canPostEventProvider.valueOrNull ?? false); - final canPostTaskList = canCreateTaskListProvider.valueOrNull ?? false; + final canPostTasklist = canPostTasklistLoader.valueOrNull ?? false; + return Wrap( alignment: WrapAlignment.spaceEvenly, spacing: 8, @@ -88,7 +91,7 @@ class QuickActionsBuilder extends ConsumerWidget { style: Theme.of(context).textTheme.labelMedium, ), ), - if (canPostTaskList) + if (canPostTasklist) OutlinedButton.icon( key: QuickJumpKeys.createTaskListAction, onPressed: () => showCreateUpdateTaskListBottomSheet(context), diff --git a/app/lib/features/search/widgets/spaces_builder.dart b/app/lib/features/search/widgets/spaces_builder.dart index 665c69b2f66a..d4397db606d9 100644 --- a/app/lib/features/search/widgets/spaces_builder.dart +++ b/app/lib/features/search/widgets/spaces_builder.dart @@ -22,8 +22,8 @@ class SpacesBuilder extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final foundSpaces = ref.watch(spacesFoundProvider); - return foundSpaces.when( + final spacesLoader = ref.watch(spacesFoundProvider); + return spacesLoader.when( loading: () => renderLoading(context), error: (e, s) { _log.severe('Failed to search spaces', e, s); @@ -32,11 +32,9 @@ class SpacesBuilder extends ConsumerWidget { Text(L10n.of(context).searchingFailed(e)), ); }, - data: (data) { - if (data.isEmpty) { - return renderEmpty(context, ref); - } - return renderItems(context, ref, data); + data: (spaces) { + if (spaces.isEmpty) return renderEmpty(context, ref); + return renderItems(context, ref, spaces); }, ); } @@ -52,9 +50,13 @@ class SpacesBuilder extends ConsumerWidget { padding: const EdgeInsets.all(10), child: const Column( children: [ - Skeletonizer(child: Icon(Icons.abc)), + Skeletonizer( + child: Icon(Icons.abc), + ), SizedBox(height: 3), - Skeletonizer(child: Text('space name')), + Skeletonizer( + child: Text('space name'), + ), ], ), ), @@ -66,35 +68,31 @@ class SpacesBuilder extends ConsumerWidget { Widget renderItems( BuildContext context, WidgetRef ref, - List items, + List spaces, ) { return inBox( context, SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( - children: items - .map( - (e) => InkWell( - child: Container( - padding: const EdgeInsets.all(10), - child: Column( - children: [ - e.icon, - const SizedBox(height: 3), - Text(e.name), - ], - ), - ), - onTap: () { - if (popBeforeRoute) { - Navigator.pop(context); - } - goToSpace(context, e.navigationTargetId); - }, + children: spaces.map((space) { + return InkWell( + child: Container( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + space.icon, + const SizedBox(height: 3), + Text(space.name), + ], ), - ) - .toList(), + ), + onTap: () { + if (popBeforeRoute) Navigator.pop(context); + goToSpace(context, space.navigationTargetId); + }, + ); + }).toList(), ), ), ); @@ -106,9 +104,7 @@ class SpacesBuilder extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - L10n.of(context).noSpacesFound, - ), + Text(L10n.of(context).noSpacesFound), ActerInlineTextButton( onPressed: () { final query = ref.read(searchValueProvider); diff --git a/app/lib/features/settings/pages/blocked_users.dart b/app/lib/features/settings/pages/blocked_users.dart index ae17f274ad39..2d833d4128fb 100644 --- a/app/lib/features/settings/pages/blocked_users.dart +++ b/app/lib/features/settings/pages/blocked_users.dart @@ -69,7 +69,7 @@ class BlockedUsersPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final users = ref.watch(ignoredUsersProvider); + final usersLoader = ref.watch(ignoredUsersProvider); return WithSidebar( sidebar: const SettingsPage(), child: Scaffold( @@ -86,38 +86,41 @@ class BlockedUsersPage extends ConsumerWidget { ), ], ), - body: users.when( - data: (users) => users.isNotEmpty - ? CustomScrollView( - slivers: [ - SliverList.builder( - itemBuilder: (BuildContext context, int index) { - final userId = users[index].toString(); - return Card( - margin: const EdgeInsets.all(5), - child: ListTile( - title: Padding( - padding: const EdgeInsets.all(10), - child: Text(userId), - ), - trailing: OutlinedButton( - child: Text(L10n.of(context).unblock), - onPressed: () async => await onDelete( - context, - ref, - userId, - ), - ), + body: usersLoader.when( + data: (users) { + if (users.isEmpty) { + return Center( + child: Text(L10n.of(context).hereYouCanSeeAllUsersYouBlocked), + ); + } + return CustomScrollView( + slivers: [ + SliverList.builder( + itemBuilder: (BuildContext context, int index) { + final userId = users[index].toString(); + return Card( + margin: const EdgeInsets.all(5), + child: ListTile( + title: Padding( + padding: const EdgeInsets.all(10), + child: Text(userId), + ), + trailing: OutlinedButton( + child: Text(L10n.of(context).unblock), + onPressed: () async => await onDelete( + context, + ref, + userId, ), - ); - }, - itemCount: users.length, - ), - ], - ) - : Center( - child: Text(L10n.of(context).hereYouCanSeeAllUsersYouBlocked), + ), + ), + ); + }, + itemCount: users.length, ), + ], + ); + }, error: (e, s) { _log.severe('Failed to load the ignored users', e, s); return Center( diff --git a/app/lib/features/settings/pages/chat_settings_page.dart b/app/lib/features/settings/pages/chat_settings_page.dart index 9c9005bc28d4..60aadfbb5420 100644 --- a/app/lib/features/settings/pages/chat_settings_page.dart +++ b/app/lib/features/settings/pages/chat_settings_page.dart @@ -17,117 +17,117 @@ class ChatSettingsPage extends ConsumerWidget { const ChatSettingsPage({super.key}); AbstractSettingsTile _autoDownload(BuildContext context, WidgetRef ref) { - return ref.watch(userAppSettingsProvider).when( - data: (settings) => OptionsSettingsTile( - selected: settings.autoDownloadChat() ?? 'always', - title: L10n.of(context).chatSettingsAutoDownload, - explainer: L10n.of(context).chatSettingsAutoDownloadExplainer, - options: [ - ('always', L10n.of(context).chatSettingsAutoDownloadAlways), - ('wifiOnly', L10n.of(context).chatSettingsAutoDownloadWifiOnly), - ('never', L10n.of(context).chatSettingsAutoDownloadNever), - ], - onSelect: (String newVal) async { - EasyLoading.show(status: L10n.of(context).settingsSubmitting); - try { - final updater = settings.updateBuilder(); - updater.autoDownloadChat(newVal); - await updater.send(); - if (!context.mounted) { - EasyLoading.dismiss(); - return; - } - EasyLoading.showToast( - L10n.of(context).settingsSubmittingSuccess, - toastPosition: EasyLoadingToastPosition.bottom, - ); - } catch (e, s) { - _log.severe('Failure submitting settings', e, s); - if (!context.mounted) { - EasyLoading.dismiss(); - return; - } - EasyLoading.showError( - L10n.of(context).settingsSubmittingFailed(e), - duration: const Duration(seconds: 3), - ); - } - }, - ), - error: (e, s) { - _log.severe('Failed to load user app settings', e, s); - return SettingsTile.navigation( - title: Text(L10n.of(context).loadingFailed(e)), + final settingsLoader = ref.watch(userAppSettingsProvider); + return settingsLoader.when( + data: (settings) => OptionsSettingsTile( + selected: settings.autoDownloadChat() ?? 'always', + title: L10n.of(context).chatSettingsAutoDownload, + explainer: L10n.of(context).chatSettingsAutoDownloadExplainer, + options: [ + ('always', L10n.of(context).chatSettingsAutoDownloadAlways), + ('wifiOnly', L10n.of(context).chatSettingsAutoDownloadWifiOnly), + ('never', L10n.of(context).chatSettingsAutoDownloadNever), + ], + onSelect: (newVal) async { + EasyLoading.show(status: L10n.of(context).settingsSubmitting); + try { + final updater = settings.updateBuilder(); + updater.autoDownloadChat(newVal); + await updater.send(); + if (!context.mounted) { + EasyLoading.dismiss(); + return; + } + EasyLoading.showToast( + L10n.of(context).settingsSubmittingSuccess, + toastPosition: EasyLoadingToastPosition.bottom, ); - }, - loading: () => SettingsTile.switchTile( - title: Skeletonizer( - child: Text(L10n.of(context).chatSettingsAutoDownload), - ), - enabled: false, - description: Skeletonizer( - child: Text(L10n.of(context).sharedCalendarAndEvents), - ), - initialValue: false, - onToggle: (newVal) {}, - ), + } catch (e, s) { + _log.severe('Failure submitting settings', e, s); + if (!context.mounted) { + EasyLoading.dismiss(); + return; + } + EasyLoading.showError( + L10n.of(context).settingsSubmittingFailed(e), + duration: const Duration(seconds: 3), + ); + } + }, + ), + error: (e, s) { + _log.severe('Failed to load user app settings', e, s); + return SettingsTile.navigation( + title: Text(L10n.of(context).loadingFailed(e)), ); + }, + loading: () => SettingsTile.switchTile( + title: Skeletonizer( + child: Text(L10n.of(context).chatSettingsAutoDownload), + ), + enabled: false, + description: Skeletonizer( + child: Text(L10n.of(context).sharedCalendarAndEvents), + ), + initialValue: false, + onToggle: (newVal) {}, + ), + ); } AbstractSettingsTile _typingNotice(BuildContext context, WidgetRef ref) { - return ref.watch(userAppSettingsProvider).when( - data: (settings) => SettingsTile.switchTile( - title: Text(L10n.of(context).chatSettingsTyping), - description: Text( - L10n.of(context).chatSettingsTypingExplainer, - ), - enabled: true, - initialValue: settings.typingNotice() ?? true, - onToggle: (newVal) async { - EasyLoading.show(status: L10n.of(context).settingsSubmitting); - try { - final updater = settings.updateBuilder(); - updater.typingNotice(newVal); - await updater.send(); - if (!context.mounted) { - EasyLoading.dismiss(); - return; - } - EasyLoading.showToast( - L10n.of(context).settingsSubmittingSuccess, - toastPosition: EasyLoadingToastPosition.bottom, - ); - } catch (e, s) { - _log.severe('Failure submitting settings', e, s); - if (!context.mounted) { - EasyLoading.dismiss(); - return; - } - EasyLoading.showError( - L10n.of(context).settingsSubmittingFailed(e), - duration: const Duration(seconds: 3), - ); - } - }, - ), - error: (e, s) { - _log.severe('Failed to load user app settings', e, s); - return SettingsTile.navigation( - title: Text(L10n.of(context).loadingFailed(e)), + final settingsLoader = ref.watch(userAppSettingsProvider); + return settingsLoader.when( + data: (settings) => SettingsTile.switchTile( + title: Text(L10n.of(context).chatSettingsTyping), + description: Text(L10n.of(context).chatSettingsTypingExplainer), + enabled: true, + initialValue: settings.typingNotice() ?? true, + onToggle: (newVal) async { + EasyLoading.show(status: L10n.of(context).settingsSubmitting); + try { + final updater = settings.updateBuilder(); + updater.typingNotice(newVal); + await updater.send(); + if (!context.mounted) { + EasyLoading.dismiss(); + return; + } + EasyLoading.showToast( + L10n.of(context).settingsSubmittingSuccess, + toastPosition: EasyLoadingToastPosition.bottom, ); - }, - loading: () => SettingsTile.switchTile( - title: Skeletonizer( - child: Text(L10n.of(context).chatSettingsTyping), - ), - enabled: false, - description: Skeletonizer( - child: Text(L10n.of(context).chatSettingsTypingExplainer), - ), - initialValue: false, - onToggle: (newVal) {}, - ), + } catch (e, s) { + _log.severe('Failure submitting settings', e, s); + if (!context.mounted) { + EasyLoading.dismiss(); + return; + } + EasyLoading.showError( + L10n.of(context).settingsSubmittingFailed(e), + duration: const Duration(seconds: 3), + ); + } + }, + ), + error: (e, s) { + _log.severe('Failed to load user app settings', e, s); + return SettingsTile.navigation( + title: Text(L10n.of(context).loadingFailed(e)), ); + }, + loading: () => SettingsTile.switchTile( + title: Skeletonizer( + child: Text(L10n.of(context).chatSettingsTyping), + ), + enabled: false, + description: Skeletonizer( + child: Text(L10n.of(context).chatSettingsTypingExplainer), + ), + initialValue: false, + onToggle: (newVal) {}, + ), + ); } @override @@ -148,9 +148,8 @@ class ChatSettingsPage extends ConsumerWidget { _typingNotice(context, ref), SettingsTile.switchTile( title: Text(L10n.of(context).chatSettingsReadReceipts), - description: Text( - L10n.of(context).chatSettingsReadReceiptsExplainer, - ), + description: + Text(L10n.of(context).chatSettingsReadReceiptsExplainer), enabled: false, initialValue: false, onToggle: (newVal) {}, diff --git a/app/lib/features/settings/pages/email_addresses.dart b/app/lib/features/settings/pages/email_addresses.dart index 008672bdba01..cd541e62f681 100644 --- a/app/lib/features/settings/pages/email_addresses.dart +++ b/app/lib/features/settings/pages/email_addresses.dart @@ -78,7 +78,7 @@ class EmailAddressesPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final emailAddresses = ref.watch(emailAddressesProvider); + final addressesLoader = ref.watch(emailAddressesProvider); return WithSidebar( sidebar: const SettingsPage(), child: Scaffold( @@ -99,7 +99,7 @@ class EmailAddressesPage extends ConsumerWidget { ), ], ), - body: emailAddresses.when( + body: addressesLoader.when( data: (addresses) => buildAddresses(context, addresses), error: (e, s) { _log.severe('Failed to load email addresses', e, s); @@ -151,12 +151,10 @@ class EmailAddressesPage extends ConsumerWidget { ), ), SliverList.builder( - itemBuilder: (BuildContext context, int index) { - return EmailAddressCard( - emailAddress: addresses.unconfirmed[index], - isConfirmed: false, - ); - }, + itemBuilder: (context, index) => EmailAddressCard( + emailAddress: addresses.unconfirmed[index], + isConfirmed: false, + ), itemCount: addresses.unconfirmed.length, ), ]; @@ -175,12 +173,10 @@ class EmailAddressesPage extends ConsumerWidget { ), ), SliverList.builder( - itemBuilder: (BuildContext context, int index) { - return EmailAddressCard( - emailAddress: addresses.confirmed[index], - isConfirmed: true, - ); - }, + itemBuilder: (context, index) => EmailAddressCard( + emailAddress: addresses.confirmed[index], + isConfirmed: true, + ), itemCount: addresses.confirmed.length, ), ]); @@ -203,12 +199,10 @@ class EmailAddressesPage extends ConsumerWidget { ), ), SliverList.builder( - itemBuilder: (BuildContext context, int index) { - return EmailAddressCard( - emailAddress: addresses.confirmed[index], - isConfirmed: true, - ); - }, + itemBuilder: (context, index) => EmailAddressCard( + emailAddress: addresses.confirmed[index], + isConfirmed: true, + ), itemCount: addresses.confirmed.length, ), ], diff --git a/app/lib/features/settings/pages/notifications_page.dart b/app/lib/features/settings/pages/notifications_page.dart index 9f2fd2207a2c..2ad5923108b0 100644 --- a/app/lib/features/settings/pages/notifications_page.dart +++ b/app/lib/features/settings/pages/notifications_page.dart @@ -200,8 +200,8 @@ class NotificationsSettingsPage extends ConsumerWidget { ) async { EasyLoading.show(status: L10n.of(context).changingNotificationMode); try { - final notifier = ref.read(notificationSettingsProvider).valueOrNull!; - await notifier.setDefaultNotificationMode( + final settings = await ref.read(notificationSettingsProvider.future); + await settings.setDefaultNotificationMode( isEncrypted, isOneToOne, newMode, @@ -228,15 +228,14 @@ class NotificationsSettingsPage extends ConsumerWidget { BuildContext context, WidgetRef ref, ) { - final potentialEmails = ref.watch(possibleEmailToAddForPushProvider); + final emailsLoader = ref.watch(possibleEmailToAddForPushProvider); + final pushersLoader = ref.watch(pushersProvider); return SettingsSectionWithTitleActions( title: Text(L10n.of(context).notificationTargets), - actions: potentialEmails.maybeWhen( + actions: emailsLoader.maybeWhen( orElse: () => [], data: (emails) { - if (emails.isEmpty) { - return []; - } + if (emails.isEmpty) return []; return [ IconButton( icon: const Icon(Atlas.plus_circle_thin), @@ -247,33 +246,33 @@ class NotificationsSettingsPage extends ConsumerWidget { ]; }, ), - tiles: ref.watch(pushersProvider).when( - data: (items) { - if (items.isEmpty) { - return [ - SettingsTile( - title: Text(L10n.of(context).noPushTargetsAddedYet), - ), - ]; - } - return items - .map((item) => _pusherTile(context, ref, item)) - .toList(); - }, - error: (e, s) { - _log.severe('Failed to load pushers', e, s); - return [ - SettingsTile( - title: Text(L10n.of(context).failedToLoadPushTargets(e)), - ), - ]; - }, - loading: () => [ + tiles: pushersLoader.when( + data: (pushers) { + if (pushers.isEmpty) { + return [ SettingsTile( - title: Text(L10n.of(context).loadingTargets), + title: Text(L10n.of(context).noPushTargetsAddedYet), ), - ], + ]; + } + return pushers + .map((pusher) => _pusherTile(context, ref, pusher)) + .toList(); + }, + error: (e, s) { + _log.severe('Failed to load pushers', e, s); + return [ + SettingsTile( + title: Text(L10n.of(context).failedToLoadPushTargets(e)), + ), + ]; + }, + loading: () => [ + SettingsTile( + title: Text(L10n.of(context).loadingTargets), ), + ], + ), ); } @@ -368,9 +367,7 @@ class NotificationsSettingsPage extends ConsumerWidget { ), ActerDangerActionButton( onPressed: () => _onTargetDelete(context, ref, item), - child: Text( - L10n.of(context).deleteTarget, - ), + child: Text(L10n.of(context).deleteTarget), ), ], ), diff --git a/app/lib/features/settings/pages/sessions_page.dart b/app/lib/features/settings/pages/sessions_page.dart index 5472677b175b..0703240d3b2a 100644 --- a/app/lib/features/settings/pages/sessions_page.dart +++ b/app/lib/features/settings/pages/sessions_page.dart @@ -17,7 +17,7 @@ class SessionsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final allSessions = ref.watch(unknownSessionsProvider); + final sessionsLoader = ref.watch(unknownSessionsProvider); return WithSidebar( sidebar: const SettingsPage(), child: Scaffold( @@ -36,7 +36,7 @@ class SessionsPage extends ConsumerWidget { ), ], ), - body: allSessions.when( + body: sessionsLoader.when( data: (sessions) => buildSessions(context, sessions), error: (e, s) { _log.severe('Failed to load unknown sessions', e, s); @@ -52,59 +52,12 @@ class SessionsPage extends ConsumerWidget { ); } - Widget buildSessions( - BuildContext context, - List sessions, - ) { + Widget buildSessions(BuildContext context, List sessions) { final unverifiedSessions = sessions.where((s) => !s.isVerified()).toList(); - if (unverifiedSessions.isNotEmpty) { - final slivers = [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsetsDirectional.symmetric( - horizontal: 20, - vertical: 15, - ), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 10), - child: Icon( - Atlas.shield_exclamation_thin, - color: Theme.of(context).colorScheme.error, - ), - ), - Text( - L10n.of(context).unverifiedSessions, - style: Theme.of(context).textTheme.headlineSmall, - ), - ], - ), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsetsDirectional.symmetric( - horizontal: 20, - vertical: 15, - ), - child: Text( - L10n.of(context).unverifiedSessionsDescription, - style: Theme.of(context).textTheme.bodyMedium, - ), - ), - ), - SliverList.builder( - itemBuilder: (BuildContext context, int index) { - return SessionCard(deviceRecord: unverifiedSessions[index]); - }, - itemCount: unverifiedSessions.length, - ), - ]; - final verifiedSessions = sessions.where((s) => s.isVerified()).toList(); - if (verifiedSessions.isNotEmpty) { - slivers.addAll([ + if (unverifiedSessions.isEmpty) { + return CustomScrollView( + slivers: [ SliverToBoxAdapter( child: Padding( padding: const EdgeInsetsDirectional.symmetric( @@ -112,24 +65,69 @@ class SessionsPage extends ConsumerWidget { vertical: 15, ), child: Text( - '${L10n.of(context).verified} ${L10n.of(context).sessions}', - style: Theme.of(context).textTheme.headlineSmall, + L10n.of(context).verifiedSessionsDescription, + style: Theme.of(context).textTheme.bodyMedium, ), ), ), SliverList.builder( - itemBuilder: (BuildContext context, int index) { - return SessionCard(deviceRecord: verifiedSessions[index]); - }, - itemCount: verifiedSessions.length, + itemBuilder: (context, index) => SessionCard( + deviceRecord: sessions[index], + ), + itemCount: sessions.length, ), - ]); - } - return CustomScrollView(slivers: slivers); + ], + ); } - return CustomScrollView( - slivers: [ + final slivers = [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 20, + vertical: 15, + ), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 10), + child: Icon( + Atlas.shield_exclamation_thin, + color: Theme.of(context).colorScheme.error, + ), + ), + Text( + L10n.of(context).unverifiedSessions, + style: Theme.of(context).textTheme.headlineSmall, + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 20, + vertical: 15, + ), + child: Text( + L10n.of(context).unverifiedSessionsDescription, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ), + SliverList.builder( + itemBuilder: (context, index) => SessionCard( + deviceRecord: unverifiedSessions[index], + ), + itemCount: unverifiedSessions.length, + ), + ]; + + final verifiedSessions = sessions.where((s) => s.isVerified()).toList(); + + if (verifiedSessions.isNotEmpty) { + slivers.addAll([ SliverToBoxAdapter( child: Padding( padding: const EdgeInsetsDirectional.symmetric( @@ -137,18 +135,19 @@ class SessionsPage extends ConsumerWidget { vertical: 15, ), child: Text( - L10n.of(context).verifiedSessionsDescription, - style: Theme.of(context).textTheme.bodyMedium, + '${L10n.of(context).verified} ${L10n.of(context).sessions}', + style: Theme.of(context).textTheme.headlineSmall, ), ), ), SliverList.builder( - itemBuilder: (BuildContext context, int index) { - return SessionCard(deviceRecord: sessions[index]); - }, - itemCount: sessions.length, + itemBuilder: (context, index) => SessionCard( + deviceRecord: verifiedSessions[index], + ), + itemCount: verifiedSessions.length, ), - ], - ); + ]); + } + return CustomScrollView(slivers: slivers); } } 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 00df9083ba49..703d69cdeaf1 100644 --- a/app/lib/features/settings/providers/notifiers/app_settings_notifier.dart +++ b/app/lib/features/settings/providers/notifiers/app_settings_notifier.dart @@ -3,6 +3,9 @@ import 'dart:async'; import 'package:acter/common/providers/common_providers.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('a3::settings::app_settings_notifier'); class UserAppSettingsNotifier extends AutoDisposeAsyncNotifier { @@ -17,10 +20,18 @@ class UserAppSettingsNotifier Future build() async { final account = ref.watch(accountProvider); _listener = account.subscribeAppSettingsStream(); - _poller = _listener.listen((e) async { - // refresh on update - state = await AsyncValue.guard(_getSettings); - }); + _poller = _listener.listen( + (data) async { + // refresh on update + state = await AsyncValue.guard(_getSettings); + }, + onError: (e, s) { + _log.severe('stream errored', e, s); + }, + onDone: () { + _log.info('stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return await _getSettings(); } diff --git a/app/lib/features/settings/providers/notifiers/devices_notifier.dart b/app/lib/features/settings/providers/notifiers/devices_notifier.dart index 8fc09eb95ad8..4965151bd0a7 100644 --- a/app/lib/features/settings/providers/notifiers/devices_notifier.dart +++ b/app/lib/features/settings/providers/notifiers/devices_notifier.dart @@ -18,7 +18,7 @@ class AsyncDevicesNotifier extends AsyncNotifier> { _listener = client.deviceEventRx(); _poller = _listener?.listen( - (evt) async { + (data) async { final sessions = (await manager.allSessions()).toList(); state = AsyncValue.data(sessions); }, diff --git a/app/lib/features/settings/widgets/app_notifications_settings_tile.dart b/app/lib/features/settings/widgets/app_notifications_settings_tile.dart index 8b977c3a28a1..722ad421f20b 100644 --- a/app/lib/features/settings/widgets/app_notifications_settings_tile.dart +++ b/app/lib/features/settings/widgets/app_notifications_settings_tile.dart @@ -23,42 +23,38 @@ class _AppNotificationSettingsTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ref.watch(appContentNotificationSetting(appKey)).when( - data: (v) => innerBuild(context, ref, v), - error: (e, s) { - _log.severe( - 'Fetching of app content notification setting failed', - e, - s, - ); - return SettingsTile( - title: Text(L10n.of(context).loadingFailed(e)), - ); - }, - loading: () => Skeletonizer( - child: SettingsTile.switchTile( - initialValue: true, - onToggle: null, - title: Text(title), - ), - ), + final settingLoader = ref.watch(appContentNotificationSetting(appKey)); + return settingLoader.when( + data: (v) => innerBuild(context, ref, v), + error: (e, s) { + _log.severe( + 'Fetching of app content notification setting failed', + e, + s, ); + return SettingsTile( + title: Text(L10n.of(context).loadingFailed(e)), + ); + }, + loading: () => Skeletonizer( + child: SettingsTile.switchTile( + initialValue: true, + onToggle: null, + title: Text(title), + ), + ), + ); } - Widget innerBuild( - BuildContext context, - WidgetRef ref, - bool currentValue, - ) { + Widget innerBuild(BuildContext context, WidgetRef ref, bool currentValue) { return SettingsTile.switchTile( title: Text(title), description: description != null ? Text(description!) : null, initialValue: currentValue, enabled: enabled ?? true, onToggle: (newVal) async { - final settingsSetter = - await ref.read(notificationSettingsProvider.future); - await settingsSetter.setGlobalContentSetting(appKey, newVal); + final settings = await ref.read(notificationSettingsProvider.future); + await settings.setGlobalContentSetting(appKey, newVal); }, ); } @@ -71,11 +67,11 @@ class AppsNotificationsSettingsTile extends AbstractSettingsTile { final bool? enabled; const AppsNotificationsSettingsTile({ + super.key, required this.title, - required this.appKey, this.description, + required this.appKey, this.enabled, - super.key, }); @override diff --git a/app/lib/features/space/pages/members_page.dart b/app/lib/features/space/pages/members_page.dart index 03f6a73a20e9..8f192953238a 100644 --- a/app/lib/features/space/pages/members_page.dart +++ b/app/lib/features/space/pages/members_page.dart @@ -19,7 +19,7 @@ class SpaceMembersPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final members = ref.watch(membersIdsProvider(spaceIdOrAlias)); + final membersLoader = ref.watch(membersIdsProvider(spaceIdOrAlias)); final membership = ref.watch(roomMembershipProvider(spaceIdOrAlias)).valueOrNull; final invited = @@ -48,7 +48,7 @@ class SpaceMembersPage extends ConsumerWidget { : const SizedBox.shrink(), ], ), - members.when( + membersLoader.when( data: (members) { final widthCount = (MediaQuery.of(context).size.width ~/ 300).toInt(); @@ -66,12 +66,10 @@ class SpaceMembersPage extends ConsumerWidget { crossAxisCount: max(1, min(widthCount, minCount)), childAspectRatio: 5.0, ), - itemBuilder: (context, index) { - return MemberListEntry( - memberId: members[index], - roomId: spaceIdOrAlias, - ); - }, + itemBuilder: (context, index) => MemberListEntry( + memberId: members[index], + roomId: spaceIdOrAlias, + ), ); }, error: (e, s) { diff --git a/app/lib/features/space/pages/space_details_page.dart b/app/lib/features/space/pages/space_details_page.dart index e48c68c647ac..f7bc25c02a37 100644 --- a/app/lib/features/space/pages/space_details_page.dart +++ b/app/lib/features/space/pages/space_details_page.dart @@ -118,27 +118,25 @@ class _SpaceDetailsPageState extends ConsumerState { } Widget spaceBodyUI() { - final spaceMenus = ref.watch(tabsProvider(widget.spaceId)); - return spaceMenus.when( + final tabsLoader = ref.watch(tabsProvider(widget.spaceId)); + return tabsLoader.when( skipLoadingOnReload: true, - data: (tabsList) { + data: (tabs) { return ScrollableListTabScroller( headerKey: SpaceDetailsPage.headerKey, - itemCount: tabsList.length, + itemCount: tabs.length, itemPositionsListener: itemPositionsListener, //Space Details Header UI - headerContainerBuilder: - (BuildContext context, Widget menuBarWidget) => - spaceHeaderUI(menuBarWidget), + headerContainerBuilder: (context, menuBarWidget) => + spaceHeaderUI(menuBarWidget), //Space Details Tab Menu UI - tabBuilder: (BuildContext context, int index, bool active) => - spaceTabMenuUI(tabsList[index], active), + tabBuilder: (context, index, active) => + spaceTabMenuUI(tabs[index], active), //Space Details Page UI - itemBuilder: (BuildContext context, int index) => - spacePageUI(tabsList[index]), + itemBuilder: (context, index) => spacePageUI(tabs[index]), // we allow this to be refreshed by over-pulling onRefresh: () async { @@ -197,16 +195,13 @@ class _SpaceDetailsPageState extends ConsumerState { Widget spaceAvatar() { final avatarData = ref.watch(roomAvatarProvider(widget.spaceId)).valueOrNull; - if (avatarData != null) { - return Image.memory( - avatarData.bytes, - height: 300, - width: MediaQuery.of(context).size.width, - fit: BoxFit.cover, - ); - } else { - return Container(height: 200, color: Colors.red); - } + if (avatarData == null) return Container(height: 200, color: Colors.red); + return Image.memory( + avatarData.bytes, + height: 300, + width: MediaQuery.of(context).size.width, + fit: BoxFit.cover, + ); } Widget spaceTabMenuUI(TabEntry tabItem, bool active) { diff --git a/app/lib/features/space/pages/sub_spaces_page.dart b/app/lib/features/space/pages/sub_spaces_page.dart index 85c73a3e820d..fb2d13fff412 100644 --- a/app/lib/features/space/pages/sub_spaces_page.dart +++ b/app/lib/features/space/pages/sub_spaces_page.dart @@ -78,7 +78,8 @@ class SubSpacesPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final spaces = ref.watch(spaceRelationsOverviewProvider(spaceIdOrAlias)); + final spacesLoader = + ref.watch(spaceRelationsOverviewProvider(spaceIdOrAlias)); final widthCount = (MediaQuery.of(context).size.width ~/ 300).toInt(); const int minCount = 3; final crossAxisCount = max(1, min(widthCount, minCount)); @@ -110,7 +111,7 @@ class SubSpacesPage extends ConsumerWidget { ref.invalidate(spaceRelationsProvider); }, ), - spaces.when( + spacesLoader.when( data: (spaces) { if (canLinkSpace) { return _renderTools(context); @@ -131,19 +132,17 @@ class SubSpacesPage extends ConsumerWidget { body: SingleChildScrollView( child: Column( children: [ - spaces.when( + spacesLoader.when( data: (spaces) { - return renderSubSpaces( - context, - ref, - spaceIdOrAlias, - spaces, - crossAxisCount: crossAxisCount, - ) ?? - renderFallback( - context, - canLinkSpace, - ); + final subspaces = renderSubSpaces( + context, + ref, + spaceIdOrAlias, + spaces, + crossAxisCount: crossAxisCount, + ); + if (subspaces != null) return subspaces; + return renderFallback(context, canLinkSpace); }, error: (e, s) { _log.severe('Failed to load the related spaces', e, s); diff --git a/app/lib/features/space/settings/pages/apps_settings_page.dart b/app/lib/features/space/settings/pages/apps_settings_page.dart index c1f8fdb33814..0f87bbb6997e 100644 --- a/app/lib/features/space/settings/pages/apps_settings_page.dart +++ b/app/lib/features/space/settings/pages/apps_settings_page.dart @@ -64,11 +64,11 @@ class SpaceAppsSettingsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final spaceSettingsWatcher = ref.watch(spaceAppSettingsProvider(spaceId)); + final settingsLoader = ref.watch(spaceAppSettingsProvider(spaceId)); return WithSidebar( sidebar: SpaceSettingsMenu(spaceId: spaceId), - child: spaceSettingsWatcher.when( + child: settingsLoader.when( data: (appSettingsAndMembership) { final appSettings = appSettingsAndMembership.settings; final powerLevels = appSettingsAndMembership.powerLevels; diff --git a/app/lib/features/space/settings/pages/visibility_accessibility_page.dart b/app/lib/features/space/settings/pages/visibility_accessibility_page.dart index 390c9732960d..0e529f6ee913 100644 --- a/app/lib/features/space/settings/pages/visibility_accessibility_page.dart +++ b/app/lib/features/space/settings/pages/visibility_accessibility_page.dart @@ -86,29 +86,28 @@ class _VisibilityAccessibilityPageState Widget _buildVisibilityUI({bool hasPermission = true}) { final spaceId = widget.roomId; - final selectedVisibility = ref.watch(roomVisibilityProvider(spaceId)); - final spaceList = ref.watch(joinRulesAllowedRoomsProvider(spaceId)); - return selectedVisibility.when( - data: (visibility) { - return RoomVisibilityType( - selectedVisibilityEnum: visibility, - canChange: hasPermission, - onVisibilityChange: !hasPermission - ? (value) => - EasyLoading.showToast(L10n.of(context).visibilityNoPermission) - : (value) { - if (value == RoomVisibility.SpaceVisible && - spaceList.valueOrNull?.isEmpty == true) { - selectSpace(spaceId); - } else { - updateSpaceVisibility( - value ?? RoomVisibility.Private, - spaceIds: (spaceList.valueOrNull ?? []), - ); - } - }, - ); - }, + final visibilityLoader = ref.watch(roomVisibilityProvider(spaceId)); + final allowedSpaces = ref.watch(joinRulesAllowedRoomsProvider(spaceId)); + return visibilityLoader.when( + data: (visibility) => RoomVisibilityType( + selectedVisibilityEnum: visibility, + canChange: hasPermission, + onVisibilityChange: (value) { + if (!hasPermission) { + EasyLoading.showToast(L10n.of(context).visibilityNoPermission); + return; + } + if (value == RoomVisibility.SpaceVisible && + allowedSpaces.valueOrNull?.isEmpty == true) { + selectSpace(spaceId); + } else { + updateSpaceVisibility( + value ?? RoomVisibility.Private, + spaceIds: (allowedSpaces.valueOrNull ?? []), + ); + } + }, + ), error: (e, s) { _log.severe('Failed to load room visibility', e, s); return const RoomVisibilityType( @@ -124,7 +123,8 @@ class _VisibilityAccessibilityPageState } Widget _buildSpaceWithAccess({bool hasPermission = true}) { - final spaceIds = ref.watch(joinRulesAllowedRoomsProvider(widget.roomId)); + final allowedSpacesLoader = + ref.watch(joinRulesAllowedRoomsProvider(widget.roomId)); return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Column( @@ -143,13 +143,13 @@ class _VisibilityAccessibilityPageState ), ], ), - spaceIds.when( - data: (spacesList) => ListView.builder( + allowedSpacesLoader.when( + data: (allowedSpaces) => ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: spacesList.length, + itemCount: allowedSpaces.length, itemBuilder: (context, index) { - return _spaceItemUI(spacesList[index], hasPermission); + return _spaceItemUI(allowedSpaces[index], hasPermission); }, ), error: (e, s) { @@ -168,9 +168,7 @@ class _VisibilityAccessibilityPageState Widget _loadingSpaceItem() { return Skeletonizer( - child: _spaceItemCard( - 'loading', - ), + child: _spaceItemCard('loading'), ); } @@ -194,9 +192,7 @@ class _VisibilityAccessibilityPageState leading: avatar ?? ActerAvatar( options: const AvatarOptions( - AvatarInfo( - uniqueId: 'unknown', - ), + AvatarInfo(uniqueId: 'unknown'), size: 45, badgesSize: 45 / 2, ), @@ -214,18 +210,19 @@ class _VisibilityAccessibilityPageState } Widget _spaceItemUI(String spaceId, bool canEdit) { - return ref.watch(briefSpaceItemProvider(spaceId)).when( - data: (d) => _spaceFoundUI(d, canEdit), - error: (e, s) { - _log.severe('Failed to load brief of space', e, s); - return _spaceItemCard( - spaceId, - subtitle: Text(L10n.of(context).failedToLoadSpace(e)), - removeAction: canEdit ? () => removeSpace(spaceId) : null, - ); - }, - loading: _loadingSpaceItem, + final spaceLoader = ref.watch(briefSpaceItemProvider(spaceId)); + return spaceLoader.when( + data: (space) => _spaceFoundUI(space, canEdit), + error: (e, s) { + _log.severe('Failed to load brief of space', e, s); + return _spaceItemCard( + spaceId, + subtitle: Text(L10n.of(context).failedToLoadSpace(e)), + removeAction: canEdit ? () => removeSpace(spaceId) : null, ); + }, + loading: _loadingSpaceItem, + ); } Widget _spaceFoundUI(SpaceItem spaceItem, bool canEdit) { @@ -242,7 +239,9 @@ class _VisibilityAccessibilityPageState badgesSize: 45 / 2, ), ), - removeAction: canEdit ? () => removeSpace(spaceItem.roomId) : null, + removeAction: () { + if (canEdit) removeSpace(spaceItem.roomId); + }, ); } @@ -286,7 +285,10 @@ class _VisibilityAccessibilityPageState List? spaceIds, }) async { try { - EasyLoading.show(status: 'Updating space settings', dismissOnTap: false); + EasyLoading.show( + status: 'Updating space settings', + dismissOnTap: false, + ); final sdk = await ref.read(sdkProvider.future); final update = sdk.api.newJoinRuleBuilder(); final room = await ref.read(maybeRoomProvider(widget.roomId).future); @@ -303,7 +305,7 @@ class _VisibilityAccessibilityPageState break; case RoomVisibility.SpaceVisible: update.joinRule('restricted'); - for (final roomId in (spaceIds ?? [])) { + for (var roomId in (spaceIds ?? [])) { update.addRoom(roomId); } break; diff --git a/app/lib/features/space/sheets/link_room_sheet.dart b/app/lib/features/space/sheets/link_room_sheet.dart index 8edf591e0388..0cec9e0581b9 100644 --- a/app/lib/features/space/sheets/link_room_sheet.dart +++ b/app/lib/features/space/sheets/link_room_sheet.dart @@ -127,19 +127,18 @@ class _LinkRoomPageConsumerState extends ConsumerState { padding: const EdgeInsets.all(12), child: spaceDetails.when( data: (space) { - return space == null - ? const SizedBox.shrink() - : Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(L10n.of(context).parentSpace), - SpaceChip( - space: space, - onTapOpenSpaceDetail: false, - ), - ], - ); + if (space == null) return const SizedBox.shrink(); + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(L10n.of(context).parentSpace), + SpaceChip( + space: space, + onTapOpenSpaceDetail: false, + ), + ], + ); }, error: (e, s) { _log.severe('Failed to load the details of selected space', e, s); diff --git a/app/lib/features/space/widgets/related/chats_helpers.dart b/app/lib/features/space/widgets/related/chats_helpers.dart index 76e93b31ec05..358bf8588ebf 100644 --- a/app/lib/features/space/widgets/related/chats_helpers.dart +++ b/app/lib/features/space/widgets/related/chats_helpers.dart @@ -34,27 +34,20 @@ Widget renderFurther( String spaceId, int? maxItems, ) { - final remoteChats = ref.watch(remoteChatRelationsProvider(spaceId)); - - return remoteChats.when( + final relatedChatsLoader = ref.watch(remoteChatRelationsProvider(spaceId)); + return relatedChatsLoader.when( data: (chats) { - if (chats.isEmpty) { - return const SizedBox.shrink(); - } - + if (chats.isEmpty) return const SizedBox.shrink(); return ListView.builder( shrinkWrap: true, padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemCount: maxItems ?? chats.length, - itemBuilder: (context, idx) { - final item = chats[idx]; - return ConvoHierarchyCard( - showIconIfSuggested: true, - parentId: spaceId, - roomInfo: item, - ); - }, + itemBuilder: (context, index) => ConvoHierarchyCard( + showIconIfSuggested: true, + parentId: spaceId, + roomInfo: chats[index], + ), ); }, error: (e, s) { diff --git a/app/lib/features/space/widgets/related/spaces_helpers.dart b/app/lib/features/space/widgets/related/spaces_helpers.dart index 62964b595b9d..accf2a44f32e 100644 --- a/app/lib/features/space/widgets/related/spaces_helpers.dart +++ b/app/lib/features/space/widgets/related/spaces_helpers.dart @@ -57,10 +57,9 @@ Widget renderMoreSubspaces( int? maxLength, EdgeInsetsGeometry? padding, }) { - final otherSubspaces = + final relatedSpacesLoader = ref.watch(remoteSubspaceRelationsProvider(spaceIdOrAlias)); - - return otherSubspaces.when( + return relatedSpacesLoader.when( data: (spaces) { if (spaces.isEmpty) { return const SizedBox.shrink(); @@ -82,10 +81,10 @@ Widget renderMoreSubspaces( ), shrinkWrap: true, itemBuilder: (context, index) { - final item = spaces[index]; + final space = spaces[index]; return SpaceHierarchyCard( - key: Key('subspace-list-item-${item.roomIdStr()}'), - roomInfo: item, + key: Key('subspace-list-item-${space.roomIdStr()}'), + roomInfo: space, parentId: spaceIdOrAlias, showIconIfSuggested: true, ); @@ -143,19 +142,14 @@ Widget? renderSubSpaces( if (moreSubspaces != null) moreSubspaces, ]; - if (items.isNotEmpty) { - if (titleBuilder != null) { - final title = titleBuilder(); - if (title != null) { - items.insert(0, title); - } + if (items.isEmpty) return null; + if (titleBuilder != null) { + final title = titleBuilder(); + if (title != null) { + items.insert(0, title); } - return SingleChildScrollView( - child: Column( - children: items, - ), - ); - } else { - return null; } + return SingleChildScrollView( + child: Column(children: items), + ); } diff --git a/app/lib/features/space/widgets/space_header_profile.dart b/app/lib/features/space/widgets/space_header_profile.dart index 6193ff825d61..3df032f94c9d 100644 --- a/app/lib/features/space/widgets/space_header_profile.dart +++ b/app/lib/features/space/widgets/space_header_profile.dart @@ -88,8 +88,8 @@ class SpaceHeaderProfile extends ConsumerWidget { WidgetRef ref, Widget? child, ) { - final spaceMembers = ref.watch(membersIdsProvider(spaceId)); - return spaceMembers.when( + final membersLoader = ref.watch(membersIdsProvider(spaceId)); + return membersLoader.when( data: (members) { final membersCount = members.length; if (membersCount > 5) { diff --git a/app/lib/features/space/widgets/space_sections/about_section.dart b/app/lib/features/space/widgets/space_sections/about_section.dart index 542cebd0304e..c51ae933098c 100644 --- a/app/lib/features/space/widgets/space_sections/about_section.dart +++ b/app/lib/features/space/widgets/space_sections/about_section.dart @@ -40,8 +40,8 @@ class AboutSection extends ConsumerWidget { } Widget spaceDescription(BuildContext context, WidgetRef ref) { - final space = ref.watch(spaceProvider(spaceId)); - return space.when( + final spaceLoader = ref.watch(spaceProvider(spaceId)); + return spaceLoader.when( data: (space) { final topic = space.topic(); return SelectionArea( diff --git a/app/lib/features/space/widgets/space_sections/chats_section.dart b/app/lib/features/space/widgets/space_sections/chats_section.dart index 81b2699f002b..eda535a3a3ae 100644 --- a/app/lib/features/space/widgets/space_sections/chats_section.dart +++ b/app/lib/features/space/widgets/space_sections/chats_section.dart @@ -24,12 +24,12 @@ class ChatsSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final chatsList = ref.watch(spaceRelationsOverviewProvider(spaceId)); - return chatsList.when( - data: (spaceRelationsOverview) => buildChatsSectionUI( + final overviewLoader = ref.watch(spaceRelationsOverviewProvider(spaceId)); + return overviewLoader.when( + data: (overview) => buildChatsSectionUI( context, ref, - spaceRelationsOverview.knownChats, + overview.knownChats, ), error: (e, s) { _log.severe('Failed to load the related spaces', e, s); @@ -50,12 +50,12 @@ class ChatsSection extends ConsumerWidget { WidgetRef ref, List chats, ) { + final relatedChats = + ref.watch(remoteChatRelationsProvider(spaceId)).valueOrNull ?? []; final config = calculateSectionConfig( localListLen: chats.length, limit: limit, - remoteListLen: - (ref.watch(remoteChatRelationsProvider(spaceId)).valueOrNull ?? []) - .length, + remoteListLen: relatedChats.length, ); return Column( mainAxisAlignment: MainAxisAlignment.start, diff --git a/app/lib/features/space/widgets/space_sections/events_section.dart b/app/lib/features/space/widgets/space_sections/events_section.dart index 3e8b12c0d33e..3d0f104e187d 100644 --- a/app/lib/features/space/widgets/space_sections/events_section.dart +++ b/app/lib/features/space/widgets/space_sections/events_section.dart @@ -23,11 +23,11 @@ class EventsSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final eventsList = ref.watch( + final calEventsLoader = ref.watch( eventListSearchFilterProvider((spaceId: spaceId, searchText: '')), ); - return eventsList.when( - data: (events) => buildEventsSectionUI(context, events), + return calEventsLoader.when( + data: (calEvents) => buildEventsSectionUI(context, calEvents), error: (e, s) { _log.severe('Failed to search cal events in space', e, s); return Center( @@ -44,29 +44,29 @@ class EventsSection extends ConsumerWidget { BuildContext context, List events, ) { - int eventsLimit = (events.length > limit) ? limit : events.length; - bool isShowSeeAllButton = events.length > eventsLimit; + final hasMore = events.length > limit; + final count = hasMore ? limit : events.length; return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionHeader( title: L10n.of(context).events, - isShowSeeAllButton: isShowSeeAllButton, + isShowSeeAllButton: hasMore, onTapSeeAll: () => context.pushNamed( Routes.spaceEvents.name, pathParameters: {'spaceId': spaceId}, ), ), - eventsListUI(events, eventsLimit), + eventsListUI(events, count), ], ); } - Widget eventsListUI(List events, int eventsLimit) { + Widget eventsListUI(List events, int count) { return ListView.builder( shrinkWrap: true, - itemCount: eventsLimit, + itemCount: count, padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { diff --git a/app/lib/features/space/widgets/space_sections/members_section.dart b/app/lib/features/space/widgets/space_sections/members_section.dart index c89bbda57161..ce1447ca52c2 100644 --- a/app/lib/features/space/widgets/space_sections/members_section.dart +++ b/app/lib/features/space/widgets/space_sections/members_section.dart @@ -22,8 +22,8 @@ class MembersSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final membersList = ref.watch(membersIdsProvider(spaceId)); - return membersList.when( + final membersLoader = ref.watch(membersIdsProvider(spaceId)); + return membersLoader.when( data: (members) => buildMembersSectionUI(context, members), error: (e, s) { _log.severe('Failed to load members in space', e, s); @@ -38,29 +38,29 @@ class MembersSection extends ConsumerWidget { } Widget buildMembersSectionUI(BuildContext context, List members) { - int membersLimit = (members.length > limit) ? limit : members.length; - bool isShowSeeAllButton = members.length > membersLimit; + final hasMore = members.length > limit; + final count = hasMore ? limit : members.length; return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionHeader( title: L10n.of(context).members, - isShowSeeAllButton: isShowSeeAllButton, + isShowSeeAllButton: hasMore, onTapSeeAll: () => context.pushNamed( Routes.spaceMembers.name, pathParameters: {'spaceId': spaceId}, ), ), - membersListUI(members, membersLimit), + membersListUI(members, count), ], ); } - Widget membersListUI(List members, int membersLimit) { + Widget membersListUI(List members, int count) { return ListView.builder( shrinkWrap: true, - itemCount: membersLimit, + itemCount: count, padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { diff --git a/app/lib/features/space/widgets/space_sections/pins_section.dart b/app/lib/features/space/widgets/space_sections/pins_section.dart index 4466ad93f3ff..12da923a6d12 100644 --- a/app/lib/features/space/widgets/space_sections/pins_section.dart +++ b/app/lib/features/space/widgets/space_sections/pins_section.dart @@ -23,8 +23,8 @@ class PinsSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final pinList = ref.watch(pinListProvider(spaceId)); - return pinList.when( + final pinsLoader = ref.watch(pinListProvider(spaceId)); + return pinsLoader.when( data: (pins) => buildPinsSectionUI(context, pins), error: (e, s) { _log.severe('Failed to load pins in space', e, s); @@ -39,29 +39,29 @@ class PinsSection extends ConsumerWidget { } Widget buildPinsSectionUI(BuildContext context, List pins) { - int pinsLimit = (pins.length > limit) ? limit : pins.length; - bool isShowSeeAllButton = pins.length > pinsLimit; + final hasMore = pins.length > limit; + final count = hasMore ? limit : pins.length; return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionHeader( title: L10n.of(context).pins, - isShowSeeAllButton: isShowSeeAllButton, + isShowSeeAllButton: hasMore, onTapSeeAll: () => context.pushNamed( Routes.spacePins.name, pathParameters: {'spaceId': spaceId}, ), ), - pinsListUI(pins, pinsLimit), + pinsListUI(pins, count), ], ); } - Widget pinsListUI(List pins, int pinsLimit) { + Widget pinsListUI(List pins, int count) { return ListView.builder( shrinkWrap: true, - itemCount: pinsLimit, + itemCount: count, padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { diff --git a/app/lib/features/space/widgets/space_sections/space_actions_section.dart b/app/lib/features/space/widgets/space_sections/space_actions_section.dart index 6e5879094e46..995d28e687d7 100644 --- a/app/lib/features/space/widgets/space_sections/space_actions_section.dart +++ b/app/lib/features/space/widgets/space_sections/space_actions_section.dart @@ -32,16 +32,11 @@ class SpaceActionsSection extends ConsumerWidget { } Widget actionButtons(BuildContext context, WidgetRef ref) { - final membership = ref.watch(roomMembershipProvider(spaceId)); - bool canAddPin = membership.valueOrNull?.canString('CanPostPin') == true; - bool canAddEvent = - membership.valueOrNull?.canString('CanPostEvent') == true; - - bool canAddTask = - membership.valueOrNull?.canString('CanPostTaskList') == true; - - bool canLinkSpaces = - membership.valueOrNull?.canString('CanLinkSpaces') == true; + final membership = ref.watch(roomMembershipProvider(spaceId)).valueOrNull; + bool canAddPin = membership?.canString('CanPostPin') == true; + bool canAddEvent = membership?.canString('CanPostEvent') == true; + bool canAddTask = membership?.canString('CanPostTaskList') == true; + bool canLinkSpaces = membership?.canString('CanLinkSpaces') == true; return Wrap( crossAxisAlignment: WrapCrossAlignment.start, @@ -71,12 +66,10 @@ class SpaceActionsSection extends ConsumerWidget { iconData: Atlas.list, title: L10n.of(context).addTask, isShow: canAddTask, - onPressed: () { - showCreateUpdateTaskListBottomSheet( - context, - initialSelectedSpace: spaceId, - ); - }, + onPressed: () => showCreateUpdateTaskListBottomSheet( + context, + initialSelectedSpace: spaceId, + ), ), spaceActionButton( context: context, @@ -130,15 +123,14 @@ class SpaceActionsSection extends ConsumerWidget { VoidCallback? onPressed, bool isShow = true, }) { - return isShow - ? TextButton.icon( - onPressed: onPressed, - icon: Icon(iconData), - label: Text( - title, - style: Theme.of(context).textTheme.bodyMedium, - ), - ) - : const SizedBox.shrink(); + if (!isShow) return const SizedBox.shrink(); + return TextButton.icon( + onPressed: onPressed, + icon: Icon(iconData), + label: Text( + title, + style: Theme.of(context).textTheme.bodyMedium, + ), + ); } } diff --git a/app/lib/features/space/widgets/space_sections/spaces_section.dart b/app/lib/features/space/widgets/space_sections/spaces_section.dart index 44d60fc22acf..3cdd16293d8f 100644 --- a/app/lib/features/space/widgets/space_sections/spaces_section.dart +++ b/app/lib/features/space/widgets/space_sections/spaces_section.dart @@ -24,12 +24,12 @@ class SpacesSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final spacesList = ref.watch(spaceRelationsOverviewProvider(spaceId)); - return spacesList.when( - data: (spaceRelationsOverview) => buildSpacesSectionUI( + final overviewLoader = ref.watch(spaceRelationsOverviewProvider(spaceId)); + return overviewLoader.when( + data: (overview) => buildSpacesSectionUI( context, ref, - spaceRelationsOverview.knownSubspaces, + overview.knownSubspaces, ), error: (e, s) { _log.severe('Failed to load the related spaces', e, s); @@ -48,13 +48,11 @@ class SpacesSection extends ConsumerWidget { WidgetRef ref, List spaces, ) { + final subspaces = ref.watch(remoteSubspaceRelationsProvider(spaceId)); final config = calculateSectionConfig( localListLen: spaces.length, limit: limit, - remoteListLen: - (ref.watch(remoteSubspaceRelationsProvider(spaceId)).valueOrNull ?? - []) - .length, + remoteListLen: (subspaces.valueOrNull ?? []).length, ); return Column( mainAxisAlignment: MainAxisAlignment.start, 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 c07ebeaee50e..00052c208501 100644 --- a/app/lib/features/space/widgets/space_sections/tasks_section.dart +++ b/app/lib/features/space/widgets/space_sections/tasks_section.dart @@ -22,8 +22,8 @@ class TasksSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final taskList = ref.watch(taskListProvider(spaceId)); - return taskList.when( + final tasksLoader = ref.watch(taskListProvider(spaceId)); + return tasksLoader.when( data: (tasks) => buildTasksSectionUI(context, tasks), error: (e, s) { _log.severe('Failed to load tasks in space', e, s); @@ -38,37 +38,35 @@ class TasksSection extends ConsumerWidget { } Widget buildTasksSectionUI(BuildContext context, List tasks) { - int taskLimit = (tasks.length > limit) ? limit : tasks.length; - bool isShowSeeAllButton = tasks.length > taskLimit; + final hasMore = tasks.length > limit; + final count = hasMore ? limit : tasks.length; return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionHeader( title: L10n.of(context).tasks, - isShowSeeAllButton: isShowSeeAllButton, + isShowSeeAllButton: hasMore, onTapSeeAll: () => context.pushNamed( Routes.spaceTasks.name, pathParameters: {'spaceId': spaceId}, ), ), - taskListUI(tasks, taskLimit), + taskListUI(tasks, count), ], ); } - Widget taskListUI(List tasks, int taskLimit) { + Widget taskListUI(List tasks, int count) { return ListView.builder( shrinkWrap: true, - itemCount: taskLimit, + itemCount: count, padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return TaskListItemCard( - taskListId: tasks[index], - initiallyExpanded: false, - ); - }, + itemBuilder: (context, index) => TaskListItemCard( + taskListId: tasks[index], + initiallyExpanded: false, + ), ); } } diff --git a/app/lib/features/super_invites/dialogs/redeem_dialog.dart b/app/lib/features/super_invites/dialogs/redeem_dialog.dart index abf8fddf985c..834d7d1901ff 100644 --- a/app/lib/features/super_invites/dialogs/redeem_dialog.dart +++ b/app/lib/features/super_invites/dialogs/redeem_dialog.dart @@ -20,7 +20,7 @@ class _ShowRedeemTokenDialog extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final info = ref.watch(superInviteInfoProvider(token)); + final infoLoader = ref.watch(superInviteInfoProvider(token)); return AlertDialog( title: Text(L10n.of(context).redeem), content: Container( @@ -31,7 +31,7 @@ class _ShowRedeemTokenDialog extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ - info.when( + infoLoader.when( data: (info) => renderInfo(context, ref, info), error: (e, s) { _log.severe('Failed to load the super invite: $token', e, s); diff --git a/app/lib/features/super_invites/pages/super_invites.dart b/app/lib/features/super_invites/pages/super_invites.dart index 6a8d141acf81..1b1ac2cfd2c3 100644 --- a/app/lib/features/super_invites/pages/super_invites.dart +++ b/app/lib/features/super_invites/pages/super_invites.dart @@ -19,7 +19,7 @@ class SuperInvitesPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final tokens = ref.watch(superInvitesTokensProvider); + final tokensLoader = ref.watch(superInvitesTokensProvider); return WithSidebar( sidebar: const SettingsPage(), child: Scaffold( @@ -50,57 +50,57 @@ class SuperInvitesPage extends ConsumerWidget { body: CustomScrollView( slivers: [ const SliverToBoxAdapter(child: RedeemToken()), - tokens.when( - data: (tokens) => tokens.isNotEmpty - ? SliverList.builder( - itemBuilder: (BuildContext context, int index) { - final token = tokens[index]; - final tokenStr = token.token().toString(); - final firstRoom = token - .rooms() - .map((t) => t.toDartString()) - .firstOrNull; - return Card( - key: Key('edit-token-$tokenStr'), - margin: const EdgeInsets.all(5), - child: ListTile( - title: Padding( - padding: const EdgeInsets.all(10), - child: Text(tokenStr), - ), - subtitle: Text( - L10n.of(context).usedTimes(token.acceptedCount()), - ), - onTap: () { - context.pushNamed( - Routes.actionCreateSuperInvite.name, - extra: token, - ); - }, - trailing: firstRoom != null - ? OutlinedButton( - onPressed: () => context.pushNamed( - Routes.shareInviteCode.name, - queryParameters: { - 'inviteCode': tokenStr, - 'roomId': firstRoom, - }, - ), - child: Text(L10n.of(context).share), - ) - : null, - ), - ); - }, - itemCount: tokens.length, - ) - : SliverToBoxAdapter( - child: Center( - child: Text( - L10n.of(context).youHaveNotCreatedInviteCodes, + tokensLoader.when( + data: (tokens) { + if (tokens.isEmpty) { + return SliverToBoxAdapter( + child: Center( + child: + Text(L10n.of(context).youHaveNotCreatedInviteCodes), + ), + ); + } + return SliverList.builder( + itemBuilder: (context, index) { + final token = tokens[index]; + final acceptedCount = + L10n.of(context).usedTimes(token.acceptedCount()); + final tokenStr = token.token().toString(); + final firstRoom = + token.rooms().map((t) => t.toDartString()).firstOrNull; + return Card( + key: Key('edit-token-$tokenStr'), + margin: const EdgeInsets.all(5), + child: ListTile( + title: Padding( + padding: const EdgeInsets.all(10), + child: Text(tokenStr), ), + subtitle: Text(acceptedCount), + onTap: () { + context.pushNamed( + Routes.actionCreateSuperInvite.name, + extra: token, + ); + }, + trailing: firstRoom != null + ? OutlinedButton( + onPressed: () => context.pushNamed( + Routes.shareInviteCode.name, + queryParameters: { + 'inviteCode': tokenStr, + 'roomId': firstRoom, + }, + ), + child: Text(L10n.of(context).share), + ) + : null, ), - ), + ); + }, + itemCount: tokens.length, + ); + }, error: (e, s) { _log.severe('Failed to load the super invite tokens', e, s); return SliverToBoxAdapter( 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 8d8cf538a44e..ae479cf0e0fc 100644 --- a/app/lib/features/tasks/pages/task_item_detail_page.dart +++ b/app/lib/features/tasks/pages/task_item_detail_page.dart @@ -35,35 +35,35 @@ class TaskItemDetailPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final task = + final taskLoader = ref.watch(taskItemProvider((taskListId: taskListId, taskId: taskId))); return Scaffold( - appBar: _buildAppBar(context, ref, task), - body: _buildBody(context, ref, task), + appBar: _buildAppBar(context, ref, taskLoader), + body: _buildBody(context, ref, taskLoader), ); } AppBar _buildAppBar( BuildContext context, WidgetRef ref, - AsyncValue task, + AsyncValue taskLoader, ) { - return task.when( - data: (data) => AppBar( + return taskLoader.when( + data: (task) => AppBar( title: SelectionArea( child: GestureDetector( onTap: () => showEditTaskItemNameBottomSheet( context: context, ref: ref, - task: data, - titleValue: data.title(), + task: task, + titleValue: task.title(), ), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - data.title(), + task.title(), style: Theme.of(context).textTheme.titleMedium, ), Row( @@ -94,9 +94,9 @@ class TaskItemDetailPage extends ConsumerWidget { showEditTitleBottomSheet( context: context, bottomSheetTitle: L10n.of(context).editName, - titleValue: data.title(), + titleValue: task.title(), onSave: (newName) => - saveTitle(context, ref, data, newName), + saveTitle(context, ref, task, newName), ); }, child: Text( @@ -105,22 +105,25 @@ class TaskItemDetailPage extends ConsumerWidget { ), ), PopupMenuItem( - onTap: () => showEditDescriptionSheet(context, ref, data), + onTap: () => showEditDescriptionSheet(context, ref, task), child: Text( L10n.of(context).editDescription, style: Theme.of(context).textTheme.bodyMedium, ), ), PopupMenuItem( - onTap: () => - showRedactDialog(context: context, ref: ref, task: data), + onTap: () => showRedactDialog( + context: context, + ref: ref, + task: task, + ), child: Text( L10n.of(context).delete, style: Theme.of(context).textTheme.bodyMedium, ), ), PopupMenuItem( - onTap: () => showReportDialog(context: context, task: data), + onTap: () => showReportDialog(context: context, task: task), child: Text( L10n.of(context).report, style: Theme.of(context).textTheme.bodyMedium, @@ -180,10 +183,10 @@ class TaskItemDetailPage extends ConsumerWidget { Widget _buildBody( BuildContext context, WidgetRef ref, - AsyncValue task, + AsyncValue taskLoader, ) { - return task.when( - data: (data) => taskData(context, data, ref), + return taskLoader.when( + data: (task) => taskData(context, task, ref), error: (e, s) { _log.severe('Failed to load task', e, s); return Text(L10n.of(context).loadingFailed(e)); @@ -386,11 +389,7 @@ class TaskItemDetailPage extends ConsumerWidget { ); } - Widget assigneeName( - BuildContext context, - Task task, - WidgetRef ref, - ) { + Widget assigneeName(BuildContext context, Task task, WidgetRef ref) { final assignees = task.assigneesStr().map((s) => s.toDartString()).toList(); return Wrap( @@ -398,9 +397,7 @@ class TaskItemDetailPage extends ConsumerWidget { children: assignees.map((i) { final displayName = ref .watch( - memberDisplayNameProvider( - (roomId: task.roomIdStr(), userId: i), - ), + memberDisplayNameProvider((roomId: task.roomIdStr(), userId: i)), ) .valueOrNull; return Padding( @@ -436,10 +433,7 @@ class TaskItemDetailPage extends ConsumerWidget { } } - Future onUnAssign( - BuildContext context, - Task task, - ) async { + Future onUnAssign(BuildContext context, Task task) async { EasyLoading.show(status: L10n.of(context).unassigningSelf); try { await task.unassignSelf(); 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 9ee56906acb0..680efb45689d 100644 --- a/app/lib/features/tasks/pages/task_list_details_page.dart +++ b/app/lib/features/tasks/pages/task_list_details_page.dart @@ -42,21 +42,20 @@ class _TaskListPageState extends ConsumerState { } AppBar _buildAppbar() { - final taskList = ref.watch(taskListItemProvider(widget.taskListId)); - - return taskList.when( - data: (d) => AppBar( + final tasklistLoader = ref.watch(taskListItemProvider(widget.taskListId)); + return tasklistLoader.when( + data: (tasklist) => AppBar( title: SelectionArea( child: GestureDetector( onTap: () => showEditTaskListNameBottomSheet( context: context, ref: ref, - taskList: d, - titleValue: d.name(), + taskList: tasklist, + titleValue: tasklist.name(), ), child: Text( key: TaskListDetailPage.taskListTitleKey, - d.name(), + tasklist.name(), style: Theme.of(context).textTheme.titleMedium, ), ), @@ -67,21 +66,21 @@ class _TaskListPageState extends ConsumerState { itemBuilder: (context) { return [ PopupMenuItem( - onTap: () => showEditDescriptionSheet(d), + onTap: () => showEditDescriptionSheet(tasklist), child: Text( L10n.of(context).editDescription, style: Theme.of(context).textTheme.bodyMedium, ), ), PopupMenuItem( - onTap: () => showRedactDialog(taskList: d), + onTap: () => showRedactDialog(taskList: tasklist), child: Text( L10n.of(context).delete, style: Theme.of(context).textTheme.bodyMedium, ), ), PopupMenuItem( - onTap: () => showReportDialog(d), + onTap: () => showReportDialog(tasklist), child: Text( L10n.of(context).report, style: Theme.of(context).textTheme.bodyMedium, @@ -130,9 +129,9 @@ class _TaskListPageState extends ConsumerState { } Widget _buildBody() { - final taskList = ref.watch(taskListItemProvider(widget.taskListId)); - return taskList.when( - data: (data) => _buildTaskListData(data), + final tasklistLoader = ref.watch(taskListItemProvider(widget.taskListId)); + return tasklistLoader.when( + data: (tasklist) => _buildTaskListData(tasklist), error: (e, s) { _log.severe('Failed to load tasklist', e, s); return Text(L10n.of(context).loadingFailed(e)); diff --git a/app/lib/features/tasks/pages/tasks_list_page.dart b/app/lib/features/tasks/pages/tasks_list_page.dart index c5157317ac51..96affd268850 100644 --- a/app/lib/features/tasks/pages/tasks_list_page.dart +++ b/app/lib/features/tasks/pages/tasks_list_page.dart @@ -94,14 +94,11 @@ class _TasksListPageConsumerState extends ConsumerState { } Widget _buildBody() { - AsyncValue> tasksList; - - tasksList = ref.watch( + final tasklistsLoader = ref.watch( tasksListSearchProvider( (spaceId: widget.spaceId, searchText: searchValue), ), ); - return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -109,8 +106,8 @@ class _TasksListPageConsumerState extends ConsumerState { searchTextController: searchTextController, ), Expanded( - child: tasksList.when( - data: (tasks) => _buildTasksList(tasks), + child: tasklistsLoader.when( + data: (tasklists) => _buildTasklists(tasklists), error: (e, s) { _log.severe('Failed to search tasklists in space', e, s); return Center( @@ -124,41 +121,39 @@ class _TasksListPageConsumerState extends ConsumerState { ); } - Widget _buildTasksList(List tasksList) { + Widget _buildTasklists(List tasklists) { final size = MediaQuery.of(context).size; final widthCount = (size.width ~/ 500).toInt(); const int minCount = 2; - if (tasksList.isEmpty) return _buildTasksListEmptyState(); + if (tasklists.isEmpty) return _buildTasklistsEmptyState(); return SingleChildScrollView( key: TasksListPage.scrollView, child: StaggeredGrid.count( crossAxisCount: max(1, min(widthCount, minCount)), children: [ - for (var taskListId in tasksList) + for (var tasklistId in tasklists) ValueListenableBuilder( valueListenable: showCompletedTask, - builder: (context, value, child) { - return TaskListItemCard( - taskListId: taskListId, - showCompletedTask: value, - showSpace: widget.spaceId == null, - ); - }, + builder: (context, value, child) => TaskListItemCard( + taskListId: tasklistId, + showCompletedTask: value, + showSpace: widget.spaceId == null, + ), ), ], ), ); } - Widget _buildTasksListEmptyState() { - bool canAdd = false; + Widget _buildTasklistsEmptyState() { + var canAdd = false; if (searchValue.isEmpty) { - canAdd = ref - .watch(hasSpaceWithPermissionProvider('CanPostTaskList')) - .valueOrNull ?? - false; + final canPostLoader = ref.watch( + hasSpaceWithPermissionProvider('CanPostTaskList'), + ); + if (canPostLoader.valueOrNull == true) canAdd = true; } return Center( heightFactor: 1, @@ -168,7 +163,7 @@ class _TasksListPageConsumerState extends ConsumerState { : L10n.of(context).noTasksListAvailableYet, subtitle: L10n.of(context).noTasksListAvailableDescription, image: 'assets/images/tasks.svg', - primaryButton: canAdd && searchValue.isEmpty + primaryButton: canAdd ? ActerPrimaryActionButton( onPressed: () => showCreateUpdateTaskListBottomSheet( context, diff --git a/app/lib/features/tasks/providers/notifiers.dart b/app/lib/features/tasks/providers/notifiers.dart index f3b60a1117c8..7c37d3019ba0 100644 --- a/app/lib/features/tasks/providers/notifiers.dart +++ b/app/lib/features/tasks/providers/notifiers.dart @@ -36,13 +36,21 @@ class TaskItemsListNotifier // Load initial todo list from the remote repository final taskList = arg; _listener = taskList.subscribeStream(); // keep it resident in memory - _poller = _listener.listen((element) async { - _log.info('got tasks list update'); - state = await AsyncValue.guard(() async { - final freshTaskList = await taskList.refresh(); - return await _refresh(freshTaskList); - }); - }); + _poller = _listener.listen( + (data) async { + _log.info('got tasks list update'); + state = await AsyncValue.guard(() async { + final freshTaskList = await taskList.refresh(); + return await _refresh(freshTaskList); + }); + }, + onError: (e, s) { + _log.severe('tasks overview stream errored', e, s); + }, + onDone: () { + _log.info('tasks overview stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return await _refresh(taskList); } @@ -63,10 +71,18 @@ class TaskListItemNotifier extends FamilyAsyncNotifier { final client = ref.watch(alwaysClientProvider); final taskList = await _refresh(client, arg); _listener = taskList.subscribeStream(); // keep it resident in memory - _poller = _listener.listen((element) async { - _log.info('got taskList update'); - state = await AsyncValue.guard(() async => await _refresh(client, arg)); - }); + _poller = _listener.listen( + (data) async { + _log.info('got taskList update'); + state = await AsyncValue.guard(() async => await _refresh(client, arg)); + }, + onError: (e, s) { + _log.severe('tasklist stream errored', e, s); + }, + onDone: () { + _log.info('tasklist stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return taskList; } @@ -82,10 +98,18 @@ class TaskItemNotifier extends FamilyAsyncNotifier { // Load initial todo list from the remote repository final task = arg; _listener = task.subscribeStream(); // keep it resident in memory - _poller = _listener.listen((element) async { - _log.info('got tasks list update'); - state = await AsyncValue.guard(() async => await task.refresh()); - }); + _poller = _listener.listen( + (data) async { + _log.info('got tasks list update'); + state = await AsyncValue.guard(() async => await task.refresh()); + }, + onError: (e, s) { + _log.severe('task stream errored', e, s); + }, + onDone: () { + _log.info('task stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return task; } @@ -104,9 +128,17 @@ class AsyncAllTaskListsNotifier extends AsyncNotifier> { //GET ALL TASKS LIST _listener = client.subscribeStream('tasks'); - _poller = _listener.listen((e) async { - state = await AsyncValue.guard(() => _getTasksList(client)); - }); + _poller = _listener.listen( + (data) async { + state = await AsyncValue.guard(() => _getTasksList(client)); + }, + onError: (e, s) { + _log.severe('all tasks stream errored', e, s); + }, + onDone: () { + _log.info('all tasks stream ended'); + }, + ); ref.onDispose(() => _poller.cancel()); return await _getTasksList(client); diff --git a/app/lib/features/tasks/widgets/task_item.dart b/app/lib/features/tasks/widgets/task_item.dart index c0e844aaf5de..3ba1a2c200f9 100644 --- a/app/lib/features/tasks/widgets/task_item.dart +++ b/app/lib/features/tasks/widgets/task_item.dart @@ -35,39 +35,39 @@ class TaskItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ref - .watch(taskItemProvider((taskListId: taskListId, taskId: taskId))) - .when( - data: (task) => ListTile( - onTap: () { - context.pushNamed( - Routes.taskItemDetails.name, - pathParameters: { - 'taskId': taskId, - 'taskListId': taskListId, - }, - ); + final taskLoader = + ref.watch(taskItemProvider((taskListId: taskListId, taskId: taskId))); + return taskLoader.when( + data: (task) => ListTile( + onTap: () { + context.pushNamed( + Routes.taskItemDetails.name, + pathParameters: { + 'taskId': taskId, + 'taskListId': taskListId, }, - horizontalTitleGap: 0, - minVerticalPadding: 0, - contentPadding: const EdgeInsets.all(3), - visualDensity: const VisualDensity(horizontal: 0, vertical: -4), - minLeadingWidth: 35, - leading: leadingWidget(task), - title: takeItemTitle(context, task), - subtitle: takeItemSubTitle(ref, context, task), - trailing: trailing(ref, task), - ), - error: (e, s) { - _log.severe('Failed to load task', e, s); - return ListTile( - title: Text(L10n.of(context).loadingFailed(e)), - ); - }, - loading: () => ListTile( - title: Text(L10n.of(context).loading), - ), + ); + }, + horizontalTitleGap: 0, + minVerticalPadding: 0, + contentPadding: const EdgeInsets.all(3), + visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + minLeadingWidth: 35, + leading: leadingWidget(task), + title: takeItemTitle(context, task), + subtitle: takeItemSubTitle(ref, context, task), + trailing: trailing(ref, task), + ), + error: (e, s) { + _log.severe('Failed to load task', e, s); + return ListTile( + title: Text(L10n.of(context).loadingFailed(e)), ); + }, + loading: () => ListTile( + title: Text(L10n.of(context).loading), + ), + ); } Widget takeItemTitle(BuildContext context, Task task) { @@ -106,35 +106,37 @@ class TaskItem extends ConsumerWidget { Widget takeItemSubTitle(WidgetRef ref, BuildContext context, Task task) { final description = task.description(); + final tasklistId = task.taskListIdStr(); + final tasklistLoader = ref.watch(taskListItemProvider(tasklistId)); return Padding( padding: const EdgeInsets.only(right: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (showBreadCrumb) - ref.watch(taskListItemProvider(task.taskListIdStr())).when( - data: (taskList) => Row( - children: [ - const Icon( - Icons.list, - color: Colors.white54, - size: 22, - ), - const SizedBox(width: 6), - Text( - taskList.name(), - style: Theme.of(context).textTheme.labelMedium, - ), - ], + tasklistLoader.when( + data: (taskList) => Row( + children: [ + const Icon( + Icons.list, + color: Colors.white54, + size: 22, ), - error: (e, s) { - _log.severe('Failed to load task', e, s); - return Text(L10n.of(context).loadingFailed(e)); - }, - loading: () => Skeletonizer( - child: Text(L10n.of(context).loading), + const SizedBox(width: 6), + Text( + taskList.name(), + style: Theme.of(context).textTheme.labelMedium, ), - ), + ], + ), + error: (e, s) { + _log.severe('Failed to load task', e, s); + return Text(L10n.of(context).loadingFailed(e)); + }, + loading: () => Skeletonizer( + child: Text(L10n.of(context).loading), + ), + ), if (description?.body() != null && !showBreadCrumb) Text( description!.body(), 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 4c70548f5ee2..0030fbca8164 100644 --- a/app/lib/features/tasks/widgets/task_items_list_widget.dart +++ b/app/lib/features/tasks/widgets/task_items_list_widget.dart @@ -28,8 +28,8 @@ class TaskItemsListWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final tasks = ref.watch(taskItemsListProvider(taskList)); - return tasks.when( + final overviewLoader = ref.watch(taskItemsListProvider(taskList)); + return overviewLoader.when( data: (overview) => taskData(context, overview), error: (e, s) { _log.severe('Failed to load tasklist', e, s); 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 8669e36e947e..84f6ad2fa9ad 100644 --- a/app/lib/features/tasks/widgets/task_list_item_card.dart +++ b/app/lib/features/tasks/widgets/task_list_item_card.dart @@ -27,36 +27,37 @@ class TaskListItemCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ref.watch(taskListItemProvider(taskListId)).when( - data: (taskList) => Card( - key: Key('task-list-card-$taskListId'), - child: ExpansionTile( - initiallyExpanded: initiallyExpanded, - 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, - ), - ], - ), - ), - error: (e, s) { - _log.severe('Failed to load tasklist', e, s); - return Card( - child: Text(L10n.of(context).errorLoadingTasks(e)), - ); - }, - loading: () => Card( - child: Text(L10n.of(context).loading), + final tasklistLoader = ref.watch(taskListItemProvider(taskListId)); + return tasklistLoader.when( + data: (taskList) => Card( + key: Key('task-list-card-$taskListId'), + child: ExpansionTile( + initiallyExpanded: initiallyExpanded, + 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, + ), + ], + ), + ), + error: (e, s) { + _log.severe('Failed to load tasklist', e, s); + return Card( + child: Text(L10n.of(context).errorLoadingTasks(e)), ); + }, + loading: () => Card( + child: Text(L10n.of(context).loading), + ), + ); } Widget title(BuildContext context, TaskList taskList) { @@ -75,9 +76,8 @@ class TaskListItemCard extends ConsumerWidget { } Widget? subtitle(WidgetRef ref, TaskList taskList) { - final spaceProfile = - ref.watch(roomAvatarInfoProvider(taskList.spaceIdStr())); - + final spaceId = taskList.spaceIdStr(); + final spaceProfile = ref.watch(roomAvatarInfoProvider(spaceId)); return showSpace ? Text(spaceProfile.displayName ?? '') : null; } }