import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:provider/provider.dart'; import '../../comment_tree.dart'; import '../../l10n/l10n.dart'; import '../../stores/config_store.dart'; import '../../util/async_store.dart'; import '../../util/async_store_listener.dart'; import '../../util/extensions/api.dart'; import '../../util/extensions/cake_day.dart'; import '../../util/extensions/datetime.dart'; import '../../util/goto.dart'; import '../../util/intl.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, Key? key, }) : super(key: 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 Provider( create: (context) => CommentStore( context.read(), commentTree: commentTree, userMentionId: userMentionId, depth: depth, canBeMarkedAsRead: canBeMarkedAsRead, detached: detached, hideOnRead: hideOnRead, ), builder: (context, child) => ObserverListener>( store: context.read().blockingState, listener: (context, store) { final errorTerm = store.errorTerm; if (errorTerm != null) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(errorTerm.tr(context)))); } else if (store.asyncState is AsyncStateData) { final state = store.asyncState as AsyncStateData; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text( state.data.blocked ? 'User blocked' : 'User unblocked'), ), ); } }, child: AsyncStoreListener( asyncStore: context.read().votingState, child: AsyncStoreListener( asyncStore: context.read().deletingState, child: AsyncStoreListener( asyncStore: context.read().savingState, 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, ), ); } // TODO: bug, the text is selectable even when disabled after following // these steps: // make selectable > show raw > show fancy > make unselectable 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.accentColor, ), ), ), if (creator.isCakeDay) const Text(' 🍰'), if (store.isOP) _CommentTag('OP', theme.accentColor), if (creator.admin) _CommentTag( L10n.of(context)!.admin.toUpperCase(), theme.accentColor, ), 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).accentColor, ), 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(), ) else if (configStore.showScores) Text( compactNumber( store.comment.counts.score, ), ), if (configStore.showScores) const Text(' · ') else const SizedBox(width: 4), Text(comment.published.fancy), ], ), ); }, ), ) ]), 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, ), ), ), ); } }