**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.
142 lines
4.3 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|