From 51d09e50db98a8a62b4dbb77dc3f8a819a17def4 Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Tue, 27 Jan 2026 11:49:09 -0600 Subject: [PATCH 1/2] adding a better scrolling experience Signed-off-by: rapterjet2004 --- .../com/nextcloud/talk/ui/ComposeUtils.kt | 62 +++++++++++++++++++ .../com/nextcloud/talk/ui/PinnedMessage.kt | 5 +- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/nextcloud/talk/ui/ComposeUtils.kt diff --git a/app/src/main/java/com/nextcloud/talk/ui/ComposeUtils.kt b/app/src/main/java/com/nextcloud/talk/ui/ComposeUtils.kt new file mode 100644 index 00000000000..51f60efab04 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/ComposeUtils.kt @@ -0,0 +1,62 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.ui + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.min + +private const val SCROLL_DUR = 150 +private const val ANIM_DUR_LONG = 500 +private const val FLOAT_100 = 100f +private const val INT_100 = 100 + +// Adapted from source - https://stackoverflow.com/a/68056586 +@Composable +fun Modifier.customVerticalScrollbar(state: ScrollState, width: Dp = 8.dp, color: Color = Color.Red): Modifier { + val targetAlpha = if (state.isScrollInProgress) 1f else 0f + val duration = if (state.isScrollInProgress) SCROLL_DUR else ANIM_DUR_LONG + val alpha by animateFloatAsState( + targetValue = targetAlpha, + animationSpec = tween(durationMillis = duration) + ) + val cr = CORNER_RADIUS.toFloat() + + return drawWithContent { + drawContent() + + val needDrawScrollbar = state.isScrollInProgress || alpha > 0.0 + + if (needDrawScrollbar) { + val elementHeight = this.size.height + val pinnedViewHeight = MAX_HEIGHT + val scrollBarHeightPercentage = (pinnedViewHeight * FLOAT_100) / elementHeight + val scrollBarHeight = (scrollBarHeightPercentage / INT_100) * pinnedViewHeight + val offset = state.scrollIndicatorState?.scrollOffset?.toFloat() ?: 0f + + drawRoundRect( + color = color, + topLeft = Offset(this.size.width - width.toPx(), min(offset, elementHeight)), + size = Size(width.toPx(), scrollBarHeight), + cornerRadius = CornerRadius(cr, cr), + alpha = alpha + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt b/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt index 263859ad3c1..c65f8372b75 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.nextcloud.talk.R import com.nextcloud.talk.chat.data.model.ChatMessage @@ -107,6 +108,7 @@ fun PinnedMessageView( .background(incomingBubbleColor, RoundedCornerShape(CORNER_RADIUS.dp)) .padding(SPACE_16.dp) .heightIn(max = MAX_HEIGHT.dp) + .customVerticalScrollbar(scrollState, color = outgoingBubbleColor) .verticalScroll(scrollState) .clickable { scrollToMessageWithIdWithOffset(message.id) @@ -160,7 +162,8 @@ fun PinnedMessageView( text = { Text( text = pinnedText, - color = highEmphasisColor + color = highEmphasisColor, + fontWeight = FontWeight.Bold ) }, onClick = { /* No-op or toggle expansion */ }, From 43b80fd8a10ef3a780112da48ccd76ed0decd38e Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Thu, 5 Feb 2026 14:29:40 +0100 Subject: [PATCH 2/2] test: Add composable previews Signed-off-by: Andy Scherzinger --- .../com/nextcloud/talk/ui/PinnedMessage.kt | 83 ++++++++++++++++++- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt b/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt index c65f8372b75..723fb220d67 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt @@ -2,6 +2,7 @@ * Nextcloud Talk - Android Client * * SPDX-FileCopyrightText: 2026 Julius Linus + * SPDX-FileCopyrightText: 2026 Andy Scherzinger * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -27,6 +28,7 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -41,12 +43,19 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.preview.ComposePreviewUtils import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -65,7 +74,8 @@ fun PinnedMessageView( currentConversation: ConversationModel?, scrollToMessageWithIdWithOffset: (String) -> Unit, hidePinnedMessage: (ChatMessage) -> Unit, - unPinMessage: (ChatMessage) -> Unit + unPinMessage: (ChatMessage) -> Unit, + composePreviewUtils: ComposePreviewUtils? = null ) { message.incoming = true @@ -77,6 +87,11 @@ fun PinnedMessageView( val scrollState = rememberScrollState() val context = LocalContext.current + val testingPreviewUtils = composePreviewUtils ?: if (NextcloudTalkApplication.sharedApplication == null) { + remember { ComposePreviewUtils.getInstance(context) } + } else { + null + } val outgoingBubbleColor = remember { val colorInt = viewThemeUtils.talk @@ -90,7 +105,6 @@ fun PinnedMessageView( } val highEmphasisColor = colorScheme.onSurfaceVariant - val incomingBubbleColor = colorResource(R.color.bg_message_list_incoming_bubble) val canPin = remember { @@ -98,6 +112,10 @@ fun PinnedMessageView( ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!) } + val adapter = remember(testingPreviewUtils) { + ComposeChatAdapter(utils = testingPreviewUtils) + } + Column( verticalArrangement = Arrangement.spacedBy((-SPACE_16).dp), modifier = Modifier @@ -115,7 +133,7 @@ fun PinnedMessageView( } ) { - ComposeChatAdapter().GetComposableForMessage(message) + adapter.GetComposableForMessage(message) } var expanded by remember { mutableStateOf(false) } @@ -225,3 +243,62 @@ fun PinnedMessageView( } } } + +@Preview(name = "Long Content") +@Composable +fun PinnedMessageLongContentPreview() { + PinnedMessagePreview( + messageContent = "This is a **very long** _pinned_ ??\ncontent that should demonstrate how the " + + "scrollable box behaves when there is more text than what can fit in the maximum height of the pinned " + + "message view. It should show a scrollbar or at least allow vertical scrolling to see the rest of " + + "the message. Adding even more text here to ensure it exceeds 100dp." + ) +} + +@Preview(name = "Dark Mode", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) +@Composable +fun PinnedMessagePreviewDark() { + PinnedMessagePreview() +} + +@Preview(name = "Light Mode and generic") +@Composable +fun PinnedMessagePreview( + messageContent: String = "This is a **pinned** message _content_" +) { + val context = LocalContext.current + val previewUtils = ComposePreviewUtils.getInstance(context) + val viewThemeUtils = previewUtils.viewThemeUtils + val colorScheme = viewThemeUtils.getColorScheme(context) + + val user = User(id = 1L, userId = "user_id") + val conversation = Conversation( + token = "token", + participantType = Participant.ParticipantType.OWNER, + type = ConversationEnums.ConversationType.ROOM_GROUP_CALL + ) + val currentConversation = ConversationModel.mapToConversationModel(conversation, user) + + val message = ChatMessage().apply { + jsonMessageId = 1 + actorDisplayName = "Author One" + pinnedActorDisplayName = "User Two" + message = messageContent + timestamp = System.currentTimeMillis() / 1000 + pinnedAt = System.currentTimeMillis() / 1000 + } + + MaterialTheme(colorScheme = colorScheme) { + Box(modifier = Modifier.padding(16.dp)) { + PinnedMessageView( + message = message, + viewThemeUtils = viewThemeUtils, + currentConversation = currentConversation, + scrollToMessageWithIdWithOffset = {}, + hidePinnedMessage = {}, + unPinMessage = {}, + composePreviewUtils = previewUtils + ) + } + } +}