diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/AddToFolderBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/AddToFolderBottomSheet.kt index 0adee3ae74..bb8366f635 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/AddToFolderBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/AddToFolderBottomSheet.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels import org.signal.core.ui.compose.BottomSheets import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.SignalPreview @@ -40,7 +41,6 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment -import org.thoughtcrime.securesms.util.viewModel /** * Bottom sheet shown when choosing to add a chat to a folder @@ -59,7 +59,11 @@ class AddToFolderBottomSheet private constructor(private val onDismissListener: OTHER(3) } - private val viewModel by viewModel { ConversationListViewModel(isArchived = false) } + private val viewModel: ConversationListViewModel by viewModels( + factoryProducer = { + ConversationListViewModel.Factory(isArchived = false) + } + ) companion object { private const val ARG_FOLDERS = "argument.folders" diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ChatFolderMappingModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ChatFolderMappingModel.kt index 485a2b1d66..e1867c730d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ChatFolderMappingModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ChatFolderMappingModel.kt @@ -1,17 +1,20 @@ package org.thoughtcrime.securesms.conversationlist +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel /** * Mapping model of folders used in [ChatFolderAdapter] */ +@Parcelize data class ChatFolderMappingModel( val chatFolder: ChatFolderRecord, val unreadCount: Int, val isMuted: Boolean, val isSelected: Boolean -) : MappingModel { +) : MappingModel, Parcelable { override fun areItemsTheSame(newItem: ChatFolderMappingModel): Boolean { return chatFolder.id == newItem.chatFolder.id } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index a4853c5aca..74cb095d42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -146,7 +146,6 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.search.MessageResult; import org.thoughtcrime.securesms.sms.MessageSender; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.BottomSheetUtil; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt index 833721bc4e..8e8f7a626d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.kt @@ -1,17 +1,25 @@ package org.thoughtcrime.securesms.conversationlist +import android.os.Parcelable +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.BackpressureStrategy import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.addTo +import io.reactivex.rxjava3.kotlin.combineLatest import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.asFlowable +import kotlinx.parcelize.Parcelize import org.signal.paging.PagedData import org.signal.paging.PagingConfig import org.signal.paging.ProxyPagingController @@ -35,15 +43,24 @@ import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState import java.util.concurrent.TimeUnit class ConversationListViewModel( - private val isArchived: Boolean + private val isArchived: Boolean, + private val savedStateHandle: SavedStateHandle ) : ViewModel() { companion object { + private const val STATE = "state" + private var coldStart = true } private val disposables: CompositeDisposable = CompositeDisposable() + private var saveableState: SaveableState + get() = savedStateHandle[STATE] ?: SaveableState() + set(value) { + savedStateHandle[STATE] = value + } + private val store = RxStore(ConversationListState()).addTo(disposables) private val conversationListDataSource: Flowable private val pagingConfig = PagingConfig.Builder() @@ -53,18 +70,18 @@ class ConversationListViewModel( val conversationsState: Flowable> = store.mapDistinctForUi { it.conversations } val selectedState: Flowable = store.mapDistinctForUi { it.selectedConversations } - val filterRequestState: Flowable = store.mapDistinctForUi { it.filterRequest } - val chatFolderState: Flowable> = store.mapDistinctForUi { it.chatFolders } + val filterRequestState: Flowable = savedStateHandle.getStateFlow(STATE, SaveableState()).map { it.filterRequest }.asFlowable() + val chatFolderState: Flowable> = savedStateHandle.getStateFlow(STATE, SaveableState()).map { it.chatFolders }.asFlowable() val hasNoConversations: Flowable val controller = ProxyPagingController() val folders: List - get() = store.state.chatFolders + get() = saveableState.chatFolders val currentFolder: ChatFolderRecord - get() = store.state.currentFolder + get() = saveableState.currentFolder val conversationFilterRequest: ConversationFilterRequest - get() = store.state.filterRequest + get() = saveableState.filterRequest val pinnedCount: Int get() = store.state.pinnedCount val webSocketState: Observable @@ -75,8 +92,9 @@ class ConversationListViewModel( get() = store.state.internalSelection init { - conversationListDataSource = store - .stateFlowable + val saveableStateFlowable = savedStateHandle.getStateFlow(STATE, SaveableState()).asFlowable() + + conversationListDataSource = saveableStateFlowable .subscribeOn(Schedulers.io()) .filter { it.currentFolder.id != -1L } .map { it.filterRequest to it.currentFolder } @@ -128,7 +146,8 @@ class ConversationListViewModel( hasNoConversations = store .stateFlowable .subscribeOn(Schedulers.io()) - .map { it.filterRequest to it.conversations } + .combineLatest(saveableStateFlowable.map { it.filterRequest }) + .map { (state, filterRequest) -> filterRequest to state.conversations } .distinctUntilChanged() .map { (filterRequest, conversations) -> if (conversations.isNotEmpty()) { @@ -187,9 +206,12 @@ class ConversationListViewModel( } fun setFiltered(isFiltered: Boolean, conversationFilterSource: ConversationFilterSource) { - store.update { - it.copy(filterRequest = ConversationFilterRequest(if (isFiltered) ConversationFilter.UNREAD else ConversationFilter.OFF, conversationFilterSource)) - } + saveableState = saveableState.copy( + filterRequest = ConversationFilterRequest( + filter = if (isFiltered) ConversationFilter.UNREAD else ConversationFilter.OFF, + source = conversationFilterSource + ) + ) } private fun loadCurrentFolders() { @@ -211,12 +233,10 @@ class ConversationListViewModel( ) } - store.update { - it.copy( - currentFolder = folders.find { folder -> folder.id == selectedFolderId } ?: ChatFolderRecord(), - chatFolders = chatFolders - ) - } + saveableState = saveableState.copy( + currentFolder = folders.find { folder -> folder.id == selectedFolderId } ?: ChatFolderRecord(), + chatFolders = chatFolders + ) } } @@ -228,14 +248,12 @@ class ConversationListViewModel( } fun select(chatFolder: ChatFolderRecord) { - store.update { - it.copy( - currentFolder = chatFolder, - chatFolders = folders.map { model -> - model.copy(isSelected = chatFolder.id == model.chatFolder.id) - } - ) - } + saveableState = saveableState.copy( + currentFolder = chatFolder, + chatFolders = folders.map { model -> + model.copy(isSelected = chatFolder.id == model.chatFolder.id) + } + ) } fun onUpdateMute(chatFolder: ChatFolderRecord, until: Long) { @@ -282,19 +300,30 @@ class ConversationListViewModel( StorageSyncHelper.scheduleSyncForDataChange() } - private data class ConversationListState( + /** + * Easily persistable state to ensure proper restoration upon VM recreation. + */ + @Parcelize + private data class SaveableState( val chatFolders: List = emptyList(), val currentFolder: ChatFolderRecord = ChatFolderRecord(), + val filterRequest: ConversationFilterRequest = ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG) + ) : Parcelable + + private data class ConversationListState( val conversations: List = emptyList(), val selectedConversations: ConversationSet = ConversationSet(), val internalSelection: Set = emptySet(), - val filterRequest: ConversationFilterRequest = ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG), val pinnedCount: Int = 0 ) - class Factory(private val isArchived: Boolean) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return modelClass.cast(ConversationListViewModel(isArchived))!! + class Factory( + private val isArchived: Boolean + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class, extras: CreationExtras): T { + val savedStateHandle = extras.createSavedStateHandle() + + return modelClass.cast(ConversationListViewModel(isArchived, savedStateHandle))!! } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterRequest.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterRequest.kt index 6b061482c8..6b3ddf91d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterRequest.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterRequest.kt @@ -1,8 +1,11 @@ package org.thoughtcrime.securesms.conversationlist.chatfilter +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter +@Parcelize data class ConversationFilterRequest( val filter: ConversationFilter, val source: ConversationFilterSource -) +) : Parcelable