diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index e9b580939..e26785c6a 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -5,6 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/utils/instance.dart'; import 'package:thunder/utils/text_input_formatter.dart'; class LoginPage extends StatefulWidget { @@ -24,6 +28,9 @@ class _LoginPageState extends State { bool showPassword = false; bool fieldsFilledIn = false; + String? instanceIcon; + String? currentInstance; + Timer? instanceTextDebounceTimer; @override void initState() { @@ -49,12 +56,30 @@ class _LoginPageState extends State { } }); - _instanceTextEditingController.addListener(() { + _instanceTextEditingController.addListener(() async { + if (currentInstance != _instanceTextEditingController.text) { + setState(() => instanceIcon = null); + currentInstance = _instanceTextEditingController.text; + } + if (_usernameTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) { setState(() => fieldsFilledIn = true); } else { setState(() => fieldsFilledIn = false); } + + // Debounce + if (instanceTextDebounceTimer?.isActive == true) { + instanceTextDebounceTimer!.cancel(); + } + instanceTextDebounceTimer = Timer(const Duration(milliseconds: 500), () async { + await getInstanceIcon(_instanceTextEditingController.text).then((value) { + // Make sure the icon we looked up still matches the text + if (currentInstance == _instanceTextEditingController.text) { + setState(() => instanceIcon = value); + } + }); + }); }); } @@ -82,8 +107,34 @@ class _LoginPageState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Image.asset('assets/logo.png', width: 196.0, height: 196.0), + AnimatedCrossFade( + duration: const Duration(milliseconds: 500), + crossFadeState: instanceIcon == null + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: Image.asset('assets/logo.png', width: 80.0, height: 80.0), + secondChild: instanceIcon == null + ? Container() + : CircleAvatar( + foregroundImage: CachedNetworkImageProvider(instanceIcon!), + backgroundColor: Colors.transparent, + maxRadius: 40, + ), + ), const SizedBox(height: 12.0), + TextField( + autocorrect: false, + controller: _instanceTextEditingController, + inputFormatters: [LowerCaseTextFormatter()], + decoration: const InputDecoration( + isDense: true, + border: OutlineInputBorder(), + labelText: 'Instance', + hintText: 'e.g., lemmy.ml, lemmy.world, etc.', + ), + enableSuggestions: false, + ), + const SizedBox(height: 35.0), AutofillGroup( child: Column( children: [ @@ -145,18 +196,6 @@ class _LoginPageState extends State { enableSuggestions: false, ), const SizedBox(height: 12.0), - TextField( - autocorrect: false, - controller: _instanceTextEditingController, - inputFormatters: [LowerCaseTextFormatter()], - decoration: const InputDecoration( - isDense: true, - border: OutlineInputBorder(), - labelText: 'Instance', - hintText: 'lemmy.ml', - ), - enableSuggestions: false, - ), const SizedBox(height: 32.0), ElevatedButton( style: ElevatedButton.styleFrom( diff --git a/lib/account/widgets/profile_modal_body.dart b/lib/account/widgets/profile_modal_body.dart index 768bdb22e..db4c3f8f3 100644 --- a/lib/account/widgets/profile_modal_body.dart +++ b/lib/account/widgets/profile_modal_body.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -7,6 +8,7 @@ import 'package:thunder/account/models/account.dart'; import 'package:thunder/account/pages/login_page.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; +import 'package:thunder/utils/instance.dart'; class ProfileModalBody extends StatelessWidget { const ProfileModalBody({super.key}); @@ -79,22 +81,28 @@ class ProfileSelect extends StatelessWidget { ); } else { return ListTile( - leading: Icon( - Icons.person, - color: currentAccountId == snapshot.data![index].id ? Colors.amber : null, - ), + leading: snapshot.data![index].instanceIcon == null + ? const Icon( + Icons.person, + ) + : CircleAvatar( + backgroundColor: Colors.transparent, + foregroundImage: snapshot.data![index].instanceIcon == null + ? null + : CachedNetworkImageProvider(snapshot.data![index].instanceIcon!), + ), title: Text( - snapshot.data![index].username ?? 'N/A', + snapshot.data![index].account.username ?? 'N/A', style: theme.textTheme.titleMedium?.copyWith(), ), - subtitle: Text(snapshot.data![index].instance?.replaceAll('https://', '') ?? 'N/A'), - onTap: (currentAccountId == snapshot.data![index].id) + subtitle: Text(snapshot.data![index].account.instance?.replaceAll('https://', '') ?? 'N/A'), + onTap: (currentAccountId == snapshot.data![index].account.id) ? null : () { - context.read().add(SwitchAccount(accountId: snapshot.data![index].id)); + context.read().add(SwitchAccount(accountId: snapshot.data![index].account.id)); context.pop(); }, - trailing: (currentAccountId == snapshot.data![index].id) + trailing: (currentAccountId == snapshot.data![index].account.id) ? const InputChip( label: Text('Active'), visualDensity: VisualDensity.compact, @@ -105,7 +113,7 @@ class ProfileSelect extends StatelessWidget { semanticLabel: 'Remove Account', ), onPressed: () { - context.read().add(RemoveAccount(accountId: snapshot.data![index].id)); + context.read().add(RemoveAccount(accountId: snapshot.data![index].account.id)); context.pop(); }), ); @@ -120,8 +128,23 @@ class ProfileSelect extends StatelessWidget { ); } - Future> fetchAccounts() async { - List accounts = await Account.accounts(); + Future> fetchAccounts() async { + List accounts = await Future.wait((await Account.accounts()).map((account) async { + final instanceIcon = await getInstanceIcon(account.instance); + return AccountExtended(account: account, instanceIcon: instanceIcon); + }).toList()); + return accounts; } } + +/// Wrapper class around Account with support for instance icon +class AccountExtended { + final Account account; + final String? instanceIcon; + + const AccountExtended({ + required this.account, + this.instanceIcon + }); +} diff --git a/lib/community/widgets/community_header.dart b/lib/community/widgets/community_header.dart index 245c99468..89adf1f28 100644 --- a/lib/community/widgets/community_header.dart +++ b/lib/community/widgets/community_header.dart @@ -62,8 +62,7 @@ class CommunityHeader extends StatelessWidget { style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w600), ), Text( - fetchInstanceNameFromUrl(communityInfo?.communityView.community.actorId) ?? 'N/A', - style: theme.textTheme.titleSmall, + '${communityInfo?.communityView.community.name ?? 'N/A'}@${fetchInstanceNameFromUrl(communityInfo?.communityView.community.actorId) ?? 'N/A'}' ), const SizedBox(height: 8.0), Row( @@ -74,8 +73,8 @@ class CommunityHeader extends StatelessWidget { ), const SizedBox(width: 8.0), IconText( - icon: const Icon(Icons.sensors_rounded), - text: (communityInfo?.online != null) ? '${communityInfo?.online}' : '-', + icon: const Icon(Icons.calendar_month_rounded ), + text: formatNumberToK(communityInfo?.communityView.counts.usersActiveMonth ?? 0), ), ], ), diff --git a/lib/community/widgets/post_card_metadata.dart b/lib/community/widgets/post_card_metadata.dart index 6fdb66c61..f04541363 100644 --- a/lib/community/widgets/post_card_metadata.dart +++ b/lib/community/widgets/post_card_metadata.dart @@ -12,7 +12,9 @@ import 'package:thunder/utils/numbers.dart'; class PostCardMetaData extends StatelessWidget { final int score; final VoteType voteType; + final int unreadComments; final int comments; + final bool hasBeenEdited; final DateTime published; final bool saved; final bool distinguised; @@ -21,7 +23,9 @@ class PostCardMetaData extends StatelessWidget { super.key, required this.score, required this.voteType, + required this.unreadComments, required this.comments, + required this.hasBeenEdited, required this.published, required this.saved, required this.distinguised, @@ -53,7 +57,7 @@ class PostCardMetaData extends StatelessWidget { : voteType == VoteType.down ? downVoteColor : theme.textTheme.titleSmall?.color?.withOpacity(0.9), - icon: Icon( voteType == VoteType.down ? Icons.arrow_downward : Icons.arrow_upward, + icon: Icon( voteType == VoteType.up ? Icons.arrow_upward : (voteType == VoteType.down ? Icons.arrow_downward : (score < 0 ? Icons.arrow_downward : Icons.arrow_upward)), size: 18.0, color: voteType == VoteType.up ? upVoteColor @@ -67,19 +71,19 @@ class PostCardMetaData extends StatelessWidget { IconText( textScaleFactor: state.contentFontSizeScale.textScaleFactor, icon: Icon( - Icons.chat, + /*unreadComments != 0 && unreadComments != comments ? Icons.mark_unread_chat_alt_rounded :*/ Icons.chat, size: 17.0, - color: theme.textTheme.titleSmall?.color?.withOpacity(0.75), + color: /*unreadComments != 0 && unreadComments != comments ? theme.primaryColor :*/ theme.textTheme.titleSmall?.color?.withOpacity(0.75), ), - text: formatNumberToK(comments), - textColor: theme.textTheme.titleSmall?.color?.withOpacity(0.9), + text: /*unreadComments != 0 && unreadComments != comments ? '+${formatNumberToK(unreadComments)}' :*/ formatNumberToK(comments), + textColor: /*unreadComments != 0 && unreadComments != comments ? theme.primaryColor :*/ theme.textTheme.titleSmall?.color?.withOpacity(0.9), padding: 5.0, ), const SizedBox(width: 10.0), IconText( textScaleFactor: state.contentFontSizeScale.textScaleFactor, icon: Icon( - Icons.history_rounded, + hasBeenEdited ? Icons.refresh_rounded : Icons.history_rounded, size: 19.0, color: theme.textTheme.titleSmall?.color?.withOpacity(0.75), ), @@ -110,13 +114,17 @@ class PostCardMetaData extends StatelessWidget { } class PostViewMetaData extends StatelessWidget { + final int unreadComments; final int comments; + final bool hasBeenEdited; final DateTime published; final bool saved; const PostViewMetaData({ super.key, + required this.unreadComments, required this.comments, + required this.hasBeenEdited, required this.published, required this.saved, }); @@ -137,6 +145,24 @@ class PostViewMetaData extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ + /*Container( + child: unreadComments != 0 && unreadComments != comments ? Row( + children: [ + IconText( + textScaleFactor: state.contentFontSizeScale.textScaleFactor, + icon: Icon( + Icons.mark_unread_chat_alt_rounded, + size: 17.0, + color: theme.primaryColor, + ), + text: '+${formatNumberToK(unreadComments)}', + textColor: theme.primaryColor, + padding: 5.0, + ), + const SizedBox(width: 10.0), + ], + ) : null, + ),*/ IconText( textScaleFactor: state.contentFontSizeScale.textScaleFactor, icon: Icon( @@ -145,17 +171,19 @@ class PostViewMetaData extends StatelessWidget { color: theme.textTheme.titleSmall?.color?.withOpacity(0.75), ), text: formatNumberToK(comments), + textColor: theme.textTheme.titleSmall?.color?.withOpacity(0.9), padding: 5.0, ), const SizedBox(width: 10.0), IconText( textScaleFactor: state.contentFontSizeScale.textScaleFactor, icon: Icon( - Icons.history_rounded, + hasBeenEdited ? Icons.refresh_rounded : Icons.history_rounded, size: 19.0, color: theme.textTheme.titleSmall?.color?.withOpacity(0.75), ), text: formatTimeToString(dateTime: published.toIso8601String()), + textColor: theme.textTheme.titleSmall?.color?.withOpacity(0.9), ), ], ), diff --git a/lib/community/widgets/post_card_view_comfortable.dart b/lib/community/widgets/post_card_view_comfortable.dart index 412f5ad07..4e4e550ea 100644 --- a/lib/community/widgets/post_card_view_comfortable.dart +++ b/lib/community/widgets/post_card_view_comfortable.dart @@ -138,7 +138,9 @@ class PostCardViewComfortable extends StatelessWidget { score: postViewMedia.postView.counts.score, voteType: postViewMedia.postView.myVote ?? VoteType.none, comments: postViewMedia.postView.counts.comments, - published: postViewMedia.postView.post.published, + unreadComments: postViewMedia.postView.unreadComments, + hasBeenEdited: postViewMedia.postView.post.updated != null ? true : false, + published: postViewMedia.postView.post.updated != null ? postViewMedia.postView.post.updated! : postViewMedia.postView.post.published, saved: postViewMedia.postView.saved, distinguised: postViewMedia.postView.post.featuredCommunity, ) diff --git a/lib/community/widgets/post_card_view_compact.dart b/lib/community/widgets/post_card_view_compact.dart index b3f67ed04..21f6d83a8 100644 --- a/lib/community/widgets/post_card_view_compact.dart +++ b/lib/community/widgets/post_card_view_compact.dart @@ -84,7 +84,9 @@ class PostCardViewCompact extends StatelessWidget { score: postViewMedia.postView.counts.score, voteType: postViewMedia.postView.myVote ?? VoteType.none, comments: postViewMedia.postView.counts.comments, - published: postViewMedia.postView.post.published, + unreadComments: postViewMedia.postView.unreadComments, + hasBeenEdited: postViewMedia.postView.post.updated != null ? true : false, + published: postViewMedia.postView.post.updated != null ? postViewMedia.postView.post.updated! : postViewMedia.postView.post.published, saved: postViewMedia.postView.saved, distinguised: postViewMedia.postView.post.featuredCommunity, ) diff --git a/lib/post/utils/comment_action_helpers.dart b/lib/post/utils/comment_action_helpers.dart new file mode 100644 index 000000000..7dd7a0cf3 --- /dev/null +++ b/lib/post/utils/comment_action_helpers.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:share_plus/share_plus.dart'; + +import '../../core/models/comment_view_tree.dart'; + +enum CommentCardAction { copyText, shareLink } + +class ExtendedCommentCardActions { + const ExtendedCommentCardActions( + {required this.commentCardAction, + required this.icon, + required this.label}); + + final CommentCardAction commentCardAction; + final IconData icon; + final String label; +} + +const commentCardActionItems = [ + ExtendedCommentCardActions( + commentCardAction: CommentCardAction.copyText, + icon: Icons.copy_rounded, + label: 'Copy Text', + ), + ExtendedCommentCardActions( + commentCardAction: CommentCardAction.shareLink, + icon: Icons.share_rounded, + label: 'Share Link' + ), +]; + +void showCommentActionBottomModalSheet( + BuildContext context, CommentViewTree commentViewTree) { + final theme = Theme.of(context); + + showModalBottomSheet( + showDragHandle: true, + context: context, + builder: (BuildContext bottomSheetContext) { + return SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: + const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + 'Actions', + style: theme.textTheme.titleLarge!.copyWith(), + ), + ), + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: commentCardActionItems.length, + itemBuilder: (BuildContext itemBuilderContext, int index) { + return ListTile( + title: Text( + commentCardActionItems[index].label, + style: theme.textTheme.bodyMedium, + ), + leading: Icon(commentCardActionItems[index].icon), + onTap: () async { + Navigator.of(context).pop(); + + CommentCardAction commentCardAction = + commentCardActionItems[index].commentCardAction; + + switch (commentCardAction) { + case CommentCardAction.copyText: + Clipboard.setData(ClipboardData( + text: commentViewTree.comment!.comment.content)) + .then((_) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Copied to clipboard"), + behavior: SnackBarBehavior.floating)); + }); + break; + case CommentCardAction.shareLink: + Share.share(commentViewTree.comment!.comment.apId); + break; + } + }, + ); + }, + ), + const SizedBox(height: 16.0), + ], + ), + ); + }, + ); +} diff --git a/lib/post/widgets/comment_card.dart b/lib/post/widgets/comment_card.dart index db0b62ec4..166de124f 100644 --- a/lib/post/widgets/comment_card.dart +++ b/lib/post/widgets/comment_card.dart @@ -12,6 +12,8 @@ import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/models/comment_view_tree.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; +import '../utils/comment_action_helpers.dart'; + class CommentCard extends StatefulWidget { final Function(int, VoteType) onVoteAction; final Function(int, bool) onSaveAction; @@ -103,6 +105,10 @@ class _CommentCardState extends State with SingleTickerProviderStat Widget build(BuildContext context) { VoteType? myVote = widget.commentViewTree.comment?.myVote; bool? saved = widget.commentViewTree.comment?.saved; + DateTime now = DateTime.now().toUtc(); + int sinceCreated = now.difference(widget.commentViewTree.comment!.comment.published).inMinutes; + + final theme = Theme.of(context); // Checks for either the same creator id to user id, or the same username final bool isOwnComment = widget.commentViewTree.comment?.creator.id == context.read().state.account?.userId || @@ -235,6 +241,7 @@ class _CommentCardState extends State with SingleTickerProviderStat children: [ GestureDetector( behavior: HitTestBehavior.translucent, + onLongPress: () => showCommentActionBottomModalSheet(context, widget.commentViewTree), onTap: () { widget.onCollapseCommentChange(widget.commentViewTree.comment!.comment.id, !isHidden); setState(() => isHidden = !isHidden); @@ -243,7 +250,13 @@ class _CommentCardState extends State with SingleTickerProviderStat crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ - CommentHeader(commentViewTree: widget.commentViewTree, isOwnComment: isOwnComment, isHidden: isHidden), + CommentHeader( + commentViewTree: widget.commentViewTree, + useDisplayNames: state.useDisplayNames, + sinceCreated: sinceCreated, + isOwnComment: isOwnComment, + isHidden: isHidden, + ), AnimatedSwitcher( duration: const Duration(milliseconds: 130), switchInCurve: Curves.easeInOut, diff --git a/lib/post/widgets/comment_header.dart b/lib/post/widgets/comment_header.dart index dea3de93a..13046cbf9 100644 --- a/lib/post/widgets/comment_header.dart +++ b/lib/post/widgets/comment_header.dart @@ -4,18 +4,26 @@ import 'package:lemmy_api_client/v3.dart'; import 'package:thunder/core/enums/font_scale.dart'; import 'package:thunder/core/models/comment_view_tree.dart'; +import 'package:thunder/account/bloc/account_bloc.dart' as account_bloc; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/utils/date_time.dart'; import 'package:thunder/utils/numbers.dart'; +import 'package:thunder/user/pages/user_page.dart'; + +import '../../core/auth/bloc/auth_bloc.dart'; class CommentHeader extends StatelessWidget { final CommentViewTree commentViewTree; + final bool useDisplayNames; final bool isOwnComment; final bool isHidden; + final int sinceCreated; const CommentHeader({ super.key, required this.commentViewTree, + required this.useDisplayNames, + required this.sinceCreated, this.isOwnComment = false, this.isHidden = false, }); @@ -29,6 +37,7 @@ class CommentHeader extends StatelessWidget { VoteType? myVote = commentViewTree.comment?.myVote; bool? saved = commentViewTree.comment?.saved; + bool? hasBeenEdited = commentViewTree.comment!.comment.updated != null ? true : false; //int score = commentViewTree.commentViewTree.comment?.counts.score ?? 0; maybe make combined scores an option? int upvotes = commentViewTree.comment?.counts.upvotes ?? 0; int downvotes = commentViewTree.comment?.counts.downvotes ?? 0; @@ -40,12 +49,33 @@ class CommentHeader extends StatelessWidget { Expanded( child: Row( children: [ - Text( - commentViewTree.comment!.creator.name, - textScaleFactor: state.contentFontSizeScale.textScaleFactor, - style: theme.textTheme.bodyMedium?.copyWith( - color: fetchUsernameColor(context, isOwnComment) ?? theme.colorScheme.onBackground, - fontWeight: FontWeight.w500, + GestureDetector( + onTap: () { + account_bloc.AccountBloc accountBloc = + context.read(); + AuthBloc authBloc = context.read(); + ThunderBloc thunderBloc = context.read(); + + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: accountBloc), + BlocProvider.value(value: authBloc), + BlocProvider.value(value: thunderBloc), + ], + child: UserPage(userId: commentViewTree.comment!.creator.id), + ), + ), + ); + }, + child: Text( + commentViewTree.comment!.creator.displayName != null && useDisplayNames ? commentViewTree.comment!.creator.displayName! : commentViewTree.comment!.creator.name, + textScaleFactor: state.contentFontSizeScale.textScaleFactor, + style: theme.textTheme.bodyMedium?.copyWith( + color: fetchUsernameColor(context, isOwnComment) ?? theme.colorScheme.onBackground, + fontWeight: FontWeight.w500, + ), ), ), const SizedBox(width: 8.0), @@ -102,12 +132,35 @@ class CommentHeader extends StatelessWidget { color: saved == true ? Colors.purple : null, size: saved == true ? 18.0 : 0, ), - const SizedBox(width: 8.0), - Text( - formatTimeToString(dateTime: commentViewTree.comment!.comment.published.toIso8601String()), - textScaleFactor: state.contentFontSizeScale.textScaleFactor, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onBackground, + SizedBox( + width: hasBeenEdited ? 32.0 : 8, + child: Icon( + hasBeenEdited ? Icons.create_rounded : null, + color: theme.colorScheme.onBackground.withOpacity(0.75), + size: 16.0, + ), + ), + Container( + decoration: sinceCreated < 15 ? BoxDecoration( + color: theme.primaryColorLight, + borderRadius: const BorderRadius.all(Radius.elliptical(5, 5)) + ) : null, + child: sinceCreated < 15 ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 5.0), + child: Text( + 'New!', + textScaleFactor: state.contentFontSizeScale.textScaleFactor, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.background, + ) + ), + ) : Text( + formatTimeToString(dateTime: hasBeenEdited ? commentViewTree.comment!.comment.updated!.toIso8601String() : commentViewTree.comment!.comment.published.toIso8601String() ), + textScaleFactor: state.contentFontSizeScale.textScaleFactor, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onBackground, + ), ), ), ], diff --git a/lib/post/widgets/comment_view.dart b/lib/post/widgets/comment_view.dart index 8485d8525..aa67946a6 100644 --- a/lib/post/widgets/comment_view.dart +++ b/lib/post/widgets/comment_view.dart @@ -51,7 +51,7 @@ class _CommentSubviewState extends State { itemCount: getCommentsListLength(), itemBuilder: (context, index) { if (widget.postViewMedia != null && index == 0) { - return PostSubview(postViewMedia: widget.postViewMedia!); + return PostSubview(useDisplayNames: state.useDisplayNames, postViewMedia: widget.postViewMedia!); } else if (widget.hasReachedCommentEnd == false && widget.comments.isEmpty) { return Column( children: [ diff --git a/lib/post/widgets/post_view.dart b/lib/post/widgets/post_view.dart index 5578a7778..46faf33af 100644 --- a/lib/post/widgets/post_view.dart +++ b/lib/post/widgets/post_view.dart @@ -18,13 +18,13 @@ import 'package:thunder/post/bloc/post_bloc.dart'; import 'package:thunder/shared/media_view.dart'; import 'package:thunder/user/pages/user_page.dart'; import 'package:thunder/utils/numbers.dart'; - import '../../utils/date_time.dart'; class PostSubview extends StatelessWidget { final PostViewMedia postViewMedia; + final bool useDisplayNames; - const PostSubview({super.key, required this.postViewMedia}); + const PostSubview({super.key, required this.useDisplayNames, required this.postViewMedia}); @override Widget build(BuildContext context) { @@ -92,7 +92,7 @@ class PostSubview extends StatelessWidget { ); }, child: Text( - postView.creator.name, + postView.creator.displayName != null && useDisplayNames ? postView.creator.displayName! : postView.creator.name, textScaleFactor: thunderState.contentFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( color: theme.textTheme.bodyMedium?.color?.withOpacity(0.75), @@ -137,7 +137,9 @@ class PostSubview extends StatelessWidget { Padding( padding: const EdgeInsets.only(right: 0.0), child: PostViewMetaData( - comments: postView.counts.comments, + comments: postViewMedia.postView.counts.comments, + unreadComments: postViewMedia.postView.unreadComments, + hasBeenEdited: postViewMedia.postView.post.updated != null ? true : false, published: post.published, saved: postView.saved, ), diff --git a/lib/settings/pages/general_settings_page.dart b/lib/settings/pages/general_settings_page.dart index daa103687..1146f59a4 100644 --- a/lib/settings/pages/general_settings_page.dart +++ b/lib/settings/pages/general_settings_page.dart @@ -42,6 +42,7 @@ class _GeneralSettingsPageState extends State { bool hideNsfwPreviews = true; bool bottomNavBarSwipeGestures = true; bool bottomNavBarDoubleTapGestures = false; + bool useDisplayNames = true; bool markPostReadOnMediaView = false; // Link Settings @@ -128,6 +129,10 @@ class _GeneralSettingsPageState extends State { await prefs.setString('setting_instance_default_instance', value); setState(() => defaultInstance = value); break; + case 'setting_use_display_names_for_users': + await prefs.setBool('setting_use_display_names_for_users', value); + setState(() => useDisplayNames = value); + break; case 'setting_post_default_comment_sort_type': await prefs.setString('setting_post_default_comment_sort_type', value); setState(() => defaultCommentSortType = CommentSortType.values.byName(value ?? DEFAULT_COMMENT_SORT_TYPE.name)); @@ -190,6 +195,7 @@ class _GeneralSettingsPageState extends State { hideNsfwPreviews = prefs.getBool('setting_general_hide_nsfw_previews') ?? true; bottomNavBarSwipeGestures = prefs.getBool('setting_general_enable_swipe_gestures') ?? true; bottomNavBarDoubleTapGestures = prefs.getBool('setting_general_enable_doubletap_gestures') ?? false; + useDisplayNames = prefs.getBool('setting_use_display_names_for_users') ?? true; defaultCommentSortType = CommentSortType.values.byName(prefs.getString("setting_post_default_comment_sort_type") ?? DEFAULT_COMMENT_SORT_TYPE.name); tabletMode = prefs.getBool('setting_post_tablet_mode') ?? false; markPostReadOnMediaView = prefs.getBool('setting_general_mark_post_read_on_media_view') ?? false; @@ -374,6 +380,13 @@ class _GeneralSettingsPageState extends State { iconDisabled: Icons.visibility, onToggle: (bool value) => setPreferences("setting_general_mark_post_read_on_media_view", value), ), + ToggleOption( + description: 'Use display names for users', + value: useDisplayNames, + iconEnabled: Icons.person_rounded, + iconDisabled: Icons.person_off_rounded, + onToggle: (bool value) => setPreferences('setting_use_display_names_for_users', value), + ), ListOption( description: 'Default Comment Sort Type', value: ListPickerItem(label: defaultCommentSortType.value, icon: Icons.local_fire_department_rounded, payload: defaultCommentSortType), @@ -386,7 +399,7 @@ class _GeneralSettingsPageState extends State { setPreferences('setting_post_default_comment_sort_type', value.payload.name); }, ), - ) + ), ], ), ), diff --git a/lib/thunder/bloc/thunder_bloc.dart b/lib/thunder/bloc/thunder_bloc.dart index 59325d434..f0d4e2a0a 100644 --- a/lib/thunder/bloc/thunder_bloc.dart +++ b/lib/thunder/bloc/thunder_bloc.dart @@ -84,6 +84,7 @@ class ThunderBloc extends Bloc { bool showEdgeToEdgeImages = prefs.getBool('setting_general_show_edge_to_edge_images') ?? false; bool showTextContent = prefs.getBool('setting_general_show_text_content') ?? false; bool hideNsfwPreviews = prefs.getBool('setting_general_hide_nsfw_previews') ?? true; + bool useDisplayNames = prefs.getBool('setting_use_display_names_for_users') ?? true; bool bottomNavBarSwipeGestures = prefs.getBool('setting_general_enable_swipe_gestures') ?? true; bool bottomNavBarDoubleTapGestures = prefs.getBool('setting_general_enable_doubletap_gestures') ?? false; bool tabletMode = prefs.getBool('setting_post_tablet_mode') ?? false; @@ -138,6 +139,7 @@ class ThunderBloc extends Bloc { tabletMode: tabletMode, showTextContent: showTextContent, hideNsfwPreviews: hideNsfwPreviews, + useDisplayNames: useDisplayNames, bottomNavBarSwipeGestures: bottomNavBarSwipeGestures, bottomNavBarDoubleTapGestures: bottomNavBarDoubleTapGestures, markPostReadOnMediaView: markPostReadOnMediaView, diff --git a/lib/thunder/bloc/thunder_state.dart b/lib/thunder/bloc/thunder_state.dart index c880a1ff4..b227f4983 100644 --- a/lib/thunder/bloc/thunder_state.dart +++ b/lib/thunder/bloc/thunder_state.dart @@ -29,6 +29,7 @@ class ThunderState extends Equatable { this.showEdgeToEdgeImages = false, this.showTextContent = false, this.hideNsfwPreviews = true, + this.useDisplayNames = true, this.bottomNavBarSwipeGestures = true, this.bottomNavBarDoubleTapGestures = false, this.tabletMode = false, @@ -91,6 +92,7 @@ class ThunderState extends Equatable { final bool showEdgeToEdgeImages; final bool showTextContent; final bool hideNsfwPreviews; + final bool useDisplayNames; final bool bottomNavBarSwipeGestures; final bool bottomNavBarDoubleTapGestures; final bool tabletMode; @@ -152,6 +154,7 @@ class ThunderState extends Equatable { bool? bottomNavBarSwipeGestures, bool? bottomNavBarDoubleTapGestures, bool? markPostReadOnMediaView, + bool? useDisplayNames, // Link Settings bool? openInExternalBrowser, // Notification Settings @@ -199,6 +202,7 @@ class ThunderState extends Equatable { showEdgeToEdgeImages: showEdgeToEdgeImages ?? this.showEdgeToEdgeImages, showTextContent: showTextContent ?? this.showTextContent, hideNsfwPreviews: hideNsfwPreviews ?? this.hideNsfwPreviews, + useDisplayNames: useDisplayNames ?? this.useDisplayNames, tabletMode: tabletMode ?? this.tabletMode, bottomNavBarSwipeGestures: bottomNavBarSwipeGestures ?? this.bottomNavBarSwipeGestures, bottomNavBarDoubleTapGestures: bottomNavBarDoubleTapGestures ?? this.bottomNavBarDoubleTapGestures, @@ -252,6 +256,7 @@ class ThunderState extends Equatable { showEdgeToEdgeImages, showTextContent, hideNsfwPreviews, + useDisplayNames, tabletMode, bottomNavBarSwipeGestures, bottomNavBarDoubleTapGestures, diff --git a/lib/user/widgets/user_header.dart b/lib/user/widgets/user_header.dart index 8f948c185..05d2cd210 100644 --- a/lib/user/widgets/user_header.dart +++ b/lib/user/widgets/user_header.dart @@ -58,28 +58,31 @@ class UserHeader extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - userInfo?.person.name ?? '-', + userInfo?.person.displayName != null ? userInfo?.person.displayName ?? '-' : userInfo?.person.name ?? '-', style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w600), softWrap: false, maxLines: 1, overflow: TextOverflow.fade, ), Text( - fetchInstanceNameFromUrl(userInfo?.person.actorId) ?? '-', - style: theme.textTheme.titleSmall, + '${userInfo?.person.name ?? '-'}@${fetchInstanceNameFromUrl(userInfo?.person.actorId) ?? '-'}', ), const SizedBox(height: 8.0), Row( children: [ - // IconText( - // icon: const Icon(Icons.people_rounded), - // text: formatNumberToK(userInfo?.communityView.counts.subscribers ?? 0), - // ), - // const SizedBox(width: 8.0), - // IconText( - // icon: const Icon(Icons.sensors_rounded), - // text: (userInfo?.online != null) ? '${userInfo?.online}' : '-', - // ), + IconText( + icon: const Icon(Icons.wysiwyg_rounded, + size: 18.0, + ), + text: formatNumberToK(userInfo?.counts.postCount ?? 0), + ), + const SizedBox(width: 8.0), + IconText( + icon: const Icon(Icons.chat_rounded, + size: 18.0, + ), + text: formatNumberToK(userInfo?.counts.commentCount ?? 0), + ), ], ), ], diff --git a/lib/utils/instance.dart b/lib/utils/instance.dart index aa237d809..91be99bd0 100644 --- a/lib/utils/instance.dart +++ b/lib/utils/instance.dart @@ -1,3 +1,5 @@ +import 'package:lemmy_api_client/v3.dart'; + String? fetchInstanceNameFromUrl(String? url) { if (url == null) { return null; @@ -22,3 +24,17 @@ String? checkLemmyInstanceUrl(String text) { if (text.contains('/c/')) return generateCommunityInstanceUrl(text); return null; } + +Future getInstanceIcon(String? url) async { + if (url?.isEmpty ?? true) { + return null; + } + + try { + final site = await LemmyApiV3(url!).run(const GetSite()); + return site.siteView?.site.icon; + } catch (e) { + // Bad instances will throw an exception, so no icon + return null; + } +}