import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; import '../../globals.dart'; import '../../models/auth/profile.dart'; import '../../models/exec_error.dart'; import '../../models/timeline_entry.dart'; import '../../riverpod_controllers/account_services.dart'; import '../../riverpod_controllers/feature_checker_services.dart'; import '../../riverpod_controllers/fediverse_server_validator_services.dart'; import '../../riverpod_controllers/timeline_entry_services.dart'; import '../../utils/dateutils.dart'; import '../../utils/interaction_availability_util.dart'; import '../../utils/snackbar_builder.dart'; class InteractionsBarControl extends ConsumerStatefulWidget { final TimelineEntry entry; final bool showOpenControl; final bool isMine; const InteractionsBarControl({ super.key, required this.entry, required this.isMine, required this.showOpenControl, }); @override ConsumerState createState() => _InteractionsBarControlState(); } class _InteractionsBarControlState extends ConsumerState { static final _logger = Logger('$InteractionsBarControl'); var isProcessing = false; bool get isPost => widget.entry.parentId.isEmpty; bool get isFavorited => widget.entry.isFavorited; bool get youReshared => widget.entry.youReshared; int get reshares => widget.entry.engagementSummary.rebloggedCount; int get comments => widget.entry.engagementSummary.repliesCount; int get likes => widget.entry.engagementSummary.favoritesCount; Future toggleFavorited(Profile profile) async { setState(() { isProcessing = true; }); final newState = !isFavorited; _logger.fine('Trying to toggle favorite from $isFavorited to $newState'); final result = await ref .read(timelineEntryManagerProvider(profile, widget.entry.id).notifier) .toggleFavorited(newState); result.match(onSuccess: (update) { setState(() { _logger.fine('Success toggling! $isFavorited -> ${update.isFavorited}'); }); }, onError: (error) { buildSnackbar(context, 'Error toggling like status: $error'); }); setState(() { isProcessing = false; }); } Future resharePost(Profile profile) async { setState(() { isProcessing = true; }); // TODO Add can reshare check if (!ref.read( featureCheckProvider(profile, RelaticaFeatures.diasporaReshare))) { final serverTypeEstimate = await ref .read(serverDataProvider(widget.entry.externalLink).future) .fold(onSuccess: (d) => d.softwareName, onError: (_) => ''); if (serverTypeEstimate == softwareTypeDiaspora) { if (mounted) { final error = ref.read( versionErrorStringProvider(RelaticaFeatures.diasporaReshare)); await showConfirmDialog(context, error); } setState(() { isProcessing = false; }); } } final id = widget.entry.id; _logger.fine('Trying to reshare $id'); final result = await ref .read(timelineEntryManagerProvider(profile, widget.entry.id).notifier) .resharePost(); result.match(onSuccess: (update) { setState(() { _logger.fine('Success resharing post by ${widget.entry.author}'); }); }, onError: (error) { buildSnackbar(context, 'Error resharing post by ${widget.entry.author}'); logError(error, _logger); }); setState(() { isProcessing = false; }); } Future addComment() async { if (mounted) { final elapsed = ElapsedDateUtils.elapsedTimeFromEpochSeconds( widget.entry.creationTimestamp); if (elapsed > const Duration(days: 30)) { final label = ElapsedDateUtils.elapsedTimeStringFromEpochSeconds( widget.entry.creationTimestamp); final confirm = await showYesNoDialog( context, 'Entry is from $label. Are you sure you want to add a comment on it?', ); if (confirm != true) { return; } } } if (mounted) { context.push('/comment/new?parent_id=${widget.entry.id}'); } } Future unResharePost(Profile profile) async { setState(() { isProcessing = true; }); final id = widget.entry.id; _logger.fine('Trying to un-reshare $id'); final result = await ref .read(timelineEntryManagerProvider(profile, widget.entry.id).notifier) .unResharePost(); result.match(onSuccess: (update) { setState(() { _logger.fine('Success un-resharing post by ${widget.entry.author}'); }); }, onError: (error) { buildSnackbar( context, 'Error un-resharing post by ${widget.entry.author}'); logError(error, _logger); }); setState(() { isProcessing = false; }); } Widget buildButton( IconData icon, int count, bool isEnabled, String tooltip, Future Function()? onPressed, ) { return Row( children: [ IconButton( onPressed: isEnabled && !isProcessing ? onPressed : null, icon: Icon(icon), tooltip: tooltip, ), Text('$count'), ], ); } Widget buildLikeButton(Profile profile) { final canReact = widget.entry.getCanReact(ref); final tooltip = canReact.canDo ? 'Press to toggle like/unlike' : canReact.reason; return buildButton( isFavorited ? Icons.thumb_up : Icons.thumb_up_outlined, likes, true, tooltip, canReact.canDo ? () async => await toggleFavorited(profile) : null, ); } Widget buildCommentButton() { final canComment = widget.entry.getCanComment(ref); final tooltip = canComment.canDo ? 'Press to add a comment' : canComment.reason; return buildButton( Icons.comment, comments, true, tooltip, canComment.canDo ? () async => await addComment() : null, ); } Widget buildReshareButton(Profile profile) { final reshareable = widget.entry.getIsReshareable(ref, profile, widget.isMine); final canReshare = reshareable.canDo; late final String tooltip; if (canReshare) { tooltip = youReshared ? 'Press to undo reshare' : 'Press to reshare'; } else { tooltip = reshareable.reason; } return buildButton( youReshared ? Icons.repeat_on_outlined : Icons.repeat, reshares, true, tooltip, canReshare && !isProcessing ? () async => youReshared ? await unResharePost(profile) : await resharePost(profile) : null, ); } @override Widget build(BuildContext context) { _logger.finest('Building: ${widget.entry.toShortString()}'); final profile = ref.watch(activeProfileProvider); return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ buildLikeButton(profile), buildCommentButton(), if (widget.entry.parentId.isEmpty) buildReshareButton(profile), ], ); } }