mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-08 09:18:39 +01:00
Ensures chat folder is remembered when we leave page.
This commit is contained in:
committed by
Cody Henthorne
parent
252a4afa79
commit
be035456f7
+6
-2
@@ -32,6 +32,7 @@ import androidx.compose.ui.res.vectorResource
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
import org.signal.core.ui.compose.BottomSheets
|
import org.signal.core.ui.compose.BottomSheets
|
||||||
import org.signal.core.ui.compose.Previews
|
import org.signal.core.ui.compose.Previews
|
||||||
import org.signal.core.ui.compose.SignalPreview
|
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.AppSettingsActivity
|
||||||
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
|
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
|
||||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||||
import org.thoughtcrime.securesms.util.viewModel
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bottom sheet shown when choosing to add a chat to a folder
|
* 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)
|
OTHER(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val viewModel by viewModel { ConversationListViewModel(isArchived = false) }
|
private val viewModel: ConversationListViewModel by viewModels(
|
||||||
|
factoryProducer = {
|
||||||
|
ConversationListViewModel.Factory(isArchived = false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_FOLDERS = "argument.folders"
|
private const val ARG_FOLDERS = "argument.folders"
|
||||||
|
|||||||
+4
-1
@@ -1,17 +1,20 @@
|
|||||||
package org.thoughtcrime.securesms.conversationlist
|
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.components.settings.app.chats.folders.ChatFolderRecord
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping model of folders used in [ChatFolderAdapter]
|
* Mapping model of folders used in [ChatFolderAdapter]
|
||||||
*/
|
*/
|
||||||
|
@Parcelize
|
||||||
data class ChatFolderMappingModel(
|
data class ChatFolderMappingModel(
|
||||||
val chatFolder: ChatFolderRecord,
|
val chatFolder: ChatFolderRecord,
|
||||||
val unreadCount: Int,
|
val unreadCount: Int,
|
||||||
val isMuted: Boolean,
|
val isMuted: Boolean,
|
||||||
val isSelected: Boolean
|
val isSelected: Boolean
|
||||||
) : MappingModel<ChatFolderMappingModel> {
|
) : MappingModel<ChatFolderMappingModel>, Parcelable {
|
||||||
override fun areItemsTheSame(newItem: ChatFolderMappingModel): Boolean {
|
override fun areItemsTheSame(newItem: ChatFolderMappingModel): Boolean {
|
||||||
return chatFolder.id == newItem.chatFolder.id
|
return chatFolder.id == newItem.chatFolder.id
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -146,7 +146,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.search.MessageResult;
|
import org.thoughtcrime.securesms.search.MessageResult;
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
|
||||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||||
import org.thoughtcrime.securesms.util.AppStartup;
|
import org.thoughtcrime.securesms.util.AppStartup;
|
||||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||||
|
|||||||
+60
-31
@@ -1,17 +1,25 @@
|
|||||||
package org.thoughtcrime.securesms.conversationlist
|
package org.thoughtcrime.securesms.conversationlist
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.createSavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.addTo
|
import io.reactivex.rxjava3.kotlin.addTo
|
||||||
|
import io.reactivex.rxjava3.kotlin.combineLatest
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.rx3.asFlowable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.signal.paging.PagedData
|
import org.signal.paging.PagedData
|
||||||
import org.signal.paging.PagingConfig
|
import org.signal.paging.PagingConfig
|
||||||
import org.signal.paging.ProxyPagingController
|
import org.signal.paging.ProxyPagingController
|
||||||
@@ -35,15 +43,24 @@ import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ConversationListViewModel(
|
class ConversationListViewModel(
|
||||||
private val isArchived: Boolean
|
private val isArchived: Boolean,
|
||||||
|
private val savedStateHandle: SavedStateHandle
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val STATE = "state"
|
||||||
|
|
||||||
private var coldStart = true
|
private var coldStart = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
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 store = RxStore(ConversationListState()).addTo(disposables)
|
||||||
private val conversationListDataSource: Flowable<ConversationListDataSource>
|
private val conversationListDataSource: Flowable<ConversationListDataSource>
|
||||||
private val pagingConfig = PagingConfig.Builder()
|
private val pagingConfig = PagingConfig.Builder()
|
||||||
@@ -53,18 +70,18 @@ class ConversationListViewModel(
|
|||||||
|
|
||||||
val conversationsState: Flowable<List<Conversation>> = store.mapDistinctForUi { it.conversations }
|
val conversationsState: Flowable<List<Conversation>> = store.mapDistinctForUi { it.conversations }
|
||||||
val selectedState: Flowable<ConversationSet> = store.mapDistinctForUi { it.selectedConversations }
|
val selectedState: Flowable<ConversationSet> = store.mapDistinctForUi { it.selectedConversations }
|
||||||
val filterRequestState: Flowable<ConversationFilterRequest> = store.mapDistinctForUi { it.filterRequest }
|
val filterRequestState: Flowable<ConversationFilterRequest> = savedStateHandle.getStateFlow(STATE, SaveableState()).map { it.filterRequest }.asFlowable()
|
||||||
val chatFolderState: Flowable<List<ChatFolderMappingModel>> = store.mapDistinctForUi { it.chatFolders }
|
val chatFolderState: Flowable<List<ChatFolderMappingModel>> = savedStateHandle.getStateFlow(STATE, SaveableState()).map { it.chatFolders }.asFlowable()
|
||||||
val hasNoConversations: Flowable<Boolean>
|
val hasNoConversations: Flowable<Boolean>
|
||||||
|
|
||||||
val controller = ProxyPagingController<Long>()
|
val controller = ProxyPagingController<Long>()
|
||||||
|
|
||||||
val folders: List<ChatFolderMappingModel>
|
val folders: List<ChatFolderMappingModel>
|
||||||
get() = store.state.chatFolders
|
get() = saveableState.chatFolders
|
||||||
val currentFolder: ChatFolderRecord
|
val currentFolder: ChatFolderRecord
|
||||||
get() = store.state.currentFolder
|
get() = saveableState.currentFolder
|
||||||
val conversationFilterRequest: ConversationFilterRequest
|
val conversationFilterRequest: ConversationFilterRequest
|
||||||
get() = store.state.filterRequest
|
get() = saveableState.filterRequest
|
||||||
val pinnedCount: Int
|
val pinnedCount: Int
|
||||||
get() = store.state.pinnedCount
|
get() = store.state.pinnedCount
|
||||||
val webSocketState: Observable<WebSocketConnectionState>
|
val webSocketState: Observable<WebSocketConnectionState>
|
||||||
@@ -75,8 +92,9 @@ class ConversationListViewModel(
|
|||||||
get() = store.state.internalSelection
|
get() = store.state.internalSelection
|
||||||
|
|
||||||
init {
|
init {
|
||||||
conversationListDataSource = store
|
val saveableStateFlowable = savedStateHandle.getStateFlow(STATE, SaveableState()).asFlowable()
|
||||||
.stateFlowable
|
|
||||||
|
conversationListDataSource = saveableStateFlowable
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.filter { it.currentFolder.id != -1L }
|
.filter { it.currentFolder.id != -1L }
|
||||||
.map { it.filterRequest to it.currentFolder }
|
.map { it.filterRequest to it.currentFolder }
|
||||||
@@ -128,7 +146,8 @@ class ConversationListViewModel(
|
|||||||
hasNoConversations = store
|
hasNoConversations = store
|
||||||
.stateFlowable
|
.stateFlowable
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.map { it.filterRequest to it.conversations }
|
.combineLatest(saveableStateFlowable.map { it.filterRequest })
|
||||||
|
.map { (state, filterRequest) -> filterRequest to state.conversations }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.map { (filterRequest, conversations) ->
|
.map { (filterRequest, conversations) ->
|
||||||
if (conversations.isNotEmpty()) {
|
if (conversations.isNotEmpty()) {
|
||||||
@@ -187,9 +206,12 @@ class ConversationListViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setFiltered(isFiltered: Boolean, conversationFilterSource: ConversationFilterSource) {
|
fun setFiltered(isFiltered: Boolean, conversationFilterSource: ConversationFilterSource) {
|
||||||
store.update {
|
saveableState = saveableState.copy(
|
||||||
it.copy(filterRequest = ConversationFilterRequest(if (isFiltered) ConversationFilter.UNREAD else ConversationFilter.OFF, conversationFilterSource))
|
filterRequest = ConversationFilterRequest(
|
||||||
}
|
filter = if (isFiltered) ConversationFilter.UNREAD else ConversationFilter.OFF,
|
||||||
|
source = conversationFilterSource
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadCurrentFolders() {
|
private fun loadCurrentFolders() {
|
||||||
@@ -211,12 +233,10 @@ class ConversationListViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
store.update {
|
saveableState = saveableState.copy(
|
||||||
it.copy(
|
currentFolder = folders.find { folder -> folder.id == selectedFolderId } ?: ChatFolderRecord(),
|
||||||
currentFolder = folders.find { folder -> folder.id == selectedFolderId } ?: ChatFolderRecord(),
|
chatFolders = chatFolders
|
||||||
chatFolders = chatFolders
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,14 +248,12 @@ class ConversationListViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun select(chatFolder: ChatFolderRecord) {
|
fun select(chatFolder: ChatFolderRecord) {
|
||||||
store.update {
|
saveableState = saveableState.copy(
|
||||||
it.copy(
|
currentFolder = chatFolder,
|
||||||
currentFolder = chatFolder,
|
chatFolders = folders.map { model ->
|
||||||
chatFolders = folders.map { model ->
|
model.copy(isSelected = chatFolder.id == model.chatFolder.id)
|
||||||
model.copy(isSelected = chatFolder.id == model.chatFolder.id)
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onUpdateMute(chatFolder: ChatFolderRecord, until: Long) {
|
fun onUpdateMute(chatFolder: ChatFolderRecord, until: Long) {
|
||||||
@@ -282,19 +300,30 @@ class ConversationListViewModel(
|
|||||||
StorageSyncHelper.scheduleSyncForDataChange()
|
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 chatFolders: List<ChatFolderMappingModel> = emptyList(),
|
||||||
val currentFolder: ChatFolderRecord = ChatFolderRecord(),
|
val currentFolder: ChatFolderRecord = ChatFolderRecord(),
|
||||||
|
val filterRequest: ConversationFilterRequest = ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG)
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
private data class ConversationListState(
|
||||||
val conversations: List<Conversation> = emptyList(),
|
val conversations: List<Conversation> = emptyList(),
|
||||||
val selectedConversations: ConversationSet = ConversationSet(),
|
val selectedConversations: ConversationSet = ConversationSet(),
|
||||||
val internalSelection: Set<Conversation> = emptySet(),
|
val internalSelection: Set<Conversation> = emptySet(),
|
||||||
val filterRequest: ConversationFilterRequest = ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG),
|
|
||||||
val pinnedCount: Int = 0
|
val pinnedCount: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
class Factory(private val isArchived: Boolean) : ViewModelProvider.Factory {
|
class Factory(
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
private val isArchived: Boolean
|
||||||
return modelClass.cast(ConversationListViewModel(isArchived))!!
|
) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||||
|
val savedStateHandle = extras.createSavedStateHandle()
|
||||||
|
|
||||||
|
return modelClass.cast(ConversationListViewModel(isArchived, savedStateHandle))!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,8 +1,11 @@
|
|||||||
package org.thoughtcrime.securesms.conversationlist.chatfilter
|
package org.thoughtcrime.securesms.conversationlist.chatfilter
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
data class ConversationFilterRequest(
|
data class ConversationFilterRequest(
|
||||||
val filter: ConversationFilter,
|
val filter: ConversationFilter,
|
||||||
val source: ConversationFilterSource
|
val source: ConversationFilterSource
|
||||||
)
|
) : Parcelable
|
||||||
|
|||||||
Reference in New Issue
Block a user