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'; import '../reactions/reaction_picker.dart'; import '../reactions/smart_reaction_button.dart'; /// Post actions with a vibrant, clear, and energetic design. /// /// Design Intent: /// - Actions are clear, tappable, and visually engaging. /// - Clear state changes: default (energetic) → active (highlighted). class PostActions extends ConsumerStatefulWidget { final Post post; final VoidCallback? onChain; final VoidCallback? onPostChanged; const PostActions({ super.key, required this.post, this.onChain, this.onPostChanged, }); @override ConsumerState createState() => _PostActionsState(); } class _PostActionsState extends ConsumerState { late bool _isSaved; bool _isSaving = false; // Reaction state final Map _reactionCounts = {}; final Set _myReactions = {}; @override void initState() { super.initState(); _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) { sojornSnackbar.showError( context: context, message: message, ); } Future _toggleSave() async { if (_isSaving) return; setState(() { _isSaving = true; _isSaved = !_isSaved; }); final apiService = ref.read(apiServiceProvider); try { if (_isSaved) { await apiService.savePost(widget.post.id); } else { await apiService.unsavePost(widget.post.id); } } catch (e) { if (mounted) { setState(() { _isSaved = !_isSaved; }); _showError(e.toString().replaceAll('Exception: ', '')); } } finally { if (mounted) { setState(() { _isSaving = false; }); } } } Future _sharePost() async { final handle = widget.post.author?.handle ?? 'sojorn'; final text = '${widget.post.body}\n\n— @$handle on sojorn'; try { await Share.share(text); } catch (e) { _showError('Unable to share right now.'); } } void _showReactionPicker() { // Sort reactions: existing reactions first (by count), then common emojis final existingReactions = _reactionCounts.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); final allReactions = [ ...existingReactions.map((e) => e.key), 'ā¤ļø', 'šŸ‘', 'šŸ˜‚', '😮', '😢', '😔', 'šŸŽ‰', 'šŸ”„', 'šŸ‘', 'šŸ™', 'šŸ’Æ', 'šŸ¤”', 'šŸ˜', '🤣', '😊', 'šŸ‘Œ', 'šŸ™Œ', 'šŸ’Ŗ', 'šŸŽÆ', '⭐', '✨', '🌟', 'šŸ’«', 'ā˜€ļø', ]; showDialog( context: context, builder: (context) => ReactionPicker( onReactionSelected: (emoji) { _toggleReaction(emoji); }, onClosed: () { // Optional: Handle picker closed without selection }, reactions: allReactions, reactionCounts: _reactionCounts, myReactions: _myReactions, ), ); } Future _toggleReaction(String emoji) async { final previousCounts = Map.from(_reactionCounts); final previousMine = Set.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?; final updatedMine = response['my_reactions'] as List?; 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; // Get top 3 reactions by count final sortedReactions = _reactionCounts.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); final topReactions = Map.fromEntries( sortedReactions.take(3) ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Reactions section - full width, but only top 3 ReactionStrip( reactions: topReactions, myReactions: _myReactions, reactionUsers: {}, onToggle: (emoji) => _toggleReaction(emoji), onAdd: _showReactionPicker, // Show picker instead of default heart ), 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), // Smart reaction button (replaces appreciate button) SmartReactionButton( reactionCounts: _reactionCounts, myReactions: _myReactions, onPressed: _showReactionPicker, ), 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), ), ), ), ], ), ], ); } } /// Icon-only action button with large touch target. class _IconActionButton extends StatelessWidget { final IconData icon; final IconData? activeIcon; final VoidCallback? onPressed; final bool isActive; final bool isLoading; final Color? activeColor; const _IconActionButton({ required this.icon, this.activeIcon, this.onPressed, this.isActive = false, this.isLoading = false, this.activeColor, }); @override Widget build(BuildContext context) { final effectiveActiveColor = activeColor ?? AppTheme.brightNavy; final effectiveDefaultColor = AppTheme.royalPurple; final color = isActive ? effectiveActiveColor : effectiveDefaultColor; final displayIcon = isActive && activeIcon != null ? activeIcon! : icon; return AnimatedOpacity( opacity: isLoading ? 0.5 : 1.0, duration: const Duration(milliseconds: 200), child: IconButton( onPressed: onPressed, iconSize: 22.0, padding: const EdgeInsets.all(8.0), constraints: const BoxConstraints( minWidth: 44, minHeight: 44, ), icon: Icon( displayIcon, size: 22.0, color: color, ), ), ); } }