Skip to content

Commit

Permalink
Customizable Post Card Metadata (#1077)
Browse files Browse the repository at this point in the history
* initial implementation for customizable post card metadata

* added datetime and url widgets

* added setting to customize post metadata

* added ability to disable dragging post metadata when view is not the correct one

* added post card metadata customization

* adjusted styling and previews for post card metadata

* fixed rendering issue with community icons

* added unread comment count indicators

* fixed issue with modifying unmodifiable list

* added localization strings

* fixed some visual bugs
  • Loading branch information
hjiangsu authored Feb 3, 2024
1 parent 1fedabe commit 799b731
Show file tree
Hide file tree
Showing 10 changed files with 805 additions and 16 deletions.
331 changes: 330 additions & 1 deletion lib/community/widgets/post_card_metadata.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import 'package:flutter/material.dart';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

import 'package:thunder/core/auth/bloc/auth_bloc.dart';
import 'package:thunder/core/enums/full_name_separator.dart';
import 'package:thunder/core/enums/view_mode.dart';
import 'package:thunder/feed/feed.dart';
import 'package:thunder/post/enums/post_card_metadata_item.dart';
import 'package:thunder/shared/avatars/community_avatar.dart';
import 'package:thunder/shared/icon_text.dart';
import 'package:thunder/shared/text/scalable_text.dart';
Expand All @@ -15,6 +18,11 @@ import 'package:thunder/utils/instance.dart';
import 'package:thunder/user/utils/navigate_user.dart';
import 'package:thunder/utils/numbers.dart';

const Color upVoteColor = Colors.orange;
const Color downVoteColor = Colors.blue;
Color readColor = Colors.grey.shade700;

@Deprecated("Use [PostViewMetaData] instead")
class PostCardMetaData extends StatelessWidget {
final int score;
final int voteType;
Expand Down Expand Up @@ -113,6 +121,327 @@ class PostCardMetaData extends StatelessWidget {
}
}

/// Contains metadata related to a given post. This is generally displayed as part of the post card.
///
/// This information is customizable, and can be changed by the user in the settings.
/// The order in which the items are displayed depends on the order in the [postCardMetadataItems] list
class PostCardMetadata extends StatelessWidget {
/// The type of view the post card is in. This is used to determine the appropriate setting to read from.
final ViewMode postCardViewType;

/// The score of the post. If null, no score will be displayed.
final int? score;

/// The number of upvotes on the post. If null, no upvote count will be displayed.
final int? upvoteCount;

/// The number of downvotes on the post. If null, no downvote count will be displayed.
final int? downvoteCount;

/// The vote for the post. This should be either 0, 1 or -1. Defaults to 0 if not specified.
/// When specified, this will change the color of the upvote/downvote/score icons.
final int? voteType;

/// The number of comments on the post. If null, no comment count will be displayed.
final int? commentCount;

/// The number of unread comments on the post. If null, no unread comment count will be displayed.
final int? unreadCommentCount;

/// The date/time the post was created or updated. This string should conform to ISO-8601 format.
final String? dateTime;

/// Whether or not the post has been edited. This determines the icon for the [dateTime] field.
final bool? hasBeenEdited;

/// Whether or not the post has been read. This is passed down to the individual [PostCardMetadataItem] widgets to determine the color.
final bool? hasBeenRead;

/// The URL to display in the metadata. If null, no URL will be displayed.
final String? url;

const PostCardMetadata({
super.key,
required this.postCardViewType,
this.score,
this.upvoteCount,
this.downvoteCount,
this.voteType = 0,
this.commentCount,
this.unreadCommentCount,
this.dateTime,
this.hasBeenEdited = false,
this.hasBeenRead = false,
this.url,
});

@override
Widget build(BuildContext context) {
final state = context.watch<AuthBloc>().state;
final showScores = state.getSiteResponse?.myUser?.localUserView.localUser.showScores ?? true;

List<PostCardMetadataItem> postCardMetadataItems = switch (postCardViewType) {
ViewMode.compact => context.read<ThunderBloc>().state.compactPostCardMetadataItems,
ViewMode.comfortable => context.read<ThunderBloc>().state.cardPostCardMetadataItems,
};

return Wrap(
spacing: 8.0,
crossAxisAlignment: WrapCrossAlignment.center,
children: postCardMetadataItems.map(
(PostCardMetadataItem postCardMetadataItem) {
return switch (postCardMetadataItem) {
PostCardMetadataItem.score => showScores ? ScorePostCardMetaData(score: score, voteType: voteType, hasBeenRead: hasBeenRead ?? false) : Container(),
PostCardMetadataItem.upvote => showScores ? UpvotePostCardMetaData(upvotes: upvoteCount, isUpvoted: voteType == 1, hasBeenRead: hasBeenRead ?? false) : Container(),
PostCardMetadataItem.downvote => showScores ? DownvotePostCardMetaData(downvotes: downvoteCount, isDownvoted: voteType == -1, hasBeenRead: hasBeenRead ?? false) : Container(),
PostCardMetadataItem.commentCount => CommentCountPostCardMetaData(commentCount: commentCount, unreadCommentCount: unreadCommentCount ?? 0, hasBeenRead: hasBeenRead ?? false),
PostCardMetadataItem.dateTime => DateTimePostCardMetaData(dateTime: dateTime!, hasBeenRead: hasBeenRead ?? false, hasBeenEdited: hasBeenEdited ?? false),
PostCardMetadataItem.url => UrlPostCardMetaData(url: url, hasBeenRead: hasBeenRead ?? false),
};
},
).toList(),
);
}
}

/// Contains metadata related to the score of a given post. This is used in the [PostCardMetadata] widget.
class ScorePostCardMetaData extends StatelessWidget {
/// The score of the post. Defaults to 0 if not specified.
final int? score;

/// The vote for the post. This should be either 0, 1 or -1. Defaults to 0 if not specified.
final int? voteType;

/// Whether or not the post has been read. This is used to determine the color.
final bool hasBeenRead;

const ScorePostCardMetaData({
super.key,
this.score = 0,
this.voteType = 0,
this.hasBeenRead = false,
});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
final state = context.read<ThunderBloc>().state;

final color = switch (voteType) {
1 => upVoteColor,
-1 => downVoteColor,
_ => hasBeenRead ? readColor : null,
};

return Wrap(
spacing: 2.0,
crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center,
children: [
Icon(Icons.arrow_upward, size: 17.0, color: color),
ScalableText(
formatNumberToK(score ?? 0),
semanticsLabel: l10n.xScore(formatNumberToK(score ?? 0)),
fontScale: state.metadataFontSizeScale,
style: theme.textTheme.bodyMedium?.copyWith(color: color),
),
Icon(Icons.arrow_downward, size: 17.0, color: color),
],
);
}
}

/// Contains metadata related to the upvotes of a given post. This is used in the [PostCardMetadata] widget.
class UpvotePostCardMetaData extends StatelessWidget {
/// The number of upvotes on the post. Defaults to 0 if not specified.
final int? upvotes;

/// Whether or not the post has been upvoted. Defaults to false if not specified.
final bool? isUpvoted;

/// Whether or not the post has been read. This is used to determine the color.
final bool hasBeenRead;

const UpvotePostCardMetaData({
super.key,
this.upvotes = 0,
this.isUpvoted = false,
this.hasBeenRead = false,
});

@override
Widget build(BuildContext context) {
final state = context.read<ThunderBloc>().state;

final color = switch (isUpvoted) {
true => upVoteColor,
_ => hasBeenRead ? readColor : null,
};

return IconText(
fontScale: state.metadataFontSizeScale,
text: formatNumberToK(upvotes ?? 0),
textColor: color,
padding: 2.0,
icon: Icon(Icons.arrow_upward, size: 17.0, color: color),
);
}
}

/// Contains metadata related to the downvotes of a given post. This is used in the [PostCardMetadata] widget.
class DownvotePostCardMetaData extends StatelessWidget {
/// The number of downvotes on the post. Defaults to 0 if not specified.
final int? downvotes;

/// Whether or not the post has been downvoted. Defaults to false if not specified.
final bool? isDownvoted;

/// Whether or not the post has been read. This is used to determine the color.
final bool hasBeenRead;

const DownvotePostCardMetaData({
super.key,
this.downvotes = 0,
this.isDownvoted = false,
this.hasBeenRead = false,
});

@override
Widget build(BuildContext context) {
final state = context.read<ThunderBloc>().state;

final color = switch (isDownvoted) {
true => downVoteColor,
_ => hasBeenRead ? readColor : null,
};

return IconText(
fontScale: state.metadataFontSizeScale,
text: formatNumberToK(downvotes ?? 0),
textColor: color,
padding: 2.0,
icon: Icon(Icons.arrow_downward, size: 17.0, color: color),
);
}
}

/// Contains metadata related to the number of comments for a given post. This is used in the [PostCardMetadata] widget.
class CommentCountPostCardMetaData extends StatelessWidget {
/// The number of comments on the post. Defaults to 0 if not specified.
final int? commentCount;

/// The number of unread comments on the post. Defaults to 0 if not specified.
final int? unreadCommentCount;

/// Whether or not the post has been read. This is used to determine the color.
final bool hasBeenRead;

const CommentCountPostCardMetaData({
super.key,
this.commentCount = 0,
this.unreadCommentCount = 0,
this.hasBeenRead = false,
});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final state = context.read<ThunderBloc>().state;

final color = switch (hasBeenRead) {
true => (unreadCommentCount != 0 && unreadCommentCount != commentCount) ? theme.primaryColor : readColor,
_ => (unreadCommentCount != 0 && unreadCommentCount != commentCount) ? theme.primaryColor : null,
};

return IconText(
fontScale: state.metadataFontSizeScale,
text: (unreadCommentCount != 0 && unreadCommentCount != commentCount) ? '+${formatNumberToK(unreadCommentCount ?? 0)}' : formatNumberToK(commentCount ?? 0),
textColor: color,
padding: 5.0,
icon: Icon(unreadCommentCount != 0 && unreadCommentCount != commentCount ? Icons.mark_unread_chat_alt_rounded : Icons.chat, size: 17.0, color: color),
);
}
}

/// Contains metadata related to the number of comments for a given post. This is used in the [PostCardMetadata] widget.
class DateTimePostCardMetaData extends StatelessWidget {
/// The date/time the post was created or updated. This string should conform to ISO-8601 format.
final String dateTime;

/// Whether or not the post has been read. This is used to determine the color.
final bool hasBeenRead;

/// Whether or not the post has been edited. This determines the icon for the [dateTime] field.
final bool hasBeenEdited;

const DateTimePostCardMetaData({
super.key,
required this.dateTime,
this.hasBeenRead = false,
this.hasBeenEdited = false,
});

@override
Widget build(BuildContext context) {
final state = context.read<ThunderBloc>().state;

final color = switch (hasBeenRead) {
true => readColor,
_ => null,
};

return IconText(
fontScale: state.metadataFontSizeScale,
text: formatTimeToString(dateTime: dateTime),
textColor: color,
padding: 2.0,
icon: Icon(hasBeenEdited ? Icons.edit : Icons.history_rounded, size: 17.0, color: color),
);
}
}

/// Contains metadata related to the url/external link for a given post. This is used in the [PostCardMetadata] widget.
class UrlPostCardMetaData extends StatelessWidget {
/// The URL to display in the metadata. If null, no URL will be displayed.
final String? url;

/// Whether or not the post has been read. This is used to determine the color.
final bool hasBeenRead;

const UrlPostCardMetaData({
super.key,
this.url,
this.hasBeenRead = false,
});

@override
Widget build(BuildContext context) {
final state = context.read<ThunderBloc>().state;

final color = switch (hasBeenRead) {
true => readColor,
_ => null,
};

if (url == null || url!.isEmpty == true) {
return Container();
}

return Tooltip(
message: url,
preferBelow: false,
child: IconText(
fontScale: state.metadataFontSizeScale,
text: Uri.parse(url ?? '').host.replaceFirst('www.', ''),
textColor: color,
padding: 3.0,
icon: Icon(Icons.public, size: 17.0, color: color),
),
);
}
}

class PostViewMetaData extends StatelessWidget {
final int unreadComments;
final int comments;
Expand Down
18 changes: 11 additions & 7 deletions lib/community/widgets/post_card_view_comfortable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:thunder/community/utils/post_card_action_helpers.dart';
import 'package:thunder/community/widgets/post_card_actions.dart';
import 'package:thunder/community/widgets/post_card_metadata.dart';
import 'package:thunder/core/enums/font_scale.dart';
import 'package:thunder/core/enums/view_mode.dart';
import 'package:thunder/core/models/post_view_media.dart';
import 'package:thunder/core/theme/bloc/theme_bloc.dart';
import 'package:thunder/shared/media_view.dart';
Expand Down Expand Up @@ -273,16 +274,19 @@ class PostCardViewComfortable extends StatelessWidget {
showCommunitySubscription: showCommunitySubscription,
),
const SizedBox(height: 8.0),
PostCardMetaData(
readColor: readColor,
hostURL: postViewMedia.media.firstOrNull != null ? postViewMedia.media.first.originalUrl : null,
PostCardMetadata(
postCardViewType: ViewMode.comfortable,
score: postViewMedia.postView.counts.score,
upvoteCount: postViewMedia.postView.counts.upvotes,
downvoteCount: postViewMedia.postView.counts.downvotes,
voteType: postViewMedia.postView.myVote ?? 0,
comments: postViewMedia.postView.counts.comments,
unreadComments: postViewMedia.postView.unreadComments,
commentCount: postViewMedia.postView.counts.comments,
unreadCommentCount: postViewMedia.postView.unreadComments,
dateTime: postViewMedia.postView.post.updated != null ? postViewMedia.postView.post.updated?.toIso8601String() : postViewMedia.postView.post.published.toIso8601String(),
hasBeenEdited: postViewMedia.postView.post.updated != null ? true : false,
published: postViewMedia.postView.post.updated != null ? postViewMedia.postView.post.updated! : postViewMedia.postView.post.published,
)
url: postViewMedia.media.firstOrNull != null ? postViewMedia.media.first.originalUrl : null,
hasBeenRead: indicateRead && postViewMedia.postView.read,
),
],
),
),
Expand Down
Loading

0 comments on commit 799b731

Please sign in to comment.