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

View File

@ -54,7 +54,7 @@ fun PostCard(
PostCardTitle(post) PostCardTitle(post)
PostCardSubtitle( PostCardSubtitle(
community = post.community, community = post.community,
creator = post.creator, creator = post.creator?.copy(avatar = null),
onOpenCommunity = onOpenCommunity, onOpenCommunity = onOpenCommunity,
onOpenCreator = onOpenCreator, 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons 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.Bookmark
import androidx.compose.material.icons.filled.BookmarkBorder import androidx.compose.material.icons.filled.BookmarkBorder
import androidx.compose.material.icons.filled.Chat import androidx.compose.material.icons.filled.Chat
import androidx.compose.material.icons.filled.ThumbDown import androidx.compose.material.icons.outlined.ArrowCircleDown
import androidx.compose.material.icons.filled.ThumbUp import androidx.compose.material.icons.outlined.ArrowCircleUp
import androidx.compose.material.icons.outlined.ThumbDown
import androidx.compose.material.icons.outlined.ThumbUp
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -80,9 +80,9 @@ fun PostCardFooter(
onUpVote?.invoke() onUpVote?.invoke()
}, },
imageVector = if (upVoted) { imageVector = if (upVoted) {
Icons.Filled.ThumbUp Icons.Filled.ArrowCircleUp
} else { } else {
Icons.Outlined.ThumbUp Icons.Outlined.ArrowCircleUp
}, },
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( colorFilter = ColorFilter.tint(
@ -101,9 +101,9 @@ fun PostCardFooter(
onDownVote?.invoke() onDownVote?.invoke()
}, },
imageVector = if (downVoted) { imageVector = if (downVoted) {
Icons.Filled.ThumbDown Icons.Filled.ArrowCircleDown
} else { } else {
Icons.Outlined.ThumbDown Icons.Outlined.ArrowCircleDown
}, },
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( 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.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -45,12 +44,12 @@ fun PostCardSubtitle(
if (communityName.isNotEmpty()) { if (communityName.isNotEmpty()) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxHeight()
.onClick { .onClick {
if (community != null) { if (community != null) {
onOpenCommunity?.invoke(community) onOpenCommunity?.invoke(community)
} }
}, },
horizontalArrangement = Arrangement.spacedBy(Spacing.xs),
) { ) {
if (communityIcon.isNotEmpty()) { if (communityIcon.isNotEmpty()) {
val painterResource = asyncPainterResource(data = communityIcon) val painterResource = asyncPainterResource(data = communityIcon)
@ -65,6 +64,7 @@ fun PostCardSubtitle(
) )
} }
Text( Text(
modifier = Modifier.padding(vertical = Spacing.xs),
text = buildString { text = buildString {
append(communityName) append(communityName)
if (communityHost.isNotEmpty()) { if (communityHost.isNotEmpty()) {
@ -78,12 +78,12 @@ fun PostCardSubtitle(
if (creatorName.isNotEmpty()) { if (creatorName.isNotEmpty()) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxHeight()
.onClick { .onClick {
if (creator != null) { if (creator != null) {
onOpenCreator?.invoke(creator) onOpenCreator?.invoke(creator)
} }
}, },
horizontalArrangement = Arrangement.spacedBy(Spacing.xs),
) { ) {
if (communityName.isNotEmpty()) { if (communityName.isNotEmpty()) {
Text( Text(
@ -105,6 +105,7 @@ fun PostCardSubtitle(
} }
} }
Text( Text(
modifier = Modifier.padding(vertical = Spacing.xs),
text = buildString { text = buildString {
append(creatorName) append(creatorName)
if (creatorHost.isNotEmpty() && communityHost != creatorHost) { 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.SwipeToDismiss
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ThumbDown import androidx.compose.material.icons.filled.ArrowCircleDown
import androidx.compose.material.icons.filled.ThumbUp import androidx.compose.material.icons.filled.ArrowCircleUp
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
@ -110,7 +110,7 @@ class PostDetailScreen(
PostCardTitle(post) PostCardTitle(post)
PostCardSubtitle( PostCardSubtitle(
community = post.community, community = post.community,
creator = post.creator, creator = post.creator?.copy(avatar = null),
onOpenCommunity = { community -> onOpenCommunity = { community ->
navigator.push( navigator.push(
CommunityDetailScreen( CommunityDetailScreen(
@ -236,8 +236,8 @@ class PostDetailScreen(
DismissDirection.EndToStart -> Alignment.CenterEnd DismissDirection.EndToStart -> Alignment.CenterEnd
} }
val icon = when (direction) { val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.ThumbDown DismissDirection.StartToEnd -> Icons.Default.ArrowCircleDown
DismissDirection.EndToStart -> Icons.Default.ThumbUp DismissDirection.EndToStart -> Icons.Default.ArrowCircleUp
} }
Box( Box(

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.comments 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.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.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.rememberDismissState
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier 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.dp
import androidx.compose.ui.unit.toSize
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
@ -55,8 +74,11 @@ internal class UserDetailCommentsScreen(
Box( Box(
modifier = modifier.pullRefresh(pullRefreshState), modifier = modifier.pullRefresh(pullRefreshState),
) { ) {
var width by remember { mutableStateOf(0f) }
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize().onGloballyPositioned {
width = it.size.toSize().width
},
verticalArrangement = Arrangement.spacedBy(Spacing.xs), verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) { ) {
item { item {
@ -76,6 +98,89 @@ internal class UserDetailCommentsScreen(
} }
} }
items(uiState.comments, key = { it.id.toString() + it.myVote }) { comment -> items(uiState.comments, key = { it.id.toString() + it.myVote }) { comment ->
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( CommentCard(
comment = comment, comment = comment,
onSave = { onSave = {
@ -104,6 +209,7 @@ internal class UserDetailCommentsScreen(
}, },
) )
} }
}
item { item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) { if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(UserCommentsMviModel.Intent.LoadNextPage) model.reduce(UserCommentsMviModel.Intent.LoadNextPage)

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.posts 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.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.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.rememberDismissState
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Alignment
import androidx.compose.ui.Modifier 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.dp
import androidx.compose.ui.unit.toSize
import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@ -63,8 +82,11 @@ internal class UserDetailPostsScreen(
Box( Box(
modifier = modifier.pullRefresh(pullRefreshState), modifier = modifier.pullRefresh(pullRefreshState),
) { ) {
var width by remember { mutableStateOf(0f) }
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize().onGloballyPositioned {
width = it.size.toSize().width
},
verticalArrangement = Arrangement.spacedBy(Spacing.xs), verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) { ) {
item { item {
@ -84,6 +106,89 @@ internal class UserDetailPostsScreen(
} }
} }
items(uiState.posts, key = { it.id.toString() + it.myVote }) { post -> items(uiState.posts, key = { it.id.toString() + it.myVote }) { post ->
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( PostCard(
post = post, post = post,
onUpVote = { onUpVote = {
@ -122,6 +227,7 @@ internal class UserDetailPostsScreen(
}, },
) )
} }
}
item { item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) { if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(UserPostsMviModel.Intent.LoadNextPage) model.reduce(UserPostsMviModel.Intent.LoadNextPage)

View File

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