From 9fcec803c62bf2e2fe2c49b019bf2d8788c66d33 Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Wed, 17 Dec 2025 11:20:59 -0600 Subject: [PATCH 1/7] WIP - redoing the querying logic to use combine Signed-off-by: rapterjet2004 --- .../talk/contacts/ContactsRepository.kt | 4 ++ .../talk/contacts/ContactsRepositoryImpl.kt | 29 +++++++++ .../viewmodels/ConversationsListViewModel.kt | 65 +++++++++++++++++++ .../data/OpenConversationsRepository.kt | 3 + .../data/OpenConversationsRepositoryImpl.kt | 27 ++++++++ 5 files changed, 128 insertions(+) diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt index c93face36c..3abdcbb809 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt @@ -9,7 +9,9 @@ package com.nextcloud.talk.contacts import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.conversations.RoomOverall +import kotlinx.coroutines.flow.Flow interface ContactsRepository { suspend fun getContacts(user: User, searchQuery: String?, shareTypes: List): AutocompleteOverall @@ -23,4 +25,6 @@ interface ContactsRepository { ): RoomOverall fun getImageUri(user: User, avatarId: String, requestBigSize: Boolean): String + + fun getContactsFlow(user: User, searchQuery: String?): Flow> } diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt index 5839eddbb2..4e355c716b 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt @@ -11,9 +11,12 @@ import com.nextcloud.talk.api.NcApiCoroutines import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.RetrofitBucket import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ContactUtils +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import javax.inject.Inject class ContactsRepositoryImpl @Inject constructor(private val ncApiCoroutines: NcApiCoroutines) : ContactsRepository { @@ -71,6 +74,32 @@ class ContactsRepositoryImpl @Inject constructor(private val ncApiCoroutines: Nc requestBigSize ) + override fun getContactsFlow( + user: User, + searchQuery: String? + ): Flow> = flow { + val credentials = ApiUtils.getCredentials(user.username, user.token) + + val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForContactsSearchFor14( + user.baseUrl!!, + searchQuery + ) + + val shareTypes = mutableListOf(ShareType.User.shareType).toList() + + val modifiedQueryMap: HashMap = HashMap(retrofitBucket.queryMap) + modifiedQueryMap["limit"] = ContactUtils.MAX_CONTACT_LIMIT + modifiedQueryMap["shareTypes[]"] = shareTypes + val response = ncApiCoroutines.getContactsWithSearchParam( + credentials, + retrofitBucket.url, + shareTypes, + modifiedQueryMap + ) + + emit(response.ocs?.data.orEmpty()) + } + companion object { private val TAG = ContactsRepositoryImpl::class.simpleName } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt index 2bc57abb97..dc09f5f1cd 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -6,25 +6,36 @@ */ package com.nextcloud.talk.conversationlist.viewmodels +import android.content.Context import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.items.ContactItem +import com.nextcloud.talk.adapters.items.ConversationItem +import com.nextcloud.talk.adapters.items.GenericTextHeaderItem import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.contacts.ContactsRepository import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.invitation.data.InvitationsModel import com.nextcloud.talk.invitation.data.InvitationsRepository +import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter +import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.openconversations.data.OpenConversationsRepository import com.nextcloud.talk.threadsoverview.data.ThreadsRepository +import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability import com.nextcloud.talk.utils.SpreedFeatures import com.nextcloud.talk.utils.UserIdUtils import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable @@ -32,7 +43,10 @@ import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -44,6 +58,7 @@ class ConversationsListViewModel @Inject constructor( private val threadsRepository: ThreadsRepository, private val currentUserProvider: CurrentUserProviderOld, private val openConversationsRepository: OpenConversationsRepository, + private val contactsRepository: ContactsRepository, var userManager: UserManager ) : ViewModel() { @@ -53,6 +68,9 @@ class ConversationsListViewModel @Inject constructor( @Inject lateinit var arbitraryStorageManager: ArbitraryStorageManager + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + private val _currentUser = currentUserProvider.currentUser.blockingGet() val currentUser: User = _currentUser val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: "" @@ -122,6 +140,53 @@ class ConversationsListViewModel @Inject constructor( } } + private val _searchResultFlow: MutableStateFlow>> = MutableStateFlow(listOf()) + val searchResultFlow = _searchResultFlow.asStateFlow() + + fun getSearchQuery(context: Context, filter: String) { + val openConversationsTitle = context.resources!!.getString(R.string.openConversations) + val openConversationsHeader = GenericTextHeaderItem(openConversationsTitle, viewThemeUtils) + val usersTitle = context.resources!!.getString(R.string.nc_user) + val usersHeader = GenericTextHeaderItem(usersTitle, viewThemeUtils) + val actorTypeConverter = EnumActorTypeConverter() + + viewModelScope.launch { + combine( + openConversationsRepository.fetchOpenConversationsFlow(currentUser, filter) + .map { list -> + list.map { conversation -> + ConversationItem( + ConversationModel.mapToConversationModel(conversation, currentUser), + currentUser, + context, + openConversationsHeader, + viewThemeUtils + ) + } + }, + contactsRepository.getContactsFlow(currentUser, filter) + .map { list -> + list.map { autocompleteUser -> + val participant = Participant() + participant.actorId = autocompleteUser.id + participant.actorType = actorTypeConverter.getFromString(autocompleteUser.source) + participant.displayName = autocompleteUser.label + + ContactItem( + participant, + currentUser, + usersHeader, + viewThemeUtils + ) + } + } + ) { openConversations, users -> openConversations + users }.collect { searchResults -> + _searchResultFlow.emit(searchResults) + // TODO can this handle messages too + } + } + } + fun getRooms(user: User) { val startNanoTime = System.nanoTime() Log.d(TAG, "fetchData - getRooms - calling: $startNanoTime") diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepository.kt index 236997995a..13e1a8c94e 100644 --- a/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepository.kt @@ -8,8 +8,11 @@ package com.nextcloud.talk.openconversations.data import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.conversations.Conversation +import kotlinx.coroutines.flow.Flow interface OpenConversationsRepository { suspend fun fetchConversations(user: User, url: String, searchTerm: String): Result> + + fun fetchOpenConversationsFlow(user: User, searchTerm: String): Flow> } diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt index d008042e20..71ea281267 100644 --- a/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt @@ -10,6 +10,8 @@ import com.nextcloud.talk.api.NcApiCoroutines import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.utils.ApiUtils +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow class OpenConversationsRepositoryImpl(private val ncApiCoroutines: NcApiCoroutines) : OpenConversationsRepository { override suspend fun fetchConversations(user: User, url: String, searchTerm: String): Result> = @@ -23,4 +25,29 @@ class OpenConversationsRepositoryImpl(private val ncApiCoroutines: NcApiCoroutin ) roomOverall.ocs?.data.orEmpty() } + + override fun fetchOpenConversationsFlow( + user: User, + searchTerm: String + ): Flow> = flow { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + + val apiVersion = ApiUtils.getConversationApiVersion( + user, + intArrayOf( + ApiUtils.API_V4, + ApiUtils.API_V3, + 1 + ) + ) + val url = ApiUtils.getUrlForOpenConversations(apiVersion, user.baseUrl!!) + + val roomOverall = ncApiCoroutines.getOpenConversations( + credentials, + url, + searchTerm + ) + + emit(roomOverall.ocs?.data.orEmpty()) + } } From a6d26aa3a29ad6bfbd39601f78bda26f5bb2e510 Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Thu, 18 Dec 2025 11:14:50 -0600 Subject: [PATCH 2/7] WIP - converting message search functionality to a flow based approach Signed-off-by: rapterjet2004 --- .../ConversationsListActivity.kt | 2 +- .../viewmodels/ConversationsListViewModel.kt | 82 ++++++++++++++++--- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 59ba7d9ad6..8dbd5d0c49 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -1605,7 +1605,7 @@ class ConversationsListActivity : } is LoadMoreResultsItem -> { - loadMoreMessages() + conversationsListViewModel.loadMoreMessages(context) } is ConversationItem -> { diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt index dc09f5f1cd..c8a793d57a 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -16,17 +16,22 @@ import com.nextcloud.talk.R import com.nextcloud.talk.adapters.items.ContactItem import com.nextcloud.talk.adapters.items.ConversationItem import com.nextcloud.talk.adapters.items.GenericTextHeaderItem +import com.nextcloud.talk.adapters.items.LoadMoreResultsItem +import com.nextcloud.talk.adapters.items.MessageResultItem import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.contacts.ContactsRepository import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.invitation.data.InvitationsModel import com.nextcloud.talk.invitation.data.InvitationsRepository +import com.nextcloud.talk.messagesearch.MessageSearchHelper +import com.nextcloud.talk.messagesearch.MessageSearchHelper.MessageSearchResults import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.openconversations.data.OpenConversationsRepository +import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository import com.nextcloud.talk.threadsoverview.data.ThreadsRepository import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.users.UserManager @@ -41,6 +46,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -48,7 +54,9 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.rx2.asFlow import kotlinx.coroutines.withContext import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -59,22 +67,19 @@ class ConversationsListViewModel @Inject constructor( private val currentUserProvider: CurrentUserProviderOld, private val openConversationsRepository: OpenConversationsRepository, private val contactsRepository: ContactsRepository, + private val viewThemeUtils: ViewThemeUtils, + private val unifiedSearchRepository: UnifiedSearchRepository, + private val invitationsRepository: InvitationsRepository, + private val arbitraryStorageManager: ArbitraryStorageManager, var userManager: UserManager ) : ViewModel() { - @Inject - lateinit var invitationsRepository: InvitationsRepository - - @Inject - lateinit var arbitraryStorageManager: ArbitraryStorageManager - - @Inject - lateinit var viewThemeUtils: ViewThemeUtils - private val _currentUser = currentUserProvider.currentUser.blockingGet() val currentUser: User = _currentUser val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: "" + private val searchHelper = MessageSearchHelper(unifiedSearchRepository, currentUser) + sealed interface ViewState sealed class ThreadsExistUiState { @@ -179,11 +184,62 @@ class ConversationsListViewModel @Inject constructor( viewThemeUtils ) } + }, + getMessagesFlow(filter) + .map { (messages, hasMore) -> + messages.mapIndexed { index, entry -> + MessageResultItem( + context, + currentUser, + entry, + index == 0, + viewThemeUtils = viewThemeUtils + ) + }.let { + if (hasMore) { + it + LoadMoreResultsItem + } else { + it + } + } } - ) { openConversations, users -> openConversations + users }.collect { searchResults -> - _searchResultFlow.emit(searchResults) - // TODO can this handle messages too - } + ) { openConversations, users, messages -> openConversations + users + messages } + .collect { searchResults -> + _searchResultFlow.emit(searchResults) + } + } + } + + private fun getMessagesFlow(search: String): Flow = + searchHelper.startMessageSearch(search).subscribeOn(Schedulers.io()).asFlow() + + fun loadMoreMessages(context: Context) { + viewModelScope.launch { + searchHelper.loadMore() + ?.asFlow() + ?.map { (messages, hasMore) -> + messages.map { entry -> + MessageResultItem( + context, + currentUser, + entry, + false, + viewThemeUtils = viewThemeUtils + ) + }.let { + if (hasMore) { + it + LoadMoreResultsItem + } else { + it + } + } + }?.collect { messages -> + _searchResultFlow.update { // works because messages are always at the bottom of the list + it.filter { item -> + item !is LoadMoreResultsItem + } + messages + } + } } } From efcbc2cac41757c2b02570991854174792ea8da6 Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Thu, 18 Dec 2025 12:43:51 -0600 Subject: [PATCH 3/7] WIP - moving the activity logic to my viewModel, and observing the result properly Signed-off-by: rapterjet2004 --- .../ConversationsListActivity.kt | 179 +++--------------- 1 file changed, 23 insertions(+), 156 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 8dbd5d0c49..9d6f2e274c 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -87,9 +87,7 @@ import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.contacts.ContactsActivity - import com.nextcloud.talk.contacts.ContactsViewModel - import com.nextcloud.talk.contextchat.ContextChatView import com.nextcloud.talk.contextchat.ContextChatViewModel import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel @@ -107,8 +105,6 @@ import com.nextcloud.talk.messagesearch.MessageSearchHelper import com.nextcloud.talk.messagesearch.MessageSearchHelper.MessageSearchResults import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.json.conversations.ConversationEnums -import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter -import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository import com.nextcloud.talk.settings.SettingsActivity import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity @@ -154,15 +150,10 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.BehaviorSubject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext import org.apache.commons.lang3.builder.CompareToBuilder import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -383,6 +374,26 @@ class ConversationsListActivity : }.collect() } + lifecycleScope.launch { + val openConversationsTitle = context.resources!!.getString(R.string.openConversations) + val openConversationsHeader = GenericTextHeaderItem(openConversationsTitle, viewThemeUtils) + + fun AbstractFlexibleItem<*>.isRegularConversationItem() = + this is ConversationItem && this.header != openConversationsHeader + + conversationsListViewModel.searchResultFlow.collect { searchResults -> + val dataset = if (hasFilterEnabled()) filterableConversationItems else searchableConversationItems + + val newDataset = dataset + .filter { it.isRegularConversationItem() } + .toMutableList() + searchResults + + adapter?.updateDataSet(newDataset) + + adapter?.filterItems() // TODO - problem here with filtering existing conversations + } + } + conversationsListViewModel.getFederationInvitationsViewState.observe(this) { state -> when (state) { is ConversationsListViewModel.GetFederationInvitationsStartState -> { @@ -456,46 +467,6 @@ class ConversationsListActivity : } } - lifecycleScope.launch { - conversationsListViewModel.openConversationsState.collect { state -> - when (state) { - is ConversationsListViewModel.OpenConversationsUiState.Success -> { - val openConversationItems: MutableList> = ArrayList() - val headerTitle = resources!!.getString(R.string.openConversations) - for (conversation in state.conversations) { - var genericTextHeaderItem: GenericTextHeaderItem - if (!callHeaderItems.containsKey(headerTitle)) { - genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils) - callHeaderItems[headerTitle] = genericTextHeaderItem - } - val conversationItem = ConversationItem( - ConversationModel.mapToConversationModel(conversation, currentUser!!), - currentUser!!, - this@ConversationsListActivity, - callHeaderItems[headerTitle], - viewThemeUtils - ) - openConversationItems.add(conversationItem) - } - - mutex.withLock { - // Filters out all old open conversation items from the previous query - searchableConversationItems = searchableConversationItems.filter { - !(it is ConversationItem && it.header == callHeaderItems[headerTitle]) - }.toMutableList() - - searchableConversationItems.addAll(openConversationItems) - } - } - is ConversationsListViewModel.OpenConversationsUiState.Error -> { - handleHttpExceptions(state.exception) - } - - else -> {} - } - } - } - lifecycleScope.launch { conversationsListViewModel.getRoomsFlow .onEach { list -> @@ -528,52 +499,6 @@ class ConversationsListActivity : }.collect() } - lifecycleScope.launch { - contactsViewModel.contactsViewState.onEach { state -> - when (state) { - is ContactsViewModel.ContactsUiState.Success -> { - if (state.contacts.isNullOrEmpty()) return@onEach - - val userItems: MutableList> = ArrayList() - val actorTypeConverter = EnumActorTypeConverter() - var genericTextHeaderItem: GenericTextHeaderItem - for (autocompleteUser in state.contacts) { - val headerTitle = resources!!.getString(R.string.nc_user) - if (!callHeaderItems.containsKey(headerTitle)) { - genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils) - callHeaderItems[headerTitle] = genericTextHeaderItem - } - - val participant = Participant() - participant.actorId = autocompleteUser.id - participant.actorType = actorTypeConverter.getFromString(autocompleteUser.source) - participant.displayName = autocompleteUser.label - - val contactItem = ContactItem( - participant, - currentUser!!, - callHeaderItems[headerTitle], - viewThemeUtils - ) - - userItems.add(contactItem) - } - - mutex.withLock { - // Filters out all old user items from the previous query - searchableConversationItems = searchableConversationItems.filter { - it !is ContactItem - }.toMutableList() - - searchableConversationItems.addAll(userItems) - } - } - - else -> {} - } - }.collect() - } - lifecycleScope.launch { chatViewModel.backgroundPlayUIFlow.onEach { msg -> binding.composeViewForBackgroundPlay.apply { @@ -1007,8 +932,8 @@ class ConversationsListActivity : initSearchDisposable() adapter?.setHeadersShown(true) adapter!!.showAllHeaders() + searchableConversationItems.addAll(conversationItemsWithHeader) if (!hasFilterEnabled()) filterableConversationItems = searchableConversationItems - // adapter!!.updateDataSet(filterableConversationItems, false) binding.swipeRefreshLayoutView.isEnabled = false searchBehaviorSubject.onNext(true) return true @@ -1459,67 +1384,9 @@ class ConversationsListActivity : private fun performFilterAndSearch(filter: String?) { if (filter!!.length >= SEARCH_MIN_CHARS) { - clearMessageSearchResults() binding.noArchivedConversationLayout.visibility = View.GONE - binding.swipeRefreshLayoutView.isRefreshing = true - - lifecycleScope.launch { - // gets users, updates collector async, which adds them to searchableConversationItems - val deferred1 = async { - fetchUsers(filter) - } - - // gets open conversations, updates collector async, which adds them to searchableConversationItems - val deferred2 = async { - fetchOpenConversations(filter) - } - - awaitAll(deferred1, deferred2) - - // Waits until both work in collectors is over, to avoid data races - mutex.withLock { - if (hasFilterEnabled()) { - val headerTitle = resources!!.getString(R.string.openConversations) - - fun AbstractFlexibleItem<*>.isRegularConversationItem() = - this is ConversationItem && this.header != callHeaderItems[headerTitle] - - // Only keeps the Open Conversations, Users - val list = searchableConversationItems.filter { - !it.isRegularConversationItem() - }.toMutableList() - - // Only keeps the conversation items with the applied Nextcloud filter [mention/archive/unread] - filterableConversationItems = filterableConversationItems.filter { - it.isRegularConversationItem() - }.toMutableList() - - filterableConversationItems.addAll(list) - adapter?.updateDataSet(filterableConversationItems) - - adapter?.setFilter(filter) - adapter?.filterItems() - } else { - // Conversation Items without Nextcloud filter + Open conversations/users - adapter?.updateDataSet(searchableConversationItems) - adapter?.setFilter(filter) - adapter?.filterItems() - } - } - - if (hasSpreedFeatureCapability( - currentUser?.capabilities?.spreedCapability, - SpreedFeatures.UNIFIED_SEARCH - ) - ) { - // gets messages async, adds them to the adapter, but NOT the searchableConversationItems - startMessageSearch(filter) - } - - withContext(Dispatchers.Main) { - binding.swipeRefreshLayoutView.isRefreshing = false - } - } + adapter?.setFilter(filter) + conversationsListViewModel.getSearchQuery(context, filter) } else { resetSearchResults() } From a574a15e40bb72333c9e7415eba76cb65d4c117e Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Fri, 19 Dec 2025 12:14:04 -0600 Subject: [PATCH 4/7] WIP - this adds the conversations to the combine flow, which simplifies the activity and filtering logic, resolving race conditions Signed-off-by: rapterjet2004 --- .../ConversationsListActivity.kt | 17 +---------- .../viewmodels/ConversationsListViewModel.kt | 28 ++++++++++++++++--- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 9d6f2e274c..0119c49910 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -375,22 +375,8 @@ class ConversationsListActivity : } lifecycleScope.launch { - val openConversationsTitle = context.resources!!.getString(R.string.openConversations) - val openConversationsHeader = GenericTextHeaderItem(openConversationsTitle, viewThemeUtils) - - fun AbstractFlexibleItem<*>.isRegularConversationItem() = - this is ConversationItem && this.header != openConversationsHeader - conversationsListViewModel.searchResultFlow.collect { searchResults -> - val dataset = if (hasFilterEnabled()) filterableConversationItems else searchableConversationItems - - val newDataset = dataset - .filter { it.isRegularConversationItem() } - .toMutableList() + searchResults - - adapter?.updateDataSet(newDataset) - - adapter?.filterItems() // TODO - problem here with filtering existing conversations + adapter?.updateDataSet(searchResults) } } @@ -1393,7 +1379,6 @@ class ConversationsListActivity : } private fun resetSearchResults() { - clearMessageSearchResults() adapter?.updateDataSet(conversationItems) adapter?.setFilter("") adapter?.filterItems() diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt index c8a793d57a..c4308bf26f 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -48,12 +48,14 @@ import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.rx2.asFlow @@ -115,6 +117,10 @@ class ConversationsListViewModel @Inject constructor( _getRoomsViewState.value = GetRoomsErrorState } + val getRoomsStateFlow = repository + .roomListFlow + .stateIn(viewModelScope, SharingStarted.Eagerly, listOf()) + object GetFederationInvitationsStartState : ViewState object GetFederationInvitationsErrorState : ViewState @@ -149,6 +155,8 @@ class ConversationsListViewModel @Inject constructor( val searchResultFlow = _searchResultFlow.asStateFlow() fun getSearchQuery(context: Context, filter: String) { + val conversationsTitle: String = context.resources!!.getString(R.string.conversations) + val conversationsHeader = GenericTextHeaderItem(conversationsTitle, viewThemeUtils) val openConversationsTitle = context.resources!!.getString(R.string.openConversations) val openConversationsHeader = GenericTextHeaderItem(openConversationsTitle, viewThemeUtils) val usersTitle = context.resources!!.getString(R.string.nc_user) @@ -157,6 +165,17 @@ class ConversationsListViewModel @Inject constructor( viewModelScope.launch { combine( + getRoomsStateFlow.map { list -> + list.map { conversation -> + ConversationItem( + conversation, + currentUser, + context, + conversationsHeader, + viewThemeUtils + ) + }.filter { it.model.displayName.contains(filter, true) } + }, openConversationsRepository.fetchOpenConversationsFlow(currentUser, filter) .map { list -> list.map { conversation -> @@ -203,10 +222,11 @@ class ConversationsListViewModel @Inject constructor( } } } - ) { openConversations, users, messages -> openConversations + users + messages } - .collect { searchResults -> - _searchResultFlow.emit(searchResults) - } + ) { conversations, openConversations, users, messages -> + conversations + openConversations + users + messages + }.collect { searchResults -> + _searchResultFlow.emit(searchResults) + } } } From 0327330d6b7edf0193ca2650ee29f6c5a68df29a Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Fri, 19 Dec 2025 12:37:39 -0600 Subject: [PATCH 5/7] WIP - fixing refreshes Signed-off-by: rapterjet2004 --- .../talk/conversationlist/ConversationsListActivity.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 0119c49910..b9f49d06f8 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -376,7 +376,9 @@ class ConversationsListActivity : lifecycleScope.launch { conversationsListViewModel.searchResultFlow.collect { searchResults -> - adapter?.updateDataSet(searchResults) + if (adapter?.hasFilter() == true) { + adapter?.updateDataSet(searchResults) + } } } From e2ca92c379b54ffaed270d57fcc52213d98f8406 Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Fri, 19 Dec 2025 12:54:15 -0600 Subject: [PATCH 6/7] linter Signed-off-by: rapterjet2004 --- .../talk/contacts/ContactsRepositoryImpl.kt | 40 +++++++++---------- .../viewmodels/ConversationsListViewModel.kt | 2 +- .../data/OpenConversationsRepositoryImpl.kt | 38 +++++++++--------- 3 files changed, 38 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt index 4e355c716b..3a071eff3b 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt @@ -74,31 +74,29 @@ class ContactsRepositoryImpl @Inject constructor(private val ncApiCoroutines: Nc requestBigSize ) - override fun getContactsFlow( - user: User, - searchQuery: String? - ): Flow> = flow { - val credentials = ApiUtils.getCredentials(user.username, user.token) + override fun getContactsFlow(user: User, searchQuery: String?): Flow> = + flow { + val credentials = ApiUtils.getCredentials(user.username, user.token) - val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForContactsSearchFor14( - user.baseUrl!!, - searchQuery - ) + val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForContactsSearchFor14( + user.baseUrl!!, + searchQuery + ) - val shareTypes = mutableListOf(ShareType.User.shareType).toList() + val shareTypes = mutableListOf(ShareType.User.shareType).toList() - val modifiedQueryMap: HashMap = HashMap(retrofitBucket.queryMap) - modifiedQueryMap["limit"] = ContactUtils.MAX_CONTACT_LIMIT - modifiedQueryMap["shareTypes[]"] = shareTypes - val response = ncApiCoroutines.getContactsWithSearchParam( - credentials, - retrofitBucket.url, - shareTypes, - modifiedQueryMap - ) + val modifiedQueryMap: HashMap = HashMap(retrofitBucket.queryMap) + modifiedQueryMap["limit"] = ContactUtils.MAX_CONTACT_LIMIT + modifiedQueryMap["shareTypes[]"] = shareTypes + val response = ncApiCoroutines.getContactsWithSearchParam( + credentials, + retrofitBucket.url, + shareTypes, + modifiedQueryMap + ) - emit(response.ocs?.data.orEmpty()) - } + emit(response.ocs?.data.orEmpty()) + } companion object { private val TAG = ContactsRepositoryImpl::class.simpleName diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt index c4308bf26f..492ff3d416 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -254,7 +254,7 @@ class ConversationsListViewModel @Inject constructor( } } }?.collect { messages -> - _searchResultFlow.update { // works because messages are always at the bottom of the list + _searchResultFlow.update { it.filter { item -> item !is LoadMoreResultsItem } + messages diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt index 71ea281267..0fdbbffb7b 100644 --- a/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt @@ -26,28 +26,26 @@ class OpenConversationsRepositoryImpl(private val ncApiCoroutines: NcApiCoroutin roomOverall.ocs?.data.orEmpty() } - override fun fetchOpenConversationsFlow( - user: User, - searchTerm: String - ): Flow> = flow { - val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + override fun fetchOpenConversationsFlow(user: User, searchTerm: String): Flow> = + flow { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! - val apiVersion = ApiUtils.getConversationApiVersion( - user, - intArrayOf( - ApiUtils.API_V4, - ApiUtils.API_V3, - 1 + val apiVersion = ApiUtils.getConversationApiVersion( + user, + intArrayOf( + ApiUtils.API_V4, + ApiUtils.API_V3, + 1 + ) ) - ) - val url = ApiUtils.getUrlForOpenConversations(apiVersion, user.baseUrl!!) + val url = ApiUtils.getUrlForOpenConversations(apiVersion, user.baseUrl!!) - val roomOverall = ncApiCoroutines.getOpenConversations( - credentials, - url, - searchTerm - ) + val roomOverall = ncApiCoroutines.getOpenConversations( + credentials, + url, + searchTerm + ) - emit(roomOverall.ocs?.data.orEmpty()) - } + emit(roomOverall.ocs?.data.orEmpty()) + } } From 1268e892529196a6eb2e98dc4e01fe08f2831a14 Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Mon, 22 Dec 2025 10:10:19 -0600 Subject: [PATCH 7/7] fixing tests Signed-off-by: rapterjet2004 --- .../viewmodels/ConversationsListViewModel.kt | 1 + .../talk/contacts/repository/FakeRepositoryError.kt | 8 ++++++++ .../talk/contacts/repository/FakeRepositorySuccess.kt | 8 ++++++++ 3 files changed, 17 insertions(+) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt index 492ff3d416..69667af046 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -154,6 +154,7 @@ class ConversationsListViewModel @Inject constructor( private val _searchResultFlow: MutableStateFlow>> = MutableStateFlow(listOf()) val searchResultFlow = _searchResultFlow.asStateFlow() + @Suppress("LongMethod") fun getSearchQuery(context: Context, filter: String) { val conversationsTitle: String = context.resources!!.getString(R.string.conversations) val conversationsHeader = GenericTextHeaderItem(conversationsTitle, viewThemeUtils) diff --git a/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositoryError.kt b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositoryError.kt index ed6ce94b4e..cb40f86fcf 100644 --- a/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositoryError.kt +++ b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositoryError.kt @@ -10,7 +10,10 @@ package com.nextcloud.talk.contacts.repository import com.nextcloud.talk.contacts.ContactsRepository import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser import com.nextcloud.talk.models.json.conversations.RoomOverall +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow class FakeRepositoryError : ContactsRepository { @Suppress("Detekt.TooGenericExceptionThrown") @@ -28,4 +31,9 @@ class FakeRepositoryError : ContactsRepository { override fun getImageUri(user: User, avatarId: String, requestBigSize: Boolean) = "https://mydoman.com/index.php/avatar/$avatarId/512" + + override fun getContactsFlow(user: User, searchQuery: String?): Flow> = + flow { + // unused atm + } } diff --git a/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt index aed3e13c6a..5bc378dc48 100644 --- a/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt +++ b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt @@ -10,6 +10,9 @@ package com.nextcloud.talk.contacts.repository import com.nextcloud.talk.contacts.ContactsRepository import com.nextcloud.talk.contacts.apiService.FakeItem import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow class FakeRepositorySuccess : ContactsRepository { override suspend fun getContacts(user: User, searchQuery: String?, shareTypes: List) = @@ -25,4 +28,9 @@ class FakeRepositorySuccess : ContactsRepository { override fun getImageUri(user: User, avatarId: String, requestBigSize: Boolean) = "https://mydomain.com/index.php/avatar/$avatarId/512" + + override fun getContactsFlow(user: User, searchQuery: String?): Flow> = + flow { + // unused atm + } }