From 3d165b6acdcece59d4b43a8707e0a3edab2fef34 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 23 Mar 2024 20:22:05 -0700 Subject: [PATCH] Links in message bubbles --- .../ConversationListViewModel.kt | 4 +++ .../ui/messagelist/MessageListScreen.kt | 21 ++++++++++-- .../kordophonedroid/ui/shared/StringUtils.kt | 33 +++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/net/buzzert/kordophonedroid/ui/shared/StringUtils.kt diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt index e4321c8..56b0afe 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListViewModel.kt @@ -6,7 +6,10 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.cache import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.buzzert.kordophone.backend.model.Conversation @@ -29,6 +32,7 @@ class ConversationListViewModel @Inject constructor( ) : ViewModel() { val conversations: Flow> get() = chatRepository.conversationChanges + .shareIn(viewModelScope, started = SharingStarted.WhileSubscribed()) .map { it.sortedBy { it.date } .reversed() diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt index 3602794..8b0d3b9 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold @@ -37,6 +38,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -58,6 +60,8 @@ import net.buzzert.kordophonedroid.ui.Destination import net.buzzert.kordophonedroid.ui.LocalNavController import net.buzzert.kordophonedroid.ui.attachments.AttachmentFetchData import net.buzzert.kordophonedroid.ui.attachments.AttachmentViewModel +import net.buzzert.kordophonedroid.ui.shared.LINK_ANNOTATION_TAG +import net.buzzert.kordophonedroid.ui.shared.linkify import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl @@ -317,16 +321,27 @@ fun MessageBubble( ) { val backgroundBubbleColor = if (mine) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + // Linkify text + val annotatedString = text.linkify() + val urlHandler = LocalUriHandler.current + BubbleScaffold(mine = mine, modifier = modifier) { Surface( color = backgroundBubbleColor, shape = if (mine) OutgoingChatBubbleShape else IncomingChatBubbleShape, ) { - Text( - text = text, + ClickableText( + text = annotatedString, style = MaterialTheme.typography.body2, modifier = Modifier - .padding(12.dp) + .padding(12.dp), + onClick = { index -> + annotatedString + .getStringAnnotations(LINK_ANNOTATION_TAG, index, index) + .firstOrNull()?.let { + urlHandler.openUri(it.item) + } + } ) } } diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/shared/StringUtils.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/shared/StringUtils.kt new file mode 100644 index 0000000..65e0da2 --- /dev/null +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/shared/StringUtils.kt @@ -0,0 +1,33 @@ +package net.buzzert.kordophonedroid.ui.shared + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration + +const val LINK_ANNOTATION_TAG = "link" +private val LINK_REGEX = "(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]".toRegex() + +fun String.linkify(): AnnotatedString { + val text = this + val matches = LINK_REGEX.findAll(this) + return buildAnnotatedString { + append(text) + + for (match in matches) { + val range = match.range.also { + // Make inclusive. + IntRange(it.first, it.last + 1) + } + + // Annotate link + addStringAnnotation(LINK_ANNOTATION_TAG, match.value, range.first, range.last) + + // Add style + addStyle( + style = SpanStyle(textDecoration = TextDecoration.Underline), + start = range.first, end = range.last + ) + } + } +} \ No newline at end of file