From b52eb95b142a7377167b46b0e78496b64dd59465 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 31 Oct 2025 19:15:22 +0800 Subject: [PATCH] :bug: Fix compose sheet --- lib/widgets/post/compose_card.dart | 154 ++++++++++++--------- lib/widgets/post/compose_sheet.dart | 6 +- lib/widgets/post/compose_submit_utils.dart | 17 +++ 3 files changed, 107 insertions(+), 70 deletions(-) diff --git a/lib/widgets/post/compose_card.dart b/lib/widgets/post/compose_card.dart index ac6507f7..3028762e 100644 --- a/lib/widgets/post/compose_card.dart +++ b/lib/widgets/post/compose_card.dart @@ -38,6 +38,7 @@ class PostComposeCard extends HookConsumerWidget { final Function(ComposeState)? onStateChanged; final bool isContained; final bool showHeader; + final ComposeState? providedState; const PostComposeCard({ super.key, @@ -48,6 +49,7 @@ class PostComposeCard extends HookConsumerWidget { this.onStateChanged, this.isContained = false, this.showHeader = true, + this.providedState, }); @override @@ -64,75 +66,79 @@ class PostComposeCard extends HookConsumerWidget { final notifier = ref.read(composeStorageNotifierProvider.notifier); // Create compose state - final state = useMemoized( - () => ComposeLogic.createState( - originalPost: originalPost, - forwardedPost: forwardedPost, - repliedPost: repliedPost, - postType: 0, - ), - [originalPost, forwardedPost, repliedPost], - ); + final ComposeState composeState = + providedState ?? + useMemoized( + () => ComposeLogic.createState( + originalPost: originalPost, + forwardedPost: forwardedPost, + repliedPost: repliedPost, + postType: 0, + ), + [originalPost, forwardedPost, repliedPost], + ); // Add a listener to the entire state to trigger rebuilds final stateNotifier = useMemoized( () => Listenable.merge([ - state.titleController, - state.descriptionController, - state.contentController, - state.visibility, - state.attachments, - state.attachmentProgress, - state.currentPublisher, - state.submitting, + composeState.titleController, + composeState.descriptionController, + composeState.contentController, + composeState.visibility, + composeState.attachments, + composeState.attachmentProgress, + composeState.currentPublisher, + composeState.submitting, ]), - [state], + [composeState], ); useListenable(stateNotifier); // Notify parent of state changes useEffect(() { - onStateChanged?.call(state); + onStateChanged?.call(composeState); return null; - }, [state]); + }, [composeState]); // Use shared state management utilities - ComposeStateUtils.usePublisherInitialization(ref, state); - ComposeStateUtils.useInitialStateLoader(state, initialState); + ComposeStateUtils.usePublisherInitialization(ref, composeState); + ComposeStateUtils.useInitialStateLoader(composeState, initialState); // Dispose state when widget is disposed useEffect(() { return () { - if (!submitted.value && - originalPost == null && - state.currentPublisher.value != null) { - final hasContent = - state.titleController.text.trim().isNotEmpty || - state.descriptionController.text.trim().isNotEmpty || - state.contentController.text.trim().isNotEmpty; - final hasAttachments = state.attachments.value.isNotEmpty; - if (hasContent || hasAttachments) { - final draft = SnPost( - id: state.draftId, - title: state.titleController.text, - description: state.descriptionController.text, - content: state.contentController.text, - visibility: state.visibility.value, - type: state.postType, - attachments: - state.attachments.value - .where((e) => e.isOnCloud) - .map((e) => e.data as SnCloudFile) - .toList(), - publisher: state.currentPublisher.value!, - updatedAt: DateTime.now(), - ); - notifier - .saveDraft(draft) - .catchError((e) => debugPrint('Failed to save draft: $e')); + if (providedState == null) { + if (!submitted.value && + originalPost == null && + composeState.currentPublisher.value != null) { + final hasContent = + composeState.titleController.text.trim().isNotEmpty || + composeState.descriptionController.text.trim().isNotEmpty || + composeState.contentController.text.trim().isNotEmpty; + final hasAttachments = composeState.attachments.value.isNotEmpty; + if (hasContent || hasAttachments) { + final draft = SnPost( + id: composeState.draftId, + title: composeState.titleController.text, + description: composeState.descriptionController.text, + content: composeState.contentController.text, + visibility: composeState.visibility.value, + type: composeState.postType, + attachments: + composeState.attachments.value + .where((e) => e.isOnCloud) + .map((e) => e.data as SnCloudFile) + .toList(), + publisher: composeState.currentPublisher.value!, + updatedAt: DateTime.now(), + ); + notifier + .saveDraft(draft) + .catchError((e) => debugPrint('Failed to save draft: $e')); + } } + ComposeLogic.dispose(composeState); } - ComposeLogic.dispose(state); }; }, []); @@ -142,14 +148,14 @@ class PostComposeCard extends HookConsumerWidget { context: context, isScrollControlled: true, useRootNavigator: true, - builder: (context) => ComposeSettingsSheet(state: state), + builder: (context) => ComposeSettingsSheet(state: composeState), ); } Future performSubmit() async { await ComposeSubmitUtils.performSubmit( ref, - state, + composeState, context, originalPost: originalPost, repliedPost: repliedPost, @@ -161,10 +167,10 @@ class PostComposeCard extends HookConsumerWidget { // Delete draft after successful submission ref .read(composeStorageNotifierProvider.notifier) - .deleteDraft(state.draftId); + .deleteDraft(composeState.draftId); // Reset the form for new composition - ComposeStateUtils.resetForm(state); + ComposeStateUtils.resetForm(composeState); onSubmit?.call(); }, @@ -219,12 +225,12 @@ class PostComposeCard extends HookConsumerWidget { ), IconButton( onPressed: - (state.submitting.value || - state.currentPublisher.value == null) + (composeState.submitting.value || + composeState.currentPublisher.value == null) ? null : performSubmit, icon: - state.submitting.value + composeState.submitting.value ? SizedBox( width: 24, height: 24, @@ -288,7 +294,7 @@ class PostComposeCard extends HookConsumerWidget { onKeyEvent: (event) => ComposeLogic.handleKeyPress( event, - state, + composeState, ref, context, originalPost: originalPost, @@ -306,22 +312,27 @@ class PostComposeCard extends HookConsumerWidget { // Publisher profile picture GestureDetector( child: ProfilePictureWidget( - fileId: state.currentPublisher.value?.picture?.id, + fileId: + composeState + .currentPublisher + .value + ?.picture + ?.id, radius: 20, fallbackIcon: - state.currentPublisher.value == null + composeState.currentPublisher.value == null ? Symbols.question_mark : null, ), onTap: () { - if (state.currentPublisher.value == null) { + if (composeState.currentPublisher.value == null) { // No publisher loaded, guide user to create one if (isContained) { Navigator.of(context).pop(); } context.pushNamed('creatorNew').then((value) { if (value != null) { - state.currentPublisher.value = + composeState.currentPublisher.value = value as SnPublisher; ref.invalidate(publishersManagedProvider); } @@ -335,7 +346,7 @@ class PostComposeCard extends HookConsumerWidget { builder: (context) => const PublisherModal(), ).then((value) { if (value != null) { - state.currentPublisher.value = value; + composeState.currentPublisher.value = value; } }); } @@ -348,10 +359,11 @@ class PostComposeCard extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ComposeFormFields( - state: state, + state: composeState, showPublisherAvatar: false, onPublisherTap: () { - if (state.currentPublisher.value == null) { + if (composeState.currentPublisher.value == + null) { // No publisher loaded, guide user to create one if (isContained) { Navigator.of(context).pop(); @@ -360,7 +372,7 @@ class PostComposeCard extends HookConsumerWidget { value, ) { if (value != null) { - state.currentPublisher.value = + composeState.currentPublisher.value = value as SnPublisher; ref.invalidate( publishersManagedProvider, @@ -377,14 +389,18 @@ class PostComposeCard extends HookConsumerWidget { (context) => const PublisherModal(), ).then((value) { if (value != null) { - state.currentPublisher.value = value; + composeState.currentPublisher.value = + value; } }); } }, ), const Gap(8), - ComposeAttachments(state: state, isCompact: true), + ComposeAttachments( + state: composeState, + isCompact: true, + ), ], ), ), @@ -404,7 +420,7 @@ class PostComposeCard extends HookConsumerWidget { bottomRight: Radius.circular(8), ), child: ComposeToolbar( - state: state, + state: composeState, originalPost: originalPost, isCompact: true, ), diff --git a/lib/widgets/post/compose_sheet.dart b/lib/widgets/post/compose_sheet.dart index 4f45950c..7b68eb9d 100644 --- a/lib/widgets/post/compose_sheet.dart +++ b/lib/widgets/post/compose_sheet.dart @@ -58,7 +58,7 @@ class PostComposeSheet extends HookConsumerWidget { initialState?.forwardingTo ?? originalPost?.forwardedPost; // Create compose state - final state = useMemoized( + final ComposeState state = useMemoized( () => ComposeLogic.createState( originalPost: originalPost, forwardedPost: forwardedPost, @@ -102,6 +102,9 @@ class PostComposeSheet extends HookConsumerWidget { return null; }, [drafts, prompted.value]); + // Dispose state when widget is disposed + useEffect(() => () => ComposeLogic.dispose(state), []); + // Helper methods for actions void showSettingsSheet() { showModalBottomSheet( @@ -165,6 +168,7 @@ class PostComposeSheet extends HookConsumerWidget { }, isContained: true, showHeader: false, + providedState: state, ), ); } diff --git a/lib/widgets/post/compose_submit_utils.dart b/lib/widgets/post/compose_submit_utils.dart index 9fab492b..7e8ec4ed 100644 --- a/lib/widgets/post/compose_submit_utils.dart +++ b/lib/widgets/post/compose_submit_utils.dart @@ -22,6 +22,23 @@ class ComposeSubmitUtils { throw Exception('Already submitting'); } + // Don't submit empty posts (no content and no attachments) + final hasContent = + state.titleController.text.trim().isNotEmpty || + state.descriptionController.text.trim().isNotEmpty || + state.contentController.text.trim().isNotEmpty; + final hasAttachments = state.attachments.value.isNotEmpty; + + if (!hasContent && !hasAttachments) { + // Show error message if context is mounted + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('postContentEmpty'))); + } + throw Exception('Post content is empty'); // Don't submit empty posts + } + try { state.submitting.value = true;