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:
parent
904996a14e
commit
c281a224d3
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue