import 'dart:math'; import 'package:flutter/material.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:nested/nested.dart'; import '../../comment_tree.dart'; import '../../l10n/l10n.dart'; import '../../stores/config_store.dart'; import '../../util/async_store_listener.dart'; import '../../util/extensions/api.dart'; import '../../util/goto.dart'; import '../../util/mobx_provider.dart'; import '../../util/observer_consumers.dart'; import '../../util/text_color.dart'; import '../avatar.dart'; import '../info_table_popup.dart'; import '../markdown_text.dart'; import 'comment_actions.dart'; import 'comment_store.dart'; /// A single comment that renders its replies class CommentWidget extends StatelessWidget { final CommentTree commentTree; final int? userMentionId; final int depth; final bool canBeMarkedAsRead; final bool detached; final bool hideOnRead; const CommentWidget( this.commentTree, { this.depth = 0, this.detached = false, this.canBeMarkedAsRead = false, this.hideOnRead = false, this.userMentionId, super.key, }); CommentWidget.fromCommentView( CommentView cv, { bool canBeMarkedAsRead = false, bool hideOnRead = false, bool detached = true, Key? key, }) : this( CommentTree(cv), detached: detached, canBeMarkedAsRead: canBeMarkedAsRead, hideOnRead: hideOnRead, key: key, ); CommentWidget.fromPersonMentionView( PersonMentionView userMentionView, { bool hideOnRead = false, Key? key, }) : this( CommentTree(CommentView.fromJson(userMentionView.toJson())), hideOnRead: hideOnRead, canBeMarkedAsRead: true, detached: true, userMentionId: userMentionView.personMention.id, key: key, ); static void showCommentInfo(BuildContext context, CommentView commentView) { final percentOfUpvotes = 100 * (commentView.counts.upvotes / (commentView.counts.upvotes + commentView.counts.downvotes)); showInfoTablePopup(context: context, table: { ...commentView.toJson(), '% of upvotes': '$percentOfUpvotes%', }); } @override Widget build(BuildContext context) { return MobxProvider( create: (context) => CommentStore( context.read(), commentTree: commentTree, userMentionId: userMentionId, depth: depth, canBeMarkedAsRead: canBeMarkedAsRead, detached: detached, hideOnRead: hideOnRead, ), builder: (context, child) => Nested( children: [ AsyncStoreListener( asyncStore: context.read().blockingState, successMessageBuilder: (context, state) { final name = state.personView.person.preferredName; return state.blocked ? '$name blocked' : '$name unblocked'; }, ), AsyncStoreListener( asyncStore: context.read().votingState, ), AsyncStoreListener( asyncStore: context.read().deletingState, ), AsyncStoreListener( asyncStore: context.read().savingState, ), AsyncStoreListener( asyncStore: context.read().reportingState, successMessageBuilder: (context, data) => 'Comment reported', ), ], child: const _CommentWidget(), ), ); } } class _CommentWidget extends StatelessWidget { static const colors = [ Colors.pink, Colors.green, Colors.amber, Colors.cyan, Colors.indigo, ]; static const indentWidth = 5.0; const _CommentWidget(); @override Widget build(BuildContext context) { final theme = Theme.of(context); final body = ObserverBuilder( builder: (context, store) { final comment = store.comment.comment; if (comment.deleted) { return Text( L10n.of(context).deleted_by_creator, style: const TextStyle(fontStyle: FontStyle.italic), ); } else if (comment.removed) { return const Text( 'comment deleted by moderator', style: TextStyle(fontStyle: FontStyle.italic), ); } else if (store.collapsed) { return Opacity( opacity: 0.3, child: Text( comment.content, maxLines: 1, overflow: TextOverflow.ellipsis, ), ); } return store.showRaw ? store.selectable ? SelectableText(comment.content) : Text(comment.content) : MarkdownText( comment.content, instanceHost: comment.instanceHost, selectable: store.selectable, ); }, ); return ObserverBuilder( builder: (context, store) { if (store.hideOnRead && store.comment.comment.read) { return const SizedBox(); } final comment = store.comment.comment; final creator = store.comment.creator; return InkWell( onLongPress: store.selectable ? null : store.toggleCollapsed, child: Column( children: [ Container( padding: const EdgeInsets.all(10), margin: EdgeInsets.only( left: max((store.depth - 1) * indentWidth, 0), ), decoration: BoxDecoration( border: Border( left: store.depth > 0 ? BorderSide( color: colors[store.depth % colors.length], width: indentWidth, ) : BorderSide.none, top: const BorderSide(width: 0.2), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ if (creator.avatar != null) Padding( padding: const EdgeInsets.only(right: 5), child: InkWell( onTap: () => goToUser.fromPersonSafe(context, creator), child: Avatar( url: creator.avatar, radius: 10, noBlank: true, ), ), ), InkWell( onTap: () => goToUser.fromPersonSafe(context, creator), child: Text( creator.originPreferredName, style: TextStyle( color: theme.colorScheme.secondary, ), ), ), if (creator.isCakeDay) const Text(' 🍰'), if (store.isOP) _CommentTag('OP', theme.colorScheme.secondary), if (creator.admin) _CommentTag( L10n.of(context).admin.toUpperCase(), theme.colorScheme.secondary, ), if (creator.banned) const _CommentTag('BANNED', Colors.red), if (store.comment.creatorBannedFromCommunity) const _CommentTag('BANNED FROM COMMUNITY', Colors.red), const Spacer(), if (store.collapsed && store.children.isNotEmpty) ...[ _CommentTag( '+${store.children.length}', Theme.of(context).colorScheme.secondary, ), const SizedBox(width: 7), ], InkWell( onTap: () => CommentWidget.showCommentInfo( context, store.comment, ), child: Consumer( builder: (context, configStore, child) { return ObserverBuilder( builder: (context, store) => Row( children: [ if (store.votingState.isLoading) SizedBox.fromSize( size: const Size.square(16), child: const CircularProgressIndicator .adaptive(), ) else if (configStore.showScores) Text( store.comment.counts.score .compact(context), ), if (configStore.showScores) const Text(' · ') else const SizedBox(width: 4), Text(comment.published.timeago(context)), ], ), ); }, ), ) ]), const SizedBox(height: 10), body, const SizedBox(height: 5), const CommentActions(), ], ), ), if (!store.collapsed) for (final c in store.children) CommentWidget(c, depth: store.depth + 1), ], ), ); }, ); } } class _CommentTag extends StatelessWidget { final String text; final Color backgroundColor; const _CommentTag(this.text, this.backgroundColor); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 5), child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(5)), color: backgroundColor, ), padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2), child: Text( text, style: TextStyle( color: textColorBasedOnBackground(backgroundColor), fontSize: Theme.of(context).textTheme.bodyText1!.fontSize! - 5, fontWeight: FontWeight.w800, ), ), ), ); } }