🐛 Fix compose sheet

This commit is contained in:
2025-10-31 19:15:22 +08:00
parent b3ef7d6ad0
commit b52eb95b14
3 changed files with 107 additions and 70 deletions

View File

@@ -38,6 +38,7 @@ class PostComposeCard extends HookConsumerWidget {
final Function(ComposeState)? onStateChanged; final Function(ComposeState)? onStateChanged;
final bool isContained; final bool isContained;
final bool showHeader; final bool showHeader;
final ComposeState? providedState;
const PostComposeCard({ const PostComposeCard({
super.key, super.key,
@@ -48,6 +49,7 @@ class PostComposeCard extends HookConsumerWidget {
this.onStateChanged, this.onStateChanged,
this.isContained = false, this.isContained = false,
this.showHeader = true, this.showHeader = true,
this.providedState,
}); });
@override @override
@@ -64,75 +66,79 @@ class PostComposeCard extends HookConsumerWidget {
final notifier = ref.read(composeStorageNotifierProvider.notifier); final notifier = ref.read(composeStorageNotifierProvider.notifier);
// Create compose state // Create compose state
final state = useMemoized( final ComposeState composeState =
() => ComposeLogic.createState( providedState ??
originalPost: originalPost, useMemoized(
forwardedPost: forwardedPost, () => ComposeLogic.createState(
repliedPost: repliedPost, originalPost: originalPost,
postType: 0, forwardedPost: forwardedPost,
), repliedPost: repliedPost,
[originalPost, forwardedPost, repliedPost], postType: 0,
); ),
[originalPost, forwardedPost, repliedPost],
);
// Add a listener to the entire state to trigger rebuilds // Add a listener to the entire state to trigger rebuilds
final stateNotifier = useMemoized( final stateNotifier = useMemoized(
() => Listenable.merge([ () => Listenable.merge([
state.titleController, composeState.titleController,
state.descriptionController, composeState.descriptionController,
state.contentController, composeState.contentController,
state.visibility, composeState.visibility,
state.attachments, composeState.attachments,
state.attachmentProgress, composeState.attachmentProgress,
state.currentPublisher, composeState.currentPublisher,
state.submitting, composeState.submitting,
]), ]),
[state], [composeState],
); );
useListenable(stateNotifier); useListenable(stateNotifier);
// Notify parent of state changes // Notify parent of state changes
useEffect(() { useEffect(() {
onStateChanged?.call(state); onStateChanged?.call(composeState);
return null; return null;
}, [state]); }, [composeState]);
// Use shared state management utilities // Use shared state management utilities
ComposeStateUtils.usePublisherInitialization(ref, state); ComposeStateUtils.usePublisherInitialization(ref, composeState);
ComposeStateUtils.useInitialStateLoader(state, initialState); ComposeStateUtils.useInitialStateLoader(composeState, initialState);
// Dispose state when widget is disposed // Dispose state when widget is disposed
useEffect(() { useEffect(() {
return () { return () {
if (!submitted.value && if (providedState == null) {
originalPost == null && if (!submitted.value &&
state.currentPublisher.value != null) { originalPost == null &&
final hasContent = composeState.currentPublisher.value != null) {
state.titleController.text.trim().isNotEmpty || final hasContent =
state.descriptionController.text.trim().isNotEmpty || composeState.titleController.text.trim().isNotEmpty ||
state.contentController.text.trim().isNotEmpty; composeState.descriptionController.text.trim().isNotEmpty ||
final hasAttachments = state.attachments.value.isNotEmpty; composeState.contentController.text.trim().isNotEmpty;
if (hasContent || hasAttachments) { final hasAttachments = composeState.attachments.value.isNotEmpty;
final draft = SnPost( if (hasContent || hasAttachments) {
id: state.draftId, final draft = SnPost(
title: state.titleController.text, id: composeState.draftId,
description: state.descriptionController.text, title: composeState.titleController.text,
content: state.contentController.text, description: composeState.descriptionController.text,
visibility: state.visibility.value, content: composeState.contentController.text,
type: state.postType, visibility: composeState.visibility.value,
attachments: type: composeState.postType,
state.attachments.value attachments:
.where((e) => e.isOnCloud) composeState.attachments.value
.map((e) => e.data as SnCloudFile) .where((e) => e.isOnCloud)
.toList(), .map((e) => e.data as SnCloudFile)
publisher: state.currentPublisher.value!, .toList(),
updatedAt: DateTime.now(), publisher: composeState.currentPublisher.value!,
); updatedAt: DateTime.now(),
notifier );
.saveDraft(draft) notifier
.catchError((e) => debugPrint('Failed to save draft: $e')); .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, context: context,
isScrollControlled: true, isScrollControlled: true,
useRootNavigator: true, useRootNavigator: true,
builder: (context) => ComposeSettingsSheet(state: state), builder: (context) => ComposeSettingsSheet(state: composeState),
); );
} }
Future<void> performSubmit() async { Future<void> performSubmit() async {
await ComposeSubmitUtils.performSubmit( await ComposeSubmitUtils.performSubmit(
ref, ref,
state, composeState,
context, context,
originalPost: originalPost, originalPost: originalPost,
repliedPost: repliedPost, repliedPost: repliedPost,
@@ -161,10 +167,10 @@ class PostComposeCard extends HookConsumerWidget {
// Delete draft after successful submission // Delete draft after successful submission
ref ref
.read(composeStorageNotifierProvider.notifier) .read(composeStorageNotifierProvider.notifier)
.deleteDraft(state.draftId); .deleteDraft(composeState.draftId);
// Reset the form for new composition // Reset the form for new composition
ComposeStateUtils.resetForm(state); ComposeStateUtils.resetForm(composeState);
onSubmit?.call(); onSubmit?.call();
}, },
@@ -219,12 +225,12 @@ class PostComposeCard extends HookConsumerWidget {
), ),
IconButton( IconButton(
onPressed: onPressed:
(state.submitting.value || (composeState.submitting.value ||
state.currentPublisher.value == null) composeState.currentPublisher.value == null)
? null ? null
: performSubmit, : performSubmit,
icon: icon:
state.submitting.value composeState.submitting.value
? SizedBox( ? SizedBox(
width: 24, width: 24,
height: 24, height: 24,
@@ -288,7 +294,7 @@ class PostComposeCard extends HookConsumerWidget {
onKeyEvent: onKeyEvent:
(event) => ComposeLogic.handleKeyPress( (event) => ComposeLogic.handleKeyPress(
event, event,
state, composeState,
ref, ref,
context, context,
originalPost: originalPost, originalPost: originalPost,
@@ -306,22 +312,27 @@ class PostComposeCard extends HookConsumerWidget {
// Publisher profile picture // Publisher profile picture
GestureDetector( GestureDetector(
child: ProfilePictureWidget( child: ProfilePictureWidget(
fileId: state.currentPublisher.value?.picture?.id, fileId:
composeState
.currentPublisher
.value
?.picture
?.id,
radius: 20, radius: 20,
fallbackIcon: fallbackIcon:
state.currentPublisher.value == null composeState.currentPublisher.value == null
? Symbols.question_mark ? Symbols.question_mark
: null, : null,
), ),
onTap: () { onTap: () {
if (state.currentPublisher.value == null) { if (composeState.currentPublisher.value == null) {
// No publisher loaded, guide user to create one // No publisher loaded, guide user to create one
if (isContained) { if (isContained) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
context.pushNamed('creatorNew').then((value) { context.pushNamed('creatorNew').then((value) {
if (value != null) { if (value != null) {
state.currentPublisher.value = composeState.currentPublisher.value =
value as SnPublisher; value as SnPublisher;
ref.invalidate(publishersManagedProvider); ref.invalidate(publishersManagedProvider);
} }
@@ -335,7 +346,7 @@ class PostComposeCard extends HookConsumerWidget {
builder: (context) => const PublisherModal(), builder: (context) => const PublisherModal(),
).then((value) { ).then((value) {
if (value != null) { if (value != null) {
state.currentPublisher.value = value; composeState.currentPublisher.value = value;
} }
}); });
} }
@@ -348,10 +359,11 @@ class PostComposeCard extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ComposeFormFields( ComposeFormFields(
state: state, state: composeState,
showPublisherAvatar: false, showPublisherAvatar: false,
onPublisherTap: () { onPublisherTap: () {
if (state.currentPublisher.value == null) { if (composeState.currentPublisher.value ==
null) {
// No publisher loaded, guide user to create one // No publisher loaded, guide user to create one
if (isContained) { if (isContained) {
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -360,7 +372,7 @@ class PostComposeCard extends HookConsumerWidget {
value, value,
) { ) {
if (value != null) { if (value != null) {
state.currentPublisher.value = composeState.currentPublisher.value =
value as SnPublisher; value as SnPublisher;
ref.invalidate( ref.invalidate(
publishersManagedProvider, publishersManagedProvider,
@@ -377,14 +389,18 @@ class PostComposeCard extends HookConsumerWidget {
(context) => const PublisherModal(), (context) => const PublisherModal(),
).then((value) { ).then((value) {
if (value != null) { if (value != null) {
state.currentPublisher.value = value; composeState.currentPublisher.value =
value;
} }
}); });
} }
}, },
), ),
const Gap(8), 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), bottomRight: Radius.circular(8),
), ),
child: ComposeToolbar( child: ComposeToolbar(
state: state, state: composeState,
originalPost: originalPost, originalPost: originalPost,
isCompact: true, isCompact: true,
), ),

View File

@@ -58,7 +58,7 @@ class PostComposeSheet extends HookConsumerWidget {
initialState?.forwardingTo ?? originalPost?.forwardedPost; initialState?.forwardingTo ?? originalPost?.forwardedPost;
// Create compose state // Create compose state
final state = useMemoized( final ComposeState state = useMemoized(
() => ComposeLogic.createState( () => ComposeLogic.createState(
originalPost: originalPost, originalPost: originalPost,
forwardedPost: forwardedPost, forwardedPost: forwardedPost,
@@ -102,6 +102,9 @@ class PostComposeSheet extends HookConsumerWidget {
return null; return null;
}, [drafts, prompted.value]); }, [drafts, prompted.value]);
// Dispose state when widget is disposed
useEffect(() => () => ComposeLogic.dispose(state), []);
// Helper methods for actions // Helper methods for actions
void showSettingsSheet() { void showSettingsSheet() {
showModalBottomSheet( showModalBottomSheet(
@@ -165,6 +168,7 @@ class PostComposeSheet extends HookConsumerWidget {
}, },
isContained: true, isContained: true,
showHeader: false, showHeader: false,
providedState: state,
), ),
); );
} }

View File

@@ -22,6 +22,23 @@ class ComposeSubmitUtils {
throw Exception('Already submitting'); 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 { try {
state.submitting.value = true; state.submitting.value = true;