Add proper selection state support to Chats and Calls tabs.

This commit is contained in:
Alex Hart
2025-10-15 14:45:05 -03:00
committed by Cody Henthorne
parent e57b47ec82
commit cbe72307a0
6 changed files with 62 additions and 22 deletions

View File

@@ -92,6 +92,7 @@ class CallLogAdapter(
fun submitCallRows(
rows: List<CallLogRow?>,
selectionState: CallLogSelectionState,
activeCallLogRowId: CallLogRow.Id?,
localCallRecipientId: RecipientId,
onCommit: () -> Unit
): Int {
@@ -99,8 +100,19 @@ class CallLogAdapter(
.filterNotNull()
.map {
when (it) {
is CallLogRow.Call -> CallModel(it, selectionState, itemCount, it.peer.id == localCallRecipientId)
is CallLogRow.CallLink -> CallLinkModel(it, selectionState, itemCount, it.recipient.id == localCallRecipientId)
is CallLogRow.Call -> CallModel(
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.ClearFilterEmpty -> ClearFilterEmptyModel()
is CallLogRow.CreateCallLink -> CreateCallLinkModel()
@@ -148,6 +160,7 @@ class CallLogAdapter(
private class CallLinkModel(
val callLink: CallLogRow.CallLink,
val selectionState: CallLogSelectionState,
val activeCallLogRowId: CallLogRow.Id?,
val itemCount: Int,
val isLocalDeviceInCall: Boolean
) : MappingModel<CallLinkModel> {
@@ -159,12 +172,13 @@ class CallLogAdapter(
override fun areContentsTheSame(newItem: CallLinkModel): Boolean {
return callLink == newItem.callLink &&
isSelectionStateTheSame(newItem) &&
isActiveIdStateTheSame(newItem) &&
isItemCountTheSame(newItem) &&
isLocalDeviceInCall == newItem.isLocalDeviceInCall
}
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
} else {
null
@@ -176,6 +190,13 @@ class CallLogAdapter(
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 {
return itemCount == newItem.itemCount
}
@@ -220,6 +241,8 @@ class CallLogAdapter(
binding.callSelected.isChecked = model.selectionState.contains(model.callLink.id)
binding.callSelected.visible = model.selectionState.isNotEmpty(model.itemCount)
itemView.isActivated = model.activeCallLogRowId == model.callLink.id
if (payload.isNotEmpty()) {
return
}

View File

@@ -18,6 +18,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.kotlin.Flowables
import io.reactivex.rxjava3.kotlin.subscribeBy
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.addTo
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.MainNavigator
import org.thoughtcrime.securesms.R
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 += Flowables.combineLatest(viewModel.data, viewModel.selected)
disposables += Flowables.combineLatest(viewModel.data, viewModel.selected, mainNavigationViewModel.observableActiveCallId.toFlowable(BackpressureStrategy.LATEST))
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (data, selected) ->
.subscribe { (data, selected, activeRowId) ->
val filteredCount = callLogAdapter.submitCallRows(
data,
selected,
activeCallLogRowId = activeRowId.orNull().takeIf { resources.getWindowSizeClass().isSplitPane() },
viewModel.callLogPeekHelper.localDeviceCallRecipientId,
scrollToPositionDelegate::notifyListCommitted
)

View File

@@ -114,7 +114,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchData;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
import org.thoughtcrime.securesms.conversation.ConversationArgs;
import org.thoughtcrime.securesms.conversation.ConversationUpdateTick;
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest;
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.keyvalue.AccountValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation;
import org.thoughtcrime.securesms.main.MainNavigationListLocation;
import org.thoughtcrime.securesms.main.MainNavigationViewModel;
import org.thoughtcrime.securesms.main.MainToolbarMode;
@@ -413,16 +411,9 @@ public class ConversationListFragment extends MainFragment implements Conversati
}));
if (WindowSizeClass.Companion.getWindowSizeClass(getResources()).isSplitPane()) {
lifecycleDisposable.add(mainNavigationViewModel.getDetailLocationObservable()
lifecycleDisposable.add(mainNavigationViewModel.getObservableActiveChatThreadId()
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(location -> {
if (location instanceof MainNavigationDetailLocation.Chats.Conversation) {
ConversationArgs args = ((MainNavigationDetailLocation.Chats.Conversation) location).getConversationArgs();
long threadId = args.threadId;
defaultAdapter.setActiveThreadId(threadId);
}
}));
.subscribe(defaultAdapter::setActiveThreadId));
} else {
defaultAdapter.setActiveThreadId(0);
}

View File

@@ -12,6 +12,7 @@ import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import org.thoughtcrime.securesms.calls.log.CallLogRow
import org.thoughtcrime.securesms.conversation.ConversationArgs
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
@@ -70,11 +71,11 @@ sealed class MainNavigationDetailLocation : Parcelable {
@Parcelize
sealed class Calls : MainNavigationDetailLocation() {
abstract val controllerKey: CallLogRow.Id
@Parcelize
sealed class CallLinks : Calls() {
abstract val controllerKey: CallLinkRoomId
@Serializable
data class CallLinkDetails(val callLinkRoomId: CallLinkRoomId) : CallLinks() {
@Transient
@@ -83,14 +84,14 @@ sealed class MainNavigationDetailLocation : Parcelable {
@Transient
@IgnoredOnParcel
override val controllerKey: CallLinkRoomId = callLinkRoomId
override val controllerKey: CallLogRow.Id = CallLogRow.Id.CallLink(callLinkRoomId)
}
@Serializable
data class EditCallLinkName(val callLinkRoomId: CallLinkRoomId) : CallLinks() {
@Transient
@IgnoredOnParcel
override val controllerKey: CallLinkRoomId = callLinkRoomId
override val controllerKey: CallLogRow.Id = CallLogRow.Id.CallLink(callLinkRoomId)
}
}
}

View File

@@ -19,10 +19,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow
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.dependencies.AppDependencies
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.window.AppScaffoldNavigator
import org.thoughtcrime.securesms.window.WindowSizeClass
import java.util.Optional
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
class MainNavigationViewModel(
@@ -48,7 +51,12 @@ class MainNavigationViewModel(
*/
private val internalDetailLocation = MutableSharedFlow<MainNavigationDetailLocation>()
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)
val megaphone: StateFlow<Megaphone> = internalMegaphone
@@ -99,6 +107,20 @@ class MainNavigationViewModel(
performStoreUpdate(MainNavigationRepository.getHasFailedOutgoingStories()) { hasFailedStories, state ->
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
}
}
}
}
/**

View File

@@ -7,7 +7,7 @@
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="2dp"
android:animateLayoutChanges="true"
android:background="@drawable/selectable_list_item_background"
android:background="@drawable/conversation_list_item_background"
android:minHeight="68dp">
<androidx.appcompat.widget.AppCompatCheckBox