Ensures chat folder is remembered when we leave page.

This commit is contained in:
Alex Hart
2025-04-23 10:43:46 -03:00
committed by Cody Henthorne
parent 252a4afa79
commit be035456f7
5 changed files with 74 additions and 36 deletions

View File

@@ -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"

View File

@@ -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<ChatFolderMappingModel> {
) : MappingModel<ChatFolderMappingModel>, Parcelable {
override fun areItemsTheSame(newItem: ChatFolderMappingModel): Boolean {
return chatFolder.id == newItem.chatFolder.id
}

View File

@@ -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;

View File

@@ -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<ConversationListDataSource>
private val pagingConfig = PagingConfig.Builder()
@@ -53,18 +70,18 @@ class ConversationListViewModel(
val conversationsState: Flowable<List<Conversation>> = store.mapDistinctForUi { it.conversations }
val selectedState: Flowable<ConversationSet> = store.mapDistinctForUi { it.selectedConversations }
val filterRequestState: Flowable<ConversationFilterRequest> = store.mapDistinctForUi { it.filterRequest }
val chatFolderState: Flowable<List<ChatFolderMappingModel>> = store.mapDistinctForUi { it.chatFolders }
val filterRequestState: Flowable<ConversationFilterRequest> = savedStateHandle.getStateFlow(STATE, SaveableState()).map { it.filterRequest }.asFlowable()
val chatFolderState: Flowable<List<ChatFolderMappingModel>> = savedStateHandle.getStateFlow(STATE, SaveableState()).map { it.chatFolders }.asFlowable()
val hasNoConversations: Flowable<Boolean>
val controller = ProxyPagingController<Long>()
val folders: List<ChatFolderMappingModel>
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<WebSocketConnectionState>
@@ -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<ChatFolderMappingModel> = emptyList(),
val currentFolder: ChatFolderRecord = ChatFolderRecord(),
val filterRequest: ConversationFilterRequest = ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG)
) : Parcelable
private data class ConversationListState(
val conversations: List<Conversation> = emptyList(),
val selectedConversations: ConversationSet = ConversationSet(),
val internalSelection: Set<Conversation> = emptySet(),
val filterRequest: ConversationFilterRequest = ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG),
val pinnedCount: Int = 0
)
class Factory(private val isArchived: Boolean) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(ConversationListViewModel(isArchived))!!
class Factory(
private val isArchived: Boolean
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val savedStateHandle = extras.createSavedStateHandle()
return modelClass.cast(ConversationListViewModel(isArchived, savedStateHandle))!!
}
}
}

View File

@@ -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