Links in message bubbles
This commit is contained in:
@@ -6,7 +6,10 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.cache
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.buzzert.kordophone.backend.model.Conversation
|
import net.buzzert.kordophone.backend.model.Conversation
|
||||||
@@ -29,6 +32,7 @@ class ConversationListViewModel @Inject constructor(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val conversations: Flow<List<Conversation>>
|
val conversations: Flow<List<Conversation>>
|
||||||
get() = chatRepository.conversationChanges
|
get() = chatRepository.conversationChanges
|
||||||
|
.shareIn(viewModelScope, started = SharingStarted.WhileSubscribed())
|
||||||
.map {
|
.map {
|
||||||
it.sortedBy { it.date }
|
it.sortedBy { it.date }
|
||||||
.reversed()
|
.reversed()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Scaffold
|
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.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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.LocalNavController
|
||||||
import net.buzzert.kordophonedroid.ui.attachments.AttachmentFetchData
|
import net.buzzert.kordophonedroid.ui.attachments.AttachmentFetchData
|
||||||
import net.buzzert.kordophonedroid.ui.attachments.AttachmentViewModel
|
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 net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
@@ -317,16 +321,27 @@ fun MessageBubble(
|
|||||||
) {
|
) {
|
||||||
val backgroundBubbleColor = if (mine) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
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) {
|
BubbleScaffold(mine = mine, modifier = modifier) {
|
||||||
Surface(
|
Surface(
|
||||||
color = backgroundBubbleColor,
|
color = backgroundBubbleColor,
|
||||||
shape = if (mine) OutgoingChatBubbleShape else IncomingChatBubbleShape,
|
shape = if (mine) OutgoingChatBubbleShape else IncomingChatBubbleShape,
|
||||||
) {
|
) {
|
||||||
Text(
|
ClickableText(
|
||||||
text = text,
|
text = annotatedString,
|
||||||
style = MaterialTheme.typography.body2,
|
style = MaterialTheme.typography.body2,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(12.dp)
|
.padding(12.dp),
|
||||||
|
onClick = { index ->
|
||||||
|
annotatedString
|
||||||
|
.getStringAnnotations(LINK_ANNOTATION_TAG, index, index)
|
||||||
|
.firstOrNull()?.let {
|
||||||
|
urlHandler.openUri(it.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user