sojorn/sojorn_app/lib/widgets/composer/composer_toolbar.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

142 lines
4.3 KiB
Dart

import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
/// Keyboard-attached toolbar for the composer with attachments, formatting, topic, and counter.
class ComposerToolbar extends StatelessWidget {
final VoidCallback onAddMedia;
final VoidCallback onToggleBold;
final VoidCallback onToggleItalic;
final VoidCallback onToggleChain;
final VoidCallback? onSelectTtl;
final bool isBold;
final bool isItalic;
final bool allowChain;
final bool ttlOverrideActive;
final String? ttlLabel;
final int characterCount;
final int maxCharacters;
final bool isUploadingImage;
final int remainingChars;
const ComposerToolbar({
super.key,
required this.onAddMedia,
required this.onToggleBold,
required this.onToggleItalic,
required this.onToggleChain,
this.onSelectTtl,
this.isBold = false,
this.isItalic = false,
this.allowChain = true,
this.ttlOverrideActive = false,
this.ttlLabel,
this.characterCount = 0,
this.maxCharacters = 500,
this.isUploadingImage = false,
this.remainingChars = 500,
});
@override
Widget build(BuildContext context) {
final isOverLimit = remainingChars < 0;
final showNumber = remainingChars <= 20;
Color ringColor;
if (isOverLimit) {
ringColor = AppTheme.error;
} else if (remainingChars <= 20) {
ringColor = AppTheme.warning;
} else {
ringColor = AppTheme.brightNavy;
}
return Row(
children: [
IconButton(
onPressed: isUploadingImage ? null : onAddMedia,
icon: isUploadingImage
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(
Icons.add_photo_alternate_outlined,
color: AppTheme.navyText.withValues(alpha: 0.75),
),
tooltip: 'Add media',
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: onToggleBold,
icon: Icon(
Icons.format_bold,
color: isBold ? AppTheme.brightNavy : AppTheme.navyText.withValues(alpha: 0.5),
),
tooltip: 'Bold',
),
IconButton(
onPressed: onToggleItalic,
icon: Icon(
Icons.format_italic,
color: isItalic ? AppTheme.brightNavy : AppTheme.navyText.withValues(alpha: 0.5),
),
tooltip: 'Italic',
),
IconButton(
onPressed: onToggleChain,
icon: Icon(
Icons.link,
color: allowChain ? AppTheme.brightNavy : AppTheme.navyText.withValues(alpha: 0.4),
),
tooltip: allowChain ? 'Allow chain' : 'Chain disabled',
),
if (onSelectTtl != null)
IconButton(
onPressed: onSelectTtl,
icon: Icon(
Icons.timer_outlined,
color: ttlOverrideActive
? AppTheme.brightNavy
: AppTheme.navyText.withValues(alpha: 0.5),
),
tooltip: ttlLabel != null ? 'Post duration: $ttlLabel' : 'Post duration',
),
],
),
const Spacer(),
SizedBox(
width: 24,
height: 24,
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
value: (characterCount / maxCharacters).clamp(0, 1),
strokeWidth: 2.5,
backgroundColor: AppTheme.queenPink.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(ringColor),
),
),
if (showNumber)
Text(
remainingChars.clamp(-99, 99).toString(),
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
color: ringColor,
),
),
],
),
),
],
);
}
}