Replace appreciate button with smart reaction button

- Remove appreciate button functionality (redundant with reactions)
- Create SmartReactionButton that shows:
  - Plus icon when no reactions exist
  - Top reaction + count when reactions exist
  - User's reaction + count when user has reacted
- Update ReactionPicker to show existing reactions first with counts
- Add visual indicators for selected reactions and counts
- Maintain full reaction functionality in single button
- Improve UX by consolidating reaction interactions
This commit is contained in:
Patrick Britton 2026-02-01 14:16:20 -06:00
parent 6cb19b056d
commit 94ffb419ae
3 changed files with 176 additions and 56 deletions

View file

@ -9,6 +9,7 @@ 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.
///
@ -32,9 +33,7 @@ class PostActions extends ConsumerStatefulWidget {
}
class _PostActionsState extends ConsumerState<PostActions> {
late bool _isLiked;
late bool _isSaved;
bool _isLiking = false;
bool _isSaving = false;
// Reaction state
@ -44,7 +43,6 @@ class _PostActionsState extends ConsumerState<PostActions> {
@override
void initState() {
super.initState();
_isLiked = widget.post.isLiked ?? false;
_isSaved = widget.post.isSaved ?? false;
_seedReactionState();
}
@ -65,37 +63,6 @@ class _PostActionsState extends ConsumerState<PostActions> {
);
}
Future<void> _toggleLike() async {
if (_isLiking) return;
setState(() {
_isLiking = true;
_isLiked = !_isLiked;
});
final apiService = ref.read(apiServiceProvider);
try {
if (_isLiked) {
await apiService.appreciatePost(widget.post.id);
} else {
await apiService.unappreciatePost(widget.post.id);
}
} catch (e) {
if (mounted) {
setState(() {
_isLiked = !_isLiked;
});
_showError(e.toString().replaceAll('Exception: ', ''));
}
} finally {
if (mounted) {
setState(() {
_isLiking = false;
});
}
}
}
Future<void> _toggleSave() async {
if (_isSaving) return;
setState(() {
@ -139,6 +106,18 @@ class _PostActionsState extends ConsumerState<PostActions> {
}
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(
@ -148,6 +127,9 @@ class _PostActionsState extends ConsumerState<PostActions> {
onClosed: () {
// Optional: Handle picker closed without selection
},
reactions: allReactions,
reactionCounts: _reactionCounts,
myReactions: _myReactions,
),
);
}
@ -257,18 +239,11 @@ class _PostActionsState extends ConsumerState<PostActions> {
),
),
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),
),
),
// Smart reaction button (replaces appreciate button)
SmartReactionButton(
reactionCounts: _reactionCounts,
myReactions: _myReactions,
onPressed: _showReactionPicker,
),
const SizedBox(width: 8),
IconButton(

View file

@ -5,11 +5,17 @@ import '../../theme/app_theme.dart';
class ReactionPicker extends StatefulWidget {
final Function(String) onReactionSelected;
final VoidCallback? onClosed;
final List<String>? reactions;
final Map<String, int>? reactionCounts;
final Set<String>? myReactions;
const ReactionPicker({
super.key,
required this.onReactionSelected,
this.onClosed,
this.reactions,
this.reactionCounts,
this.myReactions,
});
@override
@ -26,6 +32,10 @@ class _ReactionPickerState extends State<ReactionPicker> {
@override
Widget build(BuildContext context) {
final reactions = widget.reactions ?? _commonReactions;
final reactionCounts = widget.reactionCounts ?? {};
final myReactions = widget.myReactions ?? {};
return Dialog(
backgroundColor: Colors.transparent,
child: Container(
@ -85,9 +95,12 @@ class _ReactionPickerState extends State<ReactionPicker> {
mainAxisSpacing: 8,
childAspectRatio: 1,
),
itemCount: _commonReactions.length,
itemCount: reactions.length,
itemBuilder: (context, index) {
final emoji = _commonReactions[index];
final emoji = reactions[index];
final count = reactionCounts[emoji] ?? 0;
final isSelected = myReactions.contains(emoji);
return Material(
color: Colors.transparent,
child: InkWell(
@ -98,19 +111,47 @@ class _ReactionPickerState extends State<ReactionPicker> {
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
color: AppTheme.navyBlue.withValues(alpha: 0.05),
color: isSelected
? AppTheme.brightNavy.withValues(alpha: 0.2)
: AppTheme.navyBlue.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.navyBlue.withValues(alpha: 0.1),
width: 1,
color: isSelected
? AppTheme.brightNavy
: AppTheme.navyBlue.withValues(alpha: 0.1),
width: isSelected ? 2 : 1,
),
),
child: Center(
child: Stack(
children: [
Center(
child: Text(
emoji,
style: const TextStyle(fontSize: 24),
),
),
if (count > 0)
Positioned(
right: 2,
bottom: 2,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: AppTheme.brightNavy,
borderRadius: BorderRadius.circular(8),
),
child: Text(
count > 99 ? '99+' : '$count',
style: GoogleFonts.inter(
color: Colors.white,
fontSize: 8,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
);

View file

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../theme/app_theme.dart';
class SmartReactionButton extends ConsumerWidget {
final Map<String, int> reactionCounts;
final Set<String> myReactions;
final VoidCallback onPressed;
const SmartReactionButton({
super.key,
required this.reactionCounts,
required this.myReactions,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Determine what to show
if (myReactions.isNotEmpty) {
// Show user's reaction + total count
final myReaction = myReactions.first;
final totalCount = reactionCounts.values.fold(0, (a, b) => a + b);
return IconButton(
onPressed: onPressed,
icon: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
myReaction,
style: const TextStyle(fontSize: 18),
),
const SizedBox(width: 4),
Text(
totalCount > 99 ? '99+' : '$totalCount',
style: GoogleFonts.inter(
color: AppTheme.brightNavy,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
style: IconButton.styleFrom(
backgroundColor: AppTheme.brightNavy.withValues(alpha: 0.15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
} else if (reactionCounts.isNotEmpty) {
// Show top reaction + total count
final topReaction = reactionCounts.entries
.reduce((a, b) => a.value > b.value ? a : b);
final totalCount = reactionCounts.values.fold(0, (a, b) => a + b);
return IconButton(
onPressed: onPressed,
icon: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
topReaction.key,
style: const TextStyle(fontSize: 18),
),
const SizedBox(width: 4),
Text(
totalCount > 99 ? '99+' : '$totalCount',
style: GoogleFonts.inter(
color: AppTheme.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
style: IconButton.styleFrom(
backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
} else {
// Show plus button
return IconButton(
onPressed: onPressed,
icon: Icon(
Icons.add_reaction_outlined,
color: AppTheme.textSecondary,
size: 20,
),
style: IconButton.styleFrom(
backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
}
}