import 'package:poptart_lex/com/atproto/moderation/create_report.dart'; import 'package:poptart_lex/com/atproto/repo/strong_ref.dart'; import 'package:poptart/poptart.dart'; import 'package:auto_route/auto_route.dart'; import 'package:material_ui/material_ui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:spark/src/core/auth/data/repositories/auth_repository.dart'; import 'package:spark/src/core/design_system/components/atoms/icons.dart'; import 'package:spark/src/core/l10n/app_localizations.dart'; import 'package:spark/src/core/moderation/moderated_content.dart'; import 'package:spark/src/core/moderation/moderation.dart'; import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; import 'package:spark/src/core/routing/app_router.dart'; import 'package:spark/src/core/design_system/tokens/colors.dart'; import 'package:spark/src/core/ui/widgets/image_content.dart'; import 'package:spark/src/core/ui/widgets/options_panel.dart'; import 'package:spark/src/core/ui/widgets/report_dialog.dart'; import 'package:spark/src/core/design_system/components/atoms/user_avatar.dart'; import 'package:spark/src/core/utils/utils.dart'; import 'package:spark/src/features/comments/providers/comment_provider.dart'; import 'package:spark/src/features/comments/providers/comment_state.dart'; import 'package:spark/src/features/comments/providers/comments_page_provider.dart'; import 'package:spark/src/features/feed/ui/widgets/post_likes_sheet.dart'; class CommentItem extends ConsumerWidget { const CommentItem({ required this.thread, required this.mainPostUri, super.key, this.isHighlighted = false, }); final ThreadViewPost thread; final AtUri mainPostUri; final bool isHighlighted; @override Widget build(BuildContext context, WidgetRef ref) { final commentState = ref.watch(commentProvider(thread)); final post = commentState.thread.post; return ModeratedContent( subject: ModerationSubject.content( labels: post.labels ?? const [], authorLabels: post.author.labels ?? const [], subjectDid: post.author.did, ), context: ModerationContext.contentList, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CommentBody( thread: thread, mainPostUri: mainPostUri, isHighlighted: isHighlighted, ), if (post.replyCount != null && post.replyCount! > 0) _RepliesSection(commentState), Container(height: 0.5, color: Theme.of(context).colorScheme.surface), ], ), ); } } class CommentBody extends ConsumerStatefulWidget { const CommentBody({ required this.thread, required this.mainPostUri, super.key, this.isHighlighted = false, }); final ThreadViewPost thread; final AtUri mainPostUri; final bool isHighlighted; @override ConsumerState createState() => _CommentBodyState(); } class _CommentBodyState extends ConsumerState { late CommentState commentState; late final SparkLogger _logger = GetIt.instance().getLogger( 'CommentBody', ); @override void initState() { super.initState(); } void _navigateToProfile() { final author = commentState.thread.post.author; final isBskyPost = widget.mainPostUri.collection.toString().startsWith( 'app.bsky', ); context.router.push( ProfileRoute(did: author.did, initialProfile: author, bsky: isBskyPost), ); } void _handleReportComment() { showDialog( context: context, builder: (context) => ReportDialog( subject: UModerationCreateReportInputSubject.repoStrongRef( data: RepoStrongRef( uri: commentState.thread.post.uri, cid: commentState.thread.post.cid, ), ), ), ); } void _handleDeleteComment() { final l10n = AppLocalizations.of(context); showDialog( context: context, builder: (context) => AlertDialog( title: Text(l10n.dialogDeleteComment), content: Text(l10n.dialogDeleteCommentConfirm), actions: [ TextButton( onPressed: () => context.router.maybePop(), child: Text(l10n.buttonCancel), ), TextButton( onPressed: () async { try { await ref .read( commentsPageProvider(postUri: widget.mainPostUri) .notifier, ) .deleteComment(commentState.thread.post.uri.toString()); if (context.mounted) { await context.router.maybePop(); } } catch (e) { _logger.e('Error deleting comment', error: e); } }, child: Text(l10n.buttonDelete), ), ], ), ); } @override Widget build(BuildContext context) { commentState = ref.watch(commentProvider(widget.thread)); const double thumbnailSize = 120; final borderRadius = BorderRadius.circular(8); final imageUrls = commentState.thread.post.imageUrls; return ColoredBox( color: widget.isHighlighted ? AppColors.pink.withValues(alpha: 0.15) : Colors.transparent, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onTap: _navigateToProfile, child: _Avatar(thread: widget.thread), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: GestureDetector( onTap: _navigateToProfile, child: Row( children: [ Text( commentState.thread.post.author.handle, style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context) .textTheme .bodyLarge ?.color, ), ), const SizedBox(width: 8), Text( _formatDate( commentState.thread.post.indexedAt .toLocal() .toString(), ), style: TextStyle( fontSize: 12, color: Theme.of(context) .textTheme .bodyMedium ?.color, ), ), ], ), ), ), Builder( builder: (context) { final authRepository = GetIt.instance(); final userDid = authRepository.did; final isCurrentUserAuthor = userDid == commentState.thread.post.author.did; final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; final iconColor = isDark ? AppColors.white : AppColors.black; return GestureDetector( onTap: () => OptionsPanel.show( context: context, onReport: _handleReportComment, onDelete: isCurrentUserAuthor ? _handleDeleteComment : null, ), child: SizedBox( width: 28, height: 28, child: AppIcon( AppIconData.moreHoriz, color: iconColor, size: 16, ), ), ); }, ), ], ), const SizedBox(height: 4), if (commentState.thread.post.displayText.isNotEmpty) Text( commentState.thread.post.displayText, style: Theme.of(context).textTheme.bodyMedium, ), if (imageUrls.isNotEmpty) ...[ const SizedBox(height: 8), ImageContent( imageUrls: imageUrls, borderRadius: borderRadius, thumbnailSize: thumbnailSize, ), ], const SizedBox(height: 8), _ActionButtons( ref: ref, commentState: commentState, thread: widget.thread, secondaryTextColor: Theme.of(context) .textTheme .bodyMedium! .color!, ), ], ), ), ], ), ), ], ), ); } String _formatDate(String dateTimeString) { final dateTime = DateTime.parse(dateTimeString); final now = DateTime.now(); final difference = now.difference(dateTime); if (difference.inDays > 365) { return '${(difference.inDays / 365).floor()}y'; } else if (difference.inDays > 30) { return '${(difference.inDays / 30).floor()}mo'; } else if (difference.inDays > 0) { return '${difference.inDays}d'; } else if (difference.inHours > 0) { return '${difference.inHours}h'; } else if (difference.inMinutes > 0) { return '${difference.inMinutes}m'; } else { return 'now'; } } } class _RepliesSection extends StatelessWidget { const _RepliesSection(this.commentState); final CommentState commentState; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return GestureDetector( onTap: () => context.router.push( RepliesRoute(postUri: commentState.thread.post.uri.toString()), ), child: Container( margin: const EdgeInsets.only(left: 64), padding: const EdgeInsets.only(top: 4, bottom: 8), decoration: BoxDecoration( border: Border( left: BorderSide(color: Theme.of(context).colorScheme.surface), ), ), child: Text( l10n.messageShowReplies(commentState.thread.post.replyCount ?? 0), ), ), ); } } class _ActionButtons extends StatelessWidget { const _ActionButtons({ required this.ref, required this.commentState, required this.thread, required this.secondaryTextColor, }); final WidgetRef ref; final CommentState commentState; final ThreadViewPost thread; final Color secondaryTextColor; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final notifier = ref.read(commentProvider(thread).notifier); return Row( children: [ TextButton( style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, splashFactory: NoSplash.splashFactory, ), onPressed: notifier.toggleLike, child: Row( children: [ if (commentState.isLiked) AppIcons.likeFilled(size: 16, color: AppColors.pink) else AppIcons.like(size: 16, color: secondaryTextColor), const SizedBox(width: 4), GestureDetector( behavior: HitTestBehavior.opaque, onTap: commentState.likeCount == 0 ? null : () { final post = commentState.thread.post; showPostLikesSheet( context: context, uri: post.uri.toString(), cid: post.cid, ); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text( commentState.likeCount.toString(), style: TextStyle(fontSize: 12, color: secondaryTextColor), ), ), ), ], ), ), const SizedBox(width: 16), TextButton( style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), onPressed: () { context.router.push( RepliesRoute(postUri: commentState.thread.post.uri.toString()), ); }, child: Text( l10n.labelReply, style: TextStyle(fontSize: 12, color: secondaryTextColor), ), ), ], ); } } class _Avatar extends StatelessWidget { const _Avatar({required this.thread}); final ThreadViewPost thread; @override Widget build(BuildContext context) { final author = thread.post.author; return ModeratedProfileAvatar( labels: author.labels ?? const [], subjectDid: author.did, child: UserAvatar( imageUrl: author.avatar.toString(), username: author.handle, size: 36, ), ); } }