diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 380db405..46607ddd 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1085,5 +1085,7 @@ "noChangelogProvided": "No changelog provided.", "useSecondarySourceForDownload": "Use secondary source for download", "installUpdate": "Install update", - "openReleasePage": "Open release page" + "openReleasePage": "Open release page", + "postCompose": "Compose Post", + "postPublish": "Publish Post" } diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index fb43f3b4..9d991940 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -13,12 +13,13 @@ import 'package:island/pods/event_calendar.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/screens/notification.dart'; import 'package:island/services/responsive.dart'; -import 'package:island/widgets/account/fortune_graph.dart'; + import 'package:island/widgets/app_scaffold.dart'; import 'package:island/models/post.dart'; import 'package:island/widgets/check_in.dart'; import 'package:island/widgets/post/post_featured.dart'; import 'package:island/widgets/post/post_item.dart'; +import 'package:island/widgets/post/compose_card.dart'; import 'package:island/screens/tabs.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -222,29 +223,32 @@ class ExploreScreen extends HookConsumerWidget { return AppScaffold( isNoBackground: false, - floatingActionButton: InkWell( - onLongPress: () { - context.pushNamed('postCompose', queryParameters: {'type': '1'}).then( - (value) { - if (value != null) { - activitiesNotifier.forceRefresh(); - } - }, - ); - }, - child: FloatingActionButton( - heroTag: Key("explore-page-fab"), - onPressed: () { - context.pushNamed('postCompose').then((value) { - if (value != null) { - activitiesNotifier.forceRefresh(); - } - }); - }, - child: const Icon(Symbols.edit), - ), - ), - floatingActionButtonLocation: TabbedFabLocation(context), + floatingActionButton: + isWide + ? null + : InkWell( + onLongPress: () { + context + .pushNamed('postCompose', queryParameters: {'type': '1'}) + .then((value) { + if (value != null) { + activitiesNotifier.forceRefresh(); + } + }); + }, + child: FloatingActionButton( + heroTag: Key("explore-page-fab"), + onPressed: () { + context.pushNamed('postCompose').then((value) { + if (value != null) { + activitiesNotifier.forceRefresh(); + } + }); + }, + child: const Icon(Symbols.edit), + ), + ), + floatingActionButtonLocation: isWide ? null : TabbedFabLocation(context), body: isWide ? _buildWideBody( @@ -345,12 +349,9 @@ class ExploreScreen extends HookConsumerWidget { margin: EdgeInsets.zero, ), PostFeaturedList(), - FortuneGraphWidget( - margin: EdgeInsets.zero, - events: events as AsyncValue>, - constrainWidth: true, - onPointSelected: (DateTime day) { - selectedDay.value = day; + PostComposeCard( + onSubmit: (post) { + activitiesNotifier.forceRefresh(); }, ), ], diff --git a/lib/widgets/post/compose_card.dart b/lib/widgets/post/compose_card.dart new file mode 100644 index 00000000..97bf8ede --- /dev/null +++ b/lib/widgets/post/compose_card.dart @@ -0,0 +1,842 @@ +import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/post.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/screens/creators/publishers.dart'; +import 'package:island/screens/posts/compose.dart'; +import 'package:island/services/compose_storage_db.dart'; +import 'package:island/services/responsive.dart'; +import 'package:island/widgets/attachment_uploader.dart'; +import 'package:island/widgets/content/attachment_preview.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:island/widgets/post/compose_shared.dart'; +import 'package:island/widgets/post/post_item.dart'; +import 'package:island/widgets/post/publishers_modal.dart'; +import 'package:island/widgets/post/compose_settings_sheet.dart'; +import 'package:island/widgets/post/compose_toolbar.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:styled_widget/styled_widget.dart'; + +/// A dialog-compatible card widget for post composition. +/// This extracts the core compose functionality from PostComposeScreen +/// and adapts it for use within dialogs or other constrained layouts. +class PostComposeCard extends HookConsumerWidget { + final SnPost? originalPost; + final PostComposeInitialState? initialState; + final VoidCallback? onCancel; + final Function(SnPost)? onSubmit; + final Function(ComposeState)? onStateChanged; + + const PostComposeCard({ + super.key, + this.originalPost, + this.initialState, + this.onCancel, + this.onSubmit, + this.onStateChanged, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final repliedPost = initialState?.replyingTo ?? originalPost?.repliedPost; + final forwardedPost = + initialState?.forwardingTo ?? originalPost?.forwardedPost; + + final theme = Theme.of(context); + final publishers = ref.watch(publishersManagedProvider); + + // Create compose state + final state = 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, + ]), + [state], + ); + useListenable(stateNotifier); + + // Notify parent of state changes + useEffect(() { + onStateChanged?.call(state); + return null; + }, [state]); + + // Initialize publisher once when data is available + useEffect(() { + if (publishers.value?.isNotEmpty ?? false) { + if (state.currentPublisher.value == null) { + state.currentPublisher.value = publishers.value!.first; + } + } + return null; + }, [publishers]); + + // Load initial state if provided + useEffect(() { + if (initialState != null) { + state.titleController.text = initialState!.title ?? ''; + state.descriptionController.text = initialState!.description ?? ''; + state.contentController.text = initialState!.content ?? ''; + if (initialState!.visibility != null) { + state.visibility.value = initialState!.visibility!; + } + if (initialState!.attachments.isNotEmpty) { + state.attachments.value = List.from(initialState!.attachments); + } + } + return null; + }, [initialState]); + + // Dispose state when widget is disposed + useEffect(() { + return () => ComposeLogic.dispose(state); + }, []); + + // Reset form to clean state for new composition + void resetForm() { + // Clear text fields + state.titleController.clear(); + state.descriptionController.clear(); + state.contentController.clear(); + state.slugController.clear(); + + // Reset visibility to default (0 = public) + state.visibility.value = 0; + + // Clear attachments + state.attachments.value = []; + + // Clear attachment progress + state.attachmentProgress.value = {}; + + // Clear tags + state.tagsController.clearTags(); + + // Clear categories + state.categories.value = []; + + // Clear embed view + state.embedView.value = null; + + // Clear poll + state.pollId.value = null; + + // Clear realm + state.realm.value = null; + + // Generate new draft ID for fresh composition + // Note: We don't recreate the entire state, just reset the fields + // The existing state object is reused for continuity + } + + // Helper methods + void showSettingsSheet() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => ComposeSettingsSheet(state: state), + ); + } + + Future performSubmit() async { + if (state.submitting.value) return; + + // 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'.tr()))); + } + return; // Don't submit empty posts + } + + try { + state.submitting.value = true; + + // Upload any local attachments first + await Future.wait( + state.attachments.value + .asMap() + .entries + .where((entry) => entry.value.isOnDevice) + .map( + (entry) => ComposeLogic.uploadAttachment(ref, state, entry.key), + ), + ); + + // Prepare API request + final client = ref.read(apiClientProvider); + final isNewPost = originalPost == null; + final endpoint = + '/sphere${isNewPost ? '/posts' : '/posts/${originalPost!.id}'}'; + + // Create request payload + final payload = { + 'title': state.titleController.text, + 'description': state.descriptionController.text, + 'content': state.contentController.text, + if (state.slugController.text.isNotEmpty) + 'slug': state.slugController.text, + 'visibility': state.visibility.value, + 'attachments': + state.attachments.value + .where((e) => e.isOnCloud) + .map((e) => e.data.id) + .toList(), + 'type': state.postType, + if (repliedPost != null) 'replied_post_id': repliedPost.id, + if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id, + 'tags': state.tagsController.getTags, + 'categories': state.categories.value.map((e) => e.slug).toList(), + if (state.realm.value != null) 'realm_id': state.realm.value?.id, + if (state.pollId.value != null) 'poll_id': state.pollId.value, + if (state.embedView.value != null) + 'embed_view': state.embedView.value!.toJson(), + }; + + // Send request + final response = await client.request( + endpoint, + queryParameters: {'pub': state.currentPublisher.value?.name}, + data: payload, + options: Options(method: isNewPost ? 'POST' : 'PATCH'), + ); + + // Create the post object from the response for the callback + final post = SnPost.fromJson(response.data); + + // Delete draft after successful submission + await ref + .read(composeStorageNotifierProvider.notifier) + .deleteDraft(state.draftId); + + // Reset the form for new composition + resetForm(); + + // Call the success callback with the created/updated post + onSubmit?.call(post); + } catch (err) { + // Show error message if context is mounted + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error: $err'))); + } + rethrow; + } finally { + state.submitting.value = false; + } + } + + Widget buildWideAttachmentGrid() { + return GridView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: state.attachments.value.length, + itemBuilder: (context, idx) { + final progressMap = state.attachmentProgress.value; + return AttachmentPreview( + isCompact: true, + item: state.attachments.value[idx], + progress: progressMap[idx], + onRequestUpload: () async { + final config = await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: + (context) => AttachmentUploaderSheet( + ref: ref, + state: state, + index: idx, + ), + ); + if (config != null) { + await ComposeLogic.uploadAttachment( + ref, + state, + idx, + poolId: config.poolId, + ); + } + }, + onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx), + onUpdate: + (value) => ComposeLogic.updateAttachment(state, value, idx), + onMove: (delta) { + state.attachments.value = ComposeLogic.moveAttachment( + state.attachments.value, + idx, + delta, + ); + }, + ); + }, + ); + } + + Widget buildNarrowAttachmentList() { + return Column( + children: [ + for (var idx = 0; idx < state.attachments.value.length; idx++) + Container( + margin: const EdgeInsets.only(bottom: 8), + child: () { + final progressMap = state.attachmentProgress.value; + return AttachmentPreview( + item: state.attachments.value[idx], + progress: progressMap[idx], + onRequestUpload: () async { + final config = + await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: + (context) => AttachmentUploaderSheet( + ref: ref, + state: state, + index: idx, + ), + ); + if (config != null) { + await ComposeLogic.uploadAttachment( + ref, + state, + idx, + poolId: config.poolId, + ); + } + }, + onDelete: + () => ComposeLogic.deleteAttachment(ref, state, idx), + onUpdate: + (value) => + ComposeLogic.updateAttachment(state, value, idx), + onMove: (delta) { + state.attachments.value = ComposeLogic.moveAttachment( + state.attachments.value, + idx, + delta, + ); + }, + ); + }(), + ), + ], + ); + } + + return Card( + margin: EdgeInsets.zero, + child: Container( + constraints: const BoxConstraints(maxHeight: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with actions + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.colorScheme.outline.withOpacity(0.2), + ), + ), + ), + child: Row( + children: [ + Text( + originalPost != null + ? 'postEditing'.tr() + : 'postCompose'.tr(), + style: theme.textTheme.titleMedium, + ), + const Spacer(), + IconButton( + icon: const Icon(Symbols.settings), + onPressed: showSettingsSheet, + tooltip: 'postSettings'.tr(), + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + IconButton( + onPressed: state.submitting.value ? null : performSubmit, + icon: + state.submitting.value + ? SizedBox( + width: 24, + height: 24, + child: const CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : Icon( + originalPost != null + ? Symbols.edit + : Symbols.upload, + ), + tooltip: + originalPost != null + ? 'postUpdate'.tr() + : 'postPublish'.tr(), + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + if (onCancel != null) + IconButton( + icon: const Icon(Symbols.close), + onPressed: onCancel, + tooltip: 'cancel'.tr(), + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + ], + ), + ), + + // Info banner (reply/forward) + _buildInfoBanner(context), + + // Main content area + Expanded( + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: + (event) => ComposeLogic.handleKeyPress( + event, + state, + ref, + context, + originalPost: originalPost, + repliedPost: repliedPost, + forwardedPost: forwardedPost, + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: Row( + spacing: 12, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Publisher profile picture + GestureDetector( + child: ProfilePictureWidget( + fileId: state.currentPublisher.value?.picture?.id, + radius: 20, + fallbackIcon: + state.currentPublisher.value == null + ? Symbols.question_mark + : null, + ), + onTap: () { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) => const PublisherModal(), + ).then((value) { + if (value != null) { + state.currentPublisher.value = value; + } + }); + }, + ).padding(top: 8), + + // Post content form + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: state.titleController, + decoration: InputDecoration( + hintText: 'postTitle'.tr(), + border: InputBorder.none, + isCollapsed: true, + contentPadding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + ), + style: theme.textTheme.titleMedium, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus + ?.unfocus(), + ), + TextField( + controller: state.descriptionController, + decoration: InputDecoration( + hintText: 'postDescription'.tr(), + border: InputBorder.none, + isCollapsed: true, + contentPadding: const EdgeInsets.fromLTRB( + 8, + 4, + 8, + 12, + ), + ), + style: theme.textTheme.bodyMedium, + minLines: 1, + maxLines: 3, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus + ?.unfocus(), + ), + TextField( + controller: state.contentController, + style: theme.textTheme.bodyMedium, + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'postContent'.tr(), + isCollapsed: true, + contentPadding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + ), + maxLines: null, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus + ?.unfocus(), + ), + + const Gap(8), + + // Attachments preview + if (state.attachments.value.isNotEmpty) + LayoutBuilder( + builder: (context, constraints) { + final isWide = isWideScreen(context); + return isWide + ? buildWideAttachmentGrid() + : buildNarrowAttachmentList(); + }, + ) + else + const SizedBox.shrink(), + ], + ), + ), + ], + ), + ), + ), + ), + ), + + // Bottom toolbar + ClipRRect( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + child: ComposeToolbar(state: state, originalPost: originalPost), + ), + ], + ), + ), + ); + } + + Widget _buildInfoBanner(BuildContext context) { + final effectiveRepliedPost = + initialState?.replyingTo ?? originalPost?.repliedPost; + final effectiveForwardedPost = + initialState?.forwardingTo ?? originalPost?.forwardedPost; + + // Show editing banner when editing a post + if (originalPost != null) { + return Column( + children: [ + Container( + width: double.infinity, + color: Theme.of(context).colorScheme.primaryContainer, + child: Row( + children: [ + Icon( + Symbols.edit, + size: 16, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + const Gap(8), + Text( + 'postEditing'.tr(), + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ], + ).padding(horizontal: 16, vertical: 8), + ), + // Show reply/forward banners below editing banner if they exist + if (effectiveRepliedPost != null) + Container( + width: double.infinity, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Symbols.reply, size: 16), + const Gap(4), + Text( + 'postReplyingTo'.tr(), + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const Gap(8), + _buildCompactReferencePost(context, effectiveRepliedPost), + ], + ).padding(all: 16), + ), + if (effectiveForwardedPost != null) + Container( + width: double.infinity, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Symbols.forward, size: 16), + const Gap(4), + Text( + 'postForwardingTo'.tr(), + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const Gap(8), + _buildCompactReferencePost(context, effectiveForwardedPost), + ], + ).padding(all: 16), + ), + ], + ); + } + + // Show banner for replies + if (effectiveRepliedPost != null) { + return Container( + width: double.infinity, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Symbols.reply, size: 16), + const Gap(4), + Text( + 'postReplyingTo'.tr(), + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const Gap(8), + _buildCompactReferencePost(context, effectiveRepliedPost), + ], + ).padding(all: 16), + ); + } + + // Show banner for forwards + if (effectiveForwardedPost != null) { + return Container( + width: double.infinity, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Symbols.forward, size: 16), + const Gap(4), + Text( + 'postForwardingTo'.tr(), + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + const Gap(8), + _buildCompactReferencePost(context, effectiveForwardedPost), + ], + ).padding(all: 16), + ); + } + + return const SizedBox.shrink(); + } + + Widget _buildCompactReferencePost(BuildContext context, SnPost post) { + return GestureDetector( + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: + (context) => DraggableScrollableSheet( + initialChildSize: 0.7, + maxChildSize: 0.9, + minChildSize: 0.5, + builder: + (context, scrollController) => Container( + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + ), + child: Column( + children: [ + Container( + width: 40, + height: 4, + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.outline, + borderRadius: BorderRadius.circular(2), + ), + ), + Expanded( + child: SingleChildScrollView( + controller: scrollController, + padding: const EdgeInsets.all(16), + child: PostItem(item: post), + ), + ), + ], + ), + ), + ), + ); + }, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.3), + ), + ), + child: Row( + children: [ + ProfilePictureWidget( + fileId: post.publisher.picture?.id, + radius: 16, + ), + const Gap(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + post.publisher.nick, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + if (post.title?.isNotEmpty ?? false) + Text( + post.title!, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 13, + color: Theme.of(context).colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (post.content?.isNotEmpty ?? false) + Text( + post.content!, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (post.attachments.isNotEmpty) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Symbols.attach_file, + size: 12, + color: Theme.of(context).colorScheme.secondary, + ), + const Gap(4), + Text( + 'postHasAttachments'.plural(post.attachments.length), + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: 11, + ), + ), + ], + ), + ], + ), + ), + Icon( + Symbols.open_in_full, + size: 16, + color: Theme.of(context).colorScheme.outline, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/post/compose_dialog.dart b/lib/widgets/post/compose_dialog.dart new file mode 100644 index 00000000..c0184d16 --- /dev/null +++ b/lib/widgets/post/compose_dialog.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/post.dart'; +import 'package:island/screens/posts/compose.dart'; +import 'package:island/widgets/post/compose_card.dart'; + +/// A dialog that wraps PostComposeCard for easy use in dialogs. +/// This provides a convenient way to show the compose interface in a modal dialog. +class PostComposeDialog extends HookConsumerWidget { + final SnPost? originalPost; + final PostComposeInitialState? initialState; + + const PostComposeDialog({super.key, this.originalPost, this.initialState}); + + static Future show( + BuildContext context, { + SnPost? originalPost, + PostComposeInitialState? initialState, + }) { + return showDialog( + context: context, + useRootNavigator: false, + builder: + (context) => PostComposeDialog( + originalPost: originalPost, + initialState: initialState, + ), + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Dialog( + insetPadding: const EdgeInsets.all(16), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600, maxHeight: 700), + child: PostComposeCard( + originalPost: originalPost, + initialState: initialState, + onCancel: () => Navigator.of(context).pop(), + onSubmit: (post) => Navigator.of(context).pop(post), + ), + ), + ); + } +}