Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions app/src/main/java/com/nextcloud/talk/ui/ComposeUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2026 Julius Linus <juliuslinus1@gmail.com>
* 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
)
}
}
}
88 changes: 84 additions & 4 deletions app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2026 Julius Linus <juliuslinus1@gmail.com>
* SPDX-FileCopyrightText: 2026 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/

Expand All @@ -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
Expand All @@ -40,12 +42,20 @@ 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.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
Expand All @@ -64,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

Expand All @@ -76,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
Expand All @@ -89,14 +105,17 @@ fun PinnedMessageView(
}

val highEmphasisColor = colorScheme.onSurfaceVariant

val incomingBubbleColor = colorResource(R.color.bg_message_list_incoming_bubble)

val canPin = remember {
message.isOneToOneConversation ||
ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!)
}

val adapter = remember(testingPreviewUtils) {
ComposeChatAdapter(utils = testingPreviewUtils)
}

Column(
verticalArrangement = Arrangement.spacedBy((-SPACE_16).dp),
modifier = Modifier
Expand All @@ -107,13 +126,14 @@ 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)
}

) {
ComposeChatAdapter().GetComposableForMessage(message)
adapter.GetComposableForMessage(message)
}

var expanded by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -160,7 +180,8 @@ fun PinnedMessageView(
text = {
Text(
text = pinnedText,
color = highEmphasisColor
color = highEmphasisColor,
fontWeight = FontWeight.Bold
)
},
onClick = { /* No-op or toggle expansion */ },
Expand Down Expand Up @@ -222,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
)
}
}
}