mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 12:38:33 +00:00
Add proper selection state support to Chats and Calls tabs.
This commit is contained in:
committed by
Cody Henthorne
parent
e57b47ec82
commit
cbe72307a0
@@ -92,6 +92,7 @@ class CallLogAdapter(
|
|||||||
fun submitCallRows(
|
fun submitCallRows(
|
||||||
rows: List<CallLogRow?>,
|
rows: List<CallLogRow?>,
|
||||||
selectionState: CallLogSelectionState,
|
selectionState: CallLogSelectionState,
|
||||||
|
activeCallLogRowId: CallLogRow.Id?,
|
||||||
localCallRecipientId: RecipientId,
|
localCallRecipientId: RecipientId,
|
||||||
onCommit: () -> Unit
|
onCommit: () -> Unit
|
||||||
): Int {
|
): Int {
|
||||||
@@ -99,8 +100,19 @@ class CallLogAdapter(
|
|||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.map {
|
.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is CallLogRow.Call -> CallModel(it, selectionState, itemCount, it.peer.id == localCallRecipientId)
|
is CallLogRow.Call -> CallModel(
|
||||||
is CallLogRow.CallLink -> CallLinkModel(it, selectionState, itemCount, it.recipient.id == localCallRecipientId)
|
call = it,
|
||||||
|
selectionState = selectionState,
|
||||||
|
itemCount = itemCount,
|
||||||
|
isLocalDeviceInCall = it.peer.id == localCallRecipientId
|
||||||
|
)
|
||||||
|
is CallLogRow.CallLink -> CallLinkModel(
|
||||||
|
callLink = it,
|
||||||
|
selectionState = selectionState,
|
||||||
|
activeCallLogRowId = activeCallLogRowId,
|
||||||
|
itemCount = itemCount,
|
||||||
|
isLocalDeviceInCall = it.recipient.id == localCallRecipientId
|
||||||
|
)
|
||||||
is CallLogRow.ClearFilter -> ClearFilterModel()
|
is CallLogRow.ClearFilter -> ClearFilterModel()
|
||||||
is CallLogRow.ClearFilterEmpty -> ClearFilterEmptyModel()
|
is CallLogRow.ClearFilterEmpty -> ClearFilterEmptyModel()
|
||||||
is CallLogRow.CreateCallLink -> CreateCallLinkModel()
|
is CallLogRow.CreateCallLink -> CreateCallLinkModel()
|
||||||
@@ -148,6 +160,7 @@ class CallLogAdapter(
|
|||||||
private class CallLinkModel(
|
private class CallLinkModel(
|
||||||
val callLink: CallLogRow.CallLink,
|
val callLink: CallLogRow.CallLink,
|
||||||
val selectionState: CallLogSelectionState,
|
val selectionState: CallLogSelectionState,
|
||||||
|
val activeCallLogRowId: CallLogRow.Id?,
|
||||||
val itemCount: Int,
|
val itemCount: Int,
|
||||||
val isLocalDeviceInCall: Boolean
|
val isLocalDeviceInCall: Boolean
|
||||||
) : MappingModel<CallLinkModel> {
|
) : MappingModel<CallLinkModel> {
|
||||||
@@ -159,12 +172,13 @@ class CallLogAdapter(
|
|||||||
override fun areContentsTheSame(newItem: CallLinkModel): Boolean {
|
override fun areContentsTheSame(newItem: CallLinkModel): Boolean {
|
||||||
return callLink == newItem.callLink &&
|
return callLink == newItem.callLink &&
|
||||||
isSelectionStateTheSame(newItem) &&
|
isSelectionStateTheSame(newItem) &&
|
||||||
|
isActiveIdStateTheSame(newItem) &&
|
||||||
isItemCountTheSame(newItem) &&
|
isItemCountTheSame(newItem) &&
|
||||||
isLocalDeviceInCall == newItem.isLocalDeviceInCall
|
isLocalDeviceInCall == newItem.isLocalDeviceInCall
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChangePayload(newItem: CallLinkModel): Any? {
|
override fun getChangePayload(newItem: CallLinkModel): Any? {
|
||||||
return if (callLink == newItem.callLink && (!isSelectionStateTheSame(newItem) || !isItemCountTheSame(newItem))) {
|
return if (callLink == newItem.callLink && (!isSelectionStateTheSame(newItem) || !isItemCountTheSame(newItem) || !isActiveIdStateTheSame(newItem))) {
|
||||||
PAYLOAD_SELECTION_STATE
|
PAYLOAD_SELECTION_STATE
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -176,6 +190,13 @@ class CallLogAdapter(
|
|||||||
selectionState.isNotEmpty(itemCount) == newItem.selectionState.isNotEmpty(newItem.itemCount)
|
selectionState.isNotEmpty(itemCount) == newItem.selectionState.isNotEmpty(newItem.itemCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isActiveIdStateTheSame(newItem: CallLinkModel): Boolean {
|
||||||
|
val isOldItemActive = activeCallLogRowId == callLink.id
|
||||||
|
val isNewItemActive = newItem.activeCallLogRowId == newItem.callLink.id
|
||||||
|
|
||||||
|
return (isOldItemActive && isNewItemActive) || (!isOldItemActive && !isNewItemActive)
|
||||||
|
}
|
||||||
|
|
||||||
private fun isItemCountTheSame(newItem: CallLinkModel): Boolean {
|
private fun isItemCountTheSame(newItem: CallLinkModel): Boolean {
|
||||||
return itemCount == newItem.itemCount
|
return itemCount == newItem.itemCount
|
||||||
}
|
}
|
||||||
@@ -220,6 +241,8 @@ class CallLogAdapter(
|
|||||||
binding.callSelected.isChecked = model.selectionState.contains(model.callLink.id)
|
binding.callSelected.isChecked = model.selectionState.contains(model.callLink.id)
|
||||||
binding.callSelected.visible = model.selectionState.isNotEmpty(model.itemCount)
|
binding.callSelected.visible = model.selectionState.isNotEmpty(model.itemCount)
|
||||||
|
|
||||||
|
itemView.isActivated = model.activeCallLogRowId == model.callLink.id
|
||||||
|
|
||||||
if (payload.isNotEmpty()) {
|
if (payload.isNotEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||||
import io.reactivex.rxjava3.kotlin.Flowables
|
import io.reactivex.rxjava3.kotlin.Flowables
|
||||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -25,6 +26,7 @@ import org.signal.core.util.DimensionUnit
|
|||||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||||
import org.signal.core.util.concurrent.addTo
|
import org.signal.core.util.concurrent.addTo
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.core.util.orNull
|
||||||
import org.thoughtcrime.securesms.MainNavigator
|
import org.thoughtcrime.securesms.MainNavigator
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.calls.links.create.CreateCallLinkBottomSheetDialogFragment
|
import org.thoughtcrime.securesms.calls.links.create.CreateCallLinkBottomSheetDialogFragment
|
||||||
@@ -122,12 +124,13 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
|||||||
)
|
)
|
||||||
|
|
||||||
disposables += scrollToPositionDelegate
|
disposables += scrollToPositionDelegate
|
||||||
disposables += Flowables.combineLatest(viewModel.data, viewModel.selected)
|
disposables += Flowables.combineLatest(viewModel.data, viewModel.selected, mainNavigationViewModel.observableActiveCallId.toFlowable(BackpressureStrategy.LATEST))
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { (data, selected) ->
|
.subscribe { (data, selected, activeRowId) ->
|
||||||
val filteredCount = callLogAdapter.submitCallRows(
|
val filteredCount = callLogAdapter.submitCallRows(
|
||||||
data,
|
data,
|
||||||
selected,
|
selected,
|
||||||
|
activeCallLogRowId = activeRowId.orNull().takeIf { resources.getWindowSizeClass().isSplitPane() },
|
||||||
viewModel.callLogPeekHelper.localDeviceCallRecipientId,
|
viewModel.callLogPeekHelper.localDeviceCallRecipientId,
|
||||||
scrollToPositionDelegate::notifyListCommitted
|
scrollToPositionDelegate::notifyListCommitted
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchData;
|
|||||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator;
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator;
|
||||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationArgs;
|
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationUpdateTick;
|
import org.thoughtcrime.securesms.conversation.ConversationUpdateTick;
|
||||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest;
|
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest;
|
||||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource;
|
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource;
|
||||||
@@ -131,7 +130,6 @@ import org.thoughtcrime.securesms.groups.SelectionLimits;
|
|||||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.AccountValues;
|
import org.thoughtcrime.securesms.keyvalue.AccountValues;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation;
|
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationListLocation;
|
import org.thoughtcrime.securesms.main.MainNavigationListLocation;
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel;
|
import org.thoughtcrime.securesms.main.MainNavigationViewModel;
|
||||||
import org.thoughtcrime.securesms.main.MainToolbarMode;
|
import org.thoughtcrime.securesms.main.MainToolbarMode;
|
||||||
@@ -413,16 +411,9 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
if (WindowSizeClass.Companion.getWindowSizeClass(getResources()).isSplitPane()) {
|
if (WindowSizeClass.Companion.getWindowSizeClass(getResources()).isSplitPane()) {
|
||||||
lifecycleDisposable.add(mainNavigationViewModel.getDetailLocationObservable()
|
lifecycleDisposable.add(mainNavigationViewModel.getObservableActiveChatThreadId()
|
||||||
.subscribeOn(AndroidSchedulers.mainThread())
|
.subscribeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(location -> {
|
.subscribe(defaultAdapter::setActiveThreadId));
|
||||||
if (location instanceof MainNavigationDetailLocation.Chats.Conversation) {
|
|
||||||
ConversationArgs args = ((MainNavigationDetailLocation.Chats.Conversation) location).getConversationArgs();
|
|
||||||
long threadId = args.threadId;
|
|
||||||
|
|
||||||
defaultAdapter.setActiveThreadId(threadId);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
defaultAdapter.setActiveThreadId(0);
|
defaultAdapter.setActiveThreadId(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.thoughtcrime.securesms.calls.log.CallLogRow
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationArgs
|
import org.thoughtcrime.securesms.conversation.ConversationArgs
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||||
@@ -70,11 +71,11 @@ sealed class MainNavigationDetailLocation : Parcelable {
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
sealed class Calls : MainNavigationDetailLocation() {
|
sealed class Calls : MainNavigationDetailLocation() {
|
||||||
|
|
||||||
|
abstract val controllerKey: CallLogRow.Id
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
sealed class CallLinks : Calls() {
|
sealed class CallLinks : Calls() {
|
||||||
|
|
||||||
abstract val controllerKey: CallLinkRoomId
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CallLinkDetails(val callLinkRoomId: CallLinkRoomId) : CallLinks() {
|
data class CallLinkDetails(val callLinkRoomId: CallLinkRoomId) : CallLinks() {
|
||||||
@Transient
|
@Transient
|
||||||
@@ -83,14 +84,14 @@ sealed class MainNavigationDetailLocation : Parcelable {
|
|||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val controllerKey: CallLinkRoomId = callLinkRoomId
|
override val controllerKey: CallLogRow.Id = CallLogRow.Id.CallLink(callLinkRoomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EditCallLinkName(val callLinkRoomId: CallLinkRoomId) : CallLinks() {
|
data class EditCallLinkName(val callLinkRoomId: CallLinkRoomId) : CallLinks() {
|
||||||
@Transient
|
@Transient
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val controllerKey: CallLinkRoomId = callLinkRoomId
|
override val controllerKey: CallLogRow.Id = CallLogRow.Id.CallLink(callLinkRoomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.reactive.asFlow
|
import kotlinx.coroutines.reactive.asFlow
|
||||||
import kotlinx.coroutines.rx3.asObservable
|
import kotlinx.coroutines.rx3.asObservable
|
||||||
|
import org.thoughtcrime.securesms.calls.log.CallLogRow
|
||||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
|
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
@@ -32,6 +34,7 @@ import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
|||||||
import org.thoughtcrime.securesms.stories.Stories
|
import org.thoughtcrime.securesms.stories.Stories
|
||||||
import org.thoughtcrime.securesms.window.AppScaffoldNavigator
|
import org.thoughtcrime.securesms.window.AppScaffoldNavigator
|
||||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||||
class MainNavigationViewModel(
|
class MainNavigationViewModel(
|
||||||
@@ -48,7 +51,12 @@ class MainNavigationViewModel(
|
|||||||
*/
|
*/
|
||||||
private val internalDetailLocation = MutableSharedFlow<MainNavigationDetailLocation>()
|
private val internalDetailLocation = MutableSharedFlow<MainNavigationDetailLocation>()
|
||||||
val detailLocation: SharedFlow<MainNavigationDetailLocation> = internalDetailLocation
|
val detailLocation: SharedFlow<MainNavigationDetailLocation> = internalDetailLocation
|
||||||
val detailLocationObservable: Observable<MainNavigationDetailLocation> = internalDetailLocation.asObservable()
|
|
||||||
|
private val internalActiveChatThreadId = MutableStateFlow(-1L)
|
||||||
|
val observableActiveChatThreadId: Observable<Long> = internalActiveChatThreadId.asObservable()
|
||||||
|
|
||||||
|
private val internalActiveCallId = MutableStateFlow<CallLogRow.Id?>(null)
|
||||||
|
val observableActiveCallId: Observable<Optional<CallLogRow.Id>> = internalActiveCallId.map { Optional.ofNullable(it) }.asObservable()
|
||||||
|
|
||||||
private val internalMegaphone = MutableStateFlow(Megaphone.NONE)
|
private val internalMegaphone = MutableStateFlow(Megaphone.NONE)
|
||||||
val megaphone: StateFlow<Megaphone> = internalMegaphone
|
val megaphone: StateFlow<Megaphone> = internalMegaphone
|
||||||
@@ -99,6 +107,20 @@ class MainNavigationViewModel(
|
|||||||
performStoreUpdate(MainNavigationRepository.getHasFailedOutgoingStories()) { hasFailedStories, state ->
|
performStoreUpdate(MainNavigationRepository.getHasFailedOutgoingStories()) { hasFailedStories, state ->
|
||||||
state.copy(storyFailure = hasFailedStories)
|
state.copy(storyFailure = hasFailedStories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
internalDetailLocation.collect { location ->
|
||||||
|
when (location) {
|
||||||
|
is MainNavigationDetailLocation.Chats.Conversation -> {
|
||||||
|
internalActiveChatThreadId.update { location.conversationArgs.threadId }
|
||||||
|
}
|
||||||
|
is MainNavigationDetailLocation.Calls -> {
|
||||||
|
internalActiveCallId.update { location.controllerKey }
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
android:layout_marginHorizontal="12dp"
|
android:layout_marginHorizontal="12dp"
|
||||||
android:layout_marginVertical="2dp"
|
android:layout_marginVertical="2dp"
|
||||||
android:animateLayoutChanges="true"
|
android:animateLayoutChanges="true"
|
||||||
android:background="@drawable/selectable_list_item_background"
|
android:background="@drawable/conversation_list_item_background"
|
||||||
android:minHeight="68dp">
|
android:minHeight="68dp">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatCheckBox
|
<androidx.appcompat.widget.AppCompatCheckBox
|
||||||
|
|||||||
Reference in New Issue
Block a user