Match search string to contacts' TEL URIs also on main and chats screens

This commit is contained in:
Juha Heinanen
2026-07-22 11:45:16 +03:00
parent 28e69b2feb
commit c2ea745aa3
3 changed files with 180 additions and 76 deletions

View File

@ -14,9 +14,11 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
@ -67,6 +69,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@ -83,7 +86,6 @@ import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import coil.compose.AsyncImage
import com.tutpro.baresip.BaresipService.Companion.contactNames
import com.tutpro.baresip.CustomElements.AlertDialog
import com.tutpro.baresip.CustomElements.DropdownMenu
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
@ -137,6 +139,7 @@ private fun ChatsScreen(navController: NavController, aor: String) {
}
Scaffold(
modifier = Modifier.fillMaxSize().imePadding(),
containerColor = MaterialTheme.colorScheme.background,
topBar = {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
@ -260,7 +263,6 @@ private fun Chats(
account: Account,
uaMessages: MutableState<List<Message>>
) {
val ctx = LocalContext.current
val aor = account.aor
val showDialog = remember { mutableStateOf(false) }
@ -272,6 +274,14 @@ private fun Chats(
val lastButtonText = remember { mutableStateOf("") }
val lastAction = remember { mutableStateOf({}) }
val shortChatQuestion = stringResource(R.string.short_chat_question)
val longChatQuestion = stringResource(R.string.long_chat_question)
val addContactText = stringResource(R.string.add_contact)
val blockText = stringResource(R.string.block)
val deleteText = stringResource(R.string.delete)
val anonymousText = stringResource(R.string.anonymous)
val unknownText = stringResource(R.string.unknown)
if (showDialog.value)
AlertDialog(
showDialog = showDialog,
@ -341,17 +351,17 @@ private fun Chats(
CustomElements.Button(
onClick = { navController.navigate("chat/${aor}/${message.peerUri}") },
onLongClick = {
val peerName = Utils.friendlyUri(ctx, message.peerUri, account, includeLabel = false)
val peerNameWithLabel = Utils.friendlyUri(ctx, message.peerUri, account)
val peerName = Utils.friendlyUri(message.peerUri, account, includeLabel = false, anonymous = anonymousText, unknown = unknownText)
val peerNameWithLabel = Utils.friendlyUri(message.peerUri, account, anonymous = anonymousText, unknown = unknownText)
val contactExists =
Contact.nameExists(peerName, BaresipService.contacts, false)
if (contactExists) {
dialogMessage.value = String.format(
ctx.getString(R.string.short_chat_question),
shortChatQuestion,
peerNameWithLabel
)
secondButtonText.value = ""
lastButtonText.value = ctx.getString(R.string.delete)
lastButtonText.value = deleteText
lastAction.value = {
deleteMessages(uaMessages, account, message.peerUri)
}
@ -359,19 +369,19 @@ private fun Chats(
else {
dialogMessage.value =
String.format(
ctx.getString(R.string.long_chat_question),
longChatQuestion,
peerName
)
secondButtonText.value = ctx.getString(R.string.add_contact)
secondButtonText.value = addContactText
secondAction.value = { navController.navigate("contact/${message.peerUri}/new") }
thirdButtonText.value = ctx.getString(R.string.block)
thirdButtonText.value = blockText
thirdAction.value = {
if (!BlockRule.exists(message.peerUri)) {
BaresipService.blockRules.add(BlockRule(message.peerUri))
BlockRule.save()
}
}
lastButtonText.value = ctx.getString(R.string.delete)
lastButtonText.value = deleteText
lastAction.value = { deleteMessages(uaMessages, account, message.peerUri) }
}
showDialog.value = true
@ -384,7 +394,7 @@ private fun Chats(
else
MaterialTheme.colorScheme.primaryContainer
) {
val peerName = Utils.friendlyUri(ctx, message.peerUri, account)
val peerName = Utils.friendlyUri(message.peerUri, account, anonymous = anonymousText, unknown = unknownText)
val cal = GregorianCalendar()
cal.timeInMillis = message.timeStamp
val fmt: DateFormat = if (isToday(message.timeStamp))
@ -424,12 +434,17 @@ private fun Chats(
@Composable
private fun NewChatPeer(navController: NavController, account: Account) {
val ctx = LocalContext.current
val alertTitle = remember { mutableStateOf("") }
val alertMessage = remember { mutableStateOf("") }
val showAlert = remember { mutableStateOf(false) }
val noticeText = stringResource(R.string.notice)
val noTelephonyProviderText = stringResource(R.string.no_telephony_provider)
val invalidSipOrTelUriText = stringResource(R.string.invalid_sip_or_tel_uri)
val contactNoTelUriText = stringResource(R.string.contact_no_tel_uri)
val contactNoSipOrTelUriText = stringResource(R.string.contact_no_sip_or_tel_uri)
fun makeChat(navController: NavController, account: Account, chatPeer: String) {
val peerUri = if (Utils.isTelNumber(chatPeer))
"tel:$chatPeer"
@ -440,9 +455,9 @@ private fun NewChatPeer(navController: NavController, account: Account) {
peerUri
else
if (account.telProvider == "") {
alertTitle.value = ctx.getString(R.string.notice)
alertTitle.value = noticeText
alertMessage.value =
String.format(ctx.getString(R.string.no_telephony_provider), account.aor)
String.format(noTelephonyProviderText, account.aor)
showAlert.value = true
""
} else
@ -452,8 +467,8 @@ private fun NewChatPeer(navController: NavController, account: Account) {
Utils.uriComplete(peerUri, account.aor)
if (alertMessage.value.isEmpty()) {
if (!Utils.checkUri(uri)) {
alertTitle.value = ctx.getString(R.string.notice)
alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri)
alertTitle.value = noticeText
alertMessage.value = String.format(invalidSipOrTelUriText, uri)
showAlert.value = true
}
else
@ -483,8 +498,7 @@ private fun NewChatPeer(navController: NavController, account: Account) {
onNeutralClicked = {}
)
val suggestions by remember { contactNames }
var filteredSuggestions by remember { mutableStateOf<List<AnnotatedString>>(emptyList()) }
var filteredSuggestions by remember { mutableStateOf<List<Triple<Contact, AnnotatedString, Contact.ContactUri?>>>(emptyList()) }
var showSuggestions by remember { mutableStateOf(false) }
val lazyListState = rememberLazyListState()
val focusManager = LocalFocusManager.current
@ -518,23 +532,40 @@ private fun NewChatPeer(navController: NavController, account: Account) {
) {
items(
items = filteredSuggestions,
key = { suggestion -> suggestion.toString() }
) { suggestion ->
key = { (contact, _, _) -> "${contact.id()}" }
) { (contact, annotatedName, matchingUri) ->
Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
newPeer = suggestion.toString()
val uri = matchingUri?.uri ?: contact.uris().firstOrNull()?.uri ?: contact.name()
newPeer = uri.substringAfter(":")
showSuggestions = false
}
.padding(12.dp)
) {
Text(
text = suggestion,
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 18.sp
)
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = annotatedName,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 18.sp
)
if (matchingUri != null) {
val tel = matchingUri.uri.substring(4)
val annotatedTel = Utils.buildAnnotatedStringWithHighlight(
tel,
newPeer.filter { c -> c.isDigit() || c == '+' })
Text(
text = buildAnnotatedString {
if (matchingUri.label.isNotEmpty())
append("${matchingUri.label} ")
append(annotatedTel)
},
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
@ -552,14 +583,25 @@ private fun NewChatPeer(navController: NavController, account: Account) {
emptyList()
else {
val normalizedInput = Utils.unaccent(it)
suggestions
.filter { suggestion ->
it.length > 1 &&
Utils.unaccent(suggestion).contains(normalizedInput, ignoreCase = true)
val numericInput = it.filter { c -> c.isDigit() || c == '+' }
BaresipService.contacts.mapNotNull { contact ->
val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true)
var matchingUri: Contact.ContactUri? = null
if (numericInput.isNotEmpty()) {
matchingUri = contact.uris().find { u ->
u.uri.startsWith("tel:") && u.uri.substring(4).contains(numericInput)
}
}
.map { suggestion ->
Utils.buildAnnotatedStringWithHighlight(suggestion, it)
if (nameMatch || matchingUri != null) {
val annotatedName = if (nameMatch)
Utils.buildAnnotatedStringWithHighlight(contact.name(), it)
else
AnnotatedString(contact.name())
Triple(contact, annotatedName, matchingUri)
} else {
null
}
}
}
},
modifier = Modifier.padding(end = 6.dp).fillMaxWidth(),
@ -595,11 +637,11 @@ private fun NewChatPeer(navController: NavController, account: Account) {
val uris = Contact.contactContactUris(peerText, account.isMobile)
if (uris.isEmpty()) {
if (Contact.nameExists(peerText, BaresipService.contacts, true)) {
alertTitle.value = ctx.getString(R.string.notice)
alertTitle.value = noticeText
alertMessage.value = if (account.isMobile)
String.format(ctx.getString(R.string.contact_no_tel_uri), peerText)
String.format(contactNoTelUriText, peerText)
else
String.format(ctx.getString(R.string.contact_no_sip_or_tel_uri), peerText)
String.format(contactNoSipOrTelUriText, peerText)
showAlert.value = true
} else {
makeChat(navController, account, peerText)

View File

@ -135,6 +135,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
@ -153,7 +154,6 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.tutpro.baresip.BaresipService.Companion.circleGreen
import com.tutpro.baresip.BaresipService.Companion.colorblind
import com.tutpro.baresip.BaresipService.Companion.contactNames
import com.tutpro.baresip.BaresipService.Companion.uas
import com.tutpro.baresip.BaresipService.Companion.uasStatus
import com.tutpro.baresip.CustomElements.AlertDialog
@ -1177,8 +1177,7 @@ private fun CallUriRow(
val isDialer = dialerState != null
val suggestions by remember { contactNames }
var filteredSuggestions by remember { mutableStateOf<List<AnnotatedString>>(emptyList()) }
var filteredSuggestions by remember { mutableStateOf<List<Triple<Contact, AnnotatedString, Contact.ContactUri?>>>(emptyList()) }
val focusRequester = remember { FocusRequester() }
val lazyListState = rememberLazyListState()
val isDialpadVisible by viewModel.isDialpadVisible.collectAsState()
@ -1201,16 +1200,28 @@ private fun CallUriRow(
dialerState.showCallButton.value = true
dialerState.showCallConferenceButton.value = true
}
val normalizedInput = Utils.unaccent(it)
filteredSuggestions = suggestions
.filter { suggestion ->
it.length > 1 &&
Utils.unaccent(suggestion)
.contains(normalizedInput, ignoreCase = true)
}
.map { suggestion ->
Utils.buildAnnotatedStringWithHighlight(suggestion, it)
if (it.length > 1) {
val normalizedInput = Utils.unaccent(it)
val numericInput = it.filter { c -> c.isDigit() || c == '+' }
filteredSuggestions = BaresipService.contacts.mapNotNull { contact ->
val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true)
var matchingUri: Contact.ContactUri? = null
if (numericInput.isNotEmpty()) {
matchingUri = contact.uris().find { u ->
u.uri.startsWith("tel:") && u.uri.substring(4).contains(numericInput)
}
}
if (nameMatch || matchingUri != null) {
val annotatedName = if (nameMatch)
Utils.buildAnnotatedStringWithHighlight(contact.name(), it)
else
AnnotatedString(contact.name())
Triple(contact, annotatedName, matchingUri)
} else {
null
}
}
}
dialerState.showSuggestions.value = it.length > 1
}
},
@ -1305,23 +1316,40 @@ private fun CallUriRow(
) {
items(
items = filteredSuggestions,
key = { suggestion -> suggestion.toString() }
) { suggestion ->
key = { (contact, _, _) -> "${contact.id()}" }
) { (contact, annotatedName, matchingUri) ->
Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
dialerState.callUri.value = suggestion.toString()
val uri = matchingUri?.uri ?: contact.uris().firstOrNull()?.uri ?: contact.name()
dialerState.callUri.value = uri.substringAfter(":")
dialerState.showSuggestions.value = false
}
.padding(12.dp)
) {
Text(
text = suggestion,
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 18.sp
)
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = annotatedName,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 18.sp
)
if (matchingUri != null) {
val tel = matchingUri.uri.substring(4)
val annotatedTel = Utils.buildAnnotatedStringWithHighlight(
tel,
dialerState.callUri.value.filter { c -> c.isDigit() || c == '+' })
Text(
text = buildAnnotatedString {
if (matchingUri.label.isNotEmpty())
append("${matchingUri.label} ")
append(annotatedTel)
},
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
@ -1617,8 +1645,7 @@ private fun CallRow(
color = MaterialTheme.colorScheme.onSurface,
)
var transferUri by remember { mutableStateOf("") }
val suggestions by remember { contactNames }
var filteredSuggestions by remember { mutableStateOf<List<AnnotatedString>>(emptyList()) }
var filteredSuggestions by remember { mutableStateOf<List<Triple<Contact, AnnotatedString, Contact.ContactUri?>>>(emptyList()) }
val focusRequester = remember { FocusRequester() }
val lazyListState = rememberLazyListState()
OutlinedTextField(
@ -1629,14 +1656,25 @@ private fun CallRow(
transferUri = it
if (it.length > 1) {
val normalizedInput = Utils.unaccent(it)
filteredSuggestions = suggestions
.filter { suggestion ->
Utils.unaccent(suggestion)
.contains(normalizedInput, ignoreCase = true)
val numericInput = it.filter { c -> c.isDigit() || c == '+' }
filteredSuggestions = BaresipService.contacts.mapNotNull { contact ->
val nameMatch = Utils.unaccent(contact.name()).contains(normalizedInput, ignoreCase = true)
var matchingUri: Contact.ContactUri? = null
if (numericInput.isNotEmpty()) {
matchingUri = contact.uris().find { u ->
u.uri.startsWith("tel:") && u.uri.substring(4).contains(numericInput)
}
}
.map { suggestion ->
Utils.buildAnnotatedStringWithHighlight(suggestion, it)
if (nameMatch || matchingUri != null) {
val annotatedName = if (nameMatch)
Utils.buildAnnotatedStringWithHighlight(contact.name(), it)
else
AnnotatedString(contact.name())
Triple(contact, annotatedName, matchingUri)
} else {
null
}
}
}
call.showSuggestions.value = transferUri.length > 1
}
@ -1688,23 +1726,40 @@ private fun CallRow(
) {
items(
items = filteredSuggestions,
key = { suggestion -> suggestion.toString() }
) { suggestion ->
key = { (contact, _, _) -> "${contact.id()}" }
) { (contact, annotatedName, matchingUri) ->
Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
transferUri = suggestion.toString()
val uri = matchingUri?.uri ?: contact.uris().firstOrNull()?.uri ?: contact.name()
transferUri = uri.substringAfter(":")
call.showSuggestions.value = false
}
.padding(12.dp)
) {
Text(
text = suggestion,
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 18.sp
)
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = annotatedName,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 18.sp
)
if (matchingUri != null) {
val tel = matchingUri.uri.substring(4)
val annotatedTel = Utils.buildAnnotatedStringWithHighlight(
tel,
transferUri.filter { c -> c.isDigit() || c == '+' })
Text(
text = buildAnnotatedString {
if (matchingUri.label.isNotEmpty())
append("${matchingUri.label} ")
append(annotatedTel)
},
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}

View File

@ -158,6 +158,13 @@ object Utils {
fun friendlyUri(ctx: Context, uri: String, account: Account, e164Check: Boolean = true,
includeLabel: Boolean = true): String {
return friendlyUri(uri, account, e164Check, includeLabel,
ctx.getString(R.string.anonymous), ctx.getString(R.string.unknown))
}
fun friendlyUri(uri: String, account: Account, e164Check: Boolean = true,
includeLabel: Boolean = true, anonymous: String = "Anonymous",
unknown: String = "Unknown"): String {
var u = Contact.contactName(uri, includeLabel)
if (u != uri)
return u
@ -175,9 +182,9 @@ object Utils {
return if (host == aorDomain(account.aor) || params.contains("user=phone"))
user
else if (host == "anonymous.invalid")
ctx.getString(R.string.anonymous)
anonymous
else if (host == "unknown.invalid")
ctx.getString(R.string.unknown)
unknown
else if (params.isEmpty())
"$user@$host"
else