diff --git a/.changes/2440-ref-space-objects.md b/.changes/2440-ref-space-objects.md new file mode 100644 index 000000000000..ce31f457a638 --- /dev/null +++ b/.changes/2440-ref-space-objects.md @@ -0,0 +1,2 @@ +- [New] : Boost Actions : Now you can share space objects as boost action of any space in which you are part of. There is not selected space restriction anymore. +- [Enhancement] : Attachment List on space object details screen got event better by having separate reference attachments list from general attachment list. \ No newline at end of file diff --git a/app/lib/common/widgets/reference_details_item.dart b/app/lib/common/widgets/reference_details_item.dart new file mode 100644 index 000000000000..0d04eb1c52c3 --- /dev/null +++ b/app/lib/common/widgets/reference_details_item.dart @@ -0,0 +1,48 @@ +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; +import 'package:atlas_icons/atlas_icons.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +class ReferenceDetailsItem extends StatelessWidget { + final RefDetails refDetails; + + const ReferenceDetailsItem({ + super.key, + required this.refDetails, + }); + + @override + Widget build(BuildContext context) { + final refTitle = refDetails.title() ?? L10n.of(context).unknown; + final refType = refDetails.typeStr(); + final roomName = refDetails.roomDisplayName().toString(); + return Card( + margin: EdgeInsets.symmetric(vertical: 6, horizontal: 12), + child: ListTile( + leading: Icon(getIconByType(refType), size: 25), + title: Text(refTitle), + subtitle: Text(refType), + onTap: () => EasyLoading.showError( + L10n.of(context).noObjectAccess(refType, roomName), + duration: const Duration(seconds: 3), + ), + ), + ); + } + + IconData getIconByType(String refType) { + final defaultIcon = PhosphorIconsThin.tagChevron; + switch (refType) { + case 'pin': + return Atlas.pin; + case 'calendar-event': + return Atlas.calendar; + case 'task-list': + return Atlas.list; + default: + return defaultIcon; + } + } +} diff --git a/app/lib/common/widgets/share/action/share_space_object_action.dart b/app/lib/common/widgets/share/action/share_space_object_action.dart index 789c8da8b71b..3b0269871941 100644 --- a/app/lib/common/widgets/share/action/share_space_object_action.dart +++ b/app/lib/common/widgets/share/action/share_space_object_action.dart @@ -85,21 +85,21 @@ class ShareSpaceObjectActionUI extends ConsumerWidget { WidgetRef ref, SpaceObjectDetails spaceObjectDetails, ) { - String spaceId = spaceObjectDetails.spaceId; - ObjectType objectType = spaceObjectDetails.objectType; - String objectId = spaceObjectDetails.objectId; - - final newsRefType = getNewsRefTypeFromObjType(objectType); return AttachOptions( - onTapBoost: () { - Navigator.pop(context); + onTapBoost: () async { + String spaceId = spaceObjectDetails.spaceId; + final refDetails = await getRefDetails( + ref: ref, + objectDetails: spaceObjectDetails, + ); + if (!context.mounted) return; context.pushNamed( Routes.actionAddUpdate.name, queryParameters: {'spaceId': spaceId}, - extra: newsRefType != null - ? NewsReferencesModel(type: newsRefType, id: objectId) - : null, + extra: refDetails, ); + if (!context.mounted) return; + Navigator.pop(context); }, onTapPin: () async { final refDetails = await getRefDetails( @@ -234,7 +234,8 @@ class ShareSpaceObjectActionUI extends ConsumerWidget { await ref.watch(calendarEventProvider(objectId).future); return await sourceEvent.refDetails(); case ObjectType.taskList: - final sourceTaskList = await ref.watch(taskListProvider(objectId).future); + final sourceTaskList = + await ref.watch(taskListProvider(objectId).future); return await sourceTaskList.refDetails(); default: return null; diff --git a/app/lib/features/attachments/providers/attachment_providers.dart b/app/lib/features/attachments/providers/attachment_providers.dart index 238a0a7fc247..9c5c6fff5798 100644 --- a/app/lib/features/attachments/providers/attachment_providers.dart +++ b/app/lib/features/attachments/providers/attachment_providers.dart @@ -16,6 +16,24 @@ final attachmentsProvider = FutureProvider.family return (await manager.attachments()).toList(); }); +/// Provider for getting reference attachments +final referenceAttachmentsProvider = FutureProvider.family + .autoDispose, AttachmentsManager>((ref, manager) async { + final attachmentList = await ref.watch(attachmentsProvider(manager).future); + final refAttachmentList = + attachmentList.where((item) => item.refDetails() != null).toList(); + return refAttachmentList; +}); + +/// Provider for getting msgContent attachments +final msgContentAttachmentsProvider = FutureProvider.family + .autoDispose, AttachmentsManager>((ref, manager) async { + final attachmentList = await ref.watch(attachmentsProvider(manager).future); + final msgContentAttachmentList = + attachmentList.where((item) => item.msgContent() != null).toList(); + return msgContentAttachmentList; +}); + final attachmentMediaStateProvider = StateNotifierProvider.family .autoDispose( (ref, attachment) => diff --git a/app/lib/features/attachments/widgets/attachment_item.dart b/app/lib/features/attachments/widgets/attachment_item.dart deleted file mode 100644 index 092fdafe2e41..000000000000 --- a/app/lib/features/attachments/widgets/attachment_item.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:acter/features/attachments/widgets/msg_content_attachment_item.dart'; -import 'package:acter/features/attachments/widgets/reference_attachment_item.dart'; -import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' show Attachment; -import 'package:phosphor_flutter/phosphor_flutter.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logging/logging.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -final _log = Logger('a3::attachments::widget::attachment_item'); - -// Attachment item UI -class AttachmentItem extends ConsumerWidget { - final Attachment attachment; - final bool canEdit; - - // whether item can be viewed on gesture - final bool? openView; - - const AttachmentItem({ - super.key, - required this.attachment, - this.canEdit = false, - this.openView, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // FIXME: maybe we want to exclude the link here already as well? - final msgContent = attachment.msgContent(); - if (msgContent != null) { - // we have a msgContent based Item - return MsgContentAttachmentItem( - attachment: attachment, - canEdit: canEdit, - openView: openView, - msgContent: msgContent, - ); - } - // we have a reference type instead - final refDetails = attachment.refDetails(); - if (refDetails != null) { - // here goes the new ref-details code - return ReferenceAttachmentItem( - attachment: attachment, - canEdit: canEdit, - openView: openView, - refDetails: refDetails, - ); - } - - // in practice it must be either of them! - _log.severe('Neither RefDetails nor Content found on attachment item'); - return ListTile( - leading: PhosphorIcon(PhosphorIconsThin.warningCircle), - title: Text(L10n.of(context).loadingFailed(attachment.typeStr())), - ); - } -} diff --git a/app/lib/features/attachments/widgets/attachment_section.dart b/app/lib/features/attachments/widgets/attachment_section.dart index 84875f60c8eb..a6ee795dc750 100644 --- a/app/lib/features/attachments/widgets/attachment_section.dart +++ b/app/lib/features/attachments/widgets/attachment_section.dart @@ -4,9 +4,10 @@ import 'package:acter/features/attachments/actions/handle_selected_attachments.d import 'package:acter/features/attachments/actions/select_attachment.dart'; import 'package:acter/features/attachments/providers/attachment_providers.dart'; import 'package:acter/features/attachments/types.dart'; -import 'package:acter/features/attachments/widgets/attachment_item.dart'; +import 'package:acter/features/attachments/widgets/msg_content_attachment_item.dart'; +import 'package:acter/features/attachments/widgets/reference_attachment_item.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' - show Attachment, AttachmentsManager; + show AttachmentsManager; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -79,9 +80,52 @@ class FoundAttachmentSectionWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final attachmentsLoader = ref.watch(attachmentsProvider(attachmentManager)); - return attachmentsLoader.when( - data: (attachments) => attachmentData(attachments, context, ref), + return Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + referenceAttachmentsUI(context, ref), + msgContentAttachmentsUI(context, ref), + ], + ), + ); + } + + Widget referenceAttachmentsUI( + BuildContext context, + WidgetRef ref, + ) { + final referenceAttachmentsLoader = + ref.watch(referenceAttachmentsProvider(attachmentManager)); + bool canEdit = attachmentManager.canEditAttachments(); + + return referenceAttachmentsLoader.when( + data: (refAttachmentList) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + L10n.of(context).references, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 10), + ListView.builder( + shrinkWrap: true, + itemCount: refAttachmentList.length, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return ReferenceAttachmentItem( + attachment: refAttachmentList[index], + canEdit: canEdit, + ); + }, + ), + const SizedBox(height: 20), + ], + ); + }, error: (e, s) { _log.severe('Failed to load attachments', e, s); return Text(L10n.of(context).errorLoadingAttachments(e)); @@ -96,36 +140,54 @@ class FoundAttachmentSectionWidget extends ConsumerWidget { ); } - Widget attachmentData( - List list, + Widget msgContentAttachmentsUI( BuildContext context, WidgetRef ref, ) { + final msgContentAttachmentsLoader = + ref.watch(msgContentAttachmentsProvider(attachmentManager)); bool canEdit = attachmentManager.canEditAttachments(); - return Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - attachmentHeader(context, ref), - if (list.isEmpty) ...[ - const SizedBox(height: 10), - Text(L10n.of(context).attachmentEmptyStateTitle), - ], - Wrap( - spacing: 5.0, - runSpacing: 10.0, - children: [ - for (final item in list) - _buildAttachmentItem(context, item, canEdit), + return msgContentAttachmentsLoader.when( + data: (msgContentAttachmentList) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + generalAttachmentHeader(context, ref), + if (msgContentAttachmentList.isNotEmpty) + ListView.builder( + shrinkWrap: true, + itemCount: msgContentAttachmentList.length, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return MsgContentAttachmentItem( + attachment: msgContentAttachmentList[index], + canEdit: canEdit, + ); + }, + ), + if (msgContentAttachmentList.isEmpty) ...[ + const SizedBox(height: 10), + Text(L10n.of(context).attachmentEmptyStateTitle), ], - ), - ], + ], + ); + }, + error: (e, s) { + _log.severe('Failed to load attachments', e, s); + return Text(L10n.of(context).errorLoadingAttachments(e)); + }, + loading: () => const Skeletonizer( + child: Wrap( + spacing: 5.0, + runSpacing: 10.0, + children: [], + ), ), ); } - Widget attachmentHeader(BuildContext context, WidgetRef ref) { + Widget generalAttachmentHeader(BuildContext context, WidgetRef ref) { final lang = L10n.of(context); final attachmentTitleTextStyle = Theme.of(context).textTheme.titleSmall; return Row( @@ -162,17 +224,4 @@ class FoundAttachmentSectionWidget extends ConsumerWidget { ], ); } - - Widget _buildAttachmentItem( - BuildContext context, - Attachment item, - bool canEdit, - ) { - final eventId = item.attachmentIdStr(); - return AttachmentItem( - key: Key('$eventId-attachment'), - attachment: item, - canEdit: canEdit, - ); - } } diff --git a/app/lib/features/attachments/widgets/msg_content_attachment_item.dart b/app/lib/features/attachments/widgets/msg_content_attachment_item.dart index a203462ea440..e94826dc8286 100644 --- a/app/lib/features/attachments/widgets/msg_content_attachment_item.dart +++ b/app/lib/features/attachments/widgets/msg_content_attachment_item.dart @@ -20,23 +20,21 @@ import 'package:path/path.dart' as p; // Attachment item UI class MsgContentAttachmentItem extends ConsumerWidget { final Attachment attachment; - final MsgContent msgContent; final bool canEdit; - // whether item can be viewed on gesture - final bool? openView; - const MsgContentAttachmentItem({ super.key, required this.attachment, - required this.msgContent, this.canEdit = false, - this.openView, }); @override Widget build(BuildContext context, WidgetRef ref) { final lang = L10n.of(context); + + final msgContent = attachment.msgContent(); + if (msgContent == null) return SizedBox.shrink(); + final containerColor = Theme.of(context).colorScheme.surface; final attachmentType = AttachmentType.values.byName(attachment.typeStr()); final eventId = attachment.attachmentIdStr(); @@ -44,6 +42,7 @@ class MsgContentAttachmentItem extends ConsumerWidget { final mediaState = ref.watch(attachmentMediaStateProvider(attachment)); return Container( + margin: EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: containerColor, borderRadius: BorderRadius.circular(8), @@ -56,8 +55,9 @@ class MsgContentAttachmentItem extends ConsumerWidget { context, attachmentType, mediaState.mediaFile, + msgContent, ), - title: title(context, attachmentType), + title: title(context, attachmentType, msgContent), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -110,7 +110,11 @@ class MsgContentAttachmentItem extends ConsumerWidget { ); } - Widget title(BuildContext context, AttachmentType attachmentType) { + Widget title( + BuildContext context, + AttachmentType attachmentType, + MsgContent msgContent, + ) { final fileName = msgContent.body(); final title = attachment.name() ?? fileName; final fileExtension = p.extension(fileName); @@ -165,6 +169,7 @@ class MsgContentAttachmentItem extends ConsumerWidget { BuildContext context, AttachmentType attachmentType, File? mediaFile, + MsgContent msgContent, ) async { // Open attachment link if (attachmentType == AttachmentType.link) { diff --git a/app/lib/features/attachments/widgets/reference_attachment_item.dart b/app/lib/features/attachments/widgets/reference_attachment_item.dart index 7f711ee711a3..94e17bc83c91 100644 --- a/app/lib/features/attachments/widgets/reference_attachment_item.dart +++ b/app/lib/features/attachments/widgets/reference_attachment_item.dart @@ -1,213 +1,97 @@ import 'package:acter/common/actions/redact_content.dart'; -import 'package:acter/common/utils/routes.dart'; -import 'package:acter/common/widgets/acter_icon_picker/acter_icon_widget.dart'; -import 'package:acter/common/widgets/acter_icon_picker/model/acter_icons.dart'; -import 'package:acter/common/widgets/acter_icon_picker/model/color_data.dart'; -import 'package:acter/features/events/providers/event_providers.dart'; -import 'package:acter/features/pins/providers/pins_provider.dart'; -import 'package:acter/features/tasks/providers/tasklists_providers.dart'; -import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; -import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' - show Attachment, RefDetails; +import 'package:acter/features/events/widgets/event_item.dart'; +import 'package:acter/features/pins/widgets/pin_list_item_widget.dart'; +import 'package:acter/features/tasks/widgets/task_list_item_card.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' show Attachment; import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; // Attachment item UI for References class ReferenceAttachmentItem extends ConsumerWidget { final Attachment attachment; - final RefDetails refDetails; final bool canEdit; - // whether item can be viewed on gesture - final bool? openView; - const ReferenceAttachmentItem({ super.key, required this.attachment, - required this.refDetails, this.canEdit = false, - this.openView, }); @override Widget build(BuildContext context, WidgetRef ref) { + final lang = L10n.of(context); + final defaultWidget = SizedBox.shrink(); + + final eventId = attachment.attachmentIdStr(); + final roomId = attachment.roomIdStr(); + final refDetails = attachment.refDetails(); + if (refDetails == null) return defaultWidget; + + final refObjectId = refDetails.targetIdStr(); + final refObjectType = refDetails.typeStr(); + if (refObjectId == null) return defaultWidget; + + final objectWidget = switch (refObjectType) { + 'pin' => PinListItemWidget( + pinId: refObjectId, + refDetails: refDetails, + showPinIndication: true, + cardMargin: EdgeInsets.zero, + ), + 'calendar-event' => EventItem( + eventId: refObjectId, + refDetails: refDetails, + margin: EdgeInsets.zero, + ), + 'task-list' => TaskListItemCard( + taskListId: refObjectId, + refDetails: refDetails, + showOnlyTaskList: true, + canExpand: false, + showTaskListIndication: true, + cardMargin: EdgeInsets.zero, + ), + _ => defaultWidget, + }; return Container( + margin: EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all(color: Theme.of(context).unselectedWidgetColor), ), - child: ListTile( - onTap: () => onTapRefAttachment(context, ref), - leading: refAttachmentIcons(ref), - title: refAttachmentTitle(context, ref), - subtitle: refAttachmentSubTitle(), - trailing: actionMenu(context), - ), - ); - } - - Widget refAttachmentTitle(BuildContext context, WidgetRef ref) { - final defaultTitle = Text(refDetails.title() ?? L10n.of(context).unknown); - final refObjectType = refDetails.typeStr(); - final refObjectId = refDetails.targetIdStr(); - - if (refObjectId == null) return defaultTitle; - switch (refObjectType) { - case 'pin': - final pin = ref.watch(pinProvider(refObjectId)).valueOrNull; - return pin == null ? defaultTitle : Text(pin.title().toString()); - case 'calendar-event': - final event = ref.watch(calendarEventProvider(refObjectId)).valueOrNull; - return event == null ? defaultTitle : Text(event.title().toString()); - case 'task-list': - final taskList = ref.watch(taskListProvider(refObjectId)).valueOrNull; - return taskList == null - ? defaultTitle - : Text(taskList.name().toString()); - default: - return defaultTitle; - } - } - - Widget refAttachmentSubTitle() { - return Text(refDetails.typeStr().toString()); - } - - Widget refAttachmentIcons(WidgetRef ref) { - final defaultIcon = PhosphorIcon(PhosphorIconsThin.tagChevron, size: 30); - final refObjectType = refDetails.typeStr(); - final refObjectId = refDetails.targetIdStr(); - - if (refObjectId == null) return defaultIcon; - switch (refObjectType) { - case 'pin': - final pin = ref.watch(pinProvider(refObjectId)).valueOrNull; - if (pin == null) return defaultIcon; - return ActerIconWidget( - iconSize: 30, - color: convertColor( - pin.display()?.color(), - iconPickerColors[0], - ), - icon: ActerIcon.iconForPin( - pin.display()?.iconStr(), - ), - ); - case 'calendar-event': - return PhosphorIcon(PhosphorIconsThin.calendar, size: 30); - case 'task-list': - final taskList = ref.watch(taskListProvider(refObjectId)).valueOrNull; - if (taskList == null) return defaultIcon; - return ActerIconWidget( - iconSize: 30, - color: convertColor( - taskList.display()?.color(), - iconPickerColors[0], - ), - icon: ActerIcon.iconForPin( - taskList.display()?.iconStr(), - ), - ); - default: - return defaultIcon; - } - } - - void onTapRefAttachment( - BuildContext context, - WidgetRef ref, - ) { - final lang = L10n.of(context); - final refObjectType = refDetails.typeStr(); - final refObjectId = refDetails.targetIdStr(); - final roomName = refDetails.roomDisplayName().toString(); - - if (refObjectId == null) return; - switch (refObjectType) { - case 'pin': - final pin = ref.read(pinProvider(refObjectId)).valueOrNull; - if (pin != null) { - context.pushNamed( - Routes.pin.name, - pathParameters: {'pinId': refObjectId}, - ); - } else { - EasyLoading.showError( - lang.noObjectAccess(refObjectType, roomName), - duration: const Duration(seconds: 3), - ); - } - case 'calendar-event': - final event = ref.watch(calendarEventProvider(refObjectId)).valueOrNull; - if (event != null) { - context.pushNamed( - Routes.calendarEvent.name, - pathParameters: {'calendarId': refObjectId}, - ); - } else { - EasyLoading.showError( - lang.noObjectAccess(refObjectType, roomName), - duration: const Duration(seconds: 3), - ); - } - case 'task-list': - final taskList = ref.watch(taskListProvider(refObjectId)).valueOrNull; - if (taskList != null) { - context.pushNamed( - Routes.taskListDetails.name, - pathParameters: {'taskListId': refObjectId}, - ); - } else { - EasyLoading.showError( - lang.noObjectAccess(refObjectType, roomName), - duration: const Duration(seconds: 3), - ); - } - default: - return; - } - } - - Widget actionMenu(BuildContext context) { - final lang = L10n.of(context); - final eventId = attachment.attachmentIdStr(); - final roomId = attachment.roomIdStr(); - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (canEdit) - PopupMenuButton( - key: const Key('attachment-item-menu-options'), - icon: const Icon(Icons.more_vert), - itemBuilder: (context) => [ - PopupMenuItem( - key: const Key('attachment-delete'), - onTap: () { - openRedactContentDialog( - context, - eventId: eventId, - roomId: roomId, - title: lang.deleteAttachment, - description: - lang.areYouSureYouWantToRemoveAttachmentFromPin, - isSpace: true, - ); - }, - child: Text( - lang.delete, - style: TextStyle( - color: Theme.of(context).colorScheme.error, + child: Row( + children: [ + Expanded(child: objectWidget), + if (canEdit) + PopupMenuButton( + key: const Key('attachment-item-menu-options'), + icon: const Icon(Icons.more_vert), + itemBuilder: (context) => [ + PopupMenuItem( + key: const Key('attachment-delete'), + onTap: () { + openRedactContentDialog( + context, + eventId: eventId, + roomId: roomId, + title: lang.removeReference, + description: lang.removeReferenceConfirmation, + isSpace: true, + ); + }, + child: Text( + lang.remove, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ); } } diff --git a/app/lib/features/events/widgets/event_item.dart b/app/lib/features/events/widgets/event_item.dart index 85d273c8895e..52e890203c00 100644 --- a/app/lib/features/events/widgets/event_item.dart +++ b/app/lib/features/events/widgets/event_item.dart @@ -2,24 +2,27 @@ import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/providers/room_providers.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/blinking_text.dart'; +import 'package:acter/common/widgets/reference_details_item.dart'; import 'package:acter/features/events/providers/event_providers.dart'; import 'package:acter/features/events/providers/event_type_provider.dart'; import 'package:acter/features/events/utils/events_utils.dart'; import 'package:acter/features/events/widgets/event_date_widget.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart' - show CalendarEvent, RsvpStatusTag; + show CalendarEvent, RefDetails, RsvpStatusTag; 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:logging/logging.dart'; +import 'package:skeletonizer/skeletonizer.dart'; final _log = Logger('a3::cal_event::event_item'); class EventItem extends ConsumerWidget { static const eventItemClick = Key('event_item_click'); - final CalendarEvent event; + final String eventId; + final RefDetails? refDetails; final EdgeInsetsGeometry? margin; final Function(String)? onTapEventItem; final bool isShowRsvp; @@ -27,7 +30,8 @@ class EventItem extends ConsumerWidget { const EventItem({ super.key, - required this.event, + required this.eventId, + this.refDetails, this.margin, this.onTapEventItem, this.isShowRsvp = true, @@ -36,6 +40,21 @@ class EventItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final event = ref.watch(calendarEventProvider(eventId)).valueOrNull; + if (event != null) { + return _buildEventItemUI(context, ref, event); + } else if (refDetails != null) { + return ReferenceDetailsItem(refDetails: refDetails!); + } else { + return const Skeletonizer(child: SizedBox(height: 100, width: 100)); + } + } + + Widget _buildEventItemUI( + BuildContext context, + WidgetRef ref, + CalendarEvent event, + ) { final eventType = ref.watch(eventTypeProvider(event)); return InkWell( key: eventItemClick, @@ -66,8 +85,8 @@ class EventItem extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildEventTitle(context), - Consumer(builder: _buildEventSubtitle), + _buildEventTitle(context, event.title()), + _buildEventSubtitle(context, ref, event), const SizedBox(height: 4), ], ), @@ -76,7 +95,7 @@ class EventItem extends ConsumerWidget { if (eventType == EventFilters.ongoing) _buildHappeningIndication(context), const SizedBox(width: 10), - if (isShowRsvp) _buildRsvpStatus(context, ref), + if (isShowRsvp) _buildRsvpStatus(context, ref,event), const SizedBox(width: 10), ], ), @@ -86,9 +105,9 @@ class EventItem extends ConsumerWidget { ); } - Widget _buildEventTitle(BuildContext context) { + Widget _buildEventTitle(BuildContext context, String title) { return Text( - event.title(), + title, style: Theme.of(context).textTheme.bodyMedium, maxLines: 2, overflow: TextOverflow.ellipsis, @@ -98,7 +117,7 @@ class EventItem extends ConsumerWidget { Widget _buildEventSubtitle( BuildContext context, WidgetRef ref, - Widget? child, + CalendarEvent event, ) { String eventSpaceName = ref.watch(roomDisplayNameProvider(event.roomIdStr())).valueOrNull ?? @@ -112,7 +131,11 @@ class EventItem extends ConsumerWidget { ); } - Widget _buildRsvpStatus(BuildContext context, WidgetRef ref) { + Widget _buildRsvpStatus( + BuildContext context, + WidgetRef ref, + CalendarEvent event, + ) { final lang = L10n.of(context); final eventId = event.eventId().toString(); final rsvpLoader = ref.watch(myRsvpStatusProvider(eventId)); diff --git a/app/lib/features/events/widgets/event_list_widget.dart b/app/lib/features/events/widgets/event_list_widget.dart index 0923dffd4ca5..ed30bb91cbeb 100644 --- a/app/lib/features/events/widgets/event_list_widget.dart +++ b/app/lib/features/events/widgets/event_list_widget.dart @@ -105,7 +105,7 @@ class EventListWidget extends ConsumerWidget { physics: shrinkWrap ? const NeverScrollableScrollPhysics() : null, itemBuilder: (context, index) { return EventItem( - event: eventList[index], + eventId: eventList[index].eventId().toString(), isShowSpaceName: isShowSpaceName, onTapEventItem: onTapEventItem, ); diff --git a/app/lib/features/news/actions/submit_news.dart b/app/lib/features/news/actions/submit_news.dart index ab3a8a51e2dc..8ba68036067e 100644 --- a/app/lib/features/news/actions/submit_news.dart +++ b/app/lib/features/news/actions/submit_news.dart @@ -1,13 +1,10 @@ -import 'package:acter/common/extensions/options.dart'; import 'package:acter/common/providers/sdk_provider.dart'; import 'package:acter/common/providers/space_providers.dart'; import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/spaces/space_selector_drawer.dart'; import 'package:acter/features/home/providers/client_providers.dart'; -import 'package:acter/features/news/model/news_references_model.dart'; import 'package:acter/features/news/model/news_slide_model.dart'; import 'package:acter/features/news/providers/news_post_editor_providers.dart'; -import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -42,10 +39,9 @@ Future _makeTextSlide( sdk.api.newColorizeBuilder(null, slidePost.backgroundColor?.value), ); - final referenceModel = slidePost.newsReferencesModel; - - if (referenceModel != null) { - final objRef = getSlideReference(sdk, referenceModel); + final refDetails = slidePost.refDetails; + if (refDetails != null) { + final objRef = sdk.api.newObjRefBuilder(null, refDetails).build(); textSlideDraft.addReference(objRef); } @@ -80,9 +76,10 @@ Future _makeImageSlide( imageSlideDraft.color( sdk.api.newColorizeBuilder(null, slidePost.backgroundColor?.value), ); - final reference = slidePost.newsReferencesModel; - if (reference != null) { - final objRef = getSlideReference(sdk, reference); + + final refDetails = slidePost.refDetails; + if (refDetails != null) { + final objRef = sdk.api.newObjRefBuilder(null, refDetails).build(); imageSlideDraft.addReference(objRef); } return imageSlideDraft; @@ -111,9 +108,9 @@ Future _makeVideoSlide( videoSlideDraft.color( sdk.api.newColorizeBuilder(null, slidePost.backgroundColor?.value), ); - final referenceModel = slidePost.newsReferencesModel; - if (referenceModel != null) { - final objRef = getSlideReference(sdk, referenceModel); + final refDetails = slidePost.refDetails; + if (refDetails != null) { + final objRef = sdk.api.newObjRefBuilder(null, refDetails).build(); videoSlideDraft.addReference(objRef); } return videoSlideDraft; @@ -186,36 +183,3 @@ Future sendNews(BuildContext context, WidgetRef ref) async { Navigator.pop(context); context.pushReplacementNamed(Routes.main.name); // go to the home/main updates } - -ObjRef getSlideReference(ActerSdk sdk, NewsReferencesModel refModel) { - final refDetails = switch (refModel.type) { - NewsReferencesType.calendarEvent => sdk.api - .newCalendarEventRefBuilder( - refModel.id.expect('Referenced Calendar misses id'), - null, - null, - ) - .build(), - NewsReferencesType.pin => sdk.api - .newPinRefBuilder( - refModel.id.expect('Referenced Pin misses id'), - null, - null, - ) - .build(), - NewsReferencesType.taskList => sdk.api - .newTaskListRefBuilder( - refModel.id.expect('Referenced TaskList misses id'), - null, - null, - ) - .build(), - NewsReferencesType.link => sdk.api - .newLinkRefBuilder( - refModel.title.expect('Referenced link misses title'), - refModel.id.expect('Referenced link misses id'), - ) - .build(), - }; - return sdk.api.newObjRefBuilder(null, refDetails).build(); -} diff --git a/app/lib/features/news/model/news_slide_model.dart b/app/lib/features/news/model/news_slide_model.dart index 6399ad29990c..ed023e272f4f 100644 --- a/app/lib/features/news/model/news_slide_model.dart +++ b/app/lib/features/news/model/news_slide_model.dart @@ -1,4 +1,4 @@ -import 'package:acter/features/news/model/news_references_model.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -15,7 +15,7 @@ class NewsSlideItem { Color? backgroundColor; Color? foregroundColor; XFile? mediaFile; - NewsReferencesModel? newsReferencesModel; + RefDetails? refDetails; NewsSlideItem({ required this.type, @@ -24,6 +24,6 @@ class NewsSlideItem { this.backgroundColor, this.foregroundColor, this.mediaFile, - this.newsReferencesModel, + this.refDetails, }); } diff --git a/app/lib/features/news/news_utils/news_utils.dart b/app/lib/features/news/news_utils/news_utils.dart index d55f9952ad7a..09c2f05a1024 100644 --- a/app/lib/features/news/news_utils/news_utils.dart +++ b/app/lib/features/news/news_utils/news_utils.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:acter/common/utils/utils.dart'; import 'package:acter/features/news/model/news_post_color_data.dart'; -import 'package:acter/features/news/model/news_references_model.dart'; import 'package:acter/features/news/model/news_slide_model.dart'; import 'package:acter/features/news/providers/news_post_editor_providers.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; @@ -54,14 +53,14 @@ class NewsUtils { //Add text slide static void addTextSlide({ required WidgetRef ref, - NewsReferencesModel? newsReferencesModel, + RefDetails? refDetails, }) { final clr = getRandomElement(newsPostColors); NewsSlideItem textSlide = NewsSlideItem( type: NewsSlideType.text, text: '', backgroundColor: clr, - newsReferencesModel: newsReferencesModel, + refDetails: refDetails, ); ref.read(newsStateProvider.notifier).addSlide(textSlide); } @@ -69,7 +68,7 @@ class NewsUtils { //Add image slide static Future addImageSlide({ required WidgetRef ref, - NewsReferencesModel? newsReferencesModel, + RefDetails? refDetails, }) async { final clr = getRandomElement(newsPostColors); XFile? imageFile = await imagePicker.pickImage( @@ -80,7 +79,7 @@ class NewsUtils { type: NewsSlideType.image, mediaFile: imageFile, backgroundColor: clr, - newsReferencesModel: newsReferencesModel, + refDetails: refDetails, ); ref.read(newsStateProvider.notifier).addSlide(slide); } @@ -89,7 +88,7 @@ class NewsUtils { //Add video slide static Future addVideoSlide({ required WidgetRef ref, - NewsReferencesModel? newsReferencesModel, + RefDetails? refDetails, }) async { final clr = getRandomElement(newsPostColors); XFile? videoFile = await imagePicker.pickVideo( @@ -100,7 +99,7 @@ class NewsUtils { type: NewsSlideType.video, mediaFile: videoFile, backgroundColor: clr, - newsReferencesModel: newsReferencesModel, + refDetails: refDetails, ); ref.read(newsStateProvider.notifier).addSlide(slide); } diff --git a/app/lib/features/news/pages/add_news_page.dart b/app/lib/features/news/pages/add_news_page.dart index a3aa9136b5c8..75cd0472cd4c 100644 --- a/app/lib/features/news/pages/add_news_page.dart +++ b/app/lib/features/news/pages/add_news_page.dart @@ -6,13 +6,13 @@ import 'package:acter/common/widgets/acter_video_player.dart'; import 'package:acter/common/widgets/html_editor/html_editor.dart'; import 'package:acter/features/news/actions/submit_news.dart'; import 'package:acter/features/news/model/keys.dart'; -import 'package:acter/features/news/model/news_references_model.dart'; import 'package:acter/features/news/model/news_slide_model.dart'; import 'package:acter/features/news/news_utils/news_utils.dart'; import 'package:acter/features/news/providers/news_post_editor_providers.dart'; import 'package:acter/features/news/widgets/news_post_editor/news_slide_options.dart'; import 'package:acter/features/news/widgets/news_post_editor/select_action_item.dart'; import 'package:acter/features/news/widgets/news_post_editor/selected_action_button.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; @@ -25,12 +25,12 @@ const addNewsKey = Key('add-news'); class AddNewsPage extends ConsumerStatefulWidget { final String? initialSelectedSpace; - final NewsReferencesModel? newsReferencesModel; + final RefDetails? refDetails; const AddNewsPage({ super.key = addNewsKey, this.initialSelectedSpace, - this.newsReferencesModel, + this.refDetails, }); @override @@ -250,7 +250,7 @@ class AddNewsState extends ConsumerState { bottom: 10, left: 10, child: SelectedActionButton( - newsReferences: selectedNewsPost?.newsReferencesModel, + refDetails: selectedNewsPost?.refDetails, ), ), ], @@ -295,7 +295,7 @@ class AddNewsState extends ConsumerState { onPressed: () { NewsUtils.addTextSlide( ref: ref, - newsReferencesModel: widget.newsReferencesModel, + refDetails: widget.refDetails, ); }, child: Text(lang.addTextSlide), @@ -305,7 +305,7 @@ class AddNewsState extends ConsumerState { key: NewsUpdateKeys.addImageSlide, onPressed: () async => await NewsUtils.addImageSlide( ref: ref, - newsReferencesModel: widget.newsReferencesModel, + refDetails: widget.refDetails, ), child: Text(lang.addImageSlide), ), @@ -314,7 +314,7 @@ class AddNewsState extends ConsumerState { key: NewsUpdateKeys.addVideoSlide, onPressed: () async => await NewsUtils.addVideoSlide( ref: ref, - newsReferencesModel: widget.newsReferencesModel, + refDetails: widget.refDetails, ), child: Text(lang.addVideoSlide), ), diff --git a/app/lib/features/news/providers/news_post_editor_providers.dart b/app/lib/features/news/providers/news_post_editor_providers.dart index dd6d5e20f215..1bd107d13362 100644 --- a/app/lib/features/news/providers/news_post_editor_providers.dart +++ b/app/lib/features/news/providers/news_post_editor_providers.dart @@ -3,13 +3,14 @@ import 'package:acter/common/widgets/event/event_selector_drawer.dart'; import 'package:acter/common/widgets/pin/pin_selector_drawer.dart'; import 'package:acter/common/widgets/spaces/space_selector_drawer.dart'; import 'package:acter/common/widgets/task/taskList_selector_drawer.dart'; +import 'package:acter/features/events/providers/event_providers.dart'; import 'package:acter/features/news/model/news_post_color_data.dart'; import 'package:acter/features/news/model/news_post_state.dart'; -import 'package:acter/features/news/model/news_references_model.dart'; import 'package:acter/features/news/model/news_slide_model.dart'; +import 'package:acter/features/pins/providers/pins_provider.dart'; +import 'package:acter/features/tasks/providers/tasklists_providers.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:riverpod/riverpod.dart'; final newsStateProvider = @@ -33,12 +34,6 @@ class NewsStateNotifier extends StateNotifier { context: context, canCheck: 'CanPostNews', ); - //Clear object reference if news post id gets changes - state.currentNewsSlide?.newsReferencesModel = null; - for (final slide in state.newsSlideList) { - slide.newsReferencesModel = null; - } - state = state.copyWith(newsPostSpaceId: spaceId); } @@ -55,89 +50,40 @@ class NewsStateNotifier extends StateNotifier { } Future selectEventToShare(BuildContext context) async { - final lang = L10n.of(context); - final newsPostSpaceId = state.newsPostSpaceId ?? - await selectSpaceDrawer( - context: context, - canCheck: 'CanPostNews', - ); - state = state.copyWith(newsPostSpaceId: newsPostSpaceId); - - if (newsPostSpaceId == null) { - EasyLoading.showToast(lang.pleaseFirstSelectASpace); - return; + final eventId = await selectEventDrawer(context: context); + RefDetails? refDetails; + if (eventId != null) { + final selectedEvent = + await ref.watch(calendarEventProvider(eventId).future); + refDetails = await selectedEvent.refDetails(); } - if (!context.mounted) { - return; - } - final eventId = await selectEventDrawer( - context: context, - spaceId: newsPostSpaceId, - ); - final newsSpaceReference = NewsReferencesModel( - type: NewsReferencesType.calendarEvent, - id: eventId, - ); NewsSlideItem? selectedNewsSlide = state.currentNewsSlide; - selectedNewsSlide?.newsReferencesModel = newsSpaceReference; + selectedNewsSlide?.refDetails = refDetails; state = state.copyWith(currentNewsSlide: selectedNewsSlide); } Future selectPinToShare(BuildContext context) async { - final lang = L10n.of(context); - final newsPostSpaceId = state.newsPostSpaceId ?? - await selectSpaceDrawer( - context: context, - canCheck: 'CanPostPin', - ); - state = state.copyWith(newsPostSpaceId: newsPostSpaceId); - - if (newsPostSpaceId == null) { - EasyLoading.showToast(lang.pleaseFirstSelectASpace); - return; - } - if (!context.mounted) { - return; + final pinId = await selectPinDrawer(context: context); + RefDetails? refDetails; + if (pinId != null) { + final selectedPin = await ref.watch(pinProvider(pinId).future); + refDetails = await selectedPin.refDetails(); } - final pinId = await selectPinDrawer( - context: context, - spaceId: newsPostSpaceId, - ); - final newsSpaceReference = NewsReferencesModel( - type: NewsReferencesType.pin, - id: pinId, - ); NewsSlideItem? selectedNewsSlide = state.currentNewsSlide; - selectedNewsSlide?.newsReferencesModel = newsSpaceReference; + selectedNewsSlide?.refDetails = refDetails; state = state.copyWith(currentNewsSlide: selectedNewsSlide); } Future selectTaskListToShare(BuildContext context) async { - final lang = L10n.of(context); - final newsPostSpaceId = state.newsPostSpaceId ?? - await selectSpaceDrawer( - context: context, - canCheck: 'CanPostTask', - ); - state = state.copyWith(newsPostSpaceId: newsPostSpaceId); - - if (newsPostSpaceId == null) { - EasyLoading.showToast(lang.pleaseFirstSelectASpace); - return; + final taskListId = await selectTaskListDrawer(context: context); + RefDetails? refDetails; + if (taskListId != null) { + final selectedTaskList = + await ref.watch(taskListProvider(taskListId).future); + refDetails = await selectedTaskList.refDetails(); } - if (!context.mounted) { - return; - } - final taskListId = await selectTaskListDrawer( - context: context, - spaceId: newsPostSpaceId, - ); - final newsSpaceReference = NewsReferencesModel( - type: NewsReferencesType.taskList, - id: taskListId, - ); NewsSlideItem? selectedNewsSlide = state.currentNewsSlide; - selectedNewsSlide?.newsReferencesModel = newsSpaceReference; + selectedNewsSlide?.refDetails = refDetails; state = state.copyWith(currentNewsSlide: selectedNewsSlide); } diff --git a/app/lib/features/news/widgets/news_item_slide/news_slide_actions.dart b/app/lib/features/news/widgets/news_item_slide/news_slide_actions.dart index 8e5390409442..1b3d5a8f15ef 100644 --- a/app/lib/features/news/widgets/news_item_slide/news_slide_actions.dart +++ b/app/lib/features/news/widgets/news_item_slide/news_slide_actions.dart @@ -1,21 +1,13 @@ import 'package:acter/common/actions/open_link.dart'; -import 'package:acter/common/toolkit/errors/error_dialog.dart'; -import 'package:acter/features/events/providers/event_providers.dart'; import 'package:acter/features/events/widgets/event_item.dart'; -import 'package:acter/features/events/widgets/skeletons/event_item_skeleton_widget.dart'; import 'package:acter/features/news/model/news_references_model.dart'; -import 'package:acter/features/pins/providers/pins_provider.dart'; import 'package:acter/features/pins/widgets/pin_list_item_widget.dart'; -import 'package:acter/features/tasks/providers/tasklists_providers.dart'; import 'package:acter/features/tasks/widgets/task_list_item_card.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:atlas_icons/atlas_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logging/logging.dart'; - -final _log = Logger('a3::news::news_action_actions'); class NewsSlideActions extends ConsumerWidget { final NewsSlide newsSlide; @@ -33,127 +25,28 @@ class NewsSlideActions extends ConsumerWidget { final evtType = NewsReferencesType.fromStr(referenceDetails.typeStr()); final id = referenceDetails.targetIdStr() ?? ''; return switch (evtType) { - NewsReferencesType.calendarEvent => - renderCalendarEventAction(context, ref, id), - NewsReferencesType.pin => renderPinAction(context, ref, id), - NewsReferencesType.taskList => renderTaskListAction(context, ref, id), + NewsReferencesType.calendarEvent => EventItem( + eventId: id, + refDetails: referenceDetails, + ), + NewsReferencesType.pin => PinListItemWidget( + pinId: id, + refDetails: referenceDetails, + showPinIndication: true, + ), + NewsReferencesType.taskList => TaskListItemCard( + taskListId: id, + refDetails: referenceDetails, + showOnlyTaskList: true, + canExpand: false, + showTaskListIndication: true, + ), NewsReferencesType.link => renderLinkActionButton(context, ref, referenceDetails), _ => renderNotSupportedAction(context) }; } - Widget renderPinAction( - BuildContext context, - WidgetRef ref, - String pinId, - ) { - final lang = L10n.of(context); - final pinData = ref.watch(pinProvider(pinId)); - final pinError = pinData.asError; - if (pinError != null) { - _log.severe('Error loading pin', pinError.error, pinError.stackTrace); - return Card( - child: ListTile( - leading: const Icon(Icons.pin), - title: Text(lang.pinNoLongerAvailable), - subtitle: Text( - lang.pinDeletedOrFailedToLoad, - style: Theme.of(context).textTheme.labelLarge, - ), - onTap: () async { - await ActerErrorDialog.show( - context: context, - error: pinError.error, - stack: pinError.stackTrace, - onRetryTap: () => ref.invalidate(pinProvider(pinId)), - ); - }, - ), - ); - } - return PinListItemWidget( - pinId: pinId, - showPinIndication: true, - ); - } - - Widget renderTaskListAction( - BuildContext context, - WidgetRef ref, - String taskListId, - ) { - final lang = L10n.of(context); - final taskListData = ref.watch(taskListProvider(taskListId)); - final taskListError = taskListData.asError; - if (taskListError != null) { - _log.severe( - 'Error loading task list', - taskListError.error, - taskListError.stackTrace, - ); - return Card( - child: ListTile( - leading: const Icon(Icons.list), - title: Text(lang.pinNoLongerAvailable), - subtitle: Text( - lang.pinDeletedOrFailedToLoad, - style: Theme.of(context).textTheme.labelLarge, - ), - onTap: () async { - await ActerErrorDialog.show( - context: context, - error: taskListError.error, - stack: taskListError.stackTrace, - onRetryTap: () => ref.invalidate(pinProvider(taskListId)), - ); - }, - ), - ); - } - return TaskListItemCard( - taskListId: taskListId, - showOnlyTaskList: true, - canExpand: false, - showTaskListIndication: true, - ); - } - - Widget renderCalendarEventAction( - BuildContext context, - WidgetRef ref, - String eventId, - ) { - final lang = L10n.of(context); - final calEventLoader = ref.watch(calendarEventProvider(eventId)); - return calEventLoader.when( - data: (calEvent) => EventItem(event: calEvent), - loading: () => const EventItemSkeleton(), - error: (e, s) { - _log.severe('Failed to load cal event', e, s); - return Card( - child: ListTile( - leading: const Icon(Icons.calendar_month), - title: Text(lang.eventNoLongerAvailable), - subtitle: Text( - lang.eventDeletedOrFailedToLoad, - style: Theme.of(context).textTheme.labelLarge, - ), - onTap: () async { - await ActerErrorDialog.show( - context: context, - error: e, - stack: s, - onRetryTap: () => - ref.invalidate(calendarEventProvider(eventId)), - ); - }, - ), - ); - }, - ); - } - Widget renderLinkActionButton( BuildContext context, WidgetRef ref, @@ -167,7 +60,10 @@ class NewsSlideActions extends ConsumerWidget { } if (referenceDetails.title() == 'shareEvent' && uri.startsWith('\$')) { // fallback support for older, badly formatted calendar events. - return renderCalendarEventAction(context, ref, uri); + return EventItem( + eventId: uri, + refDetails: referenceDetails, + ); } final title = referenceDetails.title(); diff --git a/app/lib/features/news/widgets/news_post_editor/selected_action_button.dart b/app/lib/features/news/widgets/news_post_editor/selected_action_button.dart index d63568d47832..4eba7c4dc290 100644 --- a/app/lib/features/news/widgets/news_post_editor/selected_action_button.dart +++ b/app/lib/features/news/widgets/news_post_editor/selected_action_button.dart @@ -1,39 +1,39 @@ import 'package:acter/features/events/providers/event_providers.dart'; import 'package:acter/features/events/widgets/event_item.dart'; import 'package:acter/features/events/widgets/skeletons/event_item_skeleton_widget.dart'; -import 'package:acter/features/news/model/news_references_model.dart'; import 'package:acter/features/news/providers/news_post_editor_providers.dart'; import 'package:acter/features/pins/providers/pins_provider.dart'; import 'package:acter/features/pins/widgets/pin_list_item_widget.dart'; import 'package:acter/features/tasks/providers/tasklists_providers.dart'; import 'package:acter/features/tasks/widgets/skeleton/tasks_list_skeleton.dart'; import 'package:acter/features/tasks/widgets/task_list_item_card.dart'; +import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; final _log = Logger('a3::news::add'); class SelectedActionButton extends ConsumerWidget { - final NewsReferencesModel? newsReferences; + final RefDetails? refDetails; const SelectedActionButton({ super.key, - this.newsReferences, + this.refDetails, }); @override Widget build(BuildContext context, WidgetRef ref) { - final id = newsReferences?.id; - final type = newsReferences?.type; - if (id == null) return SizedBox(); + if (refDetails == null) return SizedBox(); + final refObjectId = refDetails!.targetIdStr(); + final refObjectType = refDetails!.typeStr(); + if (refObjectId == null) return SizedBox(); - return switch (type) { - NewsReferencesType.calendarEvent => - calendarActionButton(context, ref, id), - NewsReferencesType.pin => pinActionButton(context, ref, id), - NewsReferencesType.taskList => taskListActionButton(context, ref, id), + return switch (refObjectType) { + 'pin' => pinActionButton(context, ref, refObjectId), + 'calendar-event' => calendarActionButton(context, ref, refObjectId), + 'task-list' => taskListActionButton(context, ref, refObjectId), _ => const SizedBox(), }; } @@ -44,7 +44,7 @@ class SelectedActionButton extends ConsumerWidget { return SizedBox( width: 300, child: EventItem( - event: calendarEvent, + eventId: calendarEvent.eventId().toString(), isShowRsvp: false, onTapEventItem: (event) async { final notifier = ref.read(newsStateProvider.notifier); diff --git a/app/lib/features/pins/widgets/pin_list_item_widget.dart b/app/lib/features/pins/widgets/pin_list_item_widget.dart index 13c28b6ce1c2..623218a05c9d 100644 --- a/app/lib/features/pins/widgets/pin_list_item_widget.dart +++ b/app/lib/features/pins/widgets/pin_list_item_widget.dart @@ -2,6 +2,7 @@ import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/acter_icon_picker/acter_icon_widget.dart'; import 'package:acter/common/widgets/acter_icon_picker/model/acter_icons.dart'; import 'package:acter/common/widgets/acter_icon_picker/model/color_data.dart'; +import 'package:acter/common/widgets/reference_details_item.dart'; import 'package:acter/common/widgets/space_name_widget.dart'; import 'package:acter/features/pins/providers/pins_provider.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk.dart'; @@ -11,42 +12,41 @@ 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:logging/logging.dart'; import 'package:skeletonizer/skeletonizer.dart'; -final _log = Logger('a3::pins::list_item'); - class PinListItemWidget extends ConsumerWidget { final String pinId; + final RefDetails? refDetails; final bool showSpace; final bool showPinIndication; + final EdgeInsetsGeometry? cardMargin; final Function(String)? onTaPinItem; const PinListItemWidget({ required this.pinId, + this.refDetails, this.showSpace = false, this.showPinIndication = false, + this.cardMargin, this.onTaPinItem, super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { - final pinData = ref.watch(pinProvider(pinId)); - return pinData.when( - data: (pin) => buildPinItemUI(context, pin), - error: (e, s) { - _log.severe('Failed to load pin', e, s); - return Text(L10n.of(context).loadingFailed(e)); - }, - loading: () => const Skeletonizer( - child: SizedBox(height: 100, width: 100), - ), - ); + final pin = ref.watch(pinProvider(pinId)).valueOrNull; + if (pin != null) { + return buildPinItemUI(context, pin); + } else if (refDetails != null) { + return ReferenceDetailsItem(refDetails: refDetails!); + } else { + return const Skeletonizer(child: SizedBox(height: 100, width: 100)); + } } Widget buildPinItemUI(BuildContext context, ActerPin pin) { return Card( + margin: cardMargin, child: ListTile( onTap: () { final pinId = pin.eventIdStr(); 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 33e64682e0f0..2dd01f79dca5 100644 --- a/app/lib/features/tasks/widgets/task_list_item_card.dart +++ b/app/lib/features/tasks/widgets/task_list_item_card.dart @@ -2,6 +2,7 @@ import 'package:acter/common/utils/routes.dart'; import 'package:acter/common/widgets/acter_icon_picker/acter_icon_widget.dart'; import 'package:acter/common/widgets/acter_icon_picker/model/acter_icons.dart'; import 'package:acter/common/widgets/acter_icon_picker/model/color_data.dart'; +import 'package:acter/common/widgets/reference_details_item.dart'; import 'package:acter/features/home/widgets/space_chip.dart'; import 'package:acter/features/tasks/providers/tasklists_providers.dart'; import 'package:acter/features/tasks/widgets/task_items_list_widget.dart'; @@ -12,53 +13,50 @@ 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:logging/logging.dart'; - -final _log = Logger('a3::tasks::widgets::task_list_item_card'); +import 'package:skeletonizer/skeletonizer.dart'; class TaskListItemCard extends ConsumerWidget { final String taskListId; + final RefDetails? refDetails; final bool showSpace; final bool showTaskListIndication; final bool showCompletedTask; final bool showOnlyTaskList; final bool initiallyExpanded; final bool canExpand; + final EdgeInsetsGeometry? cardMargin; final GestureTapCallback? onTitleTap; const TaskListItemCard({ super.key, required this.taskListId, + this.refDetails, this.showSpace = false, this.showTaskListIndication = false, this.showCompletedTask = false, this.showOnlyTaskList = false, this.initiallyExpanded = true, this.canExpand = true, + this.cardMargin, this.onTitleTap, }); @override Widget build(BuildContext context, WidgetRef ref) { - final lang = L10n.of(context); - final tasklistLoader = ref.watch(taskListProvider(taskListId)); - return tasklistLoader.when( - data: (taskList) => Card( + final taskList = ref.watch(taskListProvider(taskListId)).valueOrNull; + if (taskList != null) { + return Card( + margin: cardMargin, key: Key('task-list-card-$taskListId'), child: canExpand ? expandable(context, ref, taskList) : simple(context, ref, taskList), - ), - error: (e, s) { - _log.severe('Failed to load tasklist', e, s); - return Card( - child: Text(lang.errorLoadingTasks(e)), - ); - }, - loading: () => Card( - child: Text(lang.loading), - ), - ); + ); + } else if (refDetails != null) { + return ReferenceDetailsItem(refDetails: refDetails!); + } else { + return const Skeletonizer(child: SizedBox(height: 100, width: 100)); + } } Widget expandable(BuildContext context, WidgetRef ref, TaskList taskList) => diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 4879db3aac9d..599037e3c218 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -2305,6 +2305,9 @@ "qr": "QR", "newBoost": "New\nBoost", "addComment": "Add Comment", + "references": "References", + "removeReference": "Remove Reference", + "removeReferenceConfirmation": "Are you sure you want to remove this reference?", "noObjectAccess": "You are not part of {spaceName} so you can't access this {objectType}", "@addComment": {} } diff --git a/app/lib/router/general_router.dart b/app/lib/router/general_router.dart index 8161e4419faf..2b0828dfd809 100644 --- a/app/lib/router/general_router.dart +++ b/app/lib/router/general_router.dart @@ -14,15 +14,14 @@ import 'package:acter/features/chat/widgets/create_chat.dart'; import 'package:acter/features/deep_linking/pages/scan_qr_code.dart'; import 'package:acter/features/intro/pages/intro_page.dart'; import 'package:acter/features/intro/pages/intro_profile.dart'; +import 'package:acter/features/link_room/pages/link_room_page.dart'; import 'package:acter/features/link_room/types.dart'; -import 'package:acter/features/news/model/news_references_model.dart'; import 'package:acter/features/news/pages/add_news_page.dart'; import 'package:acter/features/onboarding/pages/analytics_opt_in_page.dart'; import 'package:acter/features/onboarding/pages/link_email_page.dart'; import 'package:acter/features/onboarding/pages/save_username_page.dart'; import 'package:acter/features/onboarding/pages/upload_avatar_page.dart'; import 'package:acter/features/pins/pages/create_pin_page.dart'; -import 'package:acter/features/link_room/pages/link_room_page.dart'; import 'package:acter/features/super_invites/pages/create.dart'; import 'package:acter/router/router.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; @@ -223,12 +222,12 @@ final generalRoutes = [ redirect: authGuardRedirect, pageBuilder: (context, state) { final spaceId = state.uri.queryParameters['spaceId']; - final newsReferencesModel = state.extra as NewsReferencesModel?; + final refDetails = state.extra as RefDetails?; return MaterialPage( key: state.pageKey, child: AddNewsPage( initialSelectedSpace: spaceId?.isNotEmpty == true ? spaceId : null, - newsReferencesModel: newsReferencesModel, + refDetails: refDetails, ), ); }, diff --git a/app/test/features/events/event_item_test.dart b/app/test/features/events/event_item_test.dart index 625329d5e212..2685b2bf46c2 100644 --- a/app/test/features/events/event_item_test.dart +++ b/app/test/features/events/event_item_test.dart @@ -35,7 +35,8 @@ void main() { Function(String)? onTapEventItem, EventFilters eventFilter = EventFilters.upcoming, }) async { - final mockedNotifier = MockAsyncCalendarEventNotifier(); + final mockedNotifier = MockAsyncCalendarEventNotifier(shouldFail: false); + await tester.pumpProviderWidget( overrides: [ utcNowProvider.overrideWith((ref) => mockUtcNowNotifier), @@ -47,12 +48,14 @@ void main() { roomDisplayNameProvider.overrideWith((a, b) => 'test'), ], child: EventItem( - event: mockEvent, + eventId: mockEvent.eventId().toString(), isShowRsvp: isShowRsvp, isShowSpaceName: isShowSpaceName, onTapEventItem: onTapEventItem, ), ); + // Wait for the async provider to load + await tester.pump(); } testWidgets('displays event title', (tester) async { @@ -76,7 +79,7 @@ void main() { ); await tester.tap(find.byKey(EventItem.eventItemClick)); - await tester.pumpAndSettle(); + await tester.pump(); verify(() => mockOnTapEventItem.call('1234')).called(1); }); @@ -105,7 +108,7 @@ void main() { await createWidgetUnderTest(tester: tester); // Act: Trigger a frame - await tester.pumpAndSettle(); + await tester.pump(); // Assert: Check if the Yes icon is displayed expect(find.byIcon(Icons.check_circle), findsOneWidget); @@ -119,7 +122,7 @@ void main() { await createWidgetUnderTest(tester: tester); // Act: Trigger a frame - await tester.pumpAndSettle(); + await tester.pump(); // Assert: Check if the No icon is displayed expect(find.byIcon(Icons.cancel), findsOneWidget); @@ -133,7 +136,7 @@ void main() { await createWidgetUnderTest(tester: tester); // Act: Trigger a frame - await tester.pumpAndSettle(); + await tester.pump(); // Assert: Check if the Maybe icon is displayed expect(find.byIcon(Icons.question_mark_rounded), findsOneWidget); @@ -145,7 +148,7 @@ void main() { await createWidgetUnderTest(tester: tester, isShowRsvp: false); // Act: Trigger a frame - await tester.pumpAndSettle(); + await tester.pump(); // Assert: Check that no RSVP status icon is displayed expect(find.byIcon(Icons.check_circle), findsNothing); diff --git a/app/test/features/events/event_list_widget_test.dart b/app/test/features/events/event_list_widget_test.dart index 1473b00ceaec..4092414c1c9a 100644 --- a/app/test/features/events/event_list_widget_test.dart +++ b/app/test/features/events/event_list_widget_test.dart @@ -3,6 +3,7 @@ import 'package:acter/features/bookmarks/providers/bookmarks_provider.dart'; import 'package:acter/features/datetime/providers/utc_now_provider.dart'; import 'package:acter/features/events/providers/event_providers.dart'; import 'package:acter/features/events/providers/event_type_provider.dart'; +import 'package:acter/features/events/widgets/event_item.dart'; import 'package:acter/features/events/widgets/event_list_widget.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; import 'package:flutter/cupertino.dart'; @@ -103,21 +104,16 @@ void main() { listProvider: finalListProvider, ), ); - // Act - await tester.pumpAndSettle(); // Allow the widget to settle + // Initial build + await tester.pump(); + + // Wait for async operations + await tester.pump(const Duration(seconds: 1)); - // Assert - expect( - find.text('Fake Event1'), - findsOne, - ); - expect( - find.text('Fake Event2'), - findsOne, - ); expect( - find.text('Fake Event3'), - findsOne, + find.byType(EventItem), + findsNWidgets(3), + reason: 'Should find 3 EventItem widgets', ); }); });