sojorn/sojorn_app/lib/widgets/sojorn_dialog.dart
Patrick Britton 3c4680bdd7 Initial commit: Complete threaded conversation system with inline replies
**Major Features Added:**
- **Inline Reply System**: Replace compose screen with inline reply boxes
- **Thread Navigation**: Parent/child navigation with jump functionality
- **Chain Flow UI**: Reply counts, expand/collapse animations, visual hierarchy
- **Enhanced Animations**: Smooth transitions, hover effects, micro-interactions

 **Frontend Changes:**
- **ThreadedCommentWidget**: Complete rewrite with animations and navigation
- **ThreadNode Model**: Added parent references and descendant counting
- **ThreadedConversationScreen**: Integrated navigation handlers
- **PostDetailScreen**: Replaced with threaded conversation view
- **ComposeScreen**: Added reply indicators and context
- **PostActions**: Fixed visibility checks for chain buttons

 **Backend Changes:**
- **API Route**: Added /posts/:id/thread endpoint
- **Post Repository**: Include allow_chain and visibility fields in feed
- **Thread Handler**: Support for fetching post chains

 **UI/UX Improvements:**
- **Reply Context**: Clear indication when replying to specific posts
- **Character Counting**: 500 character limit with live counter
- **Visual Hierarchy**: Depth-based indentation and styling
- **Smooth Animations**: SizeTransition, FadeTransition, hover states
- **Chain Navigation**: Parent/child buttons with visual feedback

 **Technical Enhancements:**
- **Animation Controllers**: Proper lifecycle management
- **State Management**: Clean separation of concerns
- **Navigation Callbacks**: Reusable navigation system
- **Error Handling**: Graceful fallbacks and user feedback

This creates a Reddit-style threaded conversation experience with smooth
animations, inline replies, and intuitive navigation between posts in a chain.
2026-01-30 07:40:19 -06:00

242 lines
6.9 KiB
Dart

import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import 'sojorn_button.dart';
/// Custom dialog enforcing sojorn's visual system
class sojornDialog extends StatelessWidget {
final String title;
final String? message;
final Widget? content;
final String? primaryButtonLabel;
final VoidCallback? onPrimaryPressed;
final String? secondaryButtonLabel;
final VoidCallback? onSecondaryPressed;
final bool isDismissible;
final bool isDestructive;
const sojornDialog({
super.key,
required this.title,
this.message,
this.content,
this.primaryButtonLabel,
this.onPrimaryPressed,
this.secondaryButtonLabel,
this.onSecondaryPressed,
this.isDismissible = true,
this.isDestructive = false,
});
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: AppTheme.white, // Replaced AppTheme.surfaceElevated
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0), // Replaced AppTheme.radiusLg
),
child: Padding(
padding: const EdgeInsets.all(AppTheme.spacingLg),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Text(
title,
style: AppTheme.headlineSmall.copyWith(
color: AppTheme.navyBlue, // Replaced AppTheme.textPrimary
),
),
const SizedBox(height: AppTheme.spacingMd),
// Message or custom content
if (message != null)
Text(
message!,
style: AppTheme.bodyMedium.copyWith(
color: AppTheme.navyText
.withOpacity(0.9), // Replaced AppTheme.textSecondary
height: 1.6,
),
),
if (content != null) content!,
const SizedBox(height: AppTheme.spacingLg),
// Action buttons
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (secondaryButtonLabel != null) ...[
sojornButton(
label: secondaryButtonLabel!,
onPressed:
onSecondaryPressed ?? () => Navigator.of(context).pop(),
variant: sojornButtonVariant.tertiary,
size: sojornButtonSize.small,
),
const SizedBox(width: AppTheme.spacingSm),
],
if (primaryButtonLabel != null)
sojornButton(
label: primaryButtonLabel!,
onPressed: onPrimaryPressed,
variant: isDestructive
? sojornButtonVariant.destructive
: sojornButtonVariant.primary,
size: sojornButtonSize.small,
),
],
),
],
),
),
);
}
/// Show a confirmation dialog
static Future<bool?> showConfirmation({
required BuildContext context,
required String title,
required String message,
String confirmLabel = 'Confirm',
String cancelLabel = 'Cancel',
bool isDestructive = false,
}) {
return showDialog<bool>(
context: context,
builder: (context) => sojornDialog(
title: title,
message: message,
primaryButtonLabel: confirmLabel,
onPrimaryPressed: () => Navigator.of(context).pop(true),
secondaryButtonLabel: cancelLabel,
onSecondaryPressed: () => Navigator.of(context).pop(false),
isDestructive: isDestructive,
),
);
}
/// Show an informational dialog
static Future<void> showInfo({
required BuildContext context,
required String title,
required String message,
String buttonLabel = 'OK',
}) {
return showDialog<void>(
context: context,
builder: (context) => sojornDialog(
title: title,
message: message,
primaryButtonLabel: buttonLabel,
onPrimaryPressed: () => Navigator.of(context).pop(),
),
);
}
/// Show an error dialog
static Future<void> showError({
required BuildContext context,
required String title,
required String message,
String buttonLabel = 'OK',
}) {
return showDialog<void>(
context: context,
builder: (context) => sojornDialog(
title: title,
message: message,
primaryButtonLabel: buttonLabel,
onPrimaryPressed: () => Navigator.of(context).pop(),
isDestructive: true,
),
);
}
}
/// Bottom sheet variant for mobile-friendly actions
class sojornBottomSheet extends StatelessWidget {
final String? title;
final Widget child;
const sojornBottomSheet({
super.key,
this.title,
required this.child,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppTheme.white, // Replaced AppTheme.surfaceElevated
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12.0), // Replaced AppTheme.radiusLg
topRight: Radius.circular(12.0), // Replaced AppTheme.radiusLg
),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(top: AppTheme.spacingMd),
decoration: BoxDecoration(
color:
AppTheme.egyptianBlue, // Replaced AppTheme.borderStrong
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
),
),
),
const SizedBox(height: AppTheme.spacingMd),
// Title
if (title != null) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppTheme.spacingLg,
),
child: Text(
title!,
style: AppTheme.headlineSmall,
),
),
const SizedBox(height: AppTheme.spacingMd),
],
// Content
Padding(
padding: const EdgeInsets.all(AppTheme.spacingLg),
child: child,
),
],
),
),
);
}
/// Show a bottom sheet
static Future<T?> show<T>({
required BuildContext context,
String? title,
required Widget child,
}) {
return showModalBottomSheet<T>(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => sojornBottomSheet(
title: title,
child: child,
),
);
}
}