Redesign threaded conversation layout with dashboard-style reply chains

- Add top padding to anchor zone for better spacing
- Move reactions into stage actions to match main post width
- Left-align interaction buttons (reply, like, save)
- Create dashboard-style reply chains with:
  - Header showing reply count and 'Dashboard View' badge
  - Individual reply cards with menu buttons
  - 'View Thread' action buttons
  - Consistent width matching main post
  - Subtle shadows and borders for depth
- Add staggered animations for reply items
- Improve visual hierarchy and spacing throughout
This commit is contained in:
Patrick Britton 2026-02-01 13:44:14 -06:00
parent 904996a14e
commit c281a224d3

View file

@ -315,7 +315,7 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
return SlideTransition( return SlideTransition(
position: _slideAnimation, position: _slideAnimation,
child: Container( child: Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 0), margin: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: GestureDetector( child: GestureDetector(
onTap: () => _navigateToPost(parentPost.id), onTap: () => _navigateToPost(parentPost.id),
child: Container( child: Container(
@ -546,54 +546,69 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
Widget _buildStageActions(Post post) { Widget _buildStageActions(Post post) {
final isLiked = _likedByPost[post.id] ?? (post.isLiked ?? false); final isLiked = _likedByPost[post.id] ?? (post.isLiked ?? false);
final isSaved = _savedByPost[post.id] ?? (post.isSaved ?? false); final isSaved = _savedByPost[post.id] ?? (post.isSaved ?? false);
return Row( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( // Reactions section - full width like main post
child: ElevatedButton.icon( ReactionStrip(
onPressed: post.allowChain reactions: _reactionCountsFor(post),
? () { myReactions: _myReactionsFor(post),
_openReplyComposer(post); reactionUsers: _reactionUsersFor(post),
} onToggle: (emoji) => _toggleReaction(post.id, emoji),
: null, onAdd: () => _openReactionPicker(post.id),
icon: const Icon(Icons.reply, size: 18),
label: const Text('Reply'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.brightNavy,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
), ),
), const SizedBox(height: 16),
const SizedBox(width: 12), // Actions row - left aligned
IconButton( Row(
onPressed: () => _toggleLike(post), children: [
icon: Icon( Expanded(
isLiked ? Icons.favorite : Icons.favorite_border, child: ElevatedButton.icon(
color: isLiked ? Colors.red : AppTheme.textSecondary, onPressed: post.allowChain
), ? () {
style: IconButton.styleFrom( _openReplyComposer(post);
backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08), }
shape: RoundedRectangleBorder( : null,
borderRadius: BorderRadius.circular(12), icon: const Icon(Icons.reply, size: 18),
label: const Text('Reply'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.brightNavy,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
), ),
), const SizedBox(width: 12),
), IconButton(
const SizedBox(width: 8), onPressed: () => _toggleLike(post),
IconButton( icon: Icon(
onPressed: () => _toggleSave(post), isLiked ? Icons.favorite : Icons.favorite_border,
icon: Icon( color: isLiked ? Colors.red : AppTheme.textSecondary,
isSaved ? Icons.bookmark : Icons.bookmark_border, ),
color: isSaved ? AppTheme.brightNavy : AppTheme.textSecondary, style: IconButton.styleFrom(
), backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08),
style: IconButton.styleFrom( shape: RoundedRectangleBorder(
backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(12),
shape: RoundedRectangleBorder( ),
borderRadius: BorderRadius.circular(12), ),
), ),
), const SizedBox(width: 8),
IconButton(
onPressed: () => _toggleSave(post),
icon: Icon(
isSaved ? Icons.bookmark : Icons.bookmark_border,
color: isSaved ? AppTheme.brightNavy : AppTheme.textSecondary,
),
style: IconButton.styleFrom(
backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
],
), ),
], ],
); );
@ -657,99 +672,217 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
child: FadeTransition( child: FadeTransition(
opacity: _fadeAnimation, opacity: _fadeAnimation,
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Column( padding: const EdgeInsets.all(24),
crossAxisAlignment: CrossAxisAlignment.start, decoration: BoxDecoration(
children: [ color: AppTheme.cardSurface,
ReactionStrip( borderRadius: BorderRadius.circular(20),
reactions: _reactionCountsFor(_focusContext!.targetPost), border: Border.all(
myReactions: _myReactionsFor(_focusContext!.targetPost), color: AppTheme.navyBlue.withValues(alpha: 0.1),
reactionUsers: _reactionUsersFor(_focusContext!.targetPost), width: 1,
onToggle: (emoji) => ),
_toggleReaction(_focusContext!.targetPost.id, emoji), ),
onAdd: () => child: Center(
_openReactionPicker(_focusContext!.targetPost.id), child: Text(
'No replies yet',
style: GoogleFonts.inter(
color: AppTheme.textSecondary,
fontSize: 14,
), ),
const SizedBox(height: 12), ),
Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: AppTheme.cardSurface,
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: AppTheme.navyBlue.withValues(alpha: 0.12),
width: 2,
),
),
child: Center(
child: Column(
children: [
Icon(
Icons.chat_bubble_outline,
size: 48,
color: AppTheme.textSecondary,
),
const SizedBox(height: 16),
Text(
_focusContext!.targetPost.allowChain
? 'Be the first to reply'
: 'Replies are disabled',
style: GoogleFonts.inter(
color: AppTheme.textSecondary,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
],
), ),
), ),
), ),
); );
} }
return Padding( return SlideTransition(
padding: const EdgeInsets.symmetric(horizontal: 16), position: _slideAnimation,
child: Column( child: FadeTransition(
crossAxisAlignment: CrossAxisAlignment.start, opacity: _fadeAnimation,
children: [ child: Container(
ReactionStrip( margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
reactions: _reactionCountsFor(_focusContext!.targetPost), child: Column(
myReactions: _myReactionsFor(_focusContext!.targetPost), crossAxisAlignment: CrossAxisAlignment.start,
reactionUsers: _reactionUsersFor(_focusContext!.targetPost), children: [
onToggle: (emoji) => // Dashboard header
_toggleReaction(_focusContext!.targetPost.id, emoji), Container(
onAdd: () => padding: const EdgeInsets.all(16),
_openReactionPicker(_focusContext!.targetPost.id), decoration: BoxDecoration(
color: AppTheme.navyBlue.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.navyBlue.withValues(alpha: 0.1),
),
),
child: Row(
children: [
Icon(
Icons.chat_bubble_outline,
color: AppTheme.brightNavy,
size: 20,
),
const SizedBox(width: 8),
Text(
'${children.length} ${children.length == 1 ? 'Reply' : 'Replies'}',
style: GoogleFonts.inter(
color: AppTheme.navyBlue,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppTheme.brightNavy.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'Dashboard View',
style: GoogleFonts.inter(
color: AppTheme.brightNavy,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
const SizedBox(height: 16),
// Reply chains as dashboard items
...children.asMap().entries.map((entry) {
final index = entry.key;
final post = entry.value;
return _buildDashboardReplyItem(post, index);
}).toList(),
],
), ),
const SizedBox(height: 12), ),
Wrap( ),
spacing: 10, );
runSpacing: 10, }
children: children.asMap().entries.map((entry) {
final index = entry.key; Widget _buildDashboardReplyItem(Post post, int index) {
final post = entry.value; return Container(
return InteractiveReplyBlock( margin: const EdgeInsets.only(bottom: 12),
key: ValueKey('reply_${post.id}'), decoration: BoxDecoration(
post: post, color: AppTheme.cardSurface,
compactPreview: true, borderRadius: BorderRadius.circular(16),
isLikedOverride: border: Border.all(
_likedByPost[post.id] ?? (post.isLiked ?? false), color: AppTheme.navyBlue.withValues(alpha: 0.08),
onToggleLike: () => _toggleLike(post), width: 1,
onTap: () => _navigateToPost(post.id), ),
) boxShadow: [
.animate(delay: (index * 90).ms) BoxShadow(
.fadeIn(duration: 300.ms, curve: Curves.easeOutCubic) color: AppTheme.navyBlue.withValues(alpha: 0.04),
.slideY(begin: 0.06, end: 0, curve: Curves.easeOutBack); blurRadius: 8,
}).toList(), offset: const Offset(0, 2),
), ),
], ],
), ),
); child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _navigateToPost(post.id),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Reply header with menu button
Row(
children: [
_buildCompactAvatar(post),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
post.author?.displayName ?? 'Anonymous',
style: GoogleFonts.inter(
color: AppTheme.navyBlue,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
Text(
timeago.format(post.createdAt),
style: GoogleFonts.inter(
color: AppTheme.textSecondary,
fontSize: 11,
),
),
],
),
),
// Menu button
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppTheme.navyBlue.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.more_horiz,
size: 16,
color: AppTheme.textSecondary,
),
),
],
),
const SizedBox(height: 12),
// Reply content
Text(
post.body,
style: GoogleFonts.inter(
color: AppTheme.navyText,
fontSize: 14,
height: 1.4,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// Reply actions
Row(
children: [
ReactionStrip(
reactions: _reactionCountsFor(post),
myReactions: _myReactionsFor(post),
reactionUsers: _reactionUsersFor(post),
onToggle: (emoji) => _toggleReaction(post.id, emoji),
onAdd: () => _openReactionPicker(post.id),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.brightNavy.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'View Thread',
style: GoogleFonts.inter(
color: AppTheme.brightNavy,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
),
),
).animate(delay: (index * 100).ms)
.fadeIn(duration: 300.ms, curve: Curves.easeOutCubic)
.slideY(begin: 0.04, end: 0, curve: Curves.easeOutBack);
} }
void _toggleReaction(String postId, String emoji) { void _toggleReaction(String postId, String emoji) {