Skip to content
Snippets Groups Projects
Commit 0b6e285f authored by Kamal Bramwell's avatar Kamal Bramwell
Browse files

Merge branch 'search_qa_feedback' into 'FE-904_search_2_feature'

Search 2.0 QA feedback

See merge request elixxir/client-android!70
parents 6512877b 566fb511
No related branches found
No related tags found
4 merge requests!84Version 2.92 build 629,!77v2.9 b627,!76FE-904: search 2 feature,!70Search 2.0 QA feedback
Showing
with 300 additions and 142 deletions
...@@ -32,8 +32,7 @@ fun Contact.formattedEmail(): String? = ...@@ -32,8 +32,7 @@ fun Contact.formattedEmail(): String? =
else null else null
fun Contact.formattedPhone(flagEmoji: Boolean = false): String? = fun Contact.formattedPhone(flagEmoji: Boolean = false): String? =
if (phone.isNotBlank()) Country.toFormattedNumber(phone, flagEmoji) phone.ifBlank { null }
else null
suspend fun Contact.resolveBitmap(): Bitmap? = withContext(Dispatchers.IO) { suspend fun Contact.resolveBitmap(): Bitmap? = withContext(Dispatchers.IO) {
BitmapResolver.getBitmap(photo) BitmapResolver.getBitmap(photo)
......
...@@ -366,6 +366,7 @@ class RequestsViewModel @Inject constructor( ...@@ -366,6 +366,7 @@ class RequestsViewModel @Inject constructor(
private fun showDetails(item: RequestItem) { private fun showDetails(item: RequestItem) {
when (item) { when (item) {
is ContactRequestSearchResultItem -> showRequestDialog(item.contactRequest)
is ContactRequestItem -> showRequestDialog(item.contactRequest) is ContactRequestItem -> showRequestDialog(item.contactRequest)
is SearchResultItem -> showRequestDialog(item.contactRequest) is SearchResultItem -> showRequestDialog(item.contactRequest)
is GroupInviteItem -> showInvitationDialog(item.invite) is GroupInviteItem -> showInvitationDialog(item.invite)
...@@ -432,6 +433,7 @@ class RequestsViewModel @Inject constructor( ...@@ -432,6 +433,7 @@ class RequestsViewModel @Inject constructor(
private fun resendRequest(item: RequestItem) { private fun resendRequest(item: RequestItem) {
when (item) { when (item) {
is ContactRequestItem -> requestsDataSource.send(item.request as ContactRequest) is ContactRequestItem -> requestsDataSource.send(item.request as ContactRequest)
is ContactRequestSearchResultItem -> requestsDataSource.send(item.request as ContactRequest)
is GroupInviteItem -> invitationsDataSource.send(item.request as GroupInvitation) is GroupInviteItem -> invitationsDataSource.send(item.request as GroupInvitation)
} }
onResend(item) onResend(item)
......
...@@ -81,20 +81,17 @@ data class ContactRequestSearchResultItem( ...@@ -81,20 +81,17 @@ data class ContactRequestSearchResultItem(
val photo: Bitmap? = null, val photo: Bitmap? = null,
val statusText: String = "Request pending", val statusText: String = "Request pending",
val statusTextColor: Int = R.color.neutral_weak, val statusTextColor: Int = R.color.neutral_weak,
val actionVisible: Boolean = true val actionVisible: Boolean = true,
override val actionIcon: Int = R.drawable.ic_retry,
override val actionIconColor: Int = R.color.brand_default,
override val actionTextStyle: Int = R.style.request_item_retry,
override val actionLabel: String = if (actionVisible) appContext().getString(R.string.request_item_action_retry) else ""
) : RequestItem(contactRequest) { ) : RequestItem(contactRequest) {
override val subtitle: String = statusText override val subtitle: String = statusText
override val details: String? = null override val details: String? = null
override val itemPhoto: Bitmap? = photo override val itemPhoto: Bitmap? = photo
override val itemInitials: String = contactRequest.model.initials override val itemInitials: String = contactRequest.model.initials
override val itemIconRes: Int? = null override val itemIconRes: Int? = null
// Request search results always have the "SENT" UI even if it's failed.
// Instead, the failed status is described in the statusText property.
override val actionLabel: String = if (actionVisible) appContext().getString(R.string.request_item_action_retry) else ""
override val actionIcon: Int = R.drawable.ic_retry
override val actionIconColor: Int = R.color.brand_default
override val actionTextStyle: Int = R.style.request_item_retry
} }
data class GroupInviteItem( data class GroupInviteItem(
......
...@@ -4,6 +4,7 @@ import android.view.ViewGroup ...@@ -4,6 +4,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import io.xxlabs.messenger.requests.ui.list.adapter.RequestsAdapter.ViewType.* import io.xxlabs.messenger.requests.ui.list.adapter.RequestsAdapter.ViewType.*
import io.xxlabs.messenger.support.extensions.toBase64String
class RequestsAdapter( class RequestsAdapter(
private val listener: RequestItemListener private val listener: RequestItemListener
...@@ -68,7 +69,7 @@ class RequestsAdapter( ...@@ -68,7 +69,7 @@ class RequestsAdapter(
class RequestsDiffCallback : DiffUtil.ItemCallback<RequestItem>() { class RequestsDiffCallback : DiffUtil.ItemCallback<RequestItem>() {
override fun areItemsTheSame(oldItem: RequestItem, newItem: RequestItem): Boolean = override fun areItemsTheSame(oldItem: RequestItem, newItem: RequestItem): Boolean =
oldItem.id.contentEquals(newItem.id) oldItem.id.toBase64String() == newItem.id.toBase64String()
override fun areContentsTheSame(oldItem: RequestItem, newItem: RequestItem): Boolean = override fun areContentsTheSame(oldItem: RequestItem, newItem: RequestItem): Boolean =
oldItem == newItem oldItem == newItem
......
...@@ -40,7 +40,7 @@ abstract class FactSearchFragment : Fragment(), Injectable { ...@@ -40,7 +40,7 @@ abstract class FactSearchFragment : Fragment(), Injectable {
factoryProducer = { viewModelFactory } factoryProducer = { viewModelFactory }
) )
private val resultsAdapter: RequestsAdapter by lazy { protected val resultsAdapter: RequestsAdapter by lazy {
RequestsAdapter(requestsViewModel) RequestsAdapter(requestsViewModel)
} }
private lateinit var binding: FragmentFactSearchBinding private lateinit var binding: FragmentFactSearchBinding
...@@ -51,13 +51,13 @@ abstract class FactSearchFragment : Fragment(), Injectable { ...@@ -51,13 +51,13 @@ abstract class FactSearchFragment : Fragment(), Injectable {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentFactSearchBinding.inflate(inflater, container, false) binding = FragmentFactSearchBinding.inflate(inflater, container, false)
lifecycleScope.launch { // lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // repeatOnLifecycle(Lifecycle.State.STARTED) {
getResults().collect { results -> // getResults().collect { results ->
resultsAdapter.submitList(results) // resultsAdapter.submitList(results)
} // }
} // }
} // }
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
return binding.root return binding.root
} }
...@@ -113,7 +113,11 @@ class UsernameSearchFragment : FactSearchFragment() { ...@@ -113,7 +113,11 @@ class UsernameSearchFragment : FactSearchFragment() {
searchViewModel.usernameResults searchViewModel.usernameResults
override fun onSearchClicked(query: String?) { override fun onSearchClicked(query: String?) {
searchViewModel.onUsernameSearch(query) lifecycleScope.launch {
searchViewModel.onUsernameSearch(query).collect { results ->
resultsAdapter.submitList(results)
}
}
} }
override fun getSearchTabUi(): FactSearchUi = searchViewModel.usernameSearchUi override fun getSearchTabUi(): FactSearchUi = searchViewModel.usernameSearchUi
...@@ -124,7 +128,11 @@ class EmailSearchFragment : FactSearchFragment() { ...@@ -124,7 +128,11 @@ class EmailSearchFragment : FactSearchFragment() {
searchViewModel.emailResults searchViewModel.emailResults
override fun onSearchClicked(query: String?) { override fun onSearchClicked(query: String?) {
searchViewModel.onEmailSearch(query) lifecycleScope.launch {
searchViewModel.onEmailSearch(query).collect { results ->
resultsAdapter.submitList(results)
}
}
} }
override fun getSearchTabUi(): FactSearchUi = searchViewModel.emailSearchUi override fun getSearchTabUi(): FactSearchUi = searchViewModel.emailSearchUi
...@@ -135,7 +143,11 @@ class PhoneSearchFragment : FactSearchFragment() { ...@@ -135,7 +143,11 @@ class PhoneSearchFragment : FactSearchFragment() {
searchViewModel.phoneResults searchViewModel.phoneResults
override fun onSearchClicked(query: String?) { override fun onSearchClicked(query: String?) {
searchViewModel.onPhoneSearch(query) lifecycleScope.launch {
searchViewModel.onPhoneSearch(query).collect { results ->
resultsAdapter.submitList(results)
}
}
} }
override fun getSearchTabUi(): FactSearchUi = searchViewModel.phoneSearchUi override fun getSearchTabUi(): FactSearchUi = searchViewModel.phoneSearchUi
......
...@@ -7,18 +7,20 @@ import android.text.SpannableString ...@@ -7,18 +7,20 @@ import android.text.SpannableString
import android.text.Spanned import android.text.Spanned
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import androidx.lifecycle.* import androidx.lifecycle.*
import bindings.Fact
import io.xxlabs.messenger.R import io.xxlabs.messenger.R
import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase import io.xxlabs.messenger.bindings.wrapper.contact.ContactWrapperBase
import io.xxlabs.messenger.data.data.Country import io.xxlabs.messenger.data.data.Country
import io.xxlabs.messenger.data.datatype.ContactRequestState
import io.xxlabs.messenger.data.datatype.FactType import io.xxlabs.messenger.data.datatype.FactType
import io.xxlabs.messenger.data.datatype.RequestStatus import io.xxlabs.messenger.data.datatype.RequestStatus
import io.xxlabs.messenger.data.room.model.Contact
import io.xxlabs.messenger.data.room.model.ContactData import io.xxlabs.messenger.data.room.model.ContactData
import io.xxlabs.messenger.repository.DaoRepository import io.xxlabs.messenger.repository.DaoRepository
import io.xxlabs.messenger.repository.PreferencesRepository import io.xxlabs.messenger.repository.PreferencesRepository
import io.xxlabs.messenger.repository.base.BaseRepository import io.xxlabs.messenger.repository.base.BaseRepository
import io.xxlabs.messenger.requests.data.contact.ContactRequestData import io.xxlabs.messenger.requests.data.contact.ContactRequestData
import io.xxlabs.messenger.requests.data.contact.ContactRequestsRepository
import io.xxlabs.messenger.requests.model.ContactRequest
import io.xxlabs.messenger.requests.model.Request
import io.xxlabs.messenger.requests.ui.list.adapter.* import io.xxlabs.messenger.requests.ui.list.adapter.*
import io.xxlabs.messenger.support.appContext import io.xxlabs.messenger.support.appContext
import io.xxlabs.messenger.support.toast.ToastUI import io.xxlabs.messenger.support.toast.ToastUI
...@@ -29,18 +31,17 @@ import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI ...@@ -29,18 +31,17 @@ import io.xxlabs.messenger.ui.dialog.info.TwoButtonInfoDialogUI
import io.xxlabs.messenger.ui.dialog.info.createInfoDialog import io.xxlabs.messenger.ui.dialog.info.createInfoDialog
import io.xxlabs.messenger.ui.dialog.info.createTwoButtonDialogUi import io.xxlabs.messenger.ui.dialog.info.createTwoButtonDialogUi
import io.xxlabs.messenger.ui.main.countrycode.CountrySelectionListener import io.xxlabs.messenger.ui.main.countrycode.CountrySelectionListener
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.coroutineContext
class UserSearchViewModel @Inject constructor( class UserSearchViewModel @Inject constructor(
private val repo: BaseRepository, private val repo: BaseRepository,
private val daoRepo: DaoRepository, private val daoRepo: DaoRepository,
private val preferences: PreferencesRepository private val preferences: PreferencesRepository,
private val requestsDataSource: ContactRequestsRepository,
): ViewModel(){ ): ViewModel(){
var previousTabPosition: Int = UserSearchFragment.SEARCH_USERNAME var previousTabPosition: Int = UserSearchFragment.SEARCH_USERNAME
...@@ -203,6 +204,8 @@ class UserSearchViewModel @Inject constructor( ...@@ -203,6 +204,8 @@ class UserSearchViewModel @Inject constructor(
val phoneResults: Flow<List<RequestItem>> by ::_phoneResults val phoneResults: Flow<List<RequestItem>> by ::_phoneResults
private val _phoneResults = MutableStateFlow<List<RequestItem>>(listOf()) private val _phoneResults = MutableStateFlow<List<RequestItem>>(listOf())
private var searchJob: Job? = null
init { init {
showNewUserPopups() showNewUserPopups()
} }
...@@ -268,63 +271,154 @@ class UserSearchViewModel @Inject constructor( ...@@ -268,63 +271,154 @@ class UserSearchViewModel @Inject constructor(
repo.enableDummyTraffic(enabled) repo.enableDummyTraffic(enabled)
} }
fun onUsernameSearch(username: String?) { suspend fun onUsernameSearch(username: String?): Flow<List<RequestItem>> {
username?.let { _usernameResults.value = listOf()
val factQuery = FactQuery.UsernameQuery(it) val factQuery = FactQuery.UsernameQuery(username)
search(factQuery, _usernameResults) return search(factQuery).cancellable()
} }
suspend fun onEmailSearch(email: String?): Flow<List<RequestItem>> {
_emailResults.value = listOf()
val factQuery = FactQuery.EmailQuery(email)
return search(factQuery).cancellable()
} }
fun onEmailSearch(email: String?) { suspend fun onPhoneSearch(phone: String?): Flow<List<RequestItem>> {
email?.let { _phoneResults.value = listOf()
val factQuery = FactQuery.EmailQuery(it) val factQuery = FactQuery.PhoneQuery(phone + country.countryCode)
search(factQuery, _emailResults) return search(factQuery).cancellable()
} }
private suspend fun search(factQuery: FactQuery): Flow<List<RequestItem>> {
// Cancel previous searches, save a reference to this one.
searchJob?.cancel()
searchJob = coroutineContext.job
if (!isValidQuery(factQuery)) flowOf(listOf<RequestItem?>())
_udSearchUi.value = searchRunningState
return combine(
searchUd(factQuery),
allRequests(),
allConnections()
) { ud, allRequests, allConnections ->
val foundRequests = allRequests.matching(factQuery).toMutableSet()
val foundConnections = allConnections.matching(factQuery).toMutableSet()
// Add identical request to request results, if not already there.
allRequests.identicalTo(ud.username)?.let {
foundRequests.add(it)
} }
fun onPhoneSearch(phone: String?) { // Add identical connection to connection results, if not already there.
phone?.let { allConnections.identicalTo(ud.username)?.let {
val factQuery = FactQuery.PhoneQuery(it+country.countryCode) foundConnections.add(it)
search(factQuery, _phoneResults)
} }
val alreadyRequested = ud.username in foundRequests.map { request ->
request.username
} }
private fun search( val alreadyAdded = ud.username in foundConnections.map { connection ->
factQuery: FactQuery, connection.username
resultsEmitter: MutableStateFlow<List<RequestItem>> }
) {
if (!isValidQuery(factQuery)) return
_udSearchUi.value = searchRunningState val nonConnections =
viewModelScope.launch { if (alreadyRequested || alreadyAdded) {
clearPreviousResults(resultsEmitter) // If the UD result's userID match a request's userID, only show the request
val udResult = searchUd(factQuery) foundRequests.toList().sortedBy { it.username }
val requestResults = searchRequests(factQuery) } else {
val connectionResults = searchConnections(factQuery) // Otherwise show both
listOf(ud) + foundRequests.sortedBy { it.username }
}
val remoteResults = udResult?.let {
listOf(udResult) + requestResults
} ?: requestResults
val sortedResults = if (remoteResults.isEmpty()) { if (nonConnections.isEmpty()) {
connectionResults.ifEmpty { noResultsFor(factQuery) } // If there's no UD result or Requests, just show the Connections with no divider.
foundConnections.toList().sortedBy {
it.username
}.ifEmpty {
// Show a "no results found" placeholder if there's nothing at all.
noResultsFor(factQuery)
}
} else {
if (foundConnections.isEmpty()) {
// If there's no Connections, show the UD & Request results.
nonConnections
} else { } else {
if (connectionResults.isEmpty()) remoteResults // Or show the UD results, Requests, a divider, and finally Connections.
else remoteResults + listOf(ConnectionsDividerItem()) + connectionResults nonConnections
.plus(listOf(ConnectionsDividerItem()))
.plus(foundConnections.toList().sortedBy { it.username })
}
} }
}
}
private val RequestItem.username: String
get() = (request as? ContactRequest)?.model?.username ?: ""
resultsEmitter.emitResults(sortedResults) private suspend fun allRequests(): Flow<List<RequestItem>> =
requestsDataSource.getRequests().mapNotNull { requestsList ->
requestsList.map {
it.asRequestSearchResult()
}
}.stateIn(viewModelScope)
private suspend fun allConnections() = flow {
val connectionsList = savedUsers().filter {
it.isConnection()
}.asConnectionsSearchResult()
emit(connectionsList)
}.stateIn(viewModelScope)
private fun List<RequestItem>.identicalTo(username: String): RequestItem? =
firstOrNull { it.username == username }
private fun List<RequestItem>.matching(factQuery: FactQuery): List<RequestItem> {
return when (factQuery.type) {
FactType.USERNAME -> {
filter {
(it.request as? ContactRequest)?.model?.displayName?.contains(
factQuery.fact,
true
) ?: false
}
}
FactType.EMAIL -> {
filter {
(it.request as? ContactRequest)?.model?.email?.contains(
factQuery.fact,
true
) ?: false
}
}
FactType.PHONE -> {
filter {
(it.request as? ContactRequest)?.model?.phone?.contains(
factQuery.fact,
true
) ?: false
}
}
else -> listOf()
} }
} }
private fun isValidQuery(factQuery: FactQuery): Boolean { private fun isValidQuery(factQuery: FactQuery): Boolean {
// Prevent users from searching (and possibly requesting) themselves.
return with (factQuery.fact) { return with (factQuery.fact) {
if (isNullOrBlank()) {
// Prevent blank text
false
} else {
// Prevent users from searching (and possibly requesting) themselves.
this != repo.getStoredUsername() this != repo.getStoredUsername()
&& this != repo.getStoredEmail() && this != repo.getStoredEmail()
&& this != repo.getStoredPhone() && this != repo.getStoredPhone()
} }
} }
}
private fun noResultsFor(factQuery: FactQuery): List<RequestItem> = private fun noResultsFor(factQuery: FactQuery): List<RequestItem> =
listOf(noResultPlaceholder(factQuery)) listOf(noResultPlaceholder(factQuery))
...@@ -334,66 +428,66 @@ class UserSearchViewModel @Inject constructor( ...@@ -334,66 +428,66 @@ class UserSearchViewModel @Inject constructor(
text = "There are no users with that ${factQuery.type.name.lowercase()}." text = "There are no users with that ${factQuery.type.name.lowercase()}."
) )
private suspend fun clearPreviousResults(resultsEmitter: MutableStateFlow<List<RequestItem>>) {
resultsEmitter.emit(listOf())
}
private suspend fun MutableStateFlow<List<RequestItem>>.emitResults(results: List<RequestItem>) {
emit(results)
_udSearchUi.value = searchCompleteState
}
private suspend fun savedUsers(): List<ContactData> = private suspend fun savedUsers(): List<ContactData> =
daoRepo.getAllContacts().value() daoRepo.getAllContacts().value()
private fun ContactData.isConnection(): Boolean = private fun ContactData.isConnection(): Boolean =
RequestStatus.from(status) == RequestStatus.ACCEPTED RequestStatus.from(status) == RequestStatus.ACCEPTED
private fun ContactData.isRequest(): Boolean = !isConnection() private suspend fun searchConnections(factQuery: FactQuery) = flow {
val results = when (factQuery.type) {
private suspend fun searchConnections(factQuery: FactQuery): List<RequestItem> =
withContext(Dispatchers.IO) {
when (factQuery.type) {
FactType.USERNAME -> { FactType.USERNAME -> {
savedUsers().filter { savedUsers().filter {
it.isConnection() && it.displayName.contains(factQuery.fact) it.isConnection() && it.displayName.contains(factQuery.fact, true)
}.asConnectionsSearchResult() }.asConnectionsSearchResult()
} }
FactType.EMAIL -> { FactType.EMAIL -> {
savedUsers().filter { savedUsers().filter {
it.isConnection() && it.email.contains(factQuery.fact) it.isConnection() && it.email.contains(factQuery.fact, true)
}.asConnectionsSearchResult() }.asConnectionsSearchResult()
} }
FactType.PHONE -> { FactType.PHONE -> {
savedUsers().filter { savedUsers().filter {
it.isConnection() && it.phone.contains(factQuery.fact) it.isConnection() && it.phone.contains(factQuery.fact, true)
}.asConnectionsSearchResult() }.asConnectionsSearchResult()
} }
else -> listOf() else -> listOf()
} }
} emit(results)
}.stateIn(viewModelScope)
private suspend fun searchRequests(factQuery: FactQuery): List<RequestItem> = private suspend fun searchRequests(factQuery: FactQuery) =
withContext(Dispatchers.IO) {
when (factQuery.type) { when (factQuery.type) {
FactType.USERNAME -> { FactType.USERNAME -> {
savedUsers().filter { filterRequests {
it.isRequest() && it.displayName.contains(factQuery.fact) it.model.displayName.contains(factQuery.fact, true)
}.asRequestsSearchResult() }
} }
FactType.EMAIL -> { FactType.EMAIL -> {
savedUsers().filter { filterRequests {
it.isRequest() && it.email.contains(factQuery.fact) it.model.email.contains(factQuery.fact, true)
}.asRequestsSearchResult() }
} }
FactType.PHONE -> { FactType.PHONE -> {
savedUsers().filter { filterRequests {
it.isRequest() && it.phone.contains(factQuery.fact) it.model.phone.contains(
}.asRequestsSearchResult() Country.toFormattedNumber(factQuery.fact, false) ?: factQuery.fact
)
} }
else -> listOf()
} }
else -> flow { listOf<RequestItem>() }
}.stateIn(viewModelScope)
private suspend fun filterRequests(match: (contactRequest: ContactRequest) -> Boolean) =
requestsDataSource.getRequests().map { requestsList ->
requestsList.filter {
match(it)
}.map {
it.asRequestSearchResult()
} }
}.flowOn(Dispatchers.IO)
private suspend fun List<ContactData>.asConnectionsSearchResult(): List<RequestItem> = private suspend fun List<ContactData>.asConnectionsSearchResult(): List<RequestItem> =
map { map {
...@@ -404,22 +498,28 @@ class UserSearchViewModel @Inject constructor( ...@@ -404,22 +498,28 @@ class UserSearchViewModel @Inject constructor(
) )
} }
private suspend fun List<ContactData>.asRequestsSearchResult(): List<RequestItem> = private suspend fun ContactRequest.asRequestSearchResult(): RequestItem =
map {
ContactRequestSearchResultItem( ContactRequestSearchResultItem(
contactRequest = ContactRequestData(it), contactRequest = this,
photo = resolveBitmap(it.photo), photo = resolveBitmap(model.photo),
statusText = it.statusText(), statusText = model.statusText(),
statusTextColor = it.statusTextColor(), statusTextColor = model.statusTextColor(),
actionVisible= it.actionVisible() actionVisible = model.actionVisible(),
actionIcon = model.actionIcon(),
actionIconColor = model.actionIconColor(),
actionTextStyle = model.actionTextStyle(),
actionLabel = model.actionLabel()
) )
}
private fun ContactData.statusText(): String { private fun Contact.statusText(): String {
return when (RequestStatus.from(status)) { return when (RequestStatus.from(status)) {
RequestStatus.SENT, RequestStatus.SENT,
RequestStatus.VERIFIED, RequestStatus.VERIFIED,
RequestStatus.RESET_SENT -> "Request pending" RequestStatus.RESET_SENT,
RequestStatus.RESENT,
RequestStatus.VERIFYING,
RequestStatus.HIDDEN,
RequestStatus.SENDING -> "Request pending"
RequestStatus.SEND_FAIL, RequestStatus.SEND_FAIL,
RequestStatus.CONFIRM_FAIL, RequestStatus.CONFIRM_FAIL,
...@@ -430,7 +530,7 @@ class UserSearchViewModel @Inject constructor( ...@@ -430,7 +530,7 @@ class UserSearchViewModel @Inject constructor(
} }
} }
private fun ContactData.statusTextColor(): Int { private fun Contact.statusTextColor(): Int {
return when (RequestStatus.from(status)) { return when (RequestStatus.from(status)) {
RequestStatus.SEND_FAIL, RequestStatus.SEND_FAIL,
RequestStatus.CONFIRM_FAIL, RequestStatus.CONFIRM_FAIL,
...@@ -441,35 +541,71 @@ class UserSearchViewModel @Inject constructor( ...@@ -441,35 +541,71 @@ class UserSearchViewModel @Inject constructor(
} }
} }
private fun ContactData.actionVisible(): Boolean { private fun Contact.actionVisible(): Boolean {
return when (RequestStatus.from(status)) { return when (RequestStatus.from(status)) {
RequestStatus.VERIFIED -> false RequestStatus.VERIFIED, RequestStatus.VERIFYING, RequestStatus.HIDDEN -> false
else -> true else -> true
} }
} }
private fun Contact.actionIcon(): Int {
return when (RequestStatus.from(status)) {
RequestStatus.RESENT -> R.drawable.ic_check_green
else -> R.drawable.ic_retry
}
}
private fun Contact.actionIconColor(): Int {
return when (RequestStatus.from(status)) {
RequestStatus.RESENT -> R.color.accent_success
else -> R.color.brand_default
}
}
private fun Contact.actionTextStyle(): Int {
return when (RequestStatus.from(status)) {
RequestStatus.RESENT -> R.drawable.ic_check_green
else -> R.style.request_item_resent
}
}
private fun Contact.actionLabel(): String {
return when (RequestStatus.from(status)) {
RequestStatus.RESENT -> appContext().getString(R.string.request_item_action_resent)
else -> appContext().getString(R.string.request_item_action_retry)
}
}
private suspend fun resolveBitmap(data: ByteArray?): Bitmap? = withContext(Dispatchers.IO) { private suspend fun resolveBitmap(data: ByteArray?): Bitmap? = withContext(Dispatchers.IO) {
BitmapResolver.getBitmap(data) BitmapResolver.getBitmap(data)
} }
private suspend fun searchUd(factQuery: FactQuery): RequestItem? { private suspend fun searchUd(factQuery: FactQuery) = flow {
return try { val result = try {
val udResult = repo.searchUd(factQuery.fact, factQuery.type).value() val udResult = repo.searchUd(factQuery.fact, factQuery.type).value()
udResult.second?.let { // Error message udResult.second?.let { // Error message
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
if (!it.contains("no results found", true)) { if (!it.contains("no results found", true)) {
showToast(it) showToast(it)
} }
_udSearchUi.value = searchCompleteState
noResultPlaceholder(factQuery) noResultPlaceholder(factQuery)
} else { // Search result } else { // Search result
_udSearchUi.value = searchCompleteState
udResult.first?.asSearchResult() ?: noResultPlaceholder(factQuery)
}
} ?: run {
_udSearchUi.value = searchCompleteState
udResult.first?.asSearchResult() ?: noResultPlaceholder(factQuery) udResult.first?.asSearchResult() ?: noResultPlaceholder(factQuery)
} }
} ?: udResult.first?.asSearchResult() ?: noResultPlaceholder(factQuery)
} catch (e: Exception) { } catch (e: Exception) {
e.message?.let { showToast(it) } e.message?.let { showToast(it) }
_udSearchUi.value = searchCompleteState
noResultPlaceholder(factQuery) noResultPlaceholder(factQuery)
} }
} emit(result)
}.stateIn(viewModelScope)
private fun ContactWrapperBase.asSearchResult(): RequestItem { private fun ContactWrapperBase.asSearchResult(): RequestItem {
// ContactWrapperBase -> ContactRequestData // ContactWrapperBase -> ContactRequestData
...@@ -506,6 +642,7 @@ class UserSearchViewModel @Inject constructor( ...@@ -506,6 +642,7 @@ class UserSearchViewModel @Inject constructor(
} }
private fun onCancelSearchClicked() { private fun onCancelSearchClicked() {
searchJob?.cancel()
_udSearchUi.value = searchCompleteState _udSearchUi.value = searchCompleteState
} }
...@@ -536,18 +673,18 @@ private sealed class FactQuery { ...@@ -536,18 +673,18 @@ private sealed class FactQuery {
abstract val fact: String abstract val fact: String
abstract val type: FactType abstract val type: FactType
class UsernameQuery(query: String): FactQuery() { class UsernameQuery(query: String?): FactQuery() {
override val fact: String = query override val fact: String = query ?: ""
override val type: FactType = FactType.USERNAME override val type: FactType = FactType.USERNAME
} }
class EmailQuery(query: String): FactQuery() { class EmailQuery(query: String?): FactQuery() {
override val fact: String = query override val fact: String = query ?: ""
override val type: FactType = FactType.EMAIL override val type: FactType = FactType.EMAIL
} }
class PhoneQuery(query: String): FactQuery() { class PhoneQuery(query: String?): FactQuery() {
override val fact: String = query override val fact: String = query ?: ""
override val type: FactType = FactType.PHONE override val type: FactType = FactType.PHONE
} }
} }
\ No newline at end of file
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/spacing_30"/>
<stroke android:width="2dp" android:color="@color/white"/>
</shape>
\ No newline at end of file
...@@ -120,6 +120,7 @@ ...@@ -120,6 +120,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="1dp" android:layout_height="1dp"
android:background="@color/neutral_line" android:background="@color/neutral_line"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
......
...@@ -140,6 +140,7 @@ ...@@ -140,6 +140,7 @@
android:layout_height="1dp" android:layout_height="1dp"
android:layout_marginTop="@dimen/spacing_12" android:layout_marginTop="@dimen/spacing_12"
android:background="@color/neutral_line" android:background="@color/neutral_line"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/chatsMenu" /> app:layout_constraintTop_toBottomOf="@id/chatsMenu" />
......
...@@ -99,8 +99,8 @@ ...@@ -99,8 +99,8 @@
android:id="@+id/fadeView" android:id="@+id/fadeView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:alpha="0.85" android:alpha=".8"
android:background="@color/neutral_white" android:background="@color/black"
android:visibility="@{ui.isSearching}" android:visibility="@{ui.isSearching}"
android:elevation="5dp" android:elevation="5dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
...@@ -116,12 +116,13 @@ ...@@ -116,12 +116,13 @@
android:indeterminate="true" android:indeterminate="true"
android:visibility="@{ui.isSearching}" android:visibility="@{ui.isSearching}"
android:elevation="5dp" android:elevation="5dp"
android:indeterminateTint="@color/white"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<io.xxlabs.messenger.support.view.SingleClickButton <Button
android:id="@+id/userSearchCancelButton" android:id="@+id/userSearchCancelButton"
style="@style/dialog_button" style="@style/dialog_button"
android:layout_width="0dp" android:layout_width="0dp"
...@@ -131,6 +132,8 @@ ...@@ -131,6 +132,8 @@
android:stateListAnimator="@null" android:stateListAnimator="@null"
android:text="@string/cancel" android:text="@string/cancel"
android:visibility="@{ui.isSearching}" android:visibility="@{ui.isSearching}"
android:background="@drawable/white_rounded_square_outline"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
......
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
android:layout_marginHorizontal="0dp" android:layout_marginHorizontal="0dp"
android:drawablePadding="4dp" android:drawablePadding="4dp"
android:onClick="@{() -> listener.onActionClicked(ui)}" android:onClick="@{() -> listener.onActionClicked(ui)}"
android:visibility="@{ui.actionLabel}" android:visibility="@{ui.actionVisible}"
android:singleLine="false" android:singleLine="false"
android:gravity="center_vertical|end" android:gravity="center_vertical|end"
app:actionIcon="@{ui.actionIcon}" app:actionIcon="@{ui.actionIcon}"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment