diff --git a/app/src/main/java/com/jerboa/MainActivity.kt b/app/src/main/java/com/jerboa/MainActivity.kt index 563506fdc..6036e6415 100644 --- a/app/src/main/java/com/jerboa/MainActivity.kt +++ b/app/src/main/java/com/jerboa/MainActivity.kt @@ -13,6 +13,7 @@ import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.material3.DrawerValue import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.DisposableEffect @@ -89,6 +90,7 @@ class MainActivity : AppCompatActivity() { factoryProducer = { AccountSettingsViewModelFactory.Factory }, ) + @OptIn(ExperimentalLayoutApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { diff --git a/app/src/main/java/com/jerboa/Utils.kt b/app/src/main/java/com/jerboa/Utils.kt index 22bbc37a1..c7f982f31 100644 --- a/app/src/main/java/com/jerboa/Utils.kt +++ b/app/src/main/java/com/jerboa/Utils.kt @@ -850,7 +850,7 @@ fun getCommentIdDepthFromPath( fun nsfwCheck(postView: PostView): Boolean = nsfwCheck(postView.post, postView.community) -fun nsfwCheck( +private fun nsfwCheck( post: Post, community: Community, ): Boolean = post.nsfw || community.nsfw diff --git a/app/src/main/java/com/jerboa/datatypes/SampleData.kt b/app/src/main/java/com/jerboa/datatypes/SampleData.kt index 27bd335b5..761ba1b5f 100644 --- a/app/src/main/java/com/jerboa/datatypes/SampleData.kt +++ b/app/src/main/java/com/jerboa/datatypes/SampleData.kt @@ -1,5 +1,6 @@ package com.jerboa.datatypes +import com.jerboa.feat.InstantScores import it.vercruysse.lemmyapi.datatypes.Comment import it.vercruysse.lemmyapi.datatypes.CommentAggregates import it.vercruysse.lemmyapi.datatypes.CommentReply @@ -281,9 +282,9 @@ val samplePostAggregates = PostAggregates( post_id = 135129, comments = 4, - score = 8, + score = 5, upvotes = 8, - downvotes = 0, + downvotes = 3, published = "2022-01-02T04:02:44.592929Z", newest_comment_time = "2022-01-02T04:02:44.592929Z", ) @@ -839,3 +840,11 @@ val samplePrivateMessageReportView = creator = samplePerson2, resolver = samplePerson3, ) + +val sampleInstantScores = + InstantScores( + myVote = samplePostView.my_vote, + score = samplePostView.counts.score, + upvotes = samplePostView.counts.upvotes, + downvotes = samplePostView.counts.downvotes, + ) diff --git a/app/src/main/java/com/jerboa/feat/Voting.kt b/app/src/main/java/com/jerboa/feat/Voting.kt index 15f4eebcd..0a8bd1da4 100644 --- a/app/src/main/java/com/jerboa/feat/Voting.kt +++ b/app/src/main/java/com/jerboa/feat/Voting.kt @@ -54,7 +54,7 @@ fun upvotePercent( downvotes: Long, ): Float = (upvotes.toFloat() / (upvotes + downvotes)) -fun formatPercent(pct: Float): String = "%.0f%%".format(pct * 100F) +fun formatPercent(pct: Float): String = "%.0f".format(pct * 100F) private fun scoreOrPctStr( score: Long, diff --git a/app/src/main/java/com/jerboa/ui/components/comment/CommentNode.kt b/app/src/main/java/com/jerboa/ui/components/comment/CommentNode.kt index 78d205a85..983d9221c 100644 --- a/app/src/main/java/com/jerboa/ui/components/comment/CommentNode.kt +++ b/app/src/main/java/com/jerboa/ui/components/comment/CommentNode.kt @@ -73,7 +73,9 @@ import com.jerboa.ui.components.common.CommentOrPostNodeHeader import com.jerboa.ui.components.common.MarkdownHelper import com.jerboa.ui.components.common.MyMarkdownText import com.jerboa.ui.components.common.SwipeToAction +import com.jerboa.ui.components.common.UpvotePercentage import com.jerboa.ui.components.common.VoteGeneric +import com.jerboa.ui.components.common.VoteScore import com.jerboa.ui.components.common.rememberSwipeActionState import com.jerboa.ui.components.community.CommunityLink import com.jerboa.ui.theme.BORDER_WIDTH @@ -97,18 +99,14 @@ import it.vercruysse.lemmyapi.datatypes.PostId fun CommentNodeHeader( commentView: CommentView, onPersonClick: (personId: PersonId) -> Unit, - instantScores: InstantScores, collapsedCommentsCount: Long, isExpanded: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { CommentOrPostNodeHeader( creator = commentView.creator, - instantScores = instantScores, - voteDisplayMode = voteDisplayMode, published = commentView.comment.published, updated = commentView.comment.updated, deleted = commentView.comment.deleted, @@ -121,6 +119,7 @@ fun CommentNodeHeader( onLongCLick = onLongClick, showAvatar = showAvatar, isDistinguished = commentView.comment.distinguished, + isNsfw = false, ) } @@ -129,13 +128,6 @@ fun CommentNodeHeader( fun CommentNodeHeaderPreview() { CommentNodeHeader( commentView = sampleCommentView, - instantScores = InstantScores( - score = 23, - upvotes = 21, - downvotes = 2, - myVote = 26, - ), - voteDisplayMode = LocalUserVoteDisplayMode.default(), onPersonClick = {}, onClick = {}, onLongClick = {}, @@ -329,8 +321,6 @@ fun LazyListScope.commentNodeItem( CommentNodeHeader( commentView = commentView, onPersonClick = onPersonClick, - instantScores = instantScores, - voteDisplayMode = voteDisplayMode, onClick = { onHeaderClick(commentView) }, @@ -372,6 +362,7 @@ fun LazyListScope.commentNodeItem( admins = admins, moderators = moderators, instantScores = instantScores, + voteDisplayMode = voteDisplayMode, onUpvoteClick = { instantScores = instantScores.update(VoteType.Upvote) @@ -746,6 +737,7 @@ fun CommentFooterLine( moderators: List?, enableDownVotes: Boolean, instantScores: InstantScores, + voteDisplayMode: LocalUserVoteDisplayMode, onUpvoteClick: () -> Unit, onDownvoteClick: () -> Unit, onReplyClick: (commentView: CommentView) -> Unit, @@ -822,17 +814,30 @@ fun CommentFooterLine( ).padding(top = MEDIUM_PADDING), ) { Row( - horizontalArrangement = Arrangement.spacedBy(XXL_PADDING), + horizontalArrangement = Arrangement.spacedBy(LARGE_PADDING), ) { + VoteScore( + instantScores = instantScores, + onVoteClick = onUpvoteClick, + voteDisplayMode = voteDisplayMode, + account = account, + ) + UpvotePercentage( + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, + account = account, + ) VoteGeneric( - myVote = instantScores.myVote, + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, type = VoteType.Upvote, onVoteClick = onUpvoteClick, account = account, ) if (enableDownVotes) { VoteGeneric( - myVote = instantScores.myVote, + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, type = VoteType.Downvote, onVoteClick = onDownvoteClick, account = account, diff --git a/app/src/main/java/com/jerboa/ui/components/comment/mentionnode/CommentMentionNode.kt b/app/src/main/java/com/jerboa/ui/components/comment/mentionnode/CommentMentionNode.kt index 205e20d98..3f89072ff 100644 --- a/app/src/main/java/com/jerboa/ui/components/comment/mentionnode/CommentMentionNode.kt +++ b/app/src/main/java/com/jerboa/ui/components/comment/mentionnode/CommentMentionNode.kt @@ -27,8 +27,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.jerboa.R +import com.jerboa.datatypes.sampleInstantScores import com.jerboa.datatypes.samplePersonMentionView import com.jerboa.db.entity.Account +import com.jerboa.db.entity.AnonAccount import com.jerboa.feat.BlurNSFW import com.jerboa.feat.InstantScores import com.jerboa.feat.VoteType @@ -38,10 +40,11 @@ import com.jerboa.ui.components.comment.CommentBody import com.jerboa.ui.components.comment.PostAndCommunityContextHeader import com.jerboa.ui.components.common.ActionBarButton import com.jerboa.ui.components.common.CommentOrPostNodeHeader +import com.jerboa.ui.components.common.UpvotePercentage import com.jerboa.ui.components.common.VoteGeneric +import com.jerboa.ui.components.common.VoteScore import com.jerboa.ui.theme.LARGE_PADDING import com.jerboa.ui.theme.SMALL_PADDING -import com.jerboa.ui.theme.XXL_PADDING import it.vercruysse.lemmyapi.datatypes.Community import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode import it.vercruysse.lemmyapi.datatypes.Person @@ -54,21 +57,18 @@ import it.vercruysse.lemmyapi.datatypes.PostId fun CommentMentionNodeHeader( personMentionView: PersonMentionView, onPersonClick: (personId: PersonId) -> Unit, - instantScores: InstantScores, onClick: () -> Unit, onLongClick: () -> Unit, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { CommentOrPostNodeHeader( creator = personMentionView.creator, - instantScores = instantScores, - voteDisplayMode = voteDisplayMode, published = personMentionView.comment.published, updated = personMentionView.comment.updated, deleted = personMentionView.comment.deleted, onPersonClick = onPersonClick, isPostCreator = false, + isNsfw = false, isDistinguished = personMentionView.comment.distinguished, isCommunityBanned = personMentionView.creator_banned_from_community, onClick = onClick, @@ -82,13 +82,6 @@ fun CommentMentionNodeHeader( fun CommentMentionNodeHeaderPreview() { CommentMentionNodeHeader( personMentionView = samplePersonMentionView, - instantScores = InstantScores( - score = 23, - myVote = 26, - upvotes = 21, - downvotes = 2, - ), - voteDisplayMode = LocalUserVoteDisplayMode.default(), onPersonClick = {}, onClick = {}, onLongClick = {}, @@ -112,7 +105,8 @@ fun CommentMentionNodeFooterLine( onRemoveClick: (personMentionView: PersonMentionView) -> Unit, onLinkClick: (personMentionView: PersonMentionView) -> Unit, onBlockCreatorClick: (creator: Person) -> Unit, - myVote: Int, + instantScores: InstantScores, + voteDisplayMode: LocalUserVoteDisplayMode, account: Account, enableDownvotes: Boolean, viewSource: Boolean, @@ -153,17 +147,30 @@ fun CommentMentionNodeFooterLine( .padding(top = LARGE_PADDING, bottom = SMALL_PADDING), ) { Row( - horizontalArrangement = Arrangement.spacedBy(XXL_PADDING), + horizontalArrangement = Arrangement.spacedBy(LARGE_PADDING), ) { + VoteScore( + instantScores = instantScores, + onVoteClick = onUpvoteClick, + voteDisplayMode = voteDisplayMode, + account = account, + ) + UpvotePercentage( + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, + account = account, + ) VoteGeneric( - myVote = myVote, + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, type = VoteType.Upvote, onVoteClick = onUpvoteClick, account = account, ) if (enableDownvotes) { VoteGeneric( - myVote = myVote, + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, type = VoteType.Downvote, onVoteClick = onDownvoteClick, account = account, @@ -239,6 +246,32 @@ fun CommentMentionNodeFooterLine( } } +@Composable +@Preview +fun CommentMentionNodeFooterLinePreview() { + CommentMentionNodeFooterLine( + personMentionView = samplePersonMentionView, + admins = listOf(), + moderators = listOf(), + onUpvoteClick = { }, + onDownvoteClick = { }, + onReplyClick = {}, + onSaveClick = {}, + onMarkAsReadClick = {}, + onPersonClick = {}, + onViewSourceClick = { }, + onReportClick = {}, + onRemoveClick = {}, + onLinkClick = {}, + onBlockCreatorClick = {}, + instantScores = sampleInstantScores, + voteDisplayMode = LocalUserVoteDisplayMode.default(), + account = AnonAccount, + enableDownvotes = true, + viewSource = false, + ) +} + @Composable fun CommentMentionNode( personMentionView: PersonMentionView, @@ -294,8 +327,6 @@ fun CommentMentionNode( CommentMentionNodeHeader( personMentionView = personMentionView, onPersonClick = onPersonClick, - instantScores = instantScores, - voteDisplayMode = voteDisplayMode, onClick = { isExpanded = !isExpanded }, @@ -352,7 +383,8 @@ fun CommentMentionNode( account = account, enableDownvotes = enableDownvotes, viewSource = viewSource, - myVote = instantScores.myVote, + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, ) } } diff --git a/app/src/main/java/com/jerboa/ui/components/comment/reply/CommentReply.kt b/app/src/main/java/com/jerboa/ui/components/comment/reply/CommentReply.kt index c77185446..a52cd780d 100644 --- a/app/src/main/java/com/jerboa/ui/components/comment/reply/CommentReply.kt +++ b/app/src/main/java/com/jerboa/ui/components/comment/reply/CommentReply.kt @@ -18,18 +18,16 @@ import com.jerboa.R import com.jerboa.datatypes.getContent import com.jerboa.datatypes.sampleCommentView import com.jerboa.db.entity.Account -import com.jerboa.feat.InstantScores -import com.jerboa.feat.default +import com.jerboa.nsfwCheck import com.jerboa.ui.components.comment.CommentNodeHeader import com.jerboa.ui.components.comment.mentionnode.CommentMentionNodeHeader import com.jerboa.ui.components.comment.replynode.CommentReplyNodeHeader +import com.jerboa.ui.components.common.CommentOrPostNodeHeader import com.jerboa.ui.components.common.MarkdownTextField -import com.jerboa.ui.components.post.PostNodeHeader import com.jerboa.ui.theme.LARGE_PADDING import com.jerboa.ui.theme.MEDIUM_PADDING import it.vercruysse.lemmyapi.datatypes.CommentReplyView import it.vercruysse.lemmyapi.datatypes.CommentView -import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode import it.vercruysse.lemmyapi.datatypes.PersonId import it.vercruysse.lemmyapi.datatypes.PersonMentionView import it.vercruysse.lemmyapi.datatypes.PostView @@ -39,24 +37,16 @@ fun RepliedComment( commentView: CommentView, onPersonClick: (personId: PersonId) -> Unit, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { Column(modifier = Modifier.padding(MEDIUM_PADDING)) { CommentNodeHeader( commentView = commentView, onPersonClick = onPersonClick, - instantScores = InstantScores( - score = commentView.counts.score, - upvotes = commentView.counts.upvotes, - downvotes = commentView.counts.downvotes, - myVote = commentView.my_vote, - ), collapsedCommentsCount = 0, isExpanded = true, onClick = {}, onLongClick = {}, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, ) SelectionContainer { Text(text = commentView.comment.getContent()) @@ -69,22 +59,14 @@ fun RepliedCommentReply( commentReplyView: CommentReplyView, onPersonClick: (personId: PersonId) -> Unit, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { Column(modifier = Modifier.padding(MEDIUM_PADDING)) { CommentReplyNodeHeader( commentReplyView = commentReplyView, onPersonClick = onPersonClick, - instantScores = InstantScores( - score = commentReplyView.counts.score, - upvotes = commentReplyView.counts.upvotes, - downvotes = commentReplyView.counts.downvotes, - myVote = commentReplyView.my_vote, - ), onClick = {}, onLongClick = {}, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, ) SelectionContainer { Text(text = commentReplyView.comment.getContent()) @@ -97,22 +79,14 @@ fun RepliedMentionReply( personMentionView: PersonMentionView, onPersonClick: (personId: PersonId) -> Unit, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { Column(modifier = Modifier.padding(MEDIUM_PADDING)) { CommentMentionNodeHeader( personMentionView = personMentionView, onPersonClick = onPersonClick, - instantScores = InstantScores( - score = personMentionView.counts.score, - upvotes = personMentionView.counts.upvotes, - downvotes = personMentionView.counts.downvotes, - myVote = personMentionView.my_vote, - ), onClick = {}, onLongClick = {}, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, ) SelectionContainer { Text(text = personMentionView.comment.getContent()) @@ -127,7 +101,6 @@ fun RepliedCommentPreview() { commentView = sampleCommentView, onPersonClick = {}, showAvatar = true, - voteDisplayMode = LocalUserVoteDisplayMode.default(), ) } @@ -136,20 +109,12 @@ fun RepliedPost( postView: PostView, onPersonClick: (personId: PersonId) -> Unit, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { Column(modifier = Modifier.padding(MEDIUM_PADDING)) { PostNodeHeader( postView = postView, - instantScores = InstantScores( - myVote = postView.my_vote, - score = postView.counts.score, - upvotes = postView.counts.upvotes, - downvotes = postView.counts.downvotes, - ), onPersonClick = onPersonClick, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, ) val text = postView.post.body ?: run { postView.post.name } SelectionContainer { @@ -167,7 +132,6 @@ fun CommentReply( account: Account, modifier: Modifier = Modifier, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { val scrollState = rememberScrollState() @@ -178,7 +142,6 @@ fun CommentReply( commentView = commentView, onPersonClick = onPersonClick, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, ) HorizontalDivider(modifier = Modifier.padding(vertical = LARGE_PADDING)) MarkdownTextField( @@ -199,7 +162,6 @@ fun CommentReplyReply( account: Account, modifier: Modifier = Modifier, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { val scrollState = rememberScrollState() @@ -210,7 +172,6 @@ fun CommentReplyReply( commentReplyView = commentReplyView, onPersonClick = onPersonClick, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, ) HorizontalDivider(modifier = Modifier.padding(vertical = LARGE_PADDING)) MarkdownTextField( @@ -231,7 +192,6 @@ fun MentionReply( account: Account, modifier: Modifier = Modifier, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { val scrollState = rememberScrollState() @@ -242,7 +202,6 @@ fun MentionReply( personMentionView = personMentionView, onPersonClick = onPersonClick, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, ) HorizontalDivider(modifier = Modifier.padding(vertical = LARGE_PADDING)) MarkdownTextField( @@ -263,7 +222,6 @@ fun PostReply( account: Account, modifier: Modifier = Modifier, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { val scrollState = rememberScrollState() @@ -274,7 +232,6 @@ fun PostReply( postView = postView, onPersonClick = onPersonClick, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, ) HorizontalDivider(modifier = Modifier.padding(vertical = LARGE_PADDING)) MarkdownTextField( @@ -286,3 +243,25 @@ fun PostReply( ) } } + +@Composable +fun PostNodeHeader( + postView: PostView, + onPersonClick: (personId: PersonId) -> Unit, + showAvatar: Boolean, +) { + CommentOrPostNodeHeader( + creator = postView.creator, + published = postView.post.published, + updated = postView.post.updated, + deleted = postView.post.deleted, + onPersonClick = onPersonClick, + isPostCreator = true, + isCommunityBanned = postView.creator_banned_from_community, + onClick = {}, + onLongCLick = {}, + showAvatar = showAvatar, + isNsfw = nsfwCheck(postView), + isDistinguished = false, + ) +} diff --git a/app/src/main/java/com/jerboa/ui/components/comment/reply/CommentReplyScreen.kt b/app/src/main/java/com/jerboa/ui/components/comment/reply/CommentReplyScreen.kt index 443be178c..c875cb644 100644 --- a/app/src/main/java/com/jerboa/ui/components/comment/reply/CommentReplyScreen.kt +++ b/app/src/main/java/com/jerboa/ui/components/comment/reply/CommentReplyScreen.kt @@ -103,7 +103,6 @@ fun CommentReplyScreen( .padding(padding) .imePadding(), showAvatar = siteViewModel.showAvatar(), - voteDisplayMode = siteViewModel.voteDisplayMode(), ) is ReplyItem.PostItem -> @@ -114,7 +113,6 @@ fun CommentReplyScreen( onReplyChange = { reply = it }, onPersonClick = appState::toProfile, showAvatar = siteViewModel.showAvatar(), - voteDisplayMode = siteViewModel.voteDisplayMode(), modifier = Modifier .padding(padding) @@ -133,7 +131,6 @@ fun CommentReplyScreen( .padding(padding) .imePadding(), showAvatar = siteViewModel.showAvatar(), - voteDisplayMode = siteViewModel.voteDisplayMode(), ) is ReplyItem.MentionReplyItem -> @@ -148,7 +145,6 @@ fun CommentReplyScreen( .padding(padding) .imePadding(), showAvatar = siteViewModel.showAvatar(), - voteDisplayMode = siteViewModel.voteDisplayMode(), ) } } diff --git a/app/src/main/java/com/jerboa/ui/components/comment/replynode/CommentReplyNode.kt b/app/src/main/java/com/jerboa/ui/components/comment/replynode/CommentReplyNode.kt index dfff724f4..99b736020 100644 --- a/app/src/main/java/com/jerboa/ui/components/comment/replynode/CommentReplyNode.kt +++ b/app/src/main/java/com/jerboa/ui/components/comment/replynode/CommentReplyNode.kt @@ -32,15 +32,15 @@ import com.jerboa.db.entity.Account import com.jerboa.feat.BlurNSFW import com.jerboa.feat.InstantScores import com.jerboa.feat.VoteType -import com.jerboa.feat.default import com.jerboa.ui.components.comment.CommentBody import com.jerboa.ui.components.comment.PostAndCommunityContextHeader import com.jerboa.ui.components.common.ActionBarButton import com.jerboa.ui.components.common.CommentOrPostNodeHeader +import com.jerboa.ui.components.common.UpvotePercentage import com.jerboa.ui.components.common.VoteGeneric +import com.jerboa.ui.components.common.VoteScore import com.jerboa.ui.theme.LARGE_PADDING import com.jerboa.ui.theme.SMALL_PADDING -import com.jerboa.ui.theme.XXL_PADDING import it.vercruysse.lemmyapi.datatypes.CommentReplyView import it.vercruysse.lemmyapi.datatypes.Community import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode @@ -52,26 +52,23 @@ import it.vercruysse.lemmyapi.datatypes.PostId fun CommentReplyNodeHeader( commentReplyView: CommentReplyView, onPersonClick: (personId: PersonId) -> Unit, - instantScores: InstantScores, onClick: () -> Unit, onLongClick: () -> Unit, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { CommentOrPostNodeHeader( creator = commentReplyView.creator, - instantScores = instantScores, published = commentReplyView.comment.published, updated = commentReplyView.comment.updated, deleted = commentReplyView.comment.deleted, onPersonClick = onPersonClick, isPostCreator = false, + isNsfw = false, isDistinguished = commentReplyView.comment.distinguished, isCommunityBanned = commentReplyView.creator_banned_from_community, onClick = onClick, onLongCLick = onLongClick, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, ) } @@ -80,13 +77,6 @@ fun CommentReplyNodeHeader( fun CommentReplyNodeHeaderPreview() { CommentReplyNodeHeader( commentReplyView = sampleCommentReplyView, - instantScores = InstantScores( - score = 23, - myVote = 26, - upvotes = 21, - downvotes = 2, - ), - voteDisplayMode = LocalUserVoteDisplayMode.default(), onPersonClick = {}, onClick = {}, onLongClick = {}, @@ -107,7 +97,8 @@ fun CommentReplyNodeInboxFooterLine( onReportClick: (commentReplyView: CommentReplyView) -> Unit, onCommentLinkClick: (commentReplyView: CommentReplyView) -> Unit, onBlockCreatorClick: (creator: Person) -> Unit, - myVote: Int, + instantScores: InstantScores, + voteDisplayMode: LocalUserVoteDisplayMode, account: Account, enableDownvotes: Boolean, viewSource: Boolean, @@ -136,17 +127,30 @@ fun CommentReplyNodeInboxFooterLine( .padding(top = LARGE_PADDING, bottom = SMALL_PADDING), ) { Row( - horizontalArrangement = Arrangement.spacedBy(XXL_PADDING), + horizontalArrangement = Arrangement.spacedBy(LARGE_PADDING), ) { + VoteScore( + instantScores = instantScores, + onVoteClick = onUpvoteClick, + voteDisplayMode = voteDisplayMode, + account = account, + ) + UpvotePercentage( + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, + account = account, + ) VoteGeneric( - myVote = myVote, + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, type = VoteType.Upvote, onVoteClick = onUpvoteClick, account = account, ) if (enableDownvotes) { VoteGeneric( - myVote = myVote, + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, type = VoteType.Downvote, onVoteClick = onDownvoteClick, account = account, @@ -276,7 +280,6 @@ fun CommentReplyNodeInbox( CommentReplyNodeHeader( commentReplyView = commentReplyView, onPersonClick = onPersonClick, - instantScores = instantScores, onClick = { isExpanded = !isExpanded }, @@ -284,7 +287,6 @@ fun CommentReplyNodeInbox( isActionBarExpanded = !isActionBarExpanded }, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, ) AnimatedVisibility( visible = isExpanded, @@ -328,7 +330,8 @@ fun CommentReplyNodeInbox( onReportClick = onReportClick, onCommentLinkClick = onCommentLinkClick, onBlockCreatorClick = onBlockCreatorClick, - myVote = instantScores.myVote, + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, account = account, enableDownvotes = enableDownvotes, viewSource = viewSource, diff --git a/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt b/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt index 62023ecf9..4994e0e57 100644 --- a/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt +++ b/app/src/main/java/com/jerboa/ui/components/common/AppBars.kt @@ -3,9 +3,13 @@ package com.jerboa.ui.components.common import android.annotation.SuppressLint import android.app.Activity import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -15,6 +19,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.KeyboardArrowDown @@ -27,6 +32,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.Offset @@ -54,8 +60,6 @@ import com.jerboa.datatypes.samplePerson import com.jerboa.datatypes.samplePost import com.jerboa.db.entity.Account import com.jerboa.db.entity.AnonAccount -import com.jerboa.feat.InstantScores -import com.jerboa.feat.default import com.jerboa.feat.isReadyAndIfNotShowSimplifiedInfoToast import com.jerboa.scrollToNextParentComment import com.jerboa.scrollToPreviousParentComment @@ -64,7 +68,6 @@ import com.jerboa.ui.components.home.NavTab import com.jerboa.ui.components.person.PersonProfileLink import com.jerboa.ui.theme.* import it.vercruysse.lemmyapi.datatypes.CommunityModeratorView -import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode import it.vercruysse.lemmyapi.datatypes.Person import it.vercruysse.lemmyapi.datatypes.PersonId import it.vercruysse.lemmyapi.datatypes.PersonView @@ -242,7 +245,7 @@ fun CommentNavigationBottomAppBar( @Composable fun CommentOrPostNodeHeader( creator: Person, - instantScores: InstantScores, + isNsfw: Boolean, published: String, updated: String?, deleted: Boolean, @@ -255,11 +258,9 @@ fun CommentOrPostNodeHeader( isExpanded: Boolean = true, collapsedCommentsCount: Long = 0, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { FlowRow( horizontalArrangement = Arrangement.SpaceBetween, - verticalArrangement = Arrangement.Center, modifier = Modifier .fillMaxWidth() @@ -273,17 +274,19 @@ fun CommentOrPostNodeHeader( bottom = MEDIUM_PADDING, ), ) { + val centerMod = Modifier.align(Alignment.CenterVertically) Row( horizontalArrangement = Arrangement.spacedBy(SMALL_PADDING), - verticalAlignment = Alignment.CenterVertically, + modifier = centerMod, ) { if (deleted) { Icon( imageVector = Icons.Outlined.Delete, contentDescription = stringResource(R.string.commentOrPostHeader_deleted), tint = MaterialTheme.colorScheme.error, + modifier = centerMod, ) - DotSpacer(style = MaterialTheme.typography.bodyMedium) + DotSpacer(modifier = centerMod) } PersonProfileLink( @@ -294,16 +297,28 @@ fun CommentOrPostNodeHeader( isDistinguished = isDistinguished, isCommunityBanned = isCommunityBanned, showAvatar = showAvatar, + modifier = centerMod, + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(MEDIUM_PADDING), + modifier = centerMod, + ) { + NsfwBadge( + visible = isNsfw, + modifier = centerMod, + ) + CollapsedIndicator( + visible = !isExpanded, + descendants = collapsedCommentsCount, + modifier = centerMod, + ) + TimeAgo( + published = published, + updated = updated, + modifier = centerMod, ) } - ScoreAndTime( - instantScores = instantScores, - published = published, - updated = updated, - isExpanded = isExpanded, - collapsedCommentsCount = collapsedCommentsCount, - voteDisplayMode = voteDisplayMode, - ) } } @@ -312,12 +327,6 @@ fun CommentOrPostNodeHeader( fun CommentOrPostNodeHeaderPreview() { CommentOrPostNodeHeader( creator = samplePerson, - instantScores = InstantScores( - score = 23, - upvotes = 21, - downvotes = 2, - myVote = 1, - ), published = samplePost.published, updated = samplePost.updated, deleted = false, @@ -328,7 +337,7 @@ fun CommentOrPostNodeHeaderPreview() { onClick = {}, onLongCLick = {}, showAvatar = true, - voteDisplayMode = LocalUserVoteDisplayMode.default(), + isNsfw = true, ) } @@ -358,6 +367,7 @@ fun ActionBarButton( } Row( verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SMALLER_PADDING), modifier = barMod, ) { Icon( @@ -366,7 +376,6 @@ fun ActionBarButton( tint = contentColor, ) text?.also { - Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Text( text = text, color = contentColor, @@ -838,3 +847,72 @@ fun DualHeaderTitle( bottomText = stringResource(selectedSortType.data.shortForm), topModifier = topModifier, ) + +@Composable +fun CollapsedIndicator( + modifier: Modifier = Modifier, + visible: Boolean, + descendants: Long, +) { + AnimatedVisibility( + visible = visible && descendants > 0, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column(modifier = modifier.wrapContentSize(Alignment.Center)) { + Box( + modifier = + Modifier + .clip(RoundedCornerShape(2.dp)) + .background(MaterialTheme.colorScheme.secondary) + .padding(horizontal = SMALL_PADDING), + ) { + Text( + text = "+$descendants", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSecondary, + ) + } + } + } +} + +@Preview +@Composable +fun CollapsedIndicatorPreview() { + CollapsedIndicator(visible = true, descendants = 23) +} + +@Composable +fun NsfwBadge( + modifier: Modifier = Modifier, + visible: Boolean, +) { + AnimatedVisibility( + visible = visible, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column(modifier = modifier.wrapContentSize(Alignment.Center)) { + Box( + modifier = + Modifier + .clip(RoundedCornerShape(2.dp)) + .background(MaterialTheme.colorScheme.secondary) + .padding(horizontal = SMALL_PADDING), + ) { + Text( + text = "NSFW", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSecondary, + ) + } + } + } +} + +@Preview +@Composable +fun NsfwBadgePreview() { + NsfwBadge(visible = true) +} diff --git a/app/src/main/java/com/jerboa/ui/components/common/TextBadge.kt b/app/src/main/java/com/jerboa/ui/components/common/TextBadge.kt index a5332281e..5d384df60 100644 --- a/app/src/main/java/com/jerboa/ui/components/common/TextBadge.kt +++ b/app/src/main/java/com/jerboa/ui/components/common/TextBadge.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -73,7 +74,10 @@ fun ItemAndInstanceTitle( buildAnnotatedString { withStyle( - style = itemStyle.toSpanStyle().copy(color = itemColor), + style = itemStyle.toSpanStyle().copy( + color = itemColor, + fontWeight = FontWeight.W600, + ), ) { append(title) } diff --git a/app/src/main/java/com/jerboa/ui/components/common/TimeAgo.kt b/app/src/main/java/com/jerboa/ui/components/common/TimeAgo.kt index 91899dbfa..484c30f9f 100644 --- a/app/src/main/java/com/jerboa/ui/components/common/TimeAgo.kt +++ b/app/src/main/java/com/jerboa/ui/components/common/TimeAgo.kt @@ -1,46 +1,23 @@ package com.jerboa.ui.components.common import android.util.Log -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ArrowDownward -import androidx.compose.material.icons.outlined.ArrowUpward -import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material.icons.outlined.Edit import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.jerboa.R -import com.jerboa.SHOW_UPVOTE_PCT_THRESHOLD -import com.jerboa.api.API import com.jerboa.datatypes.samplePerson -import com.jerboa.datatypes.samplePost -import com.jerboa.feat.InstantScores -import com.jerboa.feat.formatPercent -import com.jerboa.feat.upvotePercent import com.jerboa.formatDuration -import com.jerboa.ui.theme.SMALL_PADDING -import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode import java.time.Instant import java.time.format.DateTimeParseException import java.util.Date @@ -54,6 +31,7 @@ fun TimeAgo( longTimeFormat: Boolean = false, ) { val publishedPretty = dateStringToPretty(published, longTimeFormat) + val style = MaterialTheme.typography.labelMedium if (publishedPretty == null) { SmallErrorLabel(text = stringResource(R.string.time_ago_failed_to_parse)) @@ -65,30 +43,36 @@ fun TimeAgo( stringResource(R.string.time_ago_ago, it, publishedPretty) } ?: run { publishedPretty } - Row(modifier = modifier) { - Text( - text = afterPreceding, - color = MaterialTheme.colorScheme.outline, - style = MaterialTheme.typography.labelMedium, - ) - - updated?.also { - DotSpacer( - padding = SMALL_PADDING, - style = MaterialTheme.typography.labelMedium, - ) - val updatedPretty = dateStringToPretty(it, longTimeFormat) + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + if (updated !== null) { + val updatedPretty = dateStringToPretty(updated, longTimeFormat) if (updatedPretty == null) { SmallErrorLabel(text = stringResource(R.string.time_ago_failed_to_parse)) } else { + val size = style.fontSize.value.dp + Icon( + imageVector = Icons.Outlined.Edit, + contentDescription = updatedPretty, + tint = MaterialTheme.colorScheme.outline, + modifier = Modifier.size(size), + ) Text( - text = "($updatedPretty)", - style = MaterialTheme.typography.labelMedium, + text = updatedPretty, + style = style, color = MaterialTheme.colorScheme.outline, fontStyle = FontStyle.Italic, ) } + } else { + Text( + text = afterPreceding, + color = MaterialTheme.colorScheme.outline, + style = style, + ) } } } @@ -121,287 +105,6 @@ fun TimeAgoPreview() { ) } -@Composable -fun ScoreAndTime( - instantScores: InstantScores, - published: String, - updated: String?, - isExpanded: Boolean = true, - collapsedCommentsCount: Long = 0, - isNsfw: Boolean = false, - voteDisplayMode: LocalUserVoteDisplayMode, -) { - Row( - horizontalArrangement = Arrangement.spacedBy(SMALL_PADDING), - verticalAlignment = Alignment.CenterVertically, - ) { - NsfwBadge(isNsfw) - CollapsedIndicator(visible = !isExpanded, descendants = collapsedCommentsCount) - Spacer(modifier = Modifier.padding(end = SMALL_PADDING)) - val upvotePct = upvotePercent( - upvotes = instantScores.upvotes, - downvotes = instantScores.downvotes, - ) - - // If the show_scores is disabled, and we are the instance is pre 0.19.4, we fallback to legacy behaviour - val api = API.getInstanceOrNull() - val legacyScoresHidden = api != null && !api.FF.hidePost() && !voteDisplayMode.score - - // A special case for scores, where if both are enabled, - // and the score is the same as the upvotes, then hide the score - val hideScore = - voteDisplayMode.score && voteDisplayMode.upvotes && instantScores.score == instantScores.upvotes - - if (!legacyScoresHidden) { - if (voteDisplayMode.score && !hideScore) { - VoteIndicator( - data = instantScores.score.toString(), - myVote = instantScores.myVote, - iconAndDescription = Pair( - Icons.Outlined.FavoriteBorder, - stringResource(id = R.string.score), - ), - ) - } - if (voteDisplayMode.upvote_percentage && (upvotePct < SHOW_UPVOTE_PCT_THRESHOLD)) { - // Always mute the color - VoteIndicator(data = formatPercent(upvotePct), myVote = 0) - } - if (voteDisplayMode.upvotes) { - // Mute color if not 1 - val myVote = if (instantScores.myVote == 1) 1 else 0 - VoteIndicator( - data = instantScores.upvotes.toString(), - myVote = myVote, - iconAndDescription = Pair( - Icons.Outlined.ArrowUpward, - stringResource(id = R.string.upvoted), - ), - ) - } - if (voteDisplayMode.downvotes && instantScores.downvotes > 0) { - // Mute color if not -1 - val myVote = if (instantScores.myVote == -1) -1 else 0 - VoteIndicator( - data = instantScores.downvotes.toString(), - myVote = myVote, - iconAndDescription = Pair( - Icons.Outlined.ArrowDownward, - stringResource(id = R.string.downvoted), - ), - ) - } - // Only show this spacer if at least one of the fields is enabled - if (voteDisplayMode.score || voteDisplayMode.upvotes || voteDisplayMode.downvotes || voteDisplayMode.upvote_percentage) { - DotSpacer(style = MaterialTheme.typography.labelMedium) - } - } - TimeAgo(published = published, updated = updated) - } -} - -@Composable -private fun VoteIndicator( - data: String, - myVote: Int, - iconAndDescription: Pair? = null, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = data, - color = scoreColor(myVote = myVote), - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(horizontal = 0.dp), - ) - iconAndDescription?.let { - val size = MaterialTheme.typography.labelLarge.fontSize.value.dp - Icon( - imageVector = iconAndDescription.first, - contentDescription = iconAndDescription.second, - tint = scoreColor(myVote = myVote), - modifier = Modifier.size(size), - ) - } - } -} - -@Preview -@Composable -fun UpvoteAndDownvotePreview() { - ScoreAndTime( - instantScores = InstantScores( - score = 25, - myVote = -1, - upvotes = 10, - downvotes = 15, - ), - published = samplePost.published, - updated = samplePost.updated, - voteDisplayMode = LocalUserVoteDisplayMode( - local_user_id = -1, - score = false, - upvotes = true, - downvotes = true, - upvote_percentage = false, - ), - ) -} - -@Preview -@Composable -fun ScoreAndUpvotePctAndTimePreview() { - ScoreAndTime( - instantScores = InstantScores( - score = 25, - myVote = 1, - upvotes = 10, - downvotes = 15, - ), - published = samplePost.published, - updated = samplePost.updated, - voteDisplayMode = LocalUserVoteDisplayMode( - local_user_id = -1, - score = true, - upvote_percentage = true, - upvotes = false, - downvotes = false, - ), - ) -} - -@Preview -@Composable -fun UpvotePctAndTimePreview() { - ScoreAndTime( - instantScores = InstantScores( - score = 25, - myVote = -1, - upvotes = 10, - downvotes = 15, - ), - published = samplePost.published, - updated = samplePost.updated, - voteDisplayMode = LocalUserVoteDisplayMode( - local_user_id = -1, - upvote_percentage = true, - score = false, - upvotes = false, - downvotes = false, - ), - ) -} - -@Preview -@Composable -fun ScoreAndTimePreview() { - ScoreAndTime( - instantScores = InstantScores( - score = 25, - myVote = -1, - upvotes = 10, - downvotes = 15, - ), - published = samplePost.published, - updated = samplePost.updated, - voteDisplayMode = LocalUserVoteDisplayMode( - local_user_id = -1, - score = true, - upvote_percentage = false, - upvotes = false, - downvotes = false, - ), - ) -} - -@Preview -@Composable -fun HideAllAndTimePreview() { - ScoreAndTime( - instantScores = InstantScores( - score = 25, - myVote = -1, - upvotes = 10, - downvotes = 15, - ), - published = samplePost.published, - updated = samplePost.updated, - voteDisplayMode = LocalUserVoteDisplayMode( - local_user_id = -1, - score = false, - upvote_percentage = false, - upvotes = false, - downvotes = false, - ), - ) -} - -@Composable -fun CollapsedIndicator( - visible: Boolean, - descendants: Long, -) { - AnimatedVisibility( - visible = visible && descendants > 0, - enter = fadeIn(), - exit = fadeOut(), - ) { - Column(modifier = Modifier.wrapContentSize(Alignment.Center)) { - Box( - modifier = - Modifier - .clip(RoundedCornerShape(2.dp)) - .background(MaterialTheme.colorScheme.secondary) - .padding(horizontal = SMALL_PADDING), - ) { - Text( - text = "+$descendants", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSecondary, - ) - } - } - } -} - -@Preview -@Composable -fun CollapsedIndicatorPreview() { - CollapsedIndicator(visible = true, descendants = 23) -} - -@Composable -fun NsfwBadge(visible: Boolean) { - AnimatedVisibility( - visible = visible, - enter = fadeIn(), - exit = fadeOut(), - ) { - Column(modifier = Modifier.wrapContentSize(Alignment.Center)) { - Box( - modifier = - Modifier - .clip(RoundedCornerShape(2.dp)) - .background(MaterialTheme.colorScheme.secondary) - .padding(horizontal = SMALL_PADDING), - ) { - Text( - text = "NSFW", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSecondary, - ) - } - } - } -} - -@Preview -@Composable -fun NsfwBadgePreview() { - NsfwBadge(visible = true) -} - @Composable fun SmallErrorLabel(text: String) { Text( diff --git a/app/src/main/java/com/jerboa/ui/components/common/VoteHelpers.kt b/app/src/main/java/com/jerboa/ui/components/common/VoteHelpers.kt index c2f42dbaa..dc8db1fd1 100644 --- a/app/src/main/java/com/jerboa/ui/components/common/VoteHelpers.kt +++ b/app/src/main/java/com/jerboa/ui/components/common/VoteHelpers.kt @@ -1,51 +1,179 @@ package com.jerboa.ui.components.common +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Favorite +import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material.icons.outlined.Percent import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview import com.jerboa.R +import com.jerboa.SHOW_UPVOTE_PCT_THRESHOLD +import com.jerboa.api.API +import com.jerboa.datatypes.sampleInstantScores import com.jerboa.db.entity.Account +import com.jerboa.db.entity.AnonAccount +import com.jerboa.feat.InstantScores import com.jerboa.feat.VoteType +import com.jerboa.feat.default +import com.jerboa.feat.formatPercent +import com.jerboa.feat.upvotePercent +import it.vercruysse.lemmyapi.LemmyApiBaseController +import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode @Composable fun VoteGeneric( - myVote: Int, + instantScores: InstantScores, + voteDisplayMode: LocalUserVoteDisplayMode, type: VoteType, onVoteClick: () -> Unit, account: Account, ) { - val iconAndColor = - when (type) { - VoteType.Upvote -> upvoteIconAndColor(myVote = myVote) - else -> downvoteIconAndColor(myVote = myVote) - } + val iconAndColor = iconAndColor(type, instantScores) + + val contentDescription = buildContentDescription(type, instantScores) + + val votes = when (type) { + VoteType.Upvote -> instantScores.upvotes + VoteType.Downvote -> instantScores.downvotes + } + + val hideScore = when (type) { + VoteType.Upvote -> !voteDisplayMode.upvotes + VoteType.Downvote -> !voteDisplayMode.downvotes + } && + !legacyScoresHidden(voteDisplayMode = voteDisplayMode) + + val voteStr = if (votes > 0 && !hideScore) { + votes.toString() + } else { + null + } - val contentDescription: String = - if (type == VoteType.Upvote) { - if (myVote == 1) { - stringResource(R.string.upvoted) - } else { - stringResource(R.string.upvote) - } - } else { - if (myVote == -1) { - stringResource(R.string.downvoted) - } else { - stringResource(R.string.downvote) - } - } ActionBarButton( onClick = onVoteClick, contentColor = iconAndColor.second, icon = iconAndColor.first, + text = voteStr, contentDescription = contentDescription, account = account, ) } +@Composable +@Preview +fun VoteGenericPreview() { + VoteGeneric( + instantScores = sampleInstantScores, + voteDisplayMode = LocalUserVoteDisplayMode.default(), + type = VoteType.Upvote, + onVoteClick = { }, + account = AnonAccount, + ) +} + +@Composable +fun VoteScore( + instantScores: InstantScores, + voteDisplayMode: LocalUserVoteDisplayMode, + onVoteClick: () -> Unit, + account: Account, +) { + val iconAndColor = scoreIconAndColor(myVote = instantScores.myVote) + + val hideScore = + voteDisplayMode.score && voteDisplayMode.upvotes && instantScores.score == instantScores.upvotes + + if (voteDisplayMode.score && !hideScore && !legacyScoresHidden(voteDisplayMode)) { + ActionBarButton( + onClick = onVoteClick, + contentColor = iconAndColor.second, + icon = iconAndColor.first, + text = instantScores.score.toString(), + contentDescription = stringResource(R.string.score), + account = account, + ) + } +} + +@Composable +@Preview +fun VoteScorePreview() { + VoteScore( + instantScores = sampleInstantScores, + voteDisplayMode = LocalUserVoteDisplayMode.default().copy(score = true), + onVoteClick = { }, + account = AnonAccount, + ) +} + +@Composable +fun UpvotePercentage( + instantScores: InstantScores, + voteDisplayMode: LocalUserVoteDisplayMode, + account: Account, +) { + val upvotePct = upvotePercent( + upvotes = instantScores.upvotes, + downvotes = instantScores.downvotes, + ) + + if (voteDisplayMode.upvote_percentage && (upvotePct < SHOW_UPVOTE_PCT_THRESHOLD) && !legacyScoresHidden(voteDisplayMode)) { + ActionBarButton( + onClick = {}, + noClick = true, + contentColor = MaterialTheme.colorScheme.outline, + icon = Icons.Outlined.Percent, + text = formatPercent(upvotePct), + contentDescription = stringResource(R.string.upvote_percentage), + account = account, + ) + } +} + +@Composable +@Preview +fun UpvotePercentagePreview() { + UpvotePercentage( + instantScores = sampleInstantScores, + voteDisplayMode = LocalUserVoteDisplayMode.default().copy(upvote_percentage = true), + account = AnonAccount, + ) +} + +@Composable +private fun buildContentDescription( + type: VoteType, + instantScores: InstantScores, +): String = + if (type == VoteType.Upvote) { + if (instantScores.myVote == 1) { + stringResource(R.string.upvoted) + } else { + stringResource(R.string.upvote) + } + } else { + if (instantScores.myVote == -1) { + stringResource(R.string.downvoted) + } else { + stringResource(R.string.downvote) + } + } + +@Composable +private fun iconAndColor( + type: VoteType, + instantScores: InstantScores, +): Pair = + when (type) { + VoteType.Upvote -> upvoteIconAndColor(myVote = instantScores.myVote) + else -> downvoteIconAndColor(myVote = instantScores.myVote) + } + @Composable fun upvoteIconAndColor(myVote: Int?): Pair = when (myVote) { @@ -75,3 +203,26 @@ fun downvoteIconAndColor(myVote: Int?): Pair = MaterialTheme.colorScheme.outline, ) } + +@Composable +fun scoreIconAndColor(myVote: Int?): Pair = + when (myVote) { + 1 -> + Pair( + Icons.Outlined.Favorite, + scoreColor(myVote = myVote), + ) + else -> + Pair( + Icons.Outlined.FavoriteBorder, + scoreColor(myVote = myVote), + ) + } + +/** + * If the show_scores is disabled, and we are the instance is pre 0.19.4, we fallback to legacy behaviour + */ +private fun legacyScoresHidden( + voteDisplayMode: LocalUserVoteDisplayMode, + api: LemmyApiBaseController? = API.getInstanceOrNull(), +): Boolean = api != null && !api.FF.hidePost() && !voteDisplayMode.score diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt index 0fe31ca0b..220a43176 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListing.kt @@ -1,39 +1,22 @@ package com.jerboa.ui.components.post import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Comment -import androidx.compose.material.icons.filled.Bookmark -import androidx.compose.material.icons.outlined.BookmarkBorder -import androidx.compose.material.icons.outlined.ChatBubbleOutline -import androidx.compose.material.icons.outlined.CommentsDisabled -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.Gavel import androidx.compose.material.icons.outlined.Link -import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.material.icons.outlined.PushPin import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text @@ -41,1045 +24,50 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import com.jerboa.JerboaAppState import com.jerboa.PostType import com.jerboa.PostViewMode import com.jerboa.R import com.jerboa.datatypes.BanFromCommunityData import com.jerboa.datatypes.PostFeatureData -import com.jerboa.datatypes.sampleImagePostView -import com.jerboa.datatypes.sampleLinkNoThumbnailPostView -import com.jerboa.datatypes.sampleLinkPostView -import com.jerboa.datatypes.sampleMarkdownPostView -import com.jerboa.datatypes.samplePostView import com.jerboa.db.entity.Account -import com.jerboa.db.entity.AnonAccount import com.jerboa.feat.BlurNSFW import com.jerboa.feat.InstantScores import com.jerboa.feat.PostActionBarMode import com.jerboa.feat.SwipeToActionPreset import com.jerboa.feat.SwipeToActionType import com.jerboa.feat.VoteType -import com.jerboa.feat.amMod -import com.jerboa.feat.canMod -import com.jerboa.feat.default import com.jerboa.feat.isReadyAndIfNotShowSimplifiedInfoToast -import com.jerboa.feat.needBlur -import com.jerboa.feat.simulateModerators import com.jerboa.getPostType -import com.jerboa.hostNameCleaned -import com.jerboa.isSameInstance -import com.jerboa.nsfwCheck -import com.jerboa.rememberJerboaAppState -import com.jerboa.siFormat -import com.jerboa.toHttps -import com.jerboa.ui.components.common.ActionBarButton -import com.jerboa.ui.components.common.ActionBarButtonAndBadge -import com.jerboa.ui.components.common.CircularIcon -import com.jerboa.ui.components.common.CommentOrPostNodeHeader -import com.jerboa.ui.components.common.DotSpacer -import com.jerboa.ui.components.common.MarkdownHelper.CreateMarkdownPreview import com.jerboa.ui.components.common.MyMarkdownText -import com.jerboa.ui.components.common.NsfwBadge import com.jerboa.ui.components.common.PictrsThumbnailImage -import com.jerboa.ui.components.common.PictrsUrlImage -import com.jerboa.ui.components.common.ScoreAndTime -import com.jerboa.ui.components.common.SimpleTopAppBar import com.jerboa.ui.components.common.SwipeToAction -import com.jerboa.ui.components.common.TimeAgo -import com.jerboa.ui.components.common.VoteGeneric -import com.jerboa.ui.components.common.fadingEdge import com.jerboa.ui.components.common.rememberSwipeActionState -import com.jerboa.ui.components.common.scoreColor -import com.jerboa.ui.components.community.CommunityLink -import com.jerboa.ui.components.community.CommunityName -import com.jerboa.ui.components.person.PersonProfileLink -import com.jerboa.ui.components.post.composables.PostOptionsDropdown import com.jerboa.ui.components.settings.about.TORRENT_HELP_LINK -import com.jerboa.ui.theme.ACTION_BAR_ICON_SIZE -import com.jerboa.ui.theme.LARGER_ICON_THUMBNAIL_SIZE import com.jerboa.ui.theme.LARGE_PADDING import com.jerboa.ui.theme.LINK_ICON_SIZE -import com.jerboa.ui.theme.MEDIUM_ICON_SIZE import com.jerboa.ui.theme.MEDIUM_PADDING import com.jerboa.ui.theme.POST_LINK_PIC_SIZE -import com.jerboa.ui.theme.SMALLER_PADDING -import com.jerboa.ui.theme.SMALL_PADDING import com.jerboa.ui.theme.THUMBNAIL_CARET_SIZE -import com.jerboa.ui.theme.XXL_PADDING import com.jerboa.ui.theme.jerboaColorScheme -import it.vercruysse.lemmyapi.datatypes.* -import kotlinx.coroutines.CoroutineScope - -@Composable -fun PostHeaderLine( - community: Community, - post: Post, - creator: Person, - creatorBannedFromCommunity: Boolean, - instantScores: InstantScores, - onCommunityClick: (community: Community) -> Unit, - onPersonClick: (personId: PersonId) -> Unit, - modifier: Modifier = Modifier, - showCommunityName: Boolean = true, - showAvatar: Boolean, - fullBody: Boolean, - blurNSFW: BlurNSFW, - voteDisplayMode: LocalUserVoteDisplayMode, -) { - Column(modifier = modifier) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(MEDIUM_PADDING), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f), - ) { - if (showCommunityName && showAvatar) { - CommunityIcon(community, onCommunityClick, blurNSFW) - } - Column { - if (showCommunityName) { - CommunityName( - community = community, - onClick = { onCommunityClick(community) }, - ) - } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(SMALL_PADDING), - ) { - PersonProfileLink( - person = creator, - onClick = onPersonClick, - showTags = fullBody, - // Set this to false, we already know this - isPostCreator = false, - isCommunityBanned = creatorBannedFromCommunity, - color = MaterialTheme.colorScheme.outline, - showAvatar = !showCommunityName && showAvatar, - ) - if (post.featured_local) { - DotSpacer() - Icon( - imageVector = Icons.Outlined.PushPin, - contentDescription = stringResource(R.string.postListing_featuredLocal), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(ACTION_BAR_ICON_SIZE), - ) - } - if (post.featured_community) { - DotSpacer() - Icon( - imageVector = Icons.Outlined.PushPin, - contentDescription = stringResource(R.string.postListing_featuredCommunity), - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(ACTION_BAR_ICON_SIZE), - ) - } - if (post.locked) { - DotSpacer() - Icon( - imageVector = Icons.Outlined.CommentsDisabled, - contentDescription = stringResource(R.string.postListing_locked), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(ACTION_BAR_ICON_SIZE), - ) - } - } - } - if (post.deleted) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = stringResource(R.string.postListing_deleted), - tint = MaterialTheme.colorScheme.error, - ) - } - if (post.removed) { - Icon( - imageVector = Icons.Outlined.Gavel, - contentDescription = stringResource(R.string.removed), - tint = MaterialTheme.colorScheme.error, - ) - } - } - ScoreAndTime( - instantScores = instantScores, - published = post.published, - updated = post.updated, - isNsfw = nsfwCheck(post, community), - voteDisplayMode = voteDisplayMode, - ) - } - } -} - -@Preview -@Composable -fun PostHeaderLinePreview() { - val postView = sampleLinkPostView - PostHeaderLine( - post = postView.post, - community = postView.community, - creator = postView.creator, - creatorBannedFromCommunity = postView.creator_banned_from_community, - instantScores = InstantScores( - myVote = 0, - score = 10, - upvotes = 9, - downvotes = 1, - ), - onCommunityClick = {}, - onPersonClick = {}, - showAvatar = true, - blurNSFW = BlurNSFW.NSFW, - fullBody = true, - voteDisplayMode = LocalUserVoteDisplayMode.default(), - ) -} - -@Composable -fun CommunityIcon( - community: Community, - onCommunityClick: (community: Community) -> Unit, - blurNSFW: BlurNSFW, -) { - community.icon?.let { - CircularIcon( - icon = it, - contentDescription = stringResource(R.string.postListing_goToCommunity), - size = MEDIUM_ICON_SIZE, - modifier = Modifier.clickable { onCommunityClick(community) }, - thumbnailSize = LARGER_ICON_THUMBNAIL_SIZE, - blur = blurNSFW.needBlur(community.nsfw), - ) - } -} - -@Composable -fun PostNodeHeader( - postView: PostView, - instantScores: InstantScores, - onPersonClick: (personId: PersonId) -> Unit, - showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, -) { - CommentOrPostNodeHeader( - creator = postView.creator, - instantScores = instantScores, - published = postView.post.published, - updated = postView.post.updated, - deleted = postView.post.deleted, - onPersonClick = onPersonClick, - isPostCreator = true, - isCommunityBanned = postView.creator_banned_from_community, - onClick = {}, - onLongCLick = {}, - showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, - isDistinguished = false, - ) -} - -@Composable -fun PostTitleBlock( - post: Post, - read: Boolean, - expandedImage: Boolean, - account: Account, - useCustomTabs: Boolean, - usePrivateTabs: Boolean, - blurEnabled: Boolean, - appState: JerboaAppState, - showIfRead: Boolean, -) { - val imagePost = post.url?.let { getPostType(it) == PostType.Image } ?: false - - if (imagePost && expandedImage) { - PostTitleAndImageLink( - post = post, - read = read, - appState = appState, - blurEnabled = blurEnabled, - showIfRead = showIfRead, - ) - } else { - PostTitleAndThumbnail( - post = post, - read = read, - account = account, - useCustomTabs = useCustomTabs, - usePrivateTabs = usePrivateTabs, - blurEnabled = blurEnabled, - appState = appState, - showIfRead = showIfRead, - ) - } -} - -@Composable -fun PostName( - post: Post, - read: Boolean, - showIfRead: Boolean, -) { - val color = - if (showIfRead && read) { - MaterialTheme.colorScheme.outline - } else if (post.featured_local) { - MaterialTheme.colorScheme.primary - } else if (post.featured_community) { - MaterialTheme.colorScheme.secondary - } else { - Color.Unspecified - } - - Text( - text = post.name, - style = MaterialTheme.typography.headlineMedium, - color = color, - modifier = Modifier.testTag("jerboa:posttitle"), - ) -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun PostTitleAndImageLink( - post: Post, - read: Boolean, - blurEnabled: Boolean, - appState: JerboaAppState, - showIfRead: Boolean, -) { - // This was tested, we know it exists - val url = post.url?.toHttps() - - Column( - modifier = Modifier.padding( - vertical = MEDIUM_PADDING, - horizontal = MEDIUM_PADDING, - ), - ) { - // Title of the post - PostName( - post = post, - read = read, - showIfRead = showIfRead, - ) - } - - url?.let { cUrl -> - PictrsUrlImage( - url = cUrl, - blur = blurEnabled, - contentDescription = post.alt_text, - modifier = - Modifier - .combinedClickable( - onClick = { appState.openImageViewer(cUrl) }, - onLongClick = { appState.showLinkPopup(cUrl) }, - ), - ) - } -} - -@Composable -fun PostTitleAndThumbnail( - post: Post, - read: Boolean, - account: Account, - useCustomTabs: Boolean, - usePrivateTabs: Boolean, - blurEnabled: Boolean, - appState: JerboaAppState, - showIfRead: Boolean, -) { - Column( - modifier = Modifier.padding(horizontal = MEDIUM_PADDING), - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(SMALL_PADDING), - ) { - // Title of the post - Column( - verticalArrangement = Arrangement.spacedBy(MEDIUM_PADDING), - modifier = Modifier.weight(1f), - ) { - PostName(post = post, read = read, showIfRead = showIfRead) - post.url?.also { postUrl -> - if (!isSameInstance(postUrl, account.instance)) { - val hostName = hostNameCleaned(postUrl) - - hostName?.also { - Text( - text = it, - color = MaterialTheme.colorScheme.outline, - style = MaterialTheme.typography.labelMedium, - fontFamily = FontFamily.Monospace, - ) - } - } - } - } - ThumbnailTile( - post = post, - useCustomTabs = useCustomTabs, - usePrivateTabs = usePrivateTabs, - blurEnabled = blurEnabled, - appState = appState, - ) - } - } -} - -@Composable -fun PostBody( - post: Post, - read: Boolean, - fullBody: Boolean, - viewSource: Boolean, - expandedImage: Boolean, - account: Account, - useCustomTabs: Boolean, - usePrivateTabs: Boolean, - blurEnabled: Boolean, - showPostLinkPreview: Boolean, - appState: JerboaAppState, - clickBody: () -> Unit = {}, - showIfRead: Boolean, -) { - Column( - verticalArrangement = Arrangement.spacedBy(MEDIUM_PADDING), - ) { - PostTitleBlock( - post = post, - read = read, - expandedImage = expandedImage, - account = account, - useCustomTabs = useCustomTabs, - usePrivateTabs = usePrivateTabs, - blurEnabled = blurEnabled, - appState = appState, - showIfRead = showIfRead, - ) - - // The metadata card - if (fullBody && showPostLinkPreview) { - MetadataCard(post = post) - - if (post.url?.startsWith("magnet") == true) { - TorrentHelpInfo() - } - } - - // Check to make sure body isn't empty string - val body = post.body?.trim()?.ifEmpty { null } - - // The desc - body?.also { text -> - if (fullBody) { - Column( - modifier = - Modifier - .padding(MEDIUM_PADDING), - ) { - if (viewSource) { - SelectionContainer { - Text( - text = text, - fontFamily = FontFamily.Monospace, - ) - } - } else { - MyMarkdownText( - markdown = text, - onClick = {}, - ) - } - } - } else { - val bottomFade = Brush.verticalGradient(.7f to MaterialTheme.colorScheme.background, 1f to Color.Transparent) - CreateMarkdownPreview( - markdown = text, - color = LocalContentColor.current, - onClick = clickBody, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier - .padding(MEDIUM_PADDING) - .fadingEdge(bottomFade), - ) - } - } - } -} - -@Preview -@Composable -fun PreviewStoryTitleAndMetadata() { - PostBody( - post = samplePostView.post, - read = samplePostView.read, - fullBody = false, - viewSource = false, - expandedImage = false, - account = AnonAccount, - useCustomTabs = false, - usePrivateTabs = false, - blurEnabled = BlurNSFW.NSFW.needBlur(samplePostView), - showPostLinkPreview = true, - appState = rememberJerboaAppState(), - showIfRead = true, - ) -} - -@Preview -@Composable -fun PreviewSourcePost() { - val pv = sampleMarkdownPostView - PostBody( - post = pv.post, - read = pv.read, - fullBody = true, - viewSource = true, - expandedImage = false, - account = AnonAccount, - useCustomTabs = false, - usePrivateTabs = false, - blurEnabled = BlurNSFW.NSFW.needBlur(pv), - showPostLinkPreview = true, - appState = rememberJerboaAppState(), - showIfRead = true, - ) -} - -@Composable -fun PostFooterLine( - postView: PostView, - admins: List, - moderators: List?, - instantScores: InstantScores, - onUpvoteClick: () -> Unit, - onDownvoteClick: () -> Unit, - onReplyClick: (postView: PostView) -> Unit, - onSaveClick: (postView: PostView) -> Unit, - onEditPostClick: (postView: PostView) -> Unit, - onDeletePostClick: (postView: PostView) -> Unit, - onHidePostClick: (postView: PostView) -> Unit, - onReportClick: (postView: PostView) -> Unit, - onRemoveClick: (postView: PostView) -> Unit, - onBanPersonClick: (person: Person) -> Unit, - onBanFromCommunityClick: (banData: BanFromCommunityData) -> Unit, - onLockPostClick: (postView: PostView) -> Unit, - onFeaturePostClick: (data: PostFeatureData) -> Unit, - onViewVotesClick: (PostId) -> Unit, - onCommunityClick: (community: Community) -> Unit, - onPersonClick: (personId: PersonId) -> Unit, - onViewSourceClick: () -> Unit, - modifier: Modifier = Modifier, - showReply: Boolean = false, - account: Account, - enableDownVotes: Boolean, - viewSource: Boolean, - postActionBarMode: PostActionBarMode, - fromPostActivity: Boolean, - scope: CoroutineScope, -) { - val ctx = LocalContext.current - var showMoreOptions by remember { mutableStateOf(false) } - - if (showMoreOptions) { - val fallbackModerators = remember(moderators) { - moderators ?: simulateModerators( - ctx = ctx, - account = account, - forCommunity = postView.community.id, - ) - } - - val amMod = remember(moderators) { - amMod( - moderators = fallbackModerators, - myId = account.id, - ) - } - - val canMod = remember(admins, moderators) { - canMod( - creatorId = postView.creator.id, - admins = admins, - moderators = fallbackModerators, - myId = account.id, - ) - } - - PostOptionsDropdown( - postView = postView, - onDismissRequest = { showMoreOptions = false }, - onCommunityClick = onCommunityClick, - onPersonClick = onPersonClick, - onEditPostClick = onEditPostClick, - onDeletePostClick = onDeletePostClick, - onHidePostClick = onHidePostClick, - onReportClick = onReportClick, - onRemoveClick = onRemoveClick, - onBanPersonClick = onBanPersonClick, - onBanFromCommunityClick = onBanFromCommunityClick, - onLockPostClick = onLockPostClick, - onFeaturePostClick = onFeaturePostClick, - onViewVotesClick = onViewVotesClick, - onViewSourceClick = onViewSourceClick, - isCreator = account.id == postView.creator.id, - canMod = canMod, - amAdmin = account.isAdmin, - amMod = amMod, - viewSource = viewSource, - showViewSource = fromPostActivity, - scope = scope, - ) - } - - val horizontalArrangement = - when (postActionBarMode) { - PostActionBarMode.Long -> Arrangement.spacedBy(XXL_PADDING) - PostActionBarMode.LeftHandShort -> Arrangement.spacedBy(LARGE_PADDING) - PostActionBarMode.RightHandShort -> Arrangement.spacedBy(LARGE_PADDING) - } - - Row( - horizontalArrangement = horizontalArrangement, - verticalAlignment = Alignment.Bottom, - modifier = - modifier - .fillMaxWidth() - .padding(bottom = SMALL_PADDING), - ) { - // Right handside shows the comments on the left side - if (postActionBarMode == PostActionBarMode.RightHandShort) { - CommentNewCountRework( - comments = postView.counts.comments, - unreadCount = postView.unread_comments, - account = account, - modifier = Modifier.weight(1F, true), - ) - } - - VoteGeneric( - myVote = instantScores.myVote, - type = VoteType.Upvote, - onVoteClick = onUpvoteClick, - account = account, - ) - if (enableDownVotes) { - VoteGeneric( - myVote = instantScores.myVote, - type = VoteType.Downvote, - onVoteClick = onDownvoteClick, - account = account, - ) - } - - if (postActionBarMode == PostActionBarMode.Long) { - CommentNewCountRework( - comments = postView.counts.comments, - unreadCount = postView.unread_comments, - account = account, - modifier = Modifier.weight(1F, true), - ) - } - - if (showReply) { - ActionBarButton( - icon = Icons.AutoMirrored.Outlined.Comment, - contentDescription = stringResource(R.string.postListing_reply), - onClick = { onReplyClick(postView) }, - account = account, - ) - } - SavedButton( - saved = postView.saved, - account = account, - onSaveClick = { onSaveClick(postView) }, - ) - ActionBarButton( - icon = Icons.Outlined.MoreVert, - contentDescription = stringResource(R.string.moreOptions), - account = account, - onClick = { showMoreOptions = !showMoreOptions }, - requiresAccount = false, - modifier = if (postActionBarMode == PostActionBarMode.LeftHandShort) { - Modifier.weight( - 1F, - true, - ) - } else { - Modifier - }, - ) - - if (postActionBarMode == PostActionBarMode.LeftHandShort) { - CommentNewCountRework( - comments = postView.counts.comments, - unreadCount = postView.unread_comments, - account = account, - ) - } - } -} - -@Composable -fun SavedButton( - saved: Boolean, - account: Account, - onSaveClick: () -> Unit, -) { - ActionBarButton( - icon = if (saved) { - Icons.Filled.Bookmark - } else { - Icons.Outlined.BookmarkBorder - }, - contentDescription = if (saved) { - stringResource(R.string.removeBookmark) - } else { - stringResource(R.string.addBookmark) - }, - onClick = onSaveClick, - contentColor = if (saved) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.outline - }, - account = account, - ) -} - -@Composable -fun CommentNewCountRework( - comments: Long, - unreadCount: Long, - account: Account, - modifier: Modifier = Modifier, -) { - val unread = - if (unreadCount == 0L || comments == unreadCount) { - null - } else { - (if (unreadCount > 0) "+" else "") + siFormat(unreadCount) - } - - ActionBarButtonAndBadge( - icon = Icons.Outlined.ChatBubbleOutline, - iconBadgeCount = unread, - contentDescription = null, - text = siFormat(comments), - noClick = true, - account = account, - onClick = {}, - modifier = modifier, - ) -} - -@Composable -fun CommentNewCount( - comments: Long, - unreadCount: Long, - style: TextStyle = MaterialTheme.typography.labelSmall.copy(fontStyle = FontStyle.Italic), - spacing: Dp = 0.dp, -) { - val unread = - if (unreadCount == 0L || comments == unreadCount) { - null - } else { - unreadCount - } - if (unread != null) { - Spacer(Modifier.padding(horizontal = spacing)) - - Text( - text = stringResource(R.string.post_listing_new, unread), - style = style, - color = MaterialTheme.colorScheme.outline, - ) - } -} - -@Preview -@Composable -fun CommentCountPreview() { - CommentNewCountRework(42, 0, account = AnonAccount) -} - -@Preview -@Composable -fun PostFooterLinePreview() { - val postView = samplePostView - val instantScores = - InstantScores( - myVote = postView.my_vote, - score = postView.counts.score, - upvotes = postView.counts.upvotes, - downvotes = postView.counts.downvotes, - ) - PostFooterLine( - postView = postView, - admins = emptyList(), - moderators = emptyList(), - instantScores = instantScores, - onUpvoteClick = {}, - onDownvoteClick = {}, - onReplyClick = {}, - onSaveClick = {}, - onEditPostClick = {}, - onDeletePostClick = {}, - onHidePostClick = {}, - onReportClick = {}, - onRemoveClick = {}, - onBanPersonClick = {}, - onBanFromCommunityClick = {}, - onLockPostClick = {}, - onFeaturePostClick = {}, - onViewVotesClick = {}, - onCommunityClick = {}, - onPersonClick = {}, - onViewSourceClick = {}, - account = AnonAccount, - enableDownVotes = true, - viewSource = false, - postActionBarMode = PostActionBarMode.Long, - fromPostActivity = true, - scope = rememberCoroutineScope(), - ) -} - -@Preview -@Composable -fun PreviewPostListingCard() { - PostListing( - postView = samplePostView, - admins = emptyList(), - moderators = emptyList(), - useCustomTabs = false, - usePrivateTabs = false, - onUpvoteClick = {}, - onDownvoteClick = {}, - onReplyClick = {}, - onPostClick = {}, - onSaveClick = {}, - onCommunityClick = {}, - onEditPostClick = {}, - onDeletePostClick = {}, - onHidePostClick = {}, - onReportClick = {}, - onRemoveClick = {}, - onBanPersonClick = {}, - onBanFromCommunityClick = {}, - onLockPostClick = {}, - onFeaturePostClick = {}, - onViewVotesClick = {}, - onPersonClick = {}, - fullBody = false, - account = AnonAccount, - postViewMode = PostViewMode.Card, - showVotingArrowsInListView = true, - enableDownVotes = true, - showAvatar = true, - blurNSFW = BlurNSFW.NSFW, - appState = rememberJerboaAppState(), - showPostLinkPreview = true, - showIfRead = true, - voteDisplayMode = LocalUserVoteDisplayMode.default(), - postActionBarMode = PostActionBarMode.Long, - swipeToActionPreset = SwipeToActionPreset.TwoSides, - ) -} - -@Preview -@Composable -fun PreviewLinkPostListing() { - PostListing( - postView = sampleLinkPostView, - admins = emptyList(), - moderators = emptyList(), - useCustomTabs = false, - usePrivateTabs = false, - onUpvoteClick = {}, - onDownvoteClick = {}, - onReplyClick = {}, - onPostClick = {}, - onSaveClick = {}, - onCommunityClick = {}, - onEditPostClick = {}, - onDeletePostClick = {}, - onHidePostClick = {}, - onReportClick = {}, - onRemoveClick = {}, - onBanPersonClick = {}, - onBanFromCommunityClick = {}, - onLockPostClick = {}, - onFeaturePostClick = {}, - onViewVotesClick = {}, - onPersonClick = {}, - fullBody = false, - account = AnonAccount, - postViewMode = PostViewMode.Card, - showVotingArrowsInListView = true, - enableDownVotes = true, - showAvatar = true, - blurNSFW = BlurNSFW.NSFW, - appState = rememberJerboaAppState(), - showPostLinkPreview = true, - showIfRead = true, - voteDisplayMode = LocalUserVoteDisplayMode.default(), - postActionBarMode = PostActionBarMode.Long, - swipeToActionPreset = SwipeToActionPreset.TwoSides, - ) -} - -@Preview -@Composable -fun PreviewImagePostListingCard() { - PostListing( - postView = sampleImagePostView, - admins = emptyList(), - moderators = emptyList(), - useCustomTabs = false, - usePrivateTabs = false, - onUpvoteClick = {}, - onDownvoteClick = {}, - onReplyClick = {}, - onPostClick = {}, - onSaveClick = {}, - onCommunityClick = {}, - onEditPostClick = {}, - onDeletePostClick = {}, - onHidePostClick = {}, - onReportClick = {}, - onRemoveClick = {}, - onBanPersonClick = {}, - onBanFromCommunityClick = {}, - onLockPostClick = {}, - onFeaturePostClick = {}, - onViewVotesClick = {}, - onPersonClick = {}, - fullBody = false, - account = AnonAccount, - postViewMode = PostViewMode.Card, - showVotingArrowsInListView = true, - enableDownVotes = true, - showAvatar = true, - blurNSFW = BlurNSFW.NSFW, - appState = rememberJerboaAppState(), - showPostLinkPreview = true, - showIfRead = true, - voteDisplayMode = LocalUserVoteDisplayMode.default(), - postActionBarMode = PostActionBarMode.Long, - swipeToActionPreset = SwipeToActionPreset.TwoSides, - ) -} - -@Preview -@Composable -fun PreviewImagePostListingSmallCard() { - PostListing( - postView = sampleImagePostView, - admins = emptyList(), - moderators = emptyList(), - useCustomTabs = false, - usePrivateTabs = false, - onUpvoteClick = {}, - onDownvoteClick = {}, - onReplyClick = {}, - onPostClick = {}, - onSaveClick = {}, - onCommunityClick = {}, - onEditPostClick = {}, - onDeletePostClick = {}, - onHidePostClick = {}, - onReportClick = {}, - onRemoveClick = {}, - onBanPersonClick = {}, - onBanFromCommunityClick = {}, - onLockPostClick = {}, - onFeaturePostClick = {}, - onViewVotesClick = {}, - onPersonClick = {}, - fullBody = false, - account = AnonAccount, - postViewMode = PostViewMode.SmallCard, - showVotingArrowsInListView = true, - enableDownVotes = true, - showAvatar = true, - blurNSFW = BlurNSFW.NSFW, - appState = rememberJerboaAppState(), - showPostLinkPreview = true, - showIfRead = true, - voteDisplayMode = LocalUserVoteDisplayMode.default(), - postActionBarMode = PostActionBarMode.Long, - swipeToActionPreset = SwipeToActionPreset.TwoSides, - ) -} - -@Preview -@Composable -fun PreviewLinkNoThumbnailPostListing() { - PostListing( - postView = sampleLinkNoThumbnailPostView, - admins = emptyList(), - moderators = emptyList(), - useCustomTabs = false, - usePrivateTabs = false, - onUpvoteClick = {}, - onDownvoteClick = {}, - onReplyClick = {}, - onPostClick = {}, - onSaveClick = {}, - onCommunityClick = {}, - onEditPostClick = {}, - onDeletePostClick = {}, - onHidePostClick = {}, - onReportClick = {}, - onRemoveClick = {}, - onBanPersonClick = {}, - onBanFromCommunityClick = {}, - onLockPostClick = {}, - onFeaturePostClick = {}, - onViewVotesClick = {}, - onPersonClick = {}, - fullBody = false, - account = AnonAccount, - postViewMode = PostViewMode.Card, - showVotingArrowsInListView = true, - enableDownVotes = true, - showAvatar = true, - blurNSFW = BlurNSFW.NSFW, - appState = rememberJerboaAppState(), - showPostLinkPreview = true, - showIfRead = true, - voteDisplayMode = LocalUserVoteDisplayMode.default(), - postActionBarMode = PostActionBarMode.Long, - swipeToActionPreset = SwipeToActionPreset.TwoSides, - ) -} - +import it.vercruysse.lemmyapi.datatypes.Community +import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode +import it.vercruysse.lemmyapi.datatypes.Person +import it.vercruysse.lemmyapi.datatypes.PersonId +import it.vercruysse.lemmyapi.datatypes.PersonView +import it.vercruysse.lemmyapi.datatypes.Post +import it.vercruysse.lemmyapi.datatypes.PostId +import it.vercruysse.lemmyapi.datatypes.PostView + +@ExperimentalLayoutApi @OptIn(ExperimentalMaterial3Api::class) @Composable fun PostListing( @@ -1296,257 +284,9 @@ fun PostListing( } } -@Composable -fun PostVotingTile( - instantScores: InstantScores, - onUpvoteClick: () -> Unit, - onDownvoteClick: () -> Unit, - account: Account, - enableDownVotes: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(SMALL_PADDING), - modifier = - Modifier - .fillMaxHeight() - .padding(end = MEDIUM_PADDING), - ) { - VoteGeneric( - myVote = instantScores.myVote, - type = VoteType.Upvote, - onVoteClick = onUpvoteClick, - account = account, - ) - - val scoreOrPctStr = instantScores.scoreOrPctStr(voteDisplayMode) - - Text( - text = scoreOrPctStr ?: "", - style = MaterialTheme.typography.bodyMedium, - color = scoreColor(myVote = instantScores.myVote), - // Hide the vote number if its - modifier = Modifier.alpha(if (scoreOrPctStr != null) 1f else 0f), - ) - - if (enableDownVotes) { - // invisible Text below aligns width of PostVotingTiles - Text( - text = "00000", - modifier = Modifier.height(0.dp), - style = MaterialTheme.typography.bodyMedium, - ) - VoteGeneric( - myVote = instantScores.myVote, - type = VoteType.Downvote, - onVoteClick = onDownvoteClick, - account = account, - ) - } - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun PostListingList( - postView: PostView, - instantScores: InstantScores, - onUpvoteClick: () -> Unit, - onDownvoteClick: () -> Unit, - onPostClick: (postView: PostView) -> Unit, - showCommunityName: Boolean = true, - account: Account, - showVotingArrowsInListView: Boolean, - useCustomTabs: Boolean, - usePrivateTabs: Boolean, - blurNSFW: BlurNSFW, - appState: JerboaAppState, - showIfRead: Boolean, - enableDownVotes: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, -) { - Column( - modifier = - Modifier - .padding( - horizontal = MEDIUM_PADDING, - vertical = MEDIUM_PADDING, - ).testTag("jerboa:post"), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = - Arrangement.spacedBy( - SMALL_PADDING, - ), - ) { - if (showVotingArrowsInListView) { - PostVotingTile( - instantScores = instantScores, - onUpvoteClick = onUpvoteClick, - onDownvoteClick = onDownvoteClick, - account = account, - enableDownVotes = enableDownVotes, - voteDisplayMode = voteDisplayMode, - ) - } - Column( - modifier = - Modifier - .weight(1f) - .clickable { onPostClick(postView) }, - verticalArrangement = Arrangement.spacedBy(SMALL_PADDING), - ) { - PostName(post = postView.post, read = postView.read, showIfRead = showIfRead) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(SMALLER_PADDING, Alignment.Start), - ) { - // You must use a center align modifier for each of these - val centerMod = Modifier.align(Alignment.CenterVertically) - if (showCommunityName) { - CommunityLink( - community = postView.community, - onClick = {}, - clickable = false, - showDefaultIcon = false, - showAvatar = false, - blurNSFW = blurNSFW, - modifier = centerMod, - ) - DotSpacer(modifier = centerMod) - } - PersonProfileLink( - person = postView.creator, - onClick = {}, - clickable = false, - color = MaterialTheme.colorScheme.outline, - showAvatar = false, - modifier = centerMod, - ) - DotSpacer(modifier = centerMod) - postView.post.url?.also { postUrl -> - if (!isSameInstance(postUrl, account.instance)) { - val hostName = hostNameCleaned(postUrl) - hostName?.also { - Text( - text = it, - color = MaterialTheme.colorScheme.outline, - style = MaterialTheme.typography.labelMedium, - fontFamily = FontFamily.Monospace, - modifier = centerMod, - ) - DotSpacer(modifier = centerMod) - } - } - } - TimeAgo( - published = postView.post.published, - updated = postView.post.updated, - modifier = centerMod, - ) - } - Row( - horizontalArrangement = Arrangement.spacedBy(SMALLER_PADDING), - verticalAlignment = Alignment.CenterVertically, - ) { - if (!showVotingArrowsInListView) { - Text( - text = instantScores.score.toString(), - style = MaterialTheme.typography.bodyMedium, - color = scoreColor(myVote = instantScores.myVote), - ) - DotSpacer() - } - Text( - text = - stringResource( - R.string.post_listing_comments_count, - postView.counts.comments, - ), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.outline, - ) - CommentNewCount( - comments = postView.counts.comments, - unreadCount = postView.unread_comments, - style = MaterialTheme.typography.labelMedium, - ) - NsfwBadge(nsfwCheck(postView)) - } - } - ThumbnailTile( - post = postView.post, - useCustomTabs = useCustomTabs, - usePrivateTabs = usePrivateTabs, - blurEnabled = blurNSFW.needBlur(postView), - appState = appState, - ) - } - } -} - -@Preview -@Composable -fun PostListingListPreview() { - val postView = samplePostView - val instantScores = - InstantScores( - myVote = postView.my_vote, - score = postView.counts.score, - upvotes = postView.counts.upvotes, - downvotes = postView.counts.downvotes, - ) - PostListingList( - postView = postView, - instantScores = instantScores, - onUpvoteClick = {}, - onDownvoteClick = {}, - onPostClick = {}, - account = AnonAccount, - showVotingArrowsInListView = true, - useCustomTabs = false, - usePrivateTabs = false, - blurNSFW = BlurNSFW.NSFW, - appState = rememberJerboaAppState(), - showIfRead = true, - enableDownVotes = false, - voteDisplayMode = LocalUserVoteDisplayMode.default(), - ) -} - -@Preview -@Composable -fun PostListingListWithThumbPreview() { - val postView = sampleImagePostView - val instantScores = - InstantScores( - myVote = postView.my_vote, - score = postView.counts.score, - upvotes = postView.counts.upvotes, - downvotes = postView.counts.downvotes, - ) - PostListingList( - postView = postView, - instantScores = instantScores, - onUpvoteClick = {}, - onDownvoteClick = {}, - onPostClick = {}, - account = AnonAccount, - showVotingArrowsInListView = true, - useCustomTabs = false, - usePrivateTabs = false, - blurNSFW = BlurNSFW.NSFW, - appState = rememberJerboaAppState(), - showIfRead = true, - enableDownVotes = false, - voteDisplayMode = LocalUserVoteDisplayMode.default(), - ) -} - @OptIn(ExperimentalFoundationApi::class) @Composable -private fun ThumbnailTile( +fun ThumbnailTile( post: Post, useCustomTabs: Boolean, usePrivateTabs: Boolean, @@ -1622,136 +362,6 @@ private fun ThumbnailTile( } } -@Composable -fun PostListingCard( - postView: PostView, - admins: List, - moderators: List?, - instantScores: InstantScores, - onUpvoteClick: () -> Unit, - onDownvoteClick: () -> Unit, - onReplyClick: (postView: PostView) -> Unit = {}, - onPostClick: (postView: PostView) -> Unit, - onSaveClick: (postView: PostView) -> Unit, - onCommunityClick: (community: Community) -> Unit, - onEditPostClick: (postView: PostView) -> Unit, - onDeletePostClick: (postView: PostView) -> Unit, - onHidePostClick: (postView: PostView) -> Unit, - onReportClick: (postView: PostView) -> Unit, - onRemoveClick: (postView: PostView) -> Unit, - onBanPersonClick: (person: Person) -> Unit, - onBanFromCommunityClick: (banData: BanFromCommunityData) -> Unit, - onLockPostClick: (postView: PostView) -> Unit, - onFeaturePostClick: (data: PostFeatureData) -> Unit, - onPersonClick: (personId: PersonId) -> Unit, - onViewVotesClick: (PostId) -> Unit, - onViewSourceClick: () -> Unit, - viewSource: Boolean, - showReply: Boolean = false, - showCommunityName: Boolean = true, - fullBody: Boolean, - account: Account, - expandedImage: Boolean, - enableDownVotes: Boolean, - showAvatar: Boolean, - useCustomTabs: Boolean, - usePrivateTabs: Boolean, - blurNSFW: BlurNSFW, - showPostLinkPreview: Boolean, - appState: JerboaAppState, - showIfRead: Boolean = false, - voteDisplayMode: LocalUserVoteDisplayMode, - postActionBarMode: PostActionBarMode, -) { - Column( - modifier = - Modifier - .padding(vertical = MEDIUM_PADDING) - .clickable { onPostClick(postView) } - .testTag("jerboa:post"), - // see https://stackoverflow.com/questions/77010371/prevent-popup-from-adding-padding-in-a-column-with-arrangement-spacedbylarge-p - // verticalArrangement = Arrangement.spacedBy(MEDIUM_PADDING), - ) { - // Header - PostHeaderLine( - post = postView.post, - creator = postView.creator, - community = postView.community, - creatorBannedFromCommunity = postView.creator_banned_from_community, - instantScores = instantScores, - onCommunityClick = onCommunityClick, - onPersonClick = onPersonClick, - showCommunityName = showCommunityName, - modifier = Modifier.padding(horizontal = MEDIUM_PADDING), - showAvatar = showAvatar, - blurNSFW = blurNSFW, - voteDisplayMode = voteDisplayMode, - fullBody = fullBody, - ) - - Spacer(modifier = Modifier.padding(vertical = MEDIUM_PADDING)) - - // Title + metadata - PostBody( - post = postView.post, - read = postView.read, - fullBody = fullBody, - viewSource = viewSource, - expandedImage = expandedImage, - account = account, - useCustomTabs = useCustomTabs, - usePrivateTabs = usePrivateTabs, - blurEnabled = blurNSFW.needBlur(postView), - showPostLinkPreview = showPostLinkPreview, - appState = appState, - clickBody = { onPostClick(postView) }, - showIfRead = showIfRead, - ) - - Spacer(modifier = Modifier.padding(vertical = MEDIUM_PADDING)) - - // Footer bar - PostFooterLine( - postView = postView, - admins = admins, - moderators = moderators, - instantScores = instantScores, - onUpvoteClick = onUpvoteClick, - onDownvoteClick = onDownvoteClick, - onReplyClick = onReplyClick, - onSaveClick = onSaveClick, - onEditPostClick = onEditPostClick, - onDeletePostClick = onDeletePostClick, - onHidePostClick = onHidePostClick, - onReportClick = onReportClick, - onRemoveClick = onRemoveClick, - onBanPersonClick = onBanPersonClick, - onBanFromCommunityClick = onBanFromCommunityClick, - onLockPostClick = onLockPostClick, - onFeaturePostClick = onFeaturePostClick, - onViewVotesClick = onViewVotesClick, - onCommunityClick = onCommunityClick, - onPersonClick = onPersonClick, - onViewSourceClick = onViewSourceClick, - modifier = Modifier.padding(horizontal = MEDIUM_PADDING), - showReply = showReply, - account = account, - enableDownVotes = enableDownVotes, - viewSource = viewSource, - postActionBarMode = postActionBarMode, - fromPostActivity = fullBody, - scope = appState.coroutineScope, - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Preview -@Composable -fun PostListingHeaderPreview() { - SimpleTopAppBar("Post", onClickBack = {}) -} - @Composable fun MetadataCard(post: Post) { val embedTitle = post.embed_title @@ -1809,3 +419,29 @@ fun TorrentHelpInfo() { }, ) } + +@Composable +fun PostName( + modifier: Modifier = Modifier, + post: Post, + read: Boolean, + showIfRead: Boolean, +) { + val color = + if (showIfRead && read) { + MaterialTheme.colorScheme.outline + } else if (post.featured_local) { + MaterialTheme.colorScheme.primary + } else if (post.featured_community) { + MaterialTheme.colorScheme.secondary + } else { + Color.Unspecified + } + + Text( + text = post.name, + style = MaterialTheme.typography.headlineMedium, + color = color, + modifier = modifier.testTag("jerboa:posttitle"), + ) +} diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListingCard.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListingCard.kt new file mode 100644 index 000000000..f2f3e1599 --- /dev/null +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListingCard.kt @@ -0,0 +1,1136 @@ +package com.jerboa.ui.components.post + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Comment +import androidx.compose.material.icons.filled.Bookmark +import androidx.compose.material.icons.outlined.BookmarkBorder +import androidx.compose.material.icons.outlined.ChatBubbleOutline +import androidx.compose.material.icons.outlined.CommentsDisabled +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Gavel +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.outlined.PushPin +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import com.jerboa.JerboaAppState +import com.jerboa.PostType +import com.jerboa.R +import com.jerboa.datatypes.BanFromCommunityData +import com.jerboa.datatypes.PostFeatureData +import com.jerboa.datatypes.sampleImagePostView +import com.jerboa.datatypes.sampleInstantScores +import com.jerboa.datatypes.sampleLinkNoThumbnailPostView +import com.jerboa.datatypes.sampleLinkPostView +import com.jerboa.datatypes.sampleMarkdownPostView +import com.jerboa.datatypes.samplePostView +import com.jerboa.db.entity.Account +import com.jerboa.db.entity.AnonAccount +import com.jerboa.feat.BlurNSFW +import com.jerboa.feat.InstantScores +import com.jerboa.feat.PostActionBarMode +import com.jerboa.feat.VoteType +import com.jerboa.feat.amMod +import com.jerboa.feat.canMod +import com.jerboa.feat.default +import com.jerboa.feat.needBlur +import com.jerboa.feat.simulateModerators +import com.jerboa.getPostType +import com.jerboa.hostNameCleaned +import com.jerboa.isSameInstance +import com.jerboa.rememberJerboaAppState +import com.jerboa.siFormat +import com.jerboa.toHttps +import com.jerboa.ui.components.common.ActionBarButton +import com.jerboa.ui.components.common.ActionBarButtonAndBadge +import com.jerboa.ui.components.common.CircularIcon +import com.jerboa.ui.components.common.DotSpacer +import com.jerboa.ui.components.common.MarkdownHelper.CreateMarkdownPreview +import com.jerboa.ui.components.common.MyMarkdownText +import com.jerboa.ui.components.common.PictrsUrlImage +import com.jerboa.ui.components.common.TimeAgo +import com.jerboa.ui.components.common.UpvotePercentage +import com.jerboa.ui.components.common.VoteGeneric +import com.jerboa.ui.components.common.VoteScore +import com.jerboa.ui.components.common.fadingEdge +import com.jerboa.ui.components.community.CommunityName +import com.jerboa.ui.components.person.PersonProfileLink +import com.jerboa.ui.components.post.composables.PostOptionsDropdown +import com.jerboa.ui.theme.ACTION_BAR_ICON_SIZE +import com.jerboa.ui.theme.LARGE_PADDING +import com.jerboa.ui.theme.MEDIUM_PADDING +import com.jerboa.ui.theme.SMALLER_PADDING +import com.jerboa.ui.theme.SMALL_PADDING +import com.jerboa.ui.theme.VERTICAL_SPACING +import com.jerboa.ui.theme.XXL_PADDING +import it.vercruysse.lemmyapi.datatypes.Community +import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode +import it.vercruysse.lemmyapi.datatypes.Person +import it.vercruysse.lemmyapi.datatypes.PersonId +import it.vercruysse.lemmyapi.datatypes.PersonView +import it.vercruysse.lemmyapi.datatypes.PostId +import it.vercruysse.lemmyapi.datatypes.PostView +import kotlinx.coroutines.CoroutineScope + +@ExperimentalLayoutApi +@Composable +fun PostListingCard( + postView: PostView, + admins: List, + moderators: List?, + instantScores: InstantScores, + onUpvoteClick: () -> Unit, + onDownvoteClick: () -> Unit, + onReplyClick: (postView: PostView) -> Unit = {}, + onPostClick: (postView: PostView) -> Unit, + onSaveClick: (postView: PostView) -> Unit, + onCommunityClick: (community: Community) -> Unit, + onEditPostClick: (postView: PostView) -> Unit, + onDeletePostClick: (postView: PostView) -> Unit, + onHidePostClick: (postView: PostView) -> Unit, + onReportClick: (postView: PostView) -> Unit, + onRemoveClick: (postView: PostView) -> Unit, + onBanPersonClick: (person: Person) -> Unit, + onBanFromCommunityClick: (banData: BanFromCommunityData) -> Unit, + onLockPostClick: (postView: PostView) -> Unit, + onFeaturePostClick: (data: PostFeatureData) -> Unit, + onPersonClick: (personId: PersonId) -> Unit, + onViewVotesClick: (PostId) -> Unit, + onViewSourceClick: () -> Unit, + viewSource: Boolean, + showReply: Boolean = false, + fullBody: Boolean, + account: Account, + expandedImage: Boolean, + enableDownVotes: Boolean, + showCommunityName: Boolean = true, + showAvatar: Boolean, + useCustomTabs: Boolean, + usePrivateTabs: Boolean, + blurNSFW: BlurNSFW, + showPostLinkPreview: Boolean, + appState: JerboaAppState, + showIfRead: Boolean = false, + voteDisplayMode: LocalUserVoteDisplayMode, + postActionBarMode: PostActionBarMode, +) { + Column( + modifier = + Modifier + .padding(vertical = SMALL_PADDING) + .clickable { onPostClick(postView) } + .testTag("jerboa:post"), + // see https://stackoverflow.com/questions/77010371/prevent-popup-from-adding-padding-in-a-column-with-arrangement-spacedbylarge-p + // verticalArrangement = Arrangement.spacedBy(MEDIUM_PADDING), + ) { + // Title + metadata + attribution + body + PostTitleAttributionBody( + postView = postView, + fullBody = fullBody, + viewSource = viewSource, + expandedImage = expandedImage, + account = account, + useCustomTabs = useCustomTabs, + usePrivateTabs = usePrivateTabs, + showPostLinkPreview = showPostLinkPreview, + appState = appState, + clickBody = { onPostClick(postView) }, + showIfRead = showIfRead, + blurNSFW = blurNSFW, + onPersonClick = onPersonClick, + onCommunityClick = onCommunityClick, + showAvatar = showAvatar, + showCommunityName = showCommunityName, + ) + + Spacer(modifier = Modifier.padding(vertical = VERTICAL_SPACING)) + + // Footer bar + PostFooterLine( + postView = postView, + admins = admins, + moderators = moderators, + instantScores = instantScores, + onUpvoteClick = onUpvoteClick, + onDownvoteClick = onDownvoteClick, + onReplyClick = onReplyClick, + onSaveClick = onSaveClick, + onEditPostClick = onEditPostClick, + onDeletePostClick = onDeletePostClick, + onHidePostClick = onHidePostClick, + onReportClick = onReportClick, + onRemoveClick = onRemoveClick, + onBanPersonClick = onBanPersonClick, + onBanFromCommunityClick = onBanFromCommunityClick, + onLockPostClick = onLockPostClick, + onFeaturePostClick = onFeaturePostClick, + onViewVotesClick = onViewVotesClick, + onCommunityClick = onCommunityClick, + onPersonClick = onPersonClick, + onViewSourceClick = onViewSourceClick, + modifier = Modifier.padding(horizontal = MEDIUM_PADDING), + showReply = showReply, + account = account, + enableDownVotes = enableDownVotes, + viewSource = viewSource, + postActionBarMode = postActionBarMode, + fromPostActivity = fullBody, + voteDisplayMode = voteDisplayMode, + scope = appState.coroutineScope, + ) + } +} + +@ExperimentalLayoutApi +@Preview +@Composable +fun PreviewPostListingCard() { + PostListingCard( + postView = samplePostView, + admins = emptyList(), + moderators = emptyList(), + useCustomTabs = false, + usePrivateTabs = false, + onUpvoteClick = {}, + onDownvoteClick = {}, + onReplyClick = {}, + onPostClick = {}, + onSaveClick = {}, + onCommunityClick = {}, + onEditPostClick = {}, + onDeletePostClick = {}, + onHidePostClick = {}, + onReportClick = {}, + onRemoveClick = {}, + onBanPersonClick = {}, + onBanFromCommunityClick = {}, + onLockPostClick = {}, + onFeaturePostClick = {}, + onViewVotesClick = {}, + onPersonClick = {}, + fullBody = false, + account = AnonAccount, + enableDownVotes = true, + showAvatar = true, + blurNSFW = BlurNSFW.NSFW, + appState = rememberJerboaAppState(), + showPostLinkPreview = true, + showIfRead = true, + voteDisplayMode = LocalUserVoteDisplayMode.default(), + postActionBarMode = PostActionBarMode.Long, + instantScores = sampleInstantScores, + onViewSourceClick = {}, + viewSource = false, + expandedImage = false, + ) +} + +@ExperimentalLayoutApi +@Preview +@Composable +fun PreviewLinkPostListing() { + PostListingCard( + postView = sampleLinkPostView, + admins = emptyList(), + moderators = emptyList(), + useCustomTabs = false, + usePrivateTabs = false, + onUpvoteClick = {}, + onDownvoteClick = {}, + onReplyClick = {}, + onPostClick = {}, + onSaveClick = {}, + onCommunityClick = {}, + onEditPostClick = {}, + onDeletePostClick = {}, + onHidePostClick = {}, + onReportClick = {}, + onRemoveClick = {}, + onBanPersonClick = {}, + onBanFromCommunityClick = {}, + onLockPostClick = {}, + onFeaturePostClick = {}, + onViewVotesClick = {}, + onPersonClick = {}, + fullBody = false, + account = AnonAccount, + enableDownVotes = true, + showAvatar = true, + blurNSFW = BlurNSFW.NSFW, + appState = rememberJerboaAppState(), + showPostLinkPreview = true, + showIfRead = true, + voteDisplayMode = LocalUserVoteDisplayMode.default(), + postActionBarMode = PostActionBarMode.Long, + instantScores = sampleInstantScores, + onViewSourceClick = {}, + viewSource = false, + expandedImage = false, + ) +} + +@ExperimentalLayoutApi +@Preview +@Composable +fun PreviewImagePostListingCard() { + PostListingCard( + postView = sampleImagePostView, + admins = emptyList(), + moderators = emptyList(), + useCustomTabs = false, + usePrivateTabs = false, + onUpvoteClick = {}, + onDownvoteClick = {}, + onReplyClick = {}, + onPostClick = {}, + onSaveClick = {}, + onCommunityClick = {}, + onEditPostClick = {}, + onDeletePostClick = {}, + onHidePostClick = {}, + onReportClick = {}, + onRemoveClick = {}, + onBanPersonClick = {}, + onBanFromCommunityClick = {}, + onLockPostClick = {}, + onFeaturePostClick = {}, + onViewVotesClick = {}, + onPersonClick = {}, + fullBody = false, + account = AnonAccount, + enableDownVotes = true, + showAvatar = true, + blurNSFW = BlurNSFW.NSFW, + appState = rememberJerboaAppState(), + showPostLinkPreview = true, + showIfRead = true, + voteDisplayMode = LocalUserVoteDisplayMode.default(), + postActionBarMode = PostActionBarMode.Long, + instantScores = sampleInstantScores, + onViewSourceClick = {}, + viewSource = false, + expandedImage = true, + ) +} + +@ExperimentalLayoutApi +@Preview +@Composable +fun PreviewImagePostListingSmallCard() { + PostListingCard( + postView = sampleImagePostView, + admins = emptyList(), + moderators = emptyList(), + useCustomTabs = false, + usePrivateTabs = false, + onUpvoteClick = {}, + onDownvoteClick = {}, + onReplyClick = {}, + onPostClick = {}, + onSaveClick = {}, + onCommunityClick = {}, + onEditPostClick = {}, + onDeletePostClick = {}, + onHidePostClick = {}, + onReportClick = {}, + onRemoveClick = {}, + onBanPersonClick = {}, + onBanFromCommunityClick = {}, + onLockPostClick = {}, + onFeaturePostClick = {}, + onViewVotesClick = {}, + onPersonClick = {}, + fullBody = false, + account = AnonAccount, + enableDownVotes = true, + showAvatar = true, + blurNSFW = BlurNSFW.NSFW, + appState = rememberJerboaAppState(), + showPostLinkPreview = true, + showIfRead = true, + voteDisplayMode = LocalUserVoteDisplayMode.default(), + postActionBarMode = PostActionBarMode.Long, + instantScores = sampleInstantScores, + onViewSourceClick = {}, + viewSource = false, + expandedImage = false, + ) +} + +@ExperimentalLayoutApi +@Preview +@Composable +fun PreviewLinkNoThumbnailPostListing() { + PostListingCard( + postView = sampleLinkNoThumbnailPostView, + admins = emptyList(), + moderators = emptyList(), + useCustomTabs = false, + usePrivateTabs = false, + onUpvoteClick = {}, + onDownvoteClick = {}, + onReplyClick = {}, + onPostClick = {}, + onSaveClick = {}, + onCommunityClick = {}, + onEditPostClick = {}, + onDeletePostClick = {}, + onHidePostClick = {}, + onReportClick = {}, + onRemoveClick = {}, + onBanPersonClick = {}, + onBanFromCommunityClick = {}, + onLockPostClick = {}, + onFeaturePostClick = {}, + onViewVotesClick = {}, + onPersonClick = {}, + fullBody = false, + account = AnonAccount, + enableDownVotes = true, + showAvatar = true, + blurNSFW = BlurNSFW.NSFW, + appState = rememberJerboaAppState(), + showPostLinkPreview = true, + showIfRead = true, + voteDisplayMode = LocalUserVoteDisplayMode.default(), + postActionBarMode = PostActionBarMode.Long, + instantScores = sampleInstantScores, + onViewSourceClick = {}, + viewSource = false, + expandedImage = false, + ) +} + +@Composable +fun PostFooterLine( + postView: PostView, + admins: List, + moderators: List?, + instantScores: InstantScores, + voteDisplayMode: LocalUserVoteDisplayMode, + onUpvoteClick: () -> Unit, + onDownvoteClick: () -> Unit, + onReplyClick: (postView: PostView) -> Unit, + onSaveClick: (postView: PostView) -> Unit, + onEditPostClick: (postView: PostView) -> Unit, + onDeletePostClick: (postView: PostView) -> Unit, + onHidePostClick: (postView: PostView) -> Unit, + onReportClick: (postView: PostView) -> Unit, + onRemoveClick: (postView: PostView) -> Unit, + onBanPersonClick: (person: Person) -> Unit, + onBanFromCommunityClick: (banData: BanFromCommunityData) -> Unit, + onLockPostClick: (postView: PostView) -> Unit, + onFeaturePostClick: (data: PostFeatureData) -> Unit, + onViewVotesClick: (PostId) -> Unit, + onCommunityClick: (community: Community) -> Unit, + onPersonClick: (personId: PersonId) -> Unit, + onViewSourceClick: () -> Unit, + modifier: Modifier = Modifier, + showReply: Boolean = false, + account: Account, + enableDownVotes: Boolean, + viewSource: Boolean, + postActionBarMode: PostActionBarMode, + fromPostActivity: Boolean, + scope: CoroutineScope, +) { + val ctx = LocalContext.current + var showMoreOptions by remember { mutableStateOf(false) } + + if (showMoreOptions) { + val fallbackModerators = remember(moderators) { + moderators ?: simulateModerators( + ctx = ctx, + account = account, + forCommunity = postView.community.id, + ) + } + + val amMod = remember(moderators) { + amMod( + moderators = fallbackModerators, + myId = account.id, + ) + } + + val canMod = remember(admins, moderators) { + canMod( + creatorId = postView.creator.id, + admins = admins, + moderators = fallbackModerators, + myId = account.id, + ) + } + + PostOptionsDropdown( + postView = postView, + onDismissRequest = { showMoreOptions = false }, + onCommunityClick = onCommunityClick, + onPersonClick = onPersonClick, + onEditPostClick = onEditPostClick, + onDeletePostClick = onDeletePostClick, + onHidePostClick = onHidePostClick, + onReportClick = onReportClick, + onRemoveClick = onRemoveClick, + onBanPersonClick = onBanPersonClick, + onBanFromCommunityClick = onBanFromCommunityClick, + onLockPostClick = onLockPostClick, + onFeaturePostClick = onFeaturePostClick, + onViewVotesClick = onViewVotesClick, + onViewSourceClick = onViewSourceClick, + isCreator = account.id == postView.creator.id, + canMod = canMod, + amAdmin = account.isAdmin, + amMod = amMod, + viewSource = viewSource, + showViewSource = fromPostActivity, + scope = scope, + ) + } + + val horizontalArrangement = + when (postActionBarMode) { + PostActionBarMode.Long -> Arrangement.spacedBy(XXL_PADDING) + PostActionBarMode.LeftHandShort -> Arrangement.spacedBy(LARGE_PADDING) + PostActionBarMode.RightHandShort -> Arrangement.spacedBy(LARGE_PADDING) + } + + Row( + horizontalArrangement = horizontalArrangement, + verticalAlignment = Alignment.Bottom, + modifier = + modifier + .fillMaxWidth() + .padding(bottom = SMALL_PADDING), + ) { + // Right handside shows the comments on the left side + if (postActionBarMode == PostActionBarMode.RightHandShort) { + CommentNewCountRework( + comments = postView.counts.comments, + unreadCount = postView.unread_comments, + account = account, + modifier = Modifier.weight(1F, true), + ) + } + VoteScore( + instantScores = instantScores, + onVoteClick = onUpvoteClick, + voteDisplayMode = voteDisplayMode, + account = account, + ) + UpvotePercentage( + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, + account = account, + ) + VoteGeneric( + instantScores = instantScores, + type = VoteType.Upvote, + onVoteClick = onUpvoteClick, + voteDisplayMode = voteDisplayMode, + account = account, + ) + if (enableDownVotes) { + VoteGeneric( + instantScores = instantScores, + type = VoteType.Downvote, + onVoteClick = onDownvoteClick, + voteDisplayMode = voteDisplayMode, + account = account, + ) + } + + if (postActionBarMode == PostActionBarMode.Long) { + CommentNewCountRework( + comments = postView.counts.comments, + unreadCount = postView.unread_comments, + account = account, + modifier = Modifier.weight(1F, true), + ) + } + + if (showReply) { + ActionBarButton( + icon = Icons.AutoMirrored.Outlined.Comment, + contentDescription = stringResource(R.string.postListing_reply), + onClick = { onReplyClick(postView) }, + account = account, + ) + } + SavedButton( + saved = postView.saved, + account = account, + onSaveClick = { onSaveClick(postView) }, + ) + ActionBarButton( + icon = Icons.Outlined.MoreVert, + contentDescription = stringResource(R.string.moreOptions), + account = account, + onClick = { showMoreOptions = !showMoreOptions }, + requiresAccount = false, + modifier = if (postActionBarMode == PostActionBarMode.LeftHandShort) { + Modifier.weight( + 1F, + true, + ) + } else { + Modifier + }, + ) + + if (postActionBarMode == PostActionBarMode.LeftHandShort) { + CommentNewCountRework( + comments = postView.counts.comments, + unreadCount = postView.unread_comments, + account = account, + ) + } + } +} + +@Preview +@Composable +fun PostFooterLinePreview() { + PostFooterLine( + postView = samplePostView, + admins = emptyList(), + moderators = emptyList(), + instantScores = sampleInstantScores, + voteDisplayMode = LocalUserVoteDisplayMode.default(), + onUpvoteClick = {}, + onDownvoteClick = {}, + onReplyClick = {}, + onSaveClick = {}, + onEditPostClick = {}, + onDeletePostClick = {}, + onHidePostClick = {}, + onReportClick = {}, + onRemoveClick = {}, + onBanPersonClick = {}, + onBanFromCommunityClick = {}, + onLockPostClick = {}, + onFeaturePostClick = {}, + onViewVotesClick = {}, + onCommunityClick = {}, + onPersonClick = {}, + onViewSourceClick = {}, + account = AnonAccount, + enableDownVotes = true, + viewSource = false, + postActionBarMode = PostActionBarMode.Long, + fromPostActivity = true, + scope = rememberCoroutineScope(), + ) +} + +@ExperimentalLayoutApi +@Composable +fun PostCommunityAndCreatorBlock( + postView: PostView, + onCommunityClick: (community: Community) -> Unit, + onPersonClick: (personId: PersonId) -> Unit, + modifier: Modifier = Modifier, + showCommunityName: Boolean = true, + showAvatar: Boolean, + fullBody: Boolean, + blurNSFW: BlurNSFW, +) { + FlowRow( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier.fillMaxWidth(), + ) { + val centerMod = Modifier.align(Alignment.CenterVertically) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(SMALLER_PADDING, Alignment.Start), + ) { + if (showCommunityName && showAvatar) { + CommunityIcon( + modifier = centerMod.padding(end = SMALL_PADDING), + community = postView.community, + onCommunityClick = onCommunityClick, + blurNSFW = blurNSFW, + ) + } + if (showCommunityName) { + CommunityName( + community = postView.community, + modifier = centerMod, + onClick = { onCommunityClick(postView.community) }, + ) + Text( + text = stringResource(R.string.by), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.outline, + modifier = centerMod, + ) + } + PersonProfileLink( + person = postView.creator, + onClick = onPersonClick, + showTags = fullBody, + // Set this to false, we already know this + isPostCreator = false, + isCommunityBanned = postView.creator_banned_from_community, + color = MaterialTheme.colorScheme.outline, + showAvatar = !showCommunityName && showAvatar, + modifier = centerMod, + ) + if (postView.post.featured_local) { + DotSpacer(modifier = centerMod) + Icon( + imageVector = Icons.Outlined.PushPin, + contentDescription = stringResource(R.string.postListing_featuredLocal), + tint = MaterialTheme.colorScheme.primary, + modifier = centerMod.size(ACTION_BAR_ICON_SIZE), + ) + } + if (postView.post.featured_community) { + DotSpacer(modifier = centerMod) + Icon( + imageVector = Icons.Outlined.PushPin, + contentDescription = stringResource(R.string.postListing_featuredCommunity), + tint = MaterialTheme.colorScheme.secondary, + modifier = centerMod.size(ACTION_BAR_ICON_SIZE), + ) + } + if (postView.post.locked) { + DotSpacer(modifier = centerMod) + Icon( + imageVector = Icons.Outlined.CommentsDisabled, + contentDescription = stringResource(R.string.postListing_locked), + tint = MaterialTheme.colorScheme.error, + modifier = centerMod.size(ACTION_BAR_ICON_SIZE), + ) + } + if (postView.post.deleted) { + DotSpacer(modifier = centerMod) + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = stringResource(R.string.postListing_deleted), + tint = MaterialTheme.colorScheme.error, + modifier = centerMod, + ) + } + if (postView.post.removed) { + DotSpacer(modifier = centerMod) + Icon( + imageVector = Icons.Outlined.Gavel, + contentDescription = stringResource(R.string.removed), + tint = MaterialTheme.colorScheme.error, + modifier = centerMod, + ) + } + } + TimeAgo( + published = postView.post.published, + updated = postView.post.updated, + modifier = centerMod, + ) + } +} + +@ExperimentalLayoutApi +@Preview +@Composable +fun PostCommunityAndCreatorPreview() { + val postView = sampleLinkPostView + PostCommunityAndCreatorBlock( + postView = postView, + onCommunityClick = {}, + onPersonClick = {}, + showAvatar = true, + blurNSFW = BlurNSFW.NSFW, + fullBody = true, + ) +} + +@Composable +fun CommunityIcon( + modifier: Modifier = Modifier, + community: Community, + onCommunityClick: (community: Community) -> Unit, + blurNSFW: BlurNSFW, +) { + community.icon?.let { + CircularIcon( + icon = it, + contentDescription = stringResource(R.string.postListing_goToCommunity), + modifier = modifier.clickable { onCommunityClick(community) }, + blur = blurNSFW.needBlur(community.nsfw), + ) + } +} + +@ExperimentalLayoutApi +@Composable +fun PostTitleAttributionBody( + postView: PostView, + fullBody: Boolean, + viewSource: Boolean, + expandedImage: Boolean, + account: Account, + useCustomTabs: Boolean, + usePrivateTabs: Boolean, + showPostLinkPreview: Boolean, + appState: JerboaAppState, + clickBody: () -> Unit = {}, + onCommunityClick: (community: Community) -> Unit, + onPersonClick: (personId: PersonId) -> Unit, + showIfRead: Boolean, + blurNSFW: BlurNSFW, + showCommunityName: Boolean, + showAvatar: Boolean, +) { + Column( + verticalArrangement = Arrangement.spacedBy(VERTICAL_SPACING), + ) { + PostCommunityAndCreatorBlock( + postView = postView, + onCommunityClick = onCommunityClick, + onPersonClick = onPersonClick, + showCommunityName = showCommunityName, + modifier = Modifier.padding(horizontal = MEDIUM_PADDING), + showAvatar = showAvatar, + fullBody = fullBody, + blurNSFW = blurNSFW, + ) + + PostTitleBlock( + postView = postView, + expandedImage = expandedImage, + account = account, + useCustomTabs = useCustomTabs, + usePrivateTabs = usePrivateTabs, + appState = appState, + showIfRead = showIfRead, + blurNSFW = blurNSFW, + ) + + // The metadata card + if (fullBody && showPostLinkPreview) { + MetadataCard(post = postView.post) + + if (postView.post.url?.startsWith("magnet") == true) { + TorrentHelpInfo() + } + } + + PostBody( + body = postView.post.body, + fullBody = fullBody, + viewSource = viewSource, + clickBody = clickBody, + ) + } +} + +@Composable +fun PostBody( + body: String?, + fullBody: Boolean, + viewSource: Boolean, + clickBody: () -> Unit, +) { + // Check to make sure body isn't empty string + val bodyTrimmed = body + ?.trim() + ?.ifEmpty { null } + + // The desc + bodyTrimmed?.also { text -> + if (fullBody) { + Column( + modifier = + Modifier + .padding(horizontal = MEDIUM_PADDING), + ) { + if (viewSource) { + SelectionContainer { + Text( + text = text, + fontFamily = FontFamily.Monospace, + ) + } + } else { + MyMarkdownText( + markdown = text, + onClick = {}, + ) + } + } + } else { + val bottomFade = Brush.verticalGradient(.7f to MaterialTheme.colorScheme.background, 1f to Color.Transparent) + CreateMarkdownPreview( + markdown = text, + color = LocalContentColor.current, + onClick = clickBody, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .padding(horizontal = MEDIUM_PADDING) + .fadingEdge(bottomFade), + ) + } + } +} + +@ExperimentalLayoutApi +@Preview +@Composable +fun PreviewStoryTitleAndMetadata() { + PostTitleAttributionBody( + postView = samplePostView, + fullBody = false, + viewSource = false, + expandedImage = false, + account = AnonAccount, + useCustomTabs = false, + usePrivateTabs = false, + showPostLinkPreview = true, + appState = rememberJerboaAppState(), + showIfRead = true, + blurNSFW = BlurNSFW.NSFW, + onPersonClick = {}, + onCommunityClick = {}, + showAvatar = true, + showCommunityName = true, + ) +} + +@ExperimentalLayoutApi +@Preview +@Composable +fun PreviewSourcePost() { + val pv = sampleMarkdownPostView + PostTitleAttributionBody( + postView = pv, + fullBody = true, + viewSource = true, + expandedImage = false, + account = AnonAccount, + useCustomTabs = false, + usePrivateTabs = false, + showPostLinkPreview = true, + appState = rememberJerboaAppState(), + showIfRead = true, + blurNSFW = BlurNSFW.NSFW, + onPersonClick = {}, + onCommunityClick = {}, + showAvatar = true, + showCommunityName = true, + ) +} + +@Composable +fun SavedButton( + saved: Boolean, + account: Account, + onSaveClick: () -> Unit, +) { + ActionBarButton( + icon = if (saved) { + Icons.Filled.Bookmark + } else { + Icons.Outlined.BookmarkBorder + }, + contentDescription = if (saved) { + stringResource(R.string.removeBookmark) + } else { + stringResource(R.string.addBookmark) + }, + onClick = onSaveClick, + contentColor = if (saved) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.outline + }, + account = account, + ) +} + +@ExperimentalLayoutApi +@Composable +fun PostTitleBlock( + postView: PostView, + expandedImage: Boolean, + account: Account, + useCustomTabs: Boolean, + usePrivateTabs: Boolean, + appState: JerboaAppState, + showIfRead: Boolean, + blurNSFW: BlurNSFW, +) { + val imagePost = postView.post.url?.let { getPostType(it) == PostType.Image } ?: false + + if (imagePost && expandedImage) { + PostTitleAndImageLink( + postView = postView, + appState = appState, + showIfRead = showIfRead, + blurNSFW = blurNSFW, + ) + } else { + PostTitleAndThumbnail( + postView = postView, + account = account, + useCustomTabs = useCustomTabs, + usePrivateTabs = usePrivateTabs, + appState = appState, + showIfRead = showIfRead, + blurNSFW = blurNSFW, + ) + } +} + +@ExperimentalLayoutApi +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun PostTitleAndImageLink( + postView: PostView, + appState: JerboaAppState, + showIfRead: Boolean, + blurNSFW: BlurNSFW, +) { + // This was tested, we know it exists + val url = postView.post.url?.toHttps() + + // Title of the post + PostName( + post = postView.post, + read = postView.read, + showIfRead = showIfRead, + modifier = Modifier.padding(horizontal = MEDIUM_PADDING), + ) + + url?.let { cUrl -> + PictrsUrlImage( + url = cUrl, + blur = blurNSFW.needBlur(postView), + contentDescription = postView.post.alt_text, + modifier = + Modifier + .combinedClickable( + onClick = { appState.openImageViewer(cUrl) }, + onLongClick = { appState.showLinkPopup(cUrl) }, + ), + ) + } +} + +@ExperimentalLayoutApi +@Composable +fun PostTitleAndThumbnail( + postView: PostView, + account: Account, + useCustomTabs: Boolean, + usePrivateTabs: Boolean, + appState: JerboaAppState, + showIfRead: Boolean, + blurNSFW: BlurNSFW, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(SMALL_PADDING), + modifier = Modifier.padding(horizontal = MEDIUM_PADDING), + ) { + // Title of the post + Column( + verticalArrangement = Arrangement.spacedBy(VERTICAL_SPACING), + modifier = Modifier.weight(1f), + ) { + PostName( + post = postView.post, + read = postView.read, + showIfRead = showIfRead, + ) + HostnameSimplified(postView.post.url, account) + } + ThumbnailTile( + post = postView.post, + useCustomTabs = useCustomTabs, + usePrivateTabs = usePrivateTabs, + blurEnabled = blurNSFW.needBlur(postView), + appState = appState, + ) + } +} + +@Composable +fun HostnameSimplified( + url: String?, + account: Account, +) { + url?.also { postUrl -> + if (!isSameInstance(postUrl, account.instance)) { + val hostName = hostNameCleaned(postUrl) + hostName?.also { + Text( + text = it, + color = MaterialTheme.colorScheme.outline, + style = MaterialTheme.typography.labelMedium, + fontFamily = FontFamily.Monospace, + ) + } + } + } +} + +@Composable +fun CommentNewCountRework( + comments: Long, + unreadCount: Long, + account: Account, + modifier: Modifier = Modifier, +) { + val unread = + if (unreadCount == 0L || comments == unreadCount) { + null + } else { + (if (unreadCount > 0) "+" else "") + siFormat(unreadCount) + } + + ActionBarButtonAndBadge( + icon = Icons.Outlined.ChatBubbleOutline, + iconBadgeCount = unread, + contentDescription = null, + text = siFormat(comments), + noClick = true, + account = account, + onClick = {}, + modifier = modifier, + ) +} + +@Preview +@Composable +fun CommentCountPreview() { + CommentNewCountRework(42, 0, account = AnonAccount) +} diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListingList.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListingList.kt new file mode 100644 index 000000000..7f269b3fb --- /dev/null +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListingList.kt @@ -0,0 +1,312 @@ +package com.jerboa.ui.components.post + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.jerboa.JerboaAppState +import com.jerboa.R +import com.jerboa.datatypes.sampleInstantScores +import com.jerboa.datatypes.samplePostView +import com.jerboa.db.entity.Account +import com.jerboa.db.entity.AnonAccount +import com.jerboa.feat.BlurNSFW +import com.jerboa.feat.InstantScores +import com.jerboa.feat.VoteType +import com.jerboa.feat.default +import com.jerboa.feat.needBlur +import com.jerboa.hostNameCleaned +import com.jerboa.isSameInstance +import com.jerboa.nsfwCheck +import com.jerboa.rememberJerboaAppState +import com.jerboa.ui.components.common.DotSpacer +import com.jerboa.ui.components.common.NsfwBadge +import com.jerboa.ui.components.common.TimeAgo +import com.jerboa.ui.components.common.VoteGeneric +import com.jerboa.ui.components.common.scoreColor +import com.jerboa.ui.components.community.CommunityLink +import com.jerboa.ui.components.person.PersonProfileLink +import com.jerboa.ui.theme.MEDIUM_PADDING +import com.jerboa.ui.theme.SMALLER_PADDING +import com.jerboa.ui.theme.SMALL_PADDING +import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode +import it.vercruysse.lemmyapi.datatypes.PostView + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PostListingList( + postView: PostView, + instantScores: InstantScores, + onUpvoteClick: () -> Unit, + onDownvoteClick: () -> Unit, + onPostClick: (postView: PostView) -> Unit, + showCommunityName: Boolean = true, + account: Account, + showVotingArrowsInListView: Boolean, + useCustomTabs: Boolean, + usePrivateTabs: Boolean, + blurNSFW: BlurNSFW, + appState: JerboaAppState, + showIfRead: Boolean, + enableDownVotes: Boolean, + voteDisplayMode: LocalUserVoteDisplayMode, +) { + Column( + modifier = + Modifier + .padding( + horizontal = MEDIUM_PADDING, + vertical = MEDIUM_PADDING, + ).testTag("jerboa:post"), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = + Arrangement.spacedBy( + SMALL_PADDING, + ), + ) { + if (showVotingArrowsInListView) { + PostVotingTile( + instantScores = instantScores, + onUpvoteClick = onUpvoteClick, + onDownvoteClick = onDownvoteClick, + account = account, + enableDownVotes = enableDownVotes, + voteDisplayMode = voteDisplayMode, + ) + } + Column( + modifier = + Modifier + .weight(1f) + .clickable { onPostClick(postView) }, + verticalArrangement = Arrangement.spacedBy(SMALL_PADDING), + ) { + PostName(post = postView.post, read = postView.read, showIfRead = showIfRead) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(SMALLER_PADDING, Alignment.Start), + ) { + // You must use a center align modifier for each of these + val centerMod = Modifier.align(Alignment.CenterVertically) + if (showCommunityName) { + CommunityLink( + community = postView.community, + onClick = {}, + clickable = false, + showDefaultIcon = false, + showAvatar = false, + blurNSFW = blurNSFW, + modifier = centerMod, + ) + DotSpacer(modifier = centerMod) + } + PersonProfileLink( + person = postView.creator, + onClick = {}, + clickable = false, + color = MaterialTheme.colorScheme.outline, + showAvatar = false, + modifier = centerMod, + ) + DotSpacer(modifier = centerMod) + postView.post.url?.also { postUrl -> + if (!isSameInstance(postUrl, account.instance)) { + val hostName = hostNameCleaned(postUrl) + hostName?.also { + Text( + text = it, + color = MaterialTheme.colorScheme.outline, + style = MaterialTheme.typography.labelMedium, + fontFamily = FontFamily.Monospace, + modifier = centerMod, + ) + DotSpacer(modifier = centerMod) + } + } + } + TimeAgo( + published = postView.post.published, + updated = postView.post.updated, + modifier = centerMod, + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(SMALLER_PADDING), + verticalAlignment = Alignment.CenterVertically, + ) { + if (!showVotingArrowsInListView) { + Text( + text = instantScores.score.toString(), + style = MaterialTheme.typography.bodyMedium, + color = scoreColor(myVote = instantScores.myVote), + ) + DotSpacer() + } + Text( + text = + stringResource( + R.string.post_listing_comments_count, + postView.counts.comments, + ), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.outline, + ) + CommentNewCount( + comments = postView.counts.comments, + unreadCount = postView.unread_comments, + style = MaterialTheme.typography.labelMedium, + ) + NsfwBadge(visible = nsfwCheck(postView)) + } + } + ThumbnailTile( + post = postView.post, + useCustomTabs = useCustomTabs, + usePrivateTabs = usePrivateTabs, + blurEnabled = blurNSFW.needBlur(postView), + appState = appState, + ) + } + } +} + +@Preview +@Composable +fun PostListingListPreview() { + PostListingList( + postView = samplePostView, + instantScores = sampleInstantScores, + onUpvoteClick = {}, + onDownvoteClick = {}, + onPostClick = {}, + account = AnonAccount, + showVotingArrowsInListView = true, + useCustomTabs = false, + usePrivateTabs = false, + blurNSFW = BlurNSFW.NSFW, + appState = rememberJerboaAppState(), + showIfRead = true, + enableDownVotes = false, + voteDisplayMode = LocalUserVoteDisplayMode.default(), + ) +} + +@Preview +@Composable +fun PostListingListWithThumbPreview() { + PostListingList( + postView = samplePostView, + instantScores = sampleInstantScores, + onUpvoteClick = {}, + onDownvoteClick = {}, + onPostClick = {}, + account = AnonAccount, + showVotingArrowsInListView = true, + useCustomTabs = false, + usePrivateTabs = false, + blurNSFW = BlurNSFW.NSFW, + appState = rememberJerboaAppState(), + showIfRead = true, + enableDownVotes = false, + voteDisplayMode = LocalUserVoteDisplayMode.default(), + ) +} + +@Composable +fun CommentNewCount( + comments: Long, + unreadCount: Long, + style: TextStyle = MaterialTheme.typography.labelSmall.copy(fontStyle = FontStyle.Italic), + spacing: Dp = 0.dp, +) { + val unread = + if (unreadCount == 0L || comments == unreadCount) { + null + } else { + unreadCount + } + if (unread != null) { + Spacer(Modifier.padding(horizontal = spacing)) + + Text( + text = stringResource(R.string.post_listing_new, unread), + style = style, + color = MaterialTheme.colorScheme.outline, + ) + } +} + +@Composable +fun PostVotingTile( + instantScores: InstantScores, + onUpvoteClick: () -> Unit, + onDownvoteClick: () -> Unit, + account: Account, + enableDownVotes: Boolean, + voteDisplayMode: LocalUserVoteDisplayMode, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(SMALL_PADDING), + modifier = + Modifier + .fillMaxHeight() + .padding(end = MEDIUM_PADDING), + ) { + VoteGeneric( + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, + type = VoteType.Upvote, + onVoteClick = onUpvoteClick, + account = account, + ) + + val scoreOrPctStr = instantScores.scoreOrPctStr(voteDisplayMode) + + Text( + text = scoreOrPctStr ?: "", + style = MaterialTheme.typography.bodyMedium, + color = scoreColor(myVote = instantScores.myVote), + // Hide the vote number if its + modifier = Modifier.alpha(if (scoreOrPctStr != null) 1f else 0f), + ) + + if (enableDownVotes) { + // invisible Text below aligns width of PostVotingTiles + Text( + text = "00000", + modifier = Modifier.height(0.dp), + style = MaterialTheme.typography.bodyMedium, + ) + VoteGeneric( + instantScores = instantScores, + voteDisplayMode = voteDisplayMode, + type = VoteType.Downvote, + onVoteClick = onDownvoteClick, + account = account, + ) + } + } +} diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt b/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt index 24e207cc5..e55b4dc3f 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostListings.kt @@ -1,5 +1,6 @@ package com.jerboa.ui.components.post +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -37,6 +38,7 @@ import it.vercruysse.lemmyapi.datatypes.PersonView import it.vercruysse.lemmyapi.datatypes.PostId import it.vercruysse.lemmyapi.datatypes.PostView +@OptIn(ExperimentalLayoutApi::class) @Composable fun PostListings( posts: List, diff --git a/app/src/main/java/com/jerboa/ui/components/post/PostScreen.kt b/app/src/main/java/com/jerboa/ui/components/post/PostScreen.kt index 2a55ff5f5..c0e851ded 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/PostScreen.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/PostScreen.kt @@ -4,6 +4,7 @@ import android.util.Log import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -109,6 +110,7 @@ object PostViewReturn { const val POST_VIEW = "post-view::return(post-view)" } +@ExperimentalLayoutApi @OptIn( ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class, diff --git a/app/src/main/java/com/jerboa/ui/components/reports/CommentReportItem.kt b/app/src/main/java/com/jerboa/ui/components/reports/CommentReportItem.kt index 78b9a9c9f..adb2263a2 100644 --- a/app/src/main/java/com/jerboa/ui/components/reports/CommentReportItem.kt +++ b/app/src/main/java/com/jerboa/ui/components/reports/CommentReportItem.kt @@ -8,8 +8,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.jerboa.datatypes.sampleCommentReportView -import com.jerboa.feat.InstantScores -import com.jerboa.feat.default import com.jerboa.ui.components.comment.CommentBody import com.jerboa.ui.components.comment.CommentNodeHeader import com.jerboa.ui.theme.MEDIUM_PADDING @@ -17,7 +15,6 @@ import com.jerboa.ui.theme.SMALL_PADDING import it.vercruysse.lemmyapi.datatypes.CommentId import it.vercruysse.lemmyapi.datatypes.CommentReportView import it.vercruysse.lemmyapi.datatypes.CommentView -import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode import it.vercruysse.lemmyapi.datatypes.PersonId import it.vercruysse.lemmyapi.datatypes.ResolveCommentReport import it.vercruysse.lemmyapi.dto.SubscribedType @@ -29,7 +26,6 @@ fun CommentReportItem( onPersonClick: (PersonId) -> Unit, onCommentClick: (CommentId) -> Unit, showAvatar: Boolean, - voteDisplayMode: LocalUserVoteDisplayMode, ) { // Build a comment-view using the content at the time it was reported, // not the current state. @@ -65,15 +61,8 @@ fun CommentReportItem( // Don't use the full CommentNode, as you don't need any of the actions there CommentNodeHeader( commentView = commentView, - instantScores = InstantScores( - myVote = commentView.my_vote, - score = commentView.counts.score, - upvotes = commentView.counts.upvotes, - downvotes = commentView.counts.downvotes, - ), onPersonClick = onPersonClick, showAvatar = showAvatar, - voteDisplayMode = voteDisplayMode, collapsedCommentsCount = 0, isExpanded = true, onClick = { onCommentClick(commentView.comment.id) }, @@ -124,6 +113,5 @@ fun CommentReportItemPreview() { onResolveClick = {}, onCommentClick = {}, showAvatar = false, - voteDisplayMode = LocalUserVoteDisplayMode.default(), ) } diff --git a/app/src/main/java/com/jerboa/ui/components/reports/PostReportItem.kt b/app/src/main/java/com/jerboa/ui/components/reports/PostReportItem.kt index 3924c0d40..706dbd820 100644 --- a/app/src/main/java/com/jerboa/ui/components/reports/PostReportItem.kt +++ b/app/src/main/java/com/jerboa/ui/components/reports/PostReportItem.kt @@ -2,37 +2,30 @@ package com.jerboa.ui.components.reports import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.jerboa.JerboaAppState import com.jerboa.datatypes.samplePostReportView -import com.jerboa.db.entity.Account -import com.jerboa.db.entity.AnonAccount import com.jerboa.feat.BlurNSFW -import com.jerboa.feat.InstantScores -import com.jerboa.feat.default -import com.jerboa.feat.needBlur -import com.jerboa.rememberJerboaAppState -import com.jerboa.ui.components.post.PostBody -import com.jerboa.ui.components.post.PostHeaderLine +import com.jerboa.ui.components.post.PostCommunityAndCreatorBlock +import com.jerboa.ui.components.post.PostName import com.jerboa.ui.theme.MEDIUM_PADDING import com.jerboa.ui.theme.SMALL_PADDING +import com.jerboa.ui.theme.VERTICAL_SPACING import it.vercruysse.lemmyapi.datatypes.Community -import it.vercruysse.lemmyapi.datatypes.LocalUserVoteDisplayMode import it.vercruysse.lemmyapi.datatypes.PersonId import it.vercruysse.lemmyapi.datatypes.PostReportView import it.vercruysse.lemmyapi.datatypes.PostView import it.vercruysse.lemmyapi.datatypes.ResolvePostReport import it.vercruysse.lemmyapi.dto.SubscribedType +@OptIn(ExperimentalLayoutApi::class) @Composable fun PostReportItem( - appState: JerboaAppState, postReportView: PostReportView, onResolveClick: (ResolvePostReport) -> Unit, onPersonClick: (PersonId) -> Unit, @@ -40,8 +33,6 @@ fun PostReportItem( onCommunityClick: (Community) -> Unit, showAvatar: Boolean, blurNSFW: BlurNSFW, - voteDisplayMode: LocalUserVoteDisplayMode, - account: Account, ) { // Build a post-view using the content at the time it was reported, // not the current state. @@ -82,47 +73,27 @@ fun PostReportItem( // need any of the actions there // Need to make this clickable - Box( + Column( + verticalArrangement = Arrangement.spacedBy(VERTICAL_SPACING), modifier = Modifier .clickable { onPostClick(postView) }, ) { - PostHeaderLine( - post = postView.post, - creator = postView.creator, - community = postView.community, - creatorBannedFromCommunity = postView.creator_banned_from_community, - instantScores = InstantScores( - myVote = postView.my_vote, - score = postView.counts.score, - upvotes = postView.counts.upvotes, - downvotes = postView.counts.downvotes, - ), + PostCommunityAndCreatorBlock( + postView = postView, onCommunityClick = onCommunityClick, onPersonClick = onPersonClick, showCommunityName = true, showAvatar = showAvatar, blurNSFW = blurNSFW, - voteDisplayMode = voteDisplayMode, fullBody = false, ) - } - // Title + metadata - PostBody( - post = postView.post, - read = postView.read, - fullBody = false, - viewSource = false, - expandedImage = false, - account = account, - useCustomTabs = false, - usePrivateTabs = false, - blurEnabled = blurNSFW.needBlur(postView), - showPostLinkPreview = true, - appState = appState, - clickBody = { onPostClick(postView) }, - showIfRead = true, - ) + PostName( + post = postView.post, + read = postView.read, + showIfRead = false, + ) + } ReportCreatorBlock(postReportView.creator, onPersonClick, showAvatar) @@ -163,8 +134,5 @@ fun PostReportItemPreview() { onResolveClick = {}, showAvatar = false, blurNSFW = BlurNSFW.NSFW, - voteDisplayMode = LocalUserVoteDisplayMode.default(), - account = AnonAccount, - appState = rememberJerboaAppState(), ) } diff --git a/app/src/main/java/com/jerboa/ui/components/reports/ReportsScreen.kt b/app/src/main/java/com/jerboa/ui/components/reports/ReportsScreen.kt index afaf9fb40..172011f98 100644 --- a/app/src/main/java/com/jerboa/ui/components/reports/ReportsScreen.kt +++ b/app/src/main/java/com/jerboa/ui/components/reports/ReportsScreen.kt @@ -242,11 +242,8 @@ fun ReportsTabs( contentType = { "postReport" }, ) { reportView -> PostReportItem( - appState = appState, postReportView = reportView, - account = account, blurNSFW = blurNSFW, - voteDisplayMode = siteViewModel.voteDisplayMode(), showAvatar = siteViewModel.showAvatar(), onCommunityClick = { community -> appState.toCommunity(id = community.id) @@ -333,7 +330,6 @@ fun ReportsTabs( ) { reportView -> CommentReportItem( commentReportView = reportView, - voteDisplayMode = siteViewModel.voteDisplayMode(), showAvatar = siteViewModel.showAvatar(), onPersonClick = appState::toProfile, onCommentClick = appState::toComment, diff --git a/app/src/main/java/com/jerboa/ui/theme/Sizes.kt b/app/src/main/java/com/jerboa/ui/theme/Sizes.kt index 03f5c1e8c..d34af0a6e 100644 --- a/app/src/main/java/com/jerboa/ui/theme/Sizes.kt +++ b/app/src/main/java/com/jerboa/ui/theme/Sizes.kt @@ -4,7 +4,7 @@ import androidx.compose.ui.unit.dp const val DEFAULT_FONT_SIZE = 16 -val ACTION_BAR_ICON_SIZE = 16.dp +val ACTION_BAR_ICON_SIZE = 12.dp val MARKDOWN_BAR_ICON_SIZE = 24.dp val BORDER_WIDTH = 1.dp @@ -16,7 +16,6 @@ val XL_PADDING = 16.dp val XXL_PADDING = 20.dp val ICON_SIZE = 20.dp -val MEDIUM_ICON_SIZE = 48.dp val LARGER_ICON_SIZE = 80.dp val DRAWER_BANNER_SIZE = 96.dp val PROFILE_BANNER_SIZE = 128.dp @@ -26,6 +25,8 @@ val THUMBNAIL_CARET_SIZE = 10.dp val DRAWER_ITEM_SPACING = 24.dp +val VERTICAL_SPACING = MEDIUM_PADDING + // TODO remove all DPs from code, put here. const val ICON_THUMBNAIL_SIZE = 96 const val LARGER_ICON_THUMBNAIL_SIZE = 256 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aca5b3bab..fe6b0aa94 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -494,4 +494,6 @@ You have no blocked communities You have no blocked instances Lemmy can use a technology called BitTorrent to view or stream media. [Click here](%1$s) to learn how. + by + Upvote Percentage