2022-02-24 17:40:28 +00:00
|
|
|
package org.thoughtcrime.securesms.contacts.paged
|
|
|
|
|
|
|
|
import androidx.lifecycle.LiveData
|
|
|
|
import androidx.lifecycle.MutableLiveData
|
|
|
|
import androidx.lifecycle.Transformations
|
|
|
|
import androidx.lifecycle.ViewModel
|
|
|
|
import androidx.lifecycle.ViewModelProvider
|
2022-08-23 23:24:45 +00:00
|
|
|
import io.reactivex.rxjava3.core.Observable
|
2022-02-24 17:40:28 +00:00
|
|
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|
|
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
2022-08-23 23:24:45 +00:00
|
|
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
2022-03-16 14:10:01 +00:00
|
|
|
import org.signal.paging.LivePagedData
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.signal.paging.PagedData
|
|
|
|
import org.signal.paging.PagingConfig
|
|
|
|
import org.signal.paging.PagingController
|
2022-08-18 13:11:42 +00:00
|
|
|
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.groups.SelectionLimits
|
|
|
|
import org.thoughtcrime.securesms.recipients.Recipient
|
|
|
|
import org.thoughtcrime.securesms.util.livedata.Store
|
2022-06-29 12:57:06 +00:00
|
|
|
import org.whispersystems.signalservice.api.util.Preconditions
|
2022-02-24 17:40:28 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Simple, reusable view model that manages a ContactSearchPagedDataSource as well as filter and expansion state.
|
|
|
|
*/
|
|
|
|
class ContactSearchViewModel(
|
|
|
|
private val selectionLimits: SelectionLimits,
|
2022-07-26 20:31:55 +00:00
|
|
|
private val contactSearchRepository: ContactSearchRepository,
|
|
|
|
private val performSafetyNumberChecks: Boolean,
|
|
|
|
private val safetyNumberRepository: SafetyNumberRepository = SafetyNumberRepository(),
|
2022-02-24 17:40:28 +00:00
|
|
|
) : ViewModel() {
|
|
|
|
|
|
|
|
private val disposables = CompositeDisposable()
|
|
|
|
|
|
|
|
private val pagingConfig = PagingConfig.Builder()
|
|
|
|
.setBufferPages(1)
|
|
|
|
.setPageSize(20)
|
|
|
|
.setStartIndex(0)
|
|
|
|
.build()
|
|
|
|
|
2022-03-16 14:10:01 +00:00
|
|
|
private val pagedData = MutableLiveData<LivePagedData<ContactSearchKey, ContactSearchData>>()
|
2022-02-24 17:40:28 +00:00
|
|
|
private val configurationStore = Store(ContactSearchState())
|
|
|
|
private val selectionStore = Store<Set<ContactSearchKey>>(emptySet())
|
2022-08-23 23:24:45 +00:00
|
|
|
private val errorEvents = PublishSubject.create<ContactSearchError>()
|
2022-02-24 17:40:28 +00:00
|
|
|
|
|
|
|
val controller: LiveData<PagingController<ContactSearchKey>> = Transformations.map(pagedData) { it.controller }
|
|
|
|
val data: LiveData<List<ContactSearchData>> = Transformations.switchMap(pagedData) { it.data }
|
|
|
|
val configurationState: LiveData<ContactSearchState> = configurationStore.stateLiveData
|
|
|
|
val selectionState: LiveData<Set<ContactSearchKey>> = selectionStore.stateLiveData
|
2022-08-23 23:24:45 +00:00
|
|
|
val errorEventsStream: Observable<ContactSearchError> = errorEvents
|
2022-02-24 17:40:28 +00:00
|
|
|
|
|
|
|
override fun onCleared() {
|
|
|
|
disposables.clear()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setConfiguration(contactSearchConfiguration: ContactSearchConfiguration) {
|
|
|
|
val pagedDataSource = ContactSearchPagedDataSource(contactSearchConfiguration)
|
2022-03-16 14:10:01 +00:00
|
|
|
pagedData.value = PagedData.createForLiveData(pagedDataSource, pagingConfig)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fun setQuery(query: String?) {
|
|
|
|
configurationStore.update { it.copy(query = query) }
|
|
|
|
}
|
|
|
|
|
|
|
|
fun expandSection(sectionKey: ContactSearchConfiguration.SectionKey) {
|
|
|
|
configurationStore.update { it.copy(expandedSections = it.expandedSections + sectionKey) }
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setKeysSelected(contactSearchKeys: Set<ContactSearchKey>) {
|
|
|
|
disposables += contactSearchRepository.filterOutUnselectableContactSearchKeys(contactSearchKeys).subscribe { results ->
|
|
|
|
if (results.any { !it.isSelectable }) {
|
2022-08-23 23:24:45 +00:00
|
|
|
errorEvents.onNext(ContactSearchError.CONTACT_NOT_SELECTABLE)
|
2022-02-24 17:40:28 +00:00
|
|
|
return@subscribe
|
|
|
|
}
|
|
|
|
|
|
|
|
val newSelectionEntries = results.filter { it.isSelectable }.map { it.key } - getSelectedContacts()
|
|
|
|
val newSelectionSize = newSelectionEntries.size + getSelectedContacts().size
|
|
|
|
|
|
|
|
if (selectionLimits.hasRecommendedLimit() && getSelectedContacts().size < selectionLimits.recommendedLimit && newSelectionSize >= selectionLimits.recommendedLimit) {
|
2022-08-23 23:24:45 +00:00
|
|
|
errorEvents.onNext(ContactSearchError.RECOMMENDED_LIMIT_REACHED)
|
2022-02-24 17:40:28 +00:00
|
|
|
} else if (selectionLimits.hasHardLimit() && newSelectionSize > selectionLimits.hardLimit) {
|
2022-08-23 23:24:45 +00:00
|
|
|
errorEvents.onNext(ContactSearchError.HARD_LIMIT_REACHED)
|
2022-02-24 17:40:28 +00:00
|
|
|
return@subscribe
|
|
|
|
}
|
|
|
|
|
2022-07-26 20:31:55 +00:00
|
|
|
if (performSafetyNumberChecks) {
|
|
|
|
safetyNumberRepository.batchSafetyNumberCheck(newSelectionEntries)
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
selectionStore.update { state -> state + newSelectionEntries }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setKeysNotSelected(contactSearchKeys: Set<ContactSearchKey>) {
|
|
|
|
selectionStore.update { it - contactSearchKeys }
|
|
|
|
}
|
|
|
|
|
|
|
|
fun getSelectedContacts(): Set<ContactSearchKey> {
|
|
|
|
return selectionStore.state
|
|
|
|
}
|
|
|
|
|
2023-01-18 14:49:47 +00:00
|
|
|
fun addToVisibleGroupStories(groupStories: Set<ContactSearchKey.RecipientSearchKey>) {
|
2022-09-22 16:21:53 +00:00
|
|
|
disposables += contactSearchRepository.markDisplayAsStory(groupStories.map { it.recipientId }).subscribe {
|
|
|
|
configurationStore.update { state ->
|
|
|
|
state.copy(
|
|
|
|
groupStories = state.groupStories + groupStories.map {
|
|
|
|
val recipient = Recipient.resolved(it.recipientId)
|
|
|
|
ContactSearchData.Story(recipient, recipient.participantIds.size, DistributionListPrivacyMode.ALL)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-29 12:57:06 +00:00
|
|
|
fun removeGroupStory(story: ContactSearchData.Story) {
|
|
|
|
Preconditions.checkArgument(story.recipient.isGroup)
|
|
|
|
setKeysNotSelected(setOf(story.contactSearchKey))
|
|
|
|
disposables += contactSearchRepository.unmarkDisplayAsStory(story.recipient.requireGroupId()).subscribe {
|
|
|
|
configurationStore.update { state ->
|
|
|
|
state.copy(
|
|
|
|
groupStories = state.groupStories.filter { it.recipient.id == story.recipient.id }.toSet()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
refresh()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun deletePrivateStory(story: ContactSearchData.Story) {
|
|
|
|
Preconditions.checkArgument(story.recipient.isDistributionList && !story.recipient.isMyStory)
|
|
|
|
setKeysNotSelected(setOf(story.contactSearchKey))
|
|
|
|
disposables += contactSearchRepository.deletePrivateStory(story.recipient.requireDistributionListId()).subscribe {
|
|
|
|
refresh()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun refresh() {
|
|
|
|
controller.value?.onDataInvalidated()
|
|
|
|
}
|
|
|
|
|
2022-07-26 20:31:55 +00:00
|
|
|
class Factory(
|
|
|
|
private val selectionLimits: SelectionLimits,
|
|
|
|
private val repository: ContactSearchRepository,
|
|
|
|
private val performSafetyNumberChecks: Boolean
|
|
|
|
) : ViewModelProvider.Factory {
|
2022-05-18 18:05:17 +00:00
|
|
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
2022-07-26 20:31:55 +00:00
|
|
|
return modelClass.cast(ContactSearchViewModel(selectionLimits, repository, performSafetyNumberChecks)) as T
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|