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(
position: _slideAnimation,
child: Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 0),
margin: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: GestureDetector(
onTap: () => _navigateToPost(parentPost.id),
child: Container(
@ -546,54 +546,69 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
Widget _buildStageActions(Post post) {
final isLiked = _likedByPost[post.id] ?? (post.isLiked ?? false);
final isSaved = _savedByPost[post.id] ?? (post.isSaved ?? false);
return Row(
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: post.allowChain
? () {
_openReplyComposer(post);
}
: null,
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),
),
),
// Reactions section - full width like main post
ReactionStrip(
reactions: _reactionCountsFor(post),
myReactions: _myReactionsFor(post),
reactionUsers: _reactionUsersFor(post),
onToggle: (emoji) => _toggleReaction(post.id, emoji),
onAdd: () => _openReactionPicker(post.id),
),
),
const SizedBox(width: 12),
IconButton(
onPressed: () => _toggleLike(post),
icon: Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
color: isLiked ? Colors.red : AppTheme.textSecondary,
),
style: IconButton.styleFrom(
backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
const SizedBox(height: 16),
// Actions row - left aligned
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: post.allowChain
? () {
_openReplyComposer(post);
}
: null,
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: 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),
const SizedBox(width: 12),
IconButton(
onPressed: () => _toggleLike(post),
icon: Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
color: isLiked ? Colors.red : AppTheme.textSecondary,
),
style: IconButton.styleFrom(
backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08),
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(
opacity: _fadeAnimation,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReactionStrip(
reactions: _reactionCountsFor(_focusContext!.targetPost),
myReactions: _myReactionsFor(_focusContext!.targetPost),
reactionUsers: _reactionUsersFor(_focusContext!.targetPost),
onToggle: (emoji) =>
_toggleReaction(_focusContext!.targetPost.id, emoji),
onAdd: () =>
_openReactionPicker(_focusContext!.targetPost.id),
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppTheme.cardSurface,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.navyBlue.withValues(alpha: 0.1),
width: 1,
),
),
child: Center(
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(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReactionStrip(
reactions: _reactionCountsFor(_focusContext!.targetPost),
myReactions: _myReactionsFor(_focusContext!.targetPost),
reactionUsers: _reactionUsersFor(_focusContext!.targetPost),
onToggle: (emoji) =>
_toggleReaction(_focusContext!.targetPost.id, emoji),
onAdd: () =>
_openReactionPicker(_focusContext!.targetPost.id),
return SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Container(
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Dashboard header
Container(
padding: const EdgeInsets.all(16),
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;
final post = entry.value;
return InteractiveReplyBlock(
key: ValueKey('reply_${post.id}'),
post: post,
compactPreview: true,
isLikedOverride:
_likedByPost[post.id] ?? (post.isLiked ?? false),
onToggleLike: () => _toggleLike(post),
onTap: () => _navigateToPost(post.id),
)
.animate(delay: (index * 90).ms)
.fadeIn(duration: 300.ms, curve: Curves.easeOutCubic)
.slideY(begin: 0.06, end: 0, curve: Curves.easeOutBack);
}).toList(),
),
),
);
}
Widget _buildDashboardReplyItem(Post post, int index) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: AppTheme.cardSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.navyBlue.withValues(alpha: 0.08),
width: 1,
),
boxShadow: [
BoxShadow(
color: AppTheme.navyBlue.withValues(alpha: 0.04),
blurRadius: 8,
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) {