🐛 Fix compose sheet
This commit is contained in:
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user