fix: handle user profile navigation correctly

This commit is contained in:
Harsh Shandilya 2024-04-18 01:34:28 +05:30
parent 2b9680d3d8
commit d0bf2a4fb2
7 changed files with 80 additions and 14 deletions

View file

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Fixed a crash when clicking an item on the bottom navigation bar * Fixed a crash when clicking an item on the bottom navigation bar
too quickly too quickly
* Removed buggy deeplinks * Removed buggy deeplinks
* Clicking a username now correctly navigates to the right page in-app
## [1.44.0] - 2024-03-19 ## [1.44.0] - 2024-03-19

View file

@ -261,6 +261,11 @@ fun LobstersPostsScreen(
htmlConverter = htmlConverter, htmlConverter = htmlConverter,
getSeenComments = viewModel::getSeenComments, getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments, markSeenComments = viewModel::markSeenComments,
openUserProfile = {
navController.navigate(
Destinations.User.route.replace(Destinations.User.PLACEHOLDER, it)
)
},
) )
} }
composable( composable(
@ -272,7 +277,15 @@ fun LobstersPostsScreen(
"Navigating to ${Destinations.User.route} without necessary 'username' argument" "Navigating to ${Destinations.User.route} without necessary 'username' argument"
} }
setWebUri("https://lobste.rs/u/$username") setWebUri("https://lobste.rs/u/$username")
UserProfile(username = username, getProfile = viewModel::getUserProfile) UserProfile(
username = username,
getProfile = viewModel::getUserProfile,
openUserProfile = {
navController.navigate(
Destinations.User.route.replace(Destinations.User.PLACEHOLDER, it)
)
},
)
} }
composable(route = Destinations.Settings.route) { composable(route = Destinations.Settings.route) {
SettingsScreen( SettingsScreen(

View file

@ -67,6 +67,11 @@ fun SearchScreen(
htmlConverter = htmlConverter, htmlConverter = htmlConverter,
getSeenComments = viewModel::getSeenComments, getSeenComments = viewModel::getSeenComments,
markSeenComments = viewModel::markSeenComments, markSeenComments = viewModel::markSeenComments,
openUserProfile = { username: String ->
navController.navigate(
Destinations.User.route.replace(Destinations.User.PLACEHOLDER, username)
)
},
) )
} }
} }

View file

@ -29,7 +29,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
@ -56,9 +55,9 @@ internal fun CommentsHeader(
post: UIPost, post: UIPost,
postActions: PostActions, postActions: PostActions,
htmlConverter: HTMLConverter, htmlConverter: HTMLConverter,
openUserProfile: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val uriHandler = LocalUriHandler.current
val linkMetadata by val linkMetadata by
produceState(initialValue = LinkMetadata(post.url, null)) { produceState(initialValue = LinkMetadata(post.url, null)) {
runSuspendCatching { postActions.getLinkMetadata(post.url) } runSuspendCatching { postActions.getLinkMetadata(post.url) }
@ -93,8 +92,7 @@ internal fun CommentsHeader(
text = AnnotatedString("Submitted by ${post.submitter}"), text = AnnotatedString("Submitted by ${post.submitter}"),
avatarUrl = "https://lobste.rs/avatars/${post.submitter}-100.png", avatarUrl = "https://lobste.rs/avatars/${post.submitter}-100.png",
contentDescription = "User avatar for ${post.submitter}", contentDescription = "User avatar for ${post.submitter}",
modifier = modifier = Modifier.clickable { openUserProfile(post.submitter) },
Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${post.submitter}") },
) )
} }
} }
@ -133,9 +131,9 @@ internal fun CommentEntry(
commentNode: CommentNode, commentNode: CommentNode,
htmlConverter: HTMLConverter, htmlConverter: HTMLConverter,
toggleExpanded: (CommentNode) -> Unit, toggleExpanded: (CommentNode) -> Unit,
openUserProfile: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val uriHandler = LocalUriHandler.current
val comment = commentNode.comment val comment = commentNode.comment
Box( Box(
modifier = modifier =
@ -162,7 +160,7 @@ internal fun CommentEntry(
), ),
avatarUrl = "https://lobste.rs/avatars/${comment.user}-100.png", avatarUrl = "https://lobste.rs/avatars/${comment.user}-100.png",
contentDescription = "User avatar for ${comment.user}", contentDescription = "User avatar for ${comment.user}",
modifier = Modifier.clickable { uriHandler.openUri("https://lobste.rs/u/${comment.user}") }, modifier = Modifier.clickable { openUserProfile(comment.user) },
) )
if (commentNode.isExpanded) { if (commentNode.isExpanded) {
ThemedRichText( ThemedRichText(

View file

@ -84,9 +84,15 @@ internal fun LazyListScope.nodes(
nodes: List<CommentNode>, nodes: List<CommentNode>,
htmlConverter: HTMLConverter, htmlConverter: HTMLConverter,
toggleExpanded: (CommentNode) -> Unit, toggleExpanded: (CommentNode) -> Unit,
openUserProfile: (String) -> Unit,
) { ) {
nodes.forEach { node -> nodes.forEach { node ->
node(node = node, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded) node(
node = node,
htmlConverter = htmlConverter,
toggleExpanded = toggleExpanded,
openUserProfile = openUserProfile,
)
} }
} }
@ -94,16 +100,27 @@ private fun LazyListScope.node(
node: CommentNode, node: CommentNode,
htmlConverter: HTMLConverter, htmlConverter: HTMLConverter,
toggleExpanded: (CommentNode) -> Unit, toggleExpanded: (CommentNode) -> Unit,
openUserProfile: (String) -> Unit,
) { ) {
// Skip the node if neither the node nor its parent is expanded // Skip the node if neither the node nor its parent is expanded
if (!node.isExpanded && node.parent?.isExpanded == false) { if (!node.isExpanded && node.parent?.isExpanded == false) {
return return
} }
item { item {
CommentEntry(commentNode = node, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded) CommentEntry(
commentNode = node,
htmlConverter = htmlConverter,
toggleExpanded = toggleExpanded,
openUserProfile = openUserProfile,
)
HorizontalDivider() HorizontalDivider()
} }
if (node.children.isNotEmpty()) { if (node.children.isNotEmpty()) {
nodes(node.children, htmlConverter = htmlConverter, toggleExpanded = toggleExpanded) nodes(
node.children,
htmlConverter = htmlConverter,
toggleExpanded = toggleExpanded,
openUserProfile = openUserProfile,
)
} }
} }

View file

@ -46,6 +46,7 @@ private fun CommentsPageInternal(
htmlConverter: HTMLConverter, htmlConverter: HTMLConverter,
commentState: PostComments?, commentState: PostComments?,
markSeenComments: (String, List<Comment>) -> Unit, markSeenComments: (String, List<Comment>) -> Unit,
openUserProfile: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val commentNodes = createListNode(details.comments, commentState).toMutableStateList() val commentNodes = createListNode(details.comments, commentState).toMutableStateList()
@ -54,7 +55,12 @@ private fun CommentsPageInternal(
Surface(color = MaterialTheme.colorScheme.surfaceVariant) { Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) { LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) {
item { item {
CommentsHeader(post = details, postActions = postActions, htmlConverter = htmlConverter) CommentsHeader(
post = details,
postActions = postActions,
htmlConverter = htmlConverter,
openUserProfile = openUserProfile,
)
} }
if (commentNodes.isNotEmpty()) { if (commentNodes.isNotEmpty()) {
@ -79,6 +85,7 @@ private fun CommentsPageInternal(
commentNodes.add(index, parent) commentNodes.add(index, parent)
} }
}, },
openUserProfile = openUserProfile,
) )
} else { } else {
item { item {
@ -104,6 +111,7 @@ fun CommentsPage(
getSeenComments: suspend (String) -> PostComments?, getSeenComments: suspend (String) -> PostComments?,
markSeenComments: (String, List<Comment>) -> Unit, markSeenComments: (String, List<Comment>) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
openUserProfile: (String) -> Unit,
) { ) {
val postDetails by val postDetails by
produceState<NetworkState>(Loading) { produceState<NetworkState>(Loading) {
@ -124,6 +132,7 @@ fun CommentsPage(
htmlConverter = htmlConverter, htmlConverter = htmlConverter,
commentState = commentState, commentState = commentState,
markSeenComments = markSeenComments, markSeenComments = markSeenComments,
openUserProfile = openUserProfile,
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) )
} }

View file

@ -24,6 +24,8 @@ import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.github.michaelbull.result.coroutines.runSuspendCatching import com.github.michaelbull.result.coroutines.runSuspendCatching
import com.github.michaelbull.result.fold import com.github.michaelbull.result.fold
@ -42,6 +44,7 @@ import dev.msfjarvis.claw.model.User
fun UserProfile( fun UserProfile(
username: String, username: String,
getProfile: suspend (username: String) -> User, getProfile: suspend (username: String) -> User,
openUserProfile: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val user by val user by
@ -56,7 +59,11 @@ fun UserProfile(
} }
when (user) { when (user) {
is Success<*> -> { is Success<*> -> {
UserProfileInternal(user = (user as Success<User>).data, modifier = modifier) UserProfileInternal(
user = (user as Success<User>).data,
openUserProfile = openUserProfile,
modifier = modifier,
)
} }
is Error -> { is Error -> {
val error = user as Error val error = user as Error
@ -77,7 +84,11 @@ fun UserProfile(
} }
@Composable @Composable
private fun UserProfileInternal(user: User, modifier: Modifier = Modifier) { private fun UserProfileInternal(
user: User,
openUserProfile: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Surface(modifier = modifier) { Surface(modifier = modifier) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -93,7 +104,19 @@ private fun UserProfileInternal(user: User, modifier: Modifier = Modifier) {
Text(text = user.username, style = MaterialTheme.typography.displaySmall) Text(text = user.username, style = MaterialTheme.typography.displaySmall)
ThemedRichText(text = user.about) ThemedRichText(text = user.about)
user.invitedBy?.let { invitedBy -> user.invitedBy?.let { invitedBy ->
ThemedRichText(text = "Invited by [${invitedBy}](https://lobste.rs/u/${user.invitedBy})") Text(
text =
buildAnnotatedString {
append("Invited by ")
pushLink(
LinkAnnotation.Clickable(
tag = "username",
linkInteractionListener = { openUserProfile(invitedBy) },
)
)
append(invitedBy)
}
)
} }
} }
} }