feat(user): add swipe actions to user detail

This commit is contained in:
Diego Beraldin 2023-08-11 17:57:44 +02:00
parent 578176a449
commit 0ecb9fb42a
8 changed files with 300 additions and 87 deletions

View File

@ -22,8 +22,8 @@ import androidx.compose.material.Icon
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ThumbDown
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.ArrowCircleDown
import androidx.compose.material.icons.filled.ArrowCircleUp
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
@ -261,8 +261,8 @@ class CommunityDetailScreen(
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.ThumbDown
DismissDirection.EndToStart -> Icons.Default.ThumbUp
DismissDirection.StartToEnd -> Icons.Default.ArrowCircleDown
DismissDirection.EndToStart -> Icons.Default.ArrowCircleUp
}
Box(

View File

@ -54,7 +54,7 @@ fun PostCard(
PostCardTitle(post)
PostCardSubtitle(
community = post.community,
creator = post.creator,
creator = post.creator?.copy(avatar = null),
onOpenCommunity = onOpenCommunity,
onOpenCreator = onOpenCreator,
)

View File

@ -7,13 +7,13 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowCircleDown
import androidx.compose.material.icons.filled.ArrowCircleUp
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.BookmarkBorder
import androidx.compose.material.icons.filled.Chat
import androidx.compose.material.icons.filled.ThumbDown
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.outlined.ThumbDown
import androidx.compose.material.icons.outlined.ThumbUp
import androidx.compose.material.icons.outlined.ArrowCircleDown
import androidx.compose.material.icons.outlined.ArrowCircleUp
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -80,9 +80,9 @@ fun PostCardFooter(
onUpVote?.invoke()
},
imageVector = if (upVoted) {
Icons.Filled.ThumbUp
Icons.Filled.ArrowCircleUp
} else {
Icons.Outlined.ThumbUp
Icons.Outlined.ArrowCircleUp
},
contentDescription = null,
colorFilter = ColorFilter.tint(
@ -101,9 +101,9 @@ fun PostCardFooter(
onDownVote?.invoke()
},
imageVector = if (downVoted) {
Icons.Filled.ThumbDown
Icons.Filled.ArrowCircleDown
} else {
Icons.Outlined.ThumbDown
Icons.Outlined.ArrowCircleDown
},
contentDescription = null,
colorFilter = ColorFilter.tint(

View File

@ -2,7 +2,6 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
@ -45,12 +44,12 @@ fun PostCardSubtitle(
if (communityName.isNotEmpty()) {
Row(
modifier = Modifier
.fillMaxHeight()
.onClick {
if (community != null) {
onOpenCommunity?.invoke(community)
}
},
horizontalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
if (communityIcon.isNotEmpty()) {
val painterResource = asyncPainterResource(data = communityIcon)
@ -65,6 +64,7 @@ fun PostCardSubtitle(
)
}
Text(
modifier = Modifier.padding(vertical = Spacing.xs),
text = buildString {
append(communityName)
if (communityHost.isNotEmpty()) {
@ -78,12 +78,12 @@ fun PostCardSubtitle(
if (creatorName.isNotEmpty()) {
Row(
modifier = Modifier
.fillMaxHeight()
.onClick {
if (creator != null) {
onOpenCreator?.invoke(creator)
}
},
horizontalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
if (communityName.isNotEmpty()) {
Text(
@ -105,6 +105,7 @@ fun PostCardSubtitle(
}
}
Text(
modifier = Modifier.padding(vertical = Spacing.xs),
text = buildString {
append(creatorName)
if (creatorHost.isNotEmpty() && communityHost != creatorHost) {

View File

@ -19,8 +19,8 @@ import androidx.compose.material.Icon
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ThumbDown
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.ArrowCircleDown
import androidx.compose.material.icons.filled.ArrowCircleUp
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
@ -110,7 +110,7 @@ class PostDetailScreen(
PostCardTitle(post)
PostCardSubtitle(
community = post.community,
creator = post.creator,
creator = post.creator?.copy(avatar = null),
onOpenCommunity = { community ->
navigator.push(
CommunityDetailScreen(
@ -236,8 +236,8 @@ class PostDetailScreen(
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.ThumbDown
DismissDirection.EndToStart -> Icons.Default.ThumbUp
DismissDirection.StartToEnd -> Icons.Default.ArrowCircleDown
DismissDirection.EndToStart -> Icons.Default.ArrowCircleUp
}
Box(

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.comments
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -11,18 +13,35 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.Icon
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowCircleDown
import androidx.compose.material.icons.filled.ArrowCircleUp
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.rememberDismissState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
@ -55,8 +74,11 @@ internal class UserDetailCommentsScreen(
Box(
modifier = modifier.pullRefresh(pullRefreshState),
) {
var width by remember { mutableStateOf(0f) }
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize().onGloballyPositioned {
width = it.size.toSize().width
},
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
item {
@ -76,33 +98,117 @@ internal class UserDetailCommentsScreen(
}
}
items(uiState.comments, key = { it.id.toString() + it.myVote }) { comment ->
CommentCard(
comment = comment,
onSave = {
model.reduce(
UserCommentsMviModel.Intent.SaveComment(
comment = comment,
feedback = true,
),
)
},
onUpVote = {
model.reduce(
UserCommentsMviModel.Intent.UpVoteComment(
comment = comment,
feedback = true,
),
)
},
onDownVote = {
model.reduce(
UserCommentsMviModel.Intent.DownVoteComment(
comment = comment,
feedback = true,
),
)
val dismissState = rememberDismissState(
confirmStateChange = {
when (it) {
DismissValue.DismissedToEnd -> {
model.reduce(
UserCommentsMviModel.Intent.DownVoteComment(
comment = comment,
),
)
}
DismissValue.DismissedToStart -> {
model.reduce(
UserCommentsMviModel.Intent.UpVoteComment(
comment = comment,
),
)
}
else -> Unit
}
false
},
)
var willDismissDirection: DismissDirection? by remember {
mutableStateOf(null)
}
val threshold = 0.15f
LaunchedEffect(Unit) {
snapshotFlow { dismissState.offset.value }.collect {
willDismissDirection = when {
it > width * threshold -> DismissDirection.StartToEnd
it < -width * threshold -> DismissDirection.EndToStart
else -> null
}
}
}
LaunchedEffect(willDismissDirection) {
if (willDismissDirection != null) {
model.reduce(UserCommentsMviModel.Intent.HapticIndication)
}
}
SwipeToDismiss(
state = dismissState,
directions = setOf(
DismissDirection.StartToEnd,
DismissDirection.EndToStart,
),
dismissThresholds = {
FractionalThreshold(threshold)
},
background = {
val direction =
dismissState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue) {
DismissValue.Default -> Color.Transparent
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.secondary
DismissValue.DismissedToStart,
-> MaterialTheme.colorScheme.secondary
},
)
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.ArrowCircleDown
DismissDirection.EndToStart -> Icons.Default.ArrowCircleUp
}
Box(
Modifier.fillMaxSize().background(color)
.padding(horizontal = 20.dp),
contentAlignment = alignment,
) {
Icon(
imageVector = icon,
contentDescription = null,
)
}
},
) {
CommentCard(
comment = comment,
onSave = {
model.reduce(
UserCommentsMviModel.Intent.SaveComment(
comment = comment,
feedback = true,
),
)
},
onUpVote = {
model.reduce(
UserCommentsMviModel.Intent.UpVoteComment(
comment = comment,
feedback = true,
),
)
},
onDownVote = {
model.reduce(
UserCommentsMviModel.Intent.DownVoteComment(
comment = comment,
feedback = true,
),
)
},
)
}
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.posts
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -11,18 +13,35 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.Icon
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowCircleDown
import androidx.compose.material.icons.filled.ArrowCircleUp
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.rememberDismissState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
@ -63,8 +82,11 @@ internal class UserDetailPostsScreen(
Box(
modifier = modifier.pullRefresh(pullRefreshState),
) {
var width by remember { mutableStateOf(0f) }
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize().onGloballyPositioned {
width = it.size.toSize().width
},
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
item {
@ -84,43 +106,127 @@ internal class UserDetailPostsScreen(
}
}
items(uiState.posts, key = { it.id.toString() + it.myVote }) { post ->
PostCard(
post = post,
onUpVote = {
model.reduce(
UserPostsMviModel.Intent.UpVotePost(
post = post,
feedback = true,
),
)
},
onDownVote = {
model.reduce(
UserPostsMviModel.Intent.DownVotePost(
post = post,
feedback = true,
),
)
},
onSave = {
model.reduce(
UserPostsMviModel.Intent.SavePost(
post = post,
feedback = true,
),
)
},
onOpenCommunity = { community ->
navigator.push(
CommunityDetailScreen(
community = community,
onBack = {
navigator.pop()
},
),
)
val dismissState = rememberDismissState(
confirmStateChange = {
when (it) {
DismissValue.DismissedToEnd -> {
model.reduce(
UserPostsMviModel.Intent.DownVotePost(
post = post,
),
)
}
DismissValue.DismissedToStart -> {
model.reduce(
UserPostsMviModel.Intent.UpVotePost(
post = post,
),
)
}
else -> Unit
}
false
},
)
var willDismissDirection: DismissDirection? by remember {
mutableStateOf(null)
}
val threshold = 0.15f
LaunchedEffect(Unit) {
snapshotFlow { dismissState.offset.value }.collect {
willDismissDirection = when {
it > width * threshold -> DismissDirection.StartToEnd
it < -width * threshold -> DismissDirection.EndToStart
else -> null
}
}
}
LaunchedEffect(willDismissDirection) {
if (willDismissDirection != null) {
model.reduce(UserPostsMviModel.Intent.HapticIndication)
}
}
SwipeToDismiss(
state = dismissState,
directions = setOf(
DismissDirection.StartToEnd,
DismissDirection.EndToStart,
),
dismissThresholds = {
FractionalThreshold(threshold)
},
background = {
val direction =
dismissState.dismissDirection ?: return@SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue) {
DismissValue.Default -> Color.Transparent
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.secondary
DismissValue.DismissedToStart,
-> MaterialTheme.colorScheme.secondary
},
)
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.ArrowCircleDown
DismissDirection.EndToStart -> Icons.Default.ArrowCircleUp
}
Box(
Modifier.fillMaxSize().background(color)
.padding(horizontal = 20.dp),
contentAlignment = alignment,
) {
Icon(
imageVector = icon,
contentDescription = null,
)
}
},
) {
PostCard(
post = post,
onUpVote = {
model.reduce(
UserPostsMviModel.Intent.UpVotePost(
post = post,
feedback = true,
),
)
},
onDownVote = {
model.reduce(
UserPostsMviModel.Intent.DownVotePost(
post = post,
feedback = true,
),
)
},
onSave = {
model.reduce(
UserPostsMviModel.Intent.SavePost(
post = post,
feedback = true,
),
)
},
onOpenCommunity = { community ->
navigator.push(
CommunityDetailScreen(
community = community,
onBack = {
navigator.pop()
},
),
)
},
)
}
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {

View File

@ -17,8 +17,8 @@ import androidx.compose.material.FractionalThreshold
import androidx.compose.material.Icon
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ThumbDown
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.filled.ArrowCircleDown
import androidx.compose.material.icons.filled.ArrowCircleUp
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
@ -183,8 +183,8 @@ class PostListScreen : Screen {
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.ThumbDown
DismissDirection.EndToStart -> Icons.Default.ThumbUp
DismissDirection.StartToEnd -> Icons.Default.ArrowCircleDown
DismissDirection.EndToStart -> Icons.Default.ArrowCircleUp
}
Box(