Skip to content

Commit def020f

Browse files
committed
Refactoring search for correctness and efficiency and clarity
- dealing with race conditions b/c the code was structured weirdly - contacts view model fetches users on init for some reason, this is a work around Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
1 parent cdaaf39 commit def020f

3 files changed

Lines changed: 129 additions & 49 deletions

File tree

app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import androidx.lifecycle.ViewModel
1111
import androidx.lifecycle.viewModelScope
1212
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
1313
import com.nextcloud.talk.models.json.conversations.Conversation
14+
import kotlinx.coroutines.Dispatchers
1415
import kotlinx.coroutines.flow.MutableStateFlow
1516
import kotlinx.coroutines.flow.StateFlow
1617
import kotlinx.coroutines.flow.asStateFlow
1718
import kotlinx.coroutines.launch
19+
import kotlinx.coroutines.withContext
1820
import javax.inject.Inject
1921

2022
class ContactsViewModel @Inject constructor(private val repository: ContactsRepository) : ViewModel() {
@@ -64,11 +66,7 @@ class ContactsViewModel @Inject constructor(private val repository: ContactsRepo
6466
}
6567

6668
fun updateAddButtonState() {
67-
if (_selectedContacts.value.isEmpty()) {
68-
_enableAddButton.value = false
69-
} else {
70-
_enableAddButton.value = true
71-
}
69+
_enableAddButton.value = _selectedContacts.value.isNotEmpty()
7270
}
7371

7472
fun deselectContact(contact: AutocompleteUser) {
@@ -121,6 +119,30 @@ class ContactsViewModel @Inject constructor(private val repository: ContactsRepo
121119
}
122120
}
123121

122+
@Suppress("Detekt.TooGenericExceptionCaught")
123+
suspend fun getBlockingContactsFromSearchParams(query: String = "") =
124+
withContext(Dispatchers.IO) {
125+
_contactsViewState.value = ContactsUiState.Loading
126+
try {
127+
val contacts = repository.getContacts(
128+
if (query != "") query else searchQuery.value,
129+
shareTypeList
130+
)
131+
val contactsList: MutableList<AutocompleteUser>? = contacts.ocs!!.data?.toMutableList()
132+
133+
if (hideAlreadyAddedParticipants && !_clickAddButton.value) {
134+
contactsList?.removeAll(selectedParticipants.value)
135+
}
136+
if (_clickAddButton.value) {
137+
contactsList?.removeAll(selectedParticipants.value)
138+
contactsList?.addAll(_selectedContacts.value)
139+
}
140+
_contactsViewState.value = ContactsUiState.Success(contactsList)
141+
} catch (exception: Exception) {
142+
_contactsViewState.value = ContactsUiState.Error(exception.message ?: "")
143+
}
144+
}
145+
124146
@Suppress("Detekt.TooGenericExceptionCaught")
125147
fun createRoom(roomType: String, sourceType: String?, userId: String, conversationName: String?) {
126148
viewModelScope.launch {

app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt

Lines changed: 89 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,15 @@ import io.reactivex.android.schedulers.AndroidSchedulers
154154
import io.reactivex.disposables.Disposable
155155
import io.reactivex.schedulers.Schedulers
156156
import io.reactivex.subjects.BehaviorSubject
157+
import kotlinx.coroutines.Dispatchers
158+
import kotlinx.coroutines.async
159+
import kotlinx.coroutines.awaitAll
157160
import kotlinx.coroutines.flow.collect
158161
import kotlinx.coroutines.flow.onEach
159162
import kotlinx.coroutines.launch
163+
import kotlinx.coroutines.sync.Mutex
164+
import kotlinx.coroutines.sync.withLock
165+
import kotlinx.coroutines.withContext
160166
import org.apache.commons.lang3.builder.CompareToBuilder
161167
import org.greenrobot.eventbus.Subscribe
162168
import org.greenrobot.eventbus.ThreadMode
@@ -215,6 +221,7 @@ class ConversationsListActivity :
215221
private var conversationItemsWithHeader: MutableList<AbstractFlexibleItem<*>> = ArrayList()
216222
private var searchableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
217223
private var filterableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
224+
private val mutex = Mutex()
218225
private var nearFutureEventConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
219226
private var searchItem: MenuItem? = null
220227
private var chooseAccountItem: MenuItem? = null
@@ -451,8 +458,8 @@ class ConversationsListActivity :
451458
when (state) {
452459
is ConversationsListViewModel.OpenConversationsUiState.Success -> {
453460
val openConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
461+
val headerTitle = resources!!.getString(R.string.openConversations)
454462
for (conversation in state.conversations) {
455-
val headerTitle = resources!!.getString(R.string.openConversations)
456463
var genericTextHeaderItem: GenericTextHeaderItem
457464
if (!callHeaderItems.containsKey(headerTitle)) {
458465
genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils)
@@ -467,7 +474,15 @@ class ConversationsListActivity :
467474
)
468475
openConversationItems.add(conversationItem)
469476
}
470-
searchableConversationItems.addAll(openConversationItems)
477+
478+
mutex.withLock {
479+
// Filters out all old open conversation items from the previous query
480+
searchableConversationItems = searchableConversationItems.filter {
481+
!(it is ConversationItem && it.header == callHeaderItems[headerTitle])
482+
}.toMutableList()
483+
484+
searchableConversationItems.addAll(openConversationItems)
485+
}
471486
}
472487
is ConversationsListViewModel.OpenConversationsUiState.Error -> {
473488
handleHttpExceptions(state.exception)
@@ -541,13 +556,14 @@ class ConversationsListActivity :
541556
userItems.add(contactItem)
542557
}
543558

544-
val list = searchableConversationItems.filter {
545-
it !is ContactItem
546-
}.toMutableList()
559+
mutex.withLock {
560+
// Filters out all old user items from the previous query
561+
searchableConversationItems = searchableConversationItems.filter {
562+
it !is ContactItem
563+
}.toMutableList()
547564

548-
list.addAll(userItems)
549-
550-
searchableConversationItems = list
565+
searchableConversationItems.addAll(userItems)
566+
}
551567
}
552568

553569
else -> {}
@@ -659,8 +675,6 @@ class ConversationsListActivity :
659675
}
660676

661677
Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong())
662-
663-
fetchOpenConversations()
664678
}
665679

666680
fun applyFilter() {
@@ -989,9 +1003,9 @@ class ConversationsListActivity :
9891003
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
9901004
initSearchDisposable()
9911005
adapter?.setHeadersShown(true)
992-
if (!hasFilterEnabled()) filterableConversationItems = searchableConversationItems
993-
adapter!!.updateDataSet(filterableConversationItems, false)
9941006
adapter!!.showAllHeaders()
1007+
if (!hasFilterEnabled()) filterableConversationItems = searchableConversationItems
1008+
// adapter!!.updateDataSet(filterableConversationItems, false)
9951009
binding.swipeRefreshLayoutView.isEnabled = false
9961010
searchBehaviorSubject.onNext(true)
9971011
return true
@@ -1232,14 +1246,14 @@ class ConversationsListActivity :
12321246
}
12331247
}
12341248

1235-
private fun fetchOpenConversations() {
1249+
private suspend fun fetchOpenConversations(searchTerm: String) {
12361250
searchableConversationItems.clear()
12371251
searchableConversationItems.addAll(conversationItemsWithHeader)
1238-
conversationsListViewModel.fetchOpenConversations()
1252+
conversationsListViewModel.fetchOpenConversations(searchTerm)
12391253
}
12401254

1241-
private fun fetchUsers(query: String = "") {
1242-
contactsViewModel.getContactsFromSearchParams(query)
1255+
private suspend fun fetchUsers(query: String = "") {
1256+
contactsViewModel.getBlockingContactsFromSearchParams(query)
12431257
}
12441258

12451259
private fun handleHttpExceptions(throwable: Throwable) {
@@ -1444,26 +1458,64 @@ class ConversationsListActivity :
14441458
if (filter!!.length >= SEARCH_MIN_CHARS) {
14451459
clearMessageSearchResults()
14461460
binding.noArchivedConversationLayout.visibility = View.GONE
1461+
binding.swipeRefreshLayoutView.isRefreshing = true
14471462

1448-
fetchUsers(filter)
1463+
lifecycleScope.launch {
1464+
// gets users, updates collector async, which adds them to searchableConversationItems
1465+
val deferred1 = async {
1466+
fetchUsers(filter)
1467+
}
14491468

1450-
if (hasFilterEnabled()) {
1451-
adapter?.updateDataSet(conversationItems)
1452-
adapter?.setFilter(filter)
1453-
adapter?.filterItems()
1454-
adapter?.updateDataSet(filterableConversationItems)
1455-
} else {
1456-
adapter?.updateDataSet(searchableConversationItems)
1457-
adapter?.setFilter(filter)
1458-
adapter?.filterItems()
1459-
}
1469+
// gets open conversations, updates collector async, which adds them to searchableConversationItems
1470+
val deferred2 = async {
1471+
fetchOpenConversations(filter)
1472+
}
14601473

1461-
if (hasSpreedFeatureCapability(
1462-
currentUser?.capabilities?.spreedCapability,
1463-
SpreedFeatures.UNIFIED_SEARCH
1464-
)
1465-
) {
1466-
startMessageSearch(filter)
1474+
awaitAll(deferred1, deferred2)
1475+
1476+
// Waits until both work in collectors is over, to avoid data races
1477+
mutex.withLock {
1478+
if (hasFilterEnabled()) {
1479+
val headerTitle = resources!!.getString(R.string.openConversations)
1480+
1481+
fun AbstractFlexibleItem<*>.isRegularConversationItem() =
1482+
this is ConversationItem && this.header != callHeaderItems[headerTitle]
1483+
1484+
// Only keeps the Open Conversations, Users
1485+
val list = searchableConversationItems.filter {
1486+
!it.isRegularConversationItem()
1487+
}.toMutableList()
1488+
1489+
// Only keeps the conversation items with the applied Nextcloud filter [mention/archive/unread]
1490+
filterableConversationItems = filterableConversationItems.filter {
1491+
it.isRegularConversationItem()
1492+
}.toMutableList()
1493+
1494+
filterableConversationItems.addAll(list)
1495+
adapter?.updateDataSet(filterableConversationItems)
1496+
1497+
adapter?.setFilter(filter)
1498+
adapter?.filterItems()
1499+
} else {
1500+
// Conversation Items without Nextcloud filter + Open conversations/users
1501+
adapter?.updateDataSet(searchableConversationItems)
1502+
adapter?.setFilter(filter)
1503+
adapter?.filterItems()
1504+
}
1505+
}
1506+
1507+
if (hasSpreedFeatureCapability(
1508+
currentUser?.capabilities?.spreedCapability,
1509+
SpreedFeatures.UNIFIED_SEARCH
1510+
)
1511+
) {
1512+
// gets messages async, adds them to the adapter, but NOT the searchableConversationItems
1513+
startMessageSearch(filter)
1514+
}
1515+
1516+
withContext(Dispatchers.Main) {
1517+
binding.swipeRefreshLayoutView.isRefreshing = false
1518+
}
14671519
}
14681520
} else {
14691521
resetSearchResults()
@@ -1512,7 +1564,10 @@ class ConversationsListActivity :
15121564
binding.swipeRefreshLayoutView.isRefreshing = true
15131565
val observable = searchHelper!!.loadMore()
15141566
observable?.observeOn(AndroidSchedulers.mainThread())
1515-
?.subscribe({ results: MessageSearchResults -> onMessageSearchResult(results) }) { throwable: Throwable ->
1567+
?.subscribe({ results: MessageSearchResults ->
1568+
onMessageSearchResult(results)
1569+
binding.swipeRefreshLayoutView.isRefreshing = false
1570+
}) { throwable: Throwable ->
15161571
onMessageSearchError(
15171572
throwable
15181573
)
@@ -2202,10 +2257,8 @@ class ConversationsListActivity :
22022257
}
22032258

22042259
adapter?.addItems(Int.MAX_VALUE, adapterItems)
2205-
binding.recyclerView.scrollToPosition(0)
22062260
}
22072261
}
2208-
binding.swipeRefreshLayoutView.isRefreshing = false
22092262
}
22102263

22112264
private fun onMessageSearchError(throwable: Throwable) {

app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ import io.reactivex.Observer
2929
import io.reactivex.android.schedulers.AndroidSchedulers
3030
import io.reactivex.disposables.Disposable
3131
import io.reactivex.schedulers.Schedulers
32+
import kotlinx.coroutines.Dispatchers
3233
import kotlinx.coroutines.flow.MutableStateFlow
3334
import kotlinx.coroutines.flow.StateFlow
3435
import kotlinx.coroutines.flow.catch
3536
import kotlinx.coroutines.flow.onEach
3637
import kotlinx.coroutines.launch
38+
import kotlinx.coroutines.withContext
3739
import java.util.concurrent.TimeUnit
3840
import javax.inject.Inject
3941

@@ -199,15 +201,19 @@ class ConversationsListViewModel @Inject constructor(
199201
}
200202
}
201203

202-
fun fetchOpenConversations() {
203-
_openConversationsState.value = OpenConversationsUiState.None
204+
suspend fun fetchOpenConversations(searchTerm: String) =
205+
withContext(Dispatchers.IO) {
206+
_openConversationsState.value = OpenConversationsUiState.None
204207

205-
if (!hasSpreedFeatureCapability(currentUser.capabilities?.spreedCapability, SpreedFeatures.LISTABLE_ROOMS)) {
206-
return
207-
}
208+
if (!hasSpreedFeatureCapability(
209+
currentUser.capabilities?.spreedCapability,
210+
SpreedFeatures.LISTABLE_ROOMS
211+
)
212+
) {
213+
return@withContext
214+
}
208215

209-
viewModelScope.launch {
210-
openConversationsRepository.fetchConversations("")
216+
openConversationsRepository.fetchConversations(searchTerm)
211217
.onSuccess { conversations ->
212218
if (conversations.isEmpty()) {
213219
_openConversationsState.value = OpenConversationsUiState.None
@@ -220,7 +226,6 @@ class ConversationsListViewModel @Inject constructor(
220226
_openConversationsState.value = OpenConversationsUiState.Error(exception)
221227
}
222228
}
223-
}
224229

225230
inner class FederatedInvitationsObserver : Observer<InvitationsModel> {
226231
override fun onSubscribe(d: Disposable) {

0 commit comments

Comments
 (0)