Copy threaded conversation card design to regular post cards
- Update PostActions to include reactions with full state management - Add reaction toggle functionality with optimistic updates - Redesign PostActions layout to match threaded conversation: - Reactions section at top (full width) - Left-aligned action buttons (Reply, Like, Save, Share) - Reply button as expanded ElevatedButton - Icon buttons with background styling - Update sojorn_post_card design: - Add card container with border and shadow - Remove chain context display - Remove reply boxes - Match threaded conversation visual style - Add proper reaction state seeding and sync - Import Google Fonts and ReactionStrip
This commit is contained in:
parent
2d5323a310
commit
6741c193e1
|
|
@ -1,11 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import '../../models/post.dart';
|
||||
import '../../providers/api_provider.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../sojorn_snackbar.dart';
|
||||
import '../reactions/reaction_strip.dart';
|
||||
|
||||
/// Post actions with a vibrant, clear, and energetic design.
|
||||
///
|
||||
|
|
@ -34,11 +36,25 @@ class _PostActionsState extends ConsumerState<PostActions> {
|
|||
bool _isLiking = false;
|
||||
bool _isSaving = false;
|
||||
|
||||
// Reaction state
|
||||
final Map<String, int> _reactionCounts = {};
|
||||
final Set<String> _myReactions = <String>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isLiked = widget.post.isLiked ?? false;
|
||||
_isSaved = widget.post.isSaved ?? false;
|
||||
_seedReactionState();
|
||||
}
|
||||
|
||||
void _seedReactionState() {
|
||||
if (widget.post.reactions != null) {
|
||||
_reactionCounts.addAll(widget.post.reactions!);
|
||||
}
|
||||
if (widget.post.myReactions != null) {
|
||||
_myReactions.addAll(widget.post.myReactions!);
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
|
|
@ -121,42 +137,147 @@ class _PostActionsState extends ConsumerState<PostActions> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleReaction(String emoji) async {
|
||||
final previousCounts = Map<String, int>.from(_reactionCounts);
|
||||
final previousMine = Set<String>.from(_myReactions);
|
||||
|
||||
setState(() {
|
||||
if (_myReactions.contains(emoji)) {
|
||||
_myReactions.remove(emoji);
|
||||
final next = (_reactionCounts[emoji] ?? 1) - 1;
|
||||
if (next <= 0) {
|
||||
_reactionCounts.remove(emoji);
|
||||
} else {
|
||||
_reactionCounts[emoji] = next;
|
||||
}
|
||||
} else {
|
||||
if (_myReactions.isNotEmpty) {
|
||||
final previousEmoji = _myReactions.first;
|
||||
_myReactions.clear();
|
||||
final prevCount = (_reactionCounts[previousEmoji] ?? 1) - 1;
|
||||
if (prevCount <= 0) {
|
||||
_reactionCounts.remove(previousEmoji);
|
||||
} else {
|
||||
_reactionCounts[previousEmoji] = prevCount;
|
||||
}
|
||||
}
|
||||
_myReactions.add(emoji);
|
||||
_reactionCounts[emoji] = (_reactionCounts[emoji] ?? 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
final api = ref.read(apiServiceProvider);
|
||||
final response = await api.toggleReaction(widget.post.id, emoji);
|
||||
if (!mounted) return;
|
||||
final updatedCounts = response['reactions'] as Map<String, dynamic>?;
|
||||
final updatedMine = response['my_reactions'] as List<dynamic>?;
|
||||
|
||||
if (updatedCounts != null) {
|
||||
setState(() {
|
||||
_reactionCounts.clear();
|
||||
_reactionCounts.addAll(
|
||||
updatedCounts.map((key, value) => MapEntry(key, value as int))
|
||||
);
|
||||
});
|
||||
}
|
||||
if (updatedMine != null) {
|
||||
setState(() {
|
||||
_myReactions.clear();
|
||||
_myReactions.addAll(updatedMine.map((item) => item.toString()));
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_reactionCounts.clear();
|
||||
_reactionCounts.addAll(previousCounts);
|
||||
_myReactions.clear();
|
||||
_myReactions.addAll(previousMine);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final allowChain = widget.post.allowChain && widget.post.visibility != 'private' && widget.onChain != null;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Icon-only action buttons with Row layout
|
||||
_IconActionButton(
|
||||
icon: Icons.favorite_border,
|
||||
activeIcon: Icons.favorite,
|
||||
isActive: _isLiked,
|
||||
isLoading: _isLiking,
|
||||
onPressed: _isLiking ? null : _toggleLike,
|
||||
activeColor: AppTheme.brightNavy,
|
||||
// Reactions section - full width
|
||||
ReactionStrip(
|
||||
reactions: _reactionCounts,
|
||||
myReactions: _myReactions,
|
||||
reactionUsers: {},
|
||||
onToggle: (emoji) => _toggleReaction(emoji),
|
||||
onAdd: () => _toggleReaction('❤️'), // Default to heart for now
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_IconActionButton(
|
||||
icon: Icons.bookmark_border,
|
||||
activeIcon: Icons.bookmark,
|
||||
isActive: _isSaved,
|
||||
isLoading: _isSaving,
|
||||
onPressed: _isSaving ? null : _toggleSave,
|
||||
activeColor: AppTheme.brightNavy,
|
||||
const SizedBox(height: 16),
|
||||
// Actions row - left aligned
|
||||
Row(
|
||||
children: [
|
||||
if (allowChain)
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: widget.onChain,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (allowChain) const SizedBox(width: 12),
|
||||
IconButton(
|
||||
onPressed: _isLiking ? null : _toggleLike,
|
||||
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: _isSaving ? null : _toggleSave,
|
||||
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: 8),
|
||||
IconButton(
|
||||
onPressed: _sharePost,
|
||||
icon: Icon(
|
||||
Icons.share_outlined,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_IconActionButton(
|
||||
icon: Icons.share_outlined,
|
||||
onPressed: _sharePost,
|
||||
),
|
||||
const Spacer(),
|
||||
if (allowChain)
|
||||
_IconActionButton(
|
||||
icon: Icons.reply,
|
||||
onPressed: widget.onChain,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,9 +81,25 @@ class sojornPostCard extends StatelessWidget {
|
|||
highlightColor: Colors.transparent,
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardSurface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppTheme.brightNavy,
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppTheme.brightNavy.withValues(alpha: 0.18),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
// Header row with menu
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -101,12 +117,7 @@ class sojornPostCard extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Chain context (if parent exists and not in detail view)
|
||||
if (post.chainParent != null && mode != PostViewMode.detail)
|
||||
_buildChainContext(context),
|
||||
|
||||
const SizedBox(height: AppTheme.spacingMd),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Body text
|
||||
PostBody(
|
||||
|
|
@ -117,13 +128,15 @@ class sojornPostCard extends StatelessWidget {
|
|||
),
|
||||
|
||||
// Media (if available)
|
||||
if (post.imageUrl != null && post.imageUrl!.isNotEmpty)
|
||||
if (post.imageUrl != null && post.imageUrl!.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
PostMedia(
|
||||
post: post,
|
||||
mode: mode,
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: AppTheme.spacingLg),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Actions
|
||||
PostActions(
|
||||
|
|
@ -137,43 +150,4 @@ class sojornPostCard extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChainContext(BuildContext context) {
|
||||
final parent = post.chainParent;
|
||||
if (parent == null) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: AppTheme.spacingSm),
|
||||
padding: const EdgeInsets.all(AppTheme.spacingSm),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.queenPink.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: AppTheme.egyptianBlue.withOpacity(0.5),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.subdirectory_arrow_right,
|
||||
color: AppTheme.royalPurple.withOpacity(0.7),
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"Replying to @${parent.author?.handle ?? 'unknown'}",
|
||||
style: AppTheme.textTheme.labelSmall?.copyWith(
|
||||
color: AppTheme.royalPurple,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue