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,7 +546,20 @@ 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: [
// 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(height: 16),
// Actions row - left aligned
Row(
children: [ children: [
Expanded( Expanded(
child: ElevatedButton.icon( child: ElevatedButton.icon(
@ -596,6 +609,8 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
), ),
), ),
], ],
),
],
); );
} }
@ -657,54 +672,91 @@ 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,
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),
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.cardSurface, color: AppTheme.cardSurface,
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(
color: AppTheme.navyBlue.withValues(alpha: 0.12), color: AppTheme.navyBlue.withValues(alpha: 0.1),
width: 2, width: 1,
), ),
), ),
child: Center( child: Center(
child: Text(
'No replies yet',
style: GoogleFonts.inter(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
),
),
),
);
}
return SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _fadeAnimation,
child: Container(
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Column( 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: [ children: [
Icon( Icon(
Icons.chat_bubble_outline, Icons.chat_bubble_outline,
size: 48, color: AppTheme.brightNavy,
color: AppTheme.textSecondary, size: 20,
), ),
const SizedBox(height: 16), const SizedBox(width: 8),
Text( Text(
_focusContext!.targetPost.allowChain '${children.length} ${children.length == 1 ? 'Reply' : 'Replies'}',
? 'Be the first to reply'
: 'Replies are disabled',
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: AppTheme.textSecondary, color: AppTheme.navyBlue,
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, 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(),
], ],
), ),
), ),
@ -712,44 +764,125 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
); );
} }
return Padding( Widget _buildDashboardReplyItem(Post post, int index) {
padding: const EdgeInsets.symmetric(horizontal: 16), return Container(
child: Column( margin: const EdgeInsets.only(bottom: 12),
crossAxisAlignment: CrossAxisAlignment.start, decoration: BoxDecoration(
children: [ color: AppTheme.cardSurface,
ReactionStrip( borderRadius: BorderRadius.circular(16),
reactions: _reactionCountsFor(_focusContext!.targetPost), border: Border.all(
myReactions: _myReactionsFor(_focusContext!.targetPost), color: AppTheme.navyBlue.withValues(alpha: 0.08),
reactionUsers: _reactionUsersFor(_focusContext!.targetPost), width: 1,
onToggle: (emoji) =>
_toggleReaction(_focusContext!.targetPost.id, emoji),
onAdd: () =>
_openReactionPicker(_focusContext!.targetPost.id),
), ),
const SizedBox(height: 12), boxShadow: [
Wrap( BoxShadow(
spacing: 10, color: AppTheme.navyBlue.withValues(alpha: 0.04),
runSpacing: 10, blurRadius: 8,
children: children.asMap().entries.map((entry) { offset: const Offset(0, 2),
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(),
), ),
], ],
), ),
); 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) {