diff --git a/app/src/main/java/org/session/libsignal/utilities/Validation.kt b/app/src/main/java/org/session/libsignal/utilities/Validation.kt index fdf9bd386f..eaa1fa1bab 100644 --- a/app/src/main/java/org/session/libsignal/utilities/Validation.kt +++ b/app/src/main/java/org/session/libsignal/utilities/Validation.kt @@ -4,8 +4,24 @@ object PublicKeyValidation { private val HEX_CHARACTERS = "0123456789ABCDEFabcdef".toSet() private val INVALID_PREFIXES = setOf(IdPrefix.GROUP, IdPrefix.BLINDED, IdPrefix.BLINDEDV2) - fun isValid(candidate: String, isPrefixRequired: Boolean = true): Boolean = hasValidLength(candidate) && isValidHexEncoding(candidate) && (!isPrefixRequired || IdPrefix.fromValue(candidate) != null) + fun isValid(candidate: String, isPrefixRequired: Boolean = true): Boolean { + if (!hasValidLength(candidate)) return false + + val prefix = IdPrefix.fromValue(candidate) + + // Handle invalid Account ID conditions + // Case 1: Standard prefix "05" but not valid hex + if (prefix == IdPrefix.STANDARD && !isValidHexEncoding(candidate)) return false + + // Case 2: Blinded or Group IDs should never be accepted as valid Account IDs + if (prefix in INVALID_PREFIXES) return false + + // Standard validity rules + return isValidHexEncoding(candidate) && + (!isPrefixRequired || prefix != null) + } + fun hasValidPrefix(candidate: String) = IdPrefix.fromValue(candidate) !in INVALID_PREFIXES - private fun hasValidLength(candidate: String) = candidate.length == 66 + fun hasValidLength(candidate: String) = candidate.length == 66 private fun isValidHexEncoding(candidate: String) = HEX_CHARACTERS.containsAll(candidate.toSet()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt index fc3652f386..9ce71dfba7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt @@ -16,8 +16,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -39,6 +41,7 @@ import org.thoughtcrime.securesms.home.startconversation.newmessage.State import org.thoughtcrime.securesms.openUrl import org.thoughtcrime.securesms.ui.NavigationAction import org.thoughtcrime.securesms.ui.ObserveAsEvents +import org.thoughtcrime.securesms.ui.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.ui.components.BaseBottomSheet import org.thoughtcrime.securesms.ui.horizontalSlideComposable @@ -152,13 +155,17 @@ fun StartConversationNavHost( val viewModel = hiltViewModel() val uiState by viewModel.state.collectAsState(State()) + val helpUrl = "https://getsession.org/account-ids" + LaunchedEffect(Unit) { scope.launch { viewModel.success.collect { - context.startActivity(ConversationActivityV2.createIntent( - context, - address = it.address - )) + context.startActivity( + ConversationActivityV2.createIntent( + context, + address = it.address + ) + ) onClose() } @@ -169,10 +176,16 @@ fun StartConversationNavHost( uiState, viewModel.qrErrors, viewModel, - onBack = { scope.launch { navigator.navigateUp() }}, + onBack = { scope.launch { navigator.navigateUp() } }, onClose = onClose, - onHelp = { activity?.openUrl("https://sessionapp.zendesk.com/hc/en-us/articles/4439132747033-How-do-Account-ID-usernames-work") } + onHelp = { viewModel.onCommand(NewMessageViewModel.Commands.ShowUrlDialog) } ) + if (uiState.showUrlDialog) { + OpenURLAlertDialog( + url = helpUrl, + onDismissRequest = { viewModel.onCommand(NewMessageViewModel.Commands.DismissUrlDialog) } + ) + } } // Create Group diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessage.kt index cf0cff108b..8981f6c99c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessage.kt @@ -124,7 +124,7 @@ private fun EnterAccountId( .fillMaxWidth(), style = LocalType.current.small, color = LocalColors.current.textSecondary, - iconRes = R.drawable.ic_circle_help, + iconRes = R.drawable.ic_square_arrow_up_right, onClick = onHelp ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessageViewModel.kt index 1402f7ac56..d76cdcf457 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessageViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.squareup.phrase.Phrase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow @@ -19,9 +20,11 @@ import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.upsertContact import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.PublicKeyValidation +import org.thoughtcrime.securesms.preferences.SettingsViewModel import org.thoughtcrime.securesms.ui.GetString import java.net.IDN import javax.inject.Inject @@ -69,10 +72,20 @@ class NewMessageViewModel @Inject constructor( } } - if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) { - onUnvalidatedPublicKey(publicKey = idOrONS) + if (PublicKeyValidation.hasValidLength(idOrONS)) { + if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) { + onUnvalidatedPublicKey(idOrONS) + } else { + _state.update { + it.copy( + isTextErrorColor = true, + error = GetString(R.string.accountIdErrorInvalid), + loading = false + ) + } + } } else { - resolveONS(ons = idOrONS) + resolveONS(idOrONS) } } @@ -122,7 +135,6 @@ class NewMessageViewModel @Inject constructor( if (address is Address.Standard) { viewModelScope.launch { _success.emit(Success(address)) } } - } private fun onUnvalidatedPublicKey(publicKey: String) { @@ -134,8 +146,31 @@ class NewMessageViewModel @Inject constructor( } private fun Exception.toMessage() = when (this) { - is SnodeAPI.Error.Generic -> application.getString(R.string.onsErrorNotRecognized) - else -> application.getString(R.string.onsErrorUnableToSearch) + is SnodeAPI.Error.Generic -> application.getString(R.string.errorUnregisteredOns) + else -> Phrase.from(application, R.string.errorNoLookupOns) + .put(APP_NAME_KEY, application.getString(R.string.app_name)) + .format().toString() + } + + fun onCommand(commands: Commands) { + when (commands) { + is Commands.ShowUrlDialog -> { + _state.update { it.copy(showUrlDialog = true) } + } + + is Commands.DismissUrlDialog -> { + _state.update { + it.copy( + showUrlDialog = false + ) + } + } + } + } + + sealed interface Commands { + data object ShowUrlDialog : Commands + data object DismissUrlDialog : Commands } } @@ -143,9 +178,13 @@ data class State( val newMessageIdOrOns: String = "", val isTextErrorColor: Boolean = false, val error: GetString? = null, - val loading: Boolean = false + val loading: Boolean = false, + val showUrlDialog : Boolean = false ) { val isNextButtonEnabled: Boolean get() = newMessageIdOrOns.isNotBlank() } + + + data class Success(val address: Address.Standard) \ No newline at end of file