diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ca5b6769..e5048cdb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "dev.typetype.android" minSdk = 23 targetSdk = 37 - versionCode = 10805 - versionName = "1.8.0-beta.5" + versionCode = 10806 + versionName = "1.8.0-beta.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" resValue("string", "app_name", "TypeType") } diff --git a/app/src/androidTest/java/dev/typetype/android/feature/player/components/CommentBodyTest.kt b/app/src/androidTest/java/dev/typetype/android/feature/player/components/CommentBodyTest.kt new file mode 100644 index 00000000..cf4872be --- /dev/null +++ b/app/src/androidTest/java/dev/typetype/android/feature/player/components/CommentBodyTest.kt @@ -0,0 +1,52 @@ +package dev.typetype.android.feature.player.components + +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dev.typetype.android.domain.comments.Comment +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CommentBodyTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun longCommentCollapsesAndExpandsWithReadMore() { + val suffix = " more comment detail".repeat(60) + val comment = Comment( + id = "comment", + text = "Long comment starts here$suffix", + authorName = "Reporter", + authorAvatarUrl = "", + likeCount = 2, + textualLikeCount = "2", + publishedTime = "1 day ago", + isHeartedByUploader = false, + isPinned = false, + uploaderVerified = false, + replyCount = 0, + ) + + composeRule.setContent { + CommentBody( + comment = comment, + avatarSize = 36.dp, + onUrlClick = {}, + onTimestampClick = {}, + modifier = Modifier.fillMaxWidth().padding(16.dp), + ) + } + + composeRule.onNodeWithText("Read more").assertExists().performClick() + composeRule.onNodeWithText("Long comment starts here$suffix").assertExists() + } +} diff --git a/app/src/main/java/dev/typetype/android/feature/player/components/CommentListItems.kt b/app/src/main/java/dev/typetype/android/feature/player/components/CommentListItems.kt index d9f4ef58..ef11922d 100644 --- a/app/src/main/java/dev/typetype/android/feature/player/components/CommentListItems.kt +++ b/app/src/main/java/dev/typetype/android/feature/player/components/CommentListItems.kt @@ -4,6 +4,7 @@ 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.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -20,6 +21,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -43,9 +48,10 @@ internal fun CommentBody( avatarSize: Dp, onUrlClick: (String) -> Unit, onTimestampClick: (Long) -> Unit, + modifier: Modifier = Modifier, ) { val serverBaseUrl = LocalServerBaseUrl.current - Row(modifier = Modifier.fillMaxWidth()) { + Row(modifier = modifier.fillMaxWidth()) { AsyncImage( model = buildImageUrl(serverBaseUrl, comment.authorAvatarUrl), contentDescription = null, @@ -82,10 +88,8 @@ internal fun CommentBody( ) } Spacer(Modifier.height(4.dp)) - LinkedText( - text = comment.text, - style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface), - linkColor = MaterialTheme.colorScheme.primary, + ExpandableCommentText( + comment = comment, onUrlClick = onUrlClick, onTimestampClick = onTimestampClick, ) @@ -115,6 +119,38 @@ internal fun CommentBody( } } +@Composable +private fun ExpandableCommentText( + comment: Comment, + onUrlClick: (String) -> Unit, + onTimestampClick: (Long) -> Unit, +) { + var expanded by remember(comment.text) { mutableStateOf(false) } + val needsTruncation = comment.text.length > COMMENT_COLLAPSE_CHARACTER_LIMIT || + comment.text.count { it == '\n' } >= COMMENT_COLLAPSE_LINE_LIMIT + + Column { + LinkedText( + text = comment.text, + style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface), + linkColor = MaterialTheme.colorScheme.primary, + onUrlClick = onUrlClick, + onTimestampClick = onTimestampClick, + maxLines = if (expanded) Int.MAX_VALUE else COMMENT_COLLAPSE_LINE_LIMIT, + overflow = if (expanded) TextOverflow.Clip else TextOverflow.Ellipsis, + ) + if (needsTruncation && !expanded) { + TextButton( + onClick = { expanded = true }, + contentPadding = PaddingValues(horizontal = 4.dp), + modifier = Modifier.padding(top = 2.dp), + ) { + Text(stringResource(R.string.comments_read_more)) + } + } + } +} + @Composable internal fun FooterState(items: LazyPagingItems) { val state = items.loadState @@ -156,3 +192,6 @@ private fun CommentSkeletons(count: Int) { } } } + +private const val COMMENT_COLLAPSE_CHARACTER_LIMIT = 320 +private const val COMMENT_COLLAPSE_LINE_LIMIT = 6 diff --git a/app/src/main/java/dev/typetype/android/feature/player/components/InteractiveText.kt b/app/src/main/java/dev/typetype/android/feature/player/components/InteractiveText.kt index ff205729..21f934a0 100644 --- a/app/src/main/java/dev/typetype/android/feature/player/components/InteractiveText.kt +++ b/app/src/main/java/dev/typetype/android/feature/player/components/InteractiveText.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withLink import java.net.URI import java.net.URLDecoder @@ -97,6 +98,8 @@ internal fun LinkedText( onUrlClick: (String) -> Unit, modifier: Modifier = Modifier, onTimestampClick: (Long) -> Unit = {}, + maxLines: Int = Int.MAX_VALUE, + overflow: TextOverflow = TextOverflow.Clip, ) { val latestOnUrlClick = rememberUpdatedState(onUrlClick) val latestOnTimestampClick = rememberUpdatedState(onTimestampClick) @@ -141,5 +144,11 @@ internal fun LinkedText( } append(text.substring(cursor)) } - Text(text = annotated, style = style, modifier = modifier) + Text( + text = annotated, + style = style, + modifier = modifier, + maxLines = maxLines, + overflow = overflow, + ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ea579f3..4827988b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -495,6 +495,7 @@ Couldn\'t forget account Comments No comments + Read more Likes: %1$s Couldn\'t load replies Couldn\'t load comments