Fix active chat highlighting when navigating directly to conversation settings.

This commit is contained in:
Jeffrey Starke
2026-06-24 13:38:08 -04:00
committed by jeffrey-signal
parent 97897a84aa
commit 52750e726a
8 changed files with 55 additions and 44 deletions
@@ -1,12 +1,14 @@
package org.thoughtcrime.securesms;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadWithRecipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Locale;
import java.util.Set;
@@ -18,10 +20,10 @@ public interface BindableConversationListItem extends Unbindable {
@NonNull RequestManager requestManager, @NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations,
long activeThreadId);
@Nullable RecipientId activeRecipientId);
void setSelectedConversations(@NonNull ConversationSet conversations);
void setActiveThreadId(long activeThreadId);
void setActiveRecipientId(@Nullable RecipientId activeRecipientId);
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
void updateTimestamp();
}
@@ -12,6 +12,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Controls the navigation stack used by the chats screen.
@@ -35,11 +36,14 @@ class ChatsBackStack(savedStateHandle: SavedStateHandle) {
mutableStateListOf()
}
val activeConversationThreadId: Long?
get() = entries
.filterIsInstance<MainNavigationDetailLocation.Conversation>()
.lastOrNull()
?.controllerKey
val activeRecipientId: RecipientId?
get() = entries.asReversed().firstNotNullOfOrNull {
when (it) {
is MainNavigationDetailLocation.Conversation -> it.conversationArgs.recipientId
is MainNavigationDetailLocation.Chats -> it.controllerKey
else -> null
}
}
val hasConversation: Boolean
get() = entries.any { it is MainNavigationDetailLocation.Conversation }
@@ -8,6 +8,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
@@ -20,6 +21,7 @@ import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -53,11 +55,11 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
private final OnConversationClickListener onConversationClickListener;
private final ClearFilterViewHolder.OnClearFilterClickListener onClearFilterClicked;
private final EmptyFolderViewHolder.OnFolderSettingsClickListener onFolderSettingsClicked;
private final Set<Long> typingSet = new HashSet<>();
private final Set<Long> typingSet = new HashSet<>();
private ConversationSet selectedConversations = new ConversationSet();
private long activeThreadId = 0;
private PagingController pagingController;
private ConversationSet selectedConversations = new ConversationSet();
private @Nullable RecipientId activeRecipientId = null;
private PagingController pagingController;
protected ConversationListAdapter(@NonNull LifecycleOwner lifecycleOwner,
@NonNull RequestManager requestManager,
@@ -154,7 +156,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
case TYPING_INDICATOR -> vh.getConversationListItem().updateTypingIndicator(typingSet);
case SELECTION -> vh.getConversationListItem().setSelectedConversations(selectedConversations);
case TIMESTAMP -> vh.getConversationListItem().updateTimestamp();
case ACTIVE -> vh.getConversationListItem().setActiveThreadId(activeThreadId);
case ACTIVE -> vh.getConversationListItem().setActiveRecipientId(activeRecipientId);
}
}
}
@@ -173,7 +175,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
Locale.getDefault(),
typingSet,
selectedConversations,
activeThreadId);
activeRecipientId);
} else if (holder.getItemViewType() == TYPE_HEADER) {
HeaderViewHolder casted = (HeaderViewHolder) holder;
Conversation conversation = Objects.requireNonNull(getItem(position));
@@ -232,8 +234,8 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
}
void setActiveThreadId(long activeThreadId) {
this.activeThreadId = activeThreadId;
void setActiveRecipientId(@Nullable RecipientId activeRecipientId) {
this.activeRecipientId = activeRecipientId;
notifyItemRangeChanged(0, getItemCount(), Payload.ACTIVE);
}
@@ -44,7 +44,6 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import org.signal.core.ui.compose.Snackbars;
import androidx.compose.ui.platform.ComposeView;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat;
@@ -66,7 +65,13 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.ui.BottomSheetUtil;
import org.signal.core.ui.WindowSizeClassExtensionsKt;
import org.signal.core.ui.compose.Snackbars;
import org.signal.core.ui.view.Stub;
import org.signal.core.util.AppForegroundObserver;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.ServiceUtil;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.concurrent.SignalExecutors;
@@ -88,13 +93,13 @@ import org.thoughtcrime.securesms.badges.self.expired.ExpiredOneTimeBadgeBottomS
import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment;
import org.thoughtcrime.securesms.banner.Banner;
import org.thoughtcrime.securesms.banner.BannerManager;
import org.thoughtcrime.securesms.banner.banners.ArchiveRestoreStatusBanner;
import org.thoughtcrime.securesms.banner.banners.ArchiveUploadStatusBanner;
import org.thoughtcrime.securesms.banner.banners.CdsPermanentErrorBanner;
import org.thoughtcrime.securesms.banner.banners.CdsTemporaryErrorBanner;
import org.thoughtcrime.securesms.banner.banners.DeprecatedBuildBanner;
import org.thoughtcrime.securesms.banner.banners.DeprecatedSdkBanner;
import org.thoughtcrime.securesms.banner.banners.DozeBanner;
import org.thoughtcrime.securesms.banner.banners.ArchiveRestoreStatusBanner;
import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner;
import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner;
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner;
@@ -123,7 +128,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchRepository;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchViewModel;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchViewModelKt;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
import org.thoughtcrime.securesms.conversation.ConversationUpdateTick;
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest;
@@ -156,23 +160,19 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.search.MessageResult;
import org.thoughtcrime.securesms.search.SearchFilter;
import org.thoughtcrime.securesms.search.SearchFilterBottomSheet;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.signal.core.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.signal.core.ui.BottomSheetUtil;
import org.signal.core.ui.view.Stub;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.signal.core.util.ServiceUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalProxyUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter;
import org.thoughtcrime.securesms.verify.SelfVerificationFailureSheet;
import org.signal.core.ui.WindowSizeClassExtensionsKt;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import java.lang.ref.WeakReference;
@@ -479,11 +479,11 @@ public class ConversationListFragment extends MainFragment implements Conversati
}));
if (isSplitPane(getResources())) {
lifecycleDisposable.add(mainNavigationViewModel.getObservableActiveChatThreadId()
lifecycleDisposable.add(mainNavigationViewModel.getObservableActiveRecipientId()
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe(defaultAdapter::setActiveThreadId));
.subscribe(id -> defaultAdapter.setActiveRecipientId(id.orElse(null))));
} else {
defaultAdapter.setActiveThreadId(0);
defaultAdapter.setActiveRecipientId(null);
}
requireCallback().bindScrollHelper(list, getViewLifecycleOwner(), chatFolderList, color -> {
@@ -50,7 +50,9 @@ import com.makeramen.roundedimageview.RoundedDrawable;
import org.signal.core.util.ContextUtil;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.StringUtil;
import org.signal.core.util.Util;
import org.signal.core.util.logging.Log;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.OverlayTransformation;
import org.thoughtcrime.securesms.R;
@@ -74,7 +76,6 @@ import org.thoughtcrime.securesms.database.model.ThreadWithRecipient;
import org.thoughtcrime.securesms.database.model.UpdateDescription;
import org.thoughtcrime.securesms.fonts.SignalSymbols.Glyph;
import org.thoughtcrime.securesms.glide.targets.GlideLiveDataTarget;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -85,7 +86,6 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.SignalE164Util;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.signal.core.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.List;
@@ -214,9 +214,9 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
@NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations,
long activeThreadId)
@Nullable RecipientId activeRecipientId)
{
bindThread(lifecycleOwner, thread, glideRequests, locale, typingThreads, selectedConversations, null, false, true, activeThreadId);
bindThread(lifecycleOwner, thread, glideRequests, locale, typingThreads, selectedConversations, null, false, true, activeRecipientId);
}
public void bindThread(@NonNull LifecycleOwner lifecycleOwner,
@@ -228,7 +228,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
@Nullable String highlightSubstring,
boolean appendSystemContactIcon,
boolean showPinned,
long activeThreadId)
@Nullable RecipientId activeRecipientId)
{
this.threadId = thread.getThreadId();
this.requestManager = requestManager;
@@ -285,7 +285,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
this.archivedView.setVisibility(View.GONE);
}
setActiveThreadId(activeThreadId);
setActiveRecipientId(activeRecipientId);
setStatusIcons(thread);
setSelectedConversations(selectedConversations);
setBadgeFromRecipient(recipient.get());
@@ -336,7 +336,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
alertView.setNone();
setSelectedConversations(new ConversationSet());
setActiveThreadId(0);
setActiveRecipientId(null);
setBadgeFromRecipient(recipient.get());
contactPhotoImage.setAvatar(requestManager, recipient.get(), !batchMode, false);
}
@@ -376,7 +376,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
alertView.setNone();
setSelectedConversations(new ConversationSet());
setActiveThreadId(0);
setActiveRecipientId(null);
setBadgeFromRecipient(recipient.get());
contactPhotoImage.setAvatar(requestManager, recipient.get(), !batchMode);
}
@@ -397,7 +397,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
if (this.recipient != null) {
observeRecipient(null, null);
setSelectedConversations(new ConversationSet());
setActiveThreadId(0);
setActiveRecipientId(null);
contactPhotoImage.setAvatar(requestManager, null, !batchMode);
}
@@ -407,8 +407,8 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
}
@Override
public void setActiveThreadId(long activeThreadId) {
setActivated(activeThreadId > 0 && this.threadId == activeThreadId);
public void setActiveRecipientId(@Nullable RecipientId activeRecipientId) {
setActivated(activeRecipientId != null && this.recipient != null && this.recipient.getId().equals(activeRecipientId));
}
@Override
@@ -6,11 +6,13 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadWithRecipient;
@@ -47,7 +49,7 @@ public class ConversationListItemAction extends FrameLayout implements BindableC
@NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations,
long activeThreadId)
@Nullable RecipientId activeRecipientId)
{
this.description.setText(getContext().getString(R.string.ConversationListItemAction_archived_conversations_d, thread.getUnreadCount()));
}
@@ -58,7 +60,7 @@ public class ConversationListItemAction extends FrameLayout implements BindableC
}
@Override
public void setActiveThreadId(long activeThreadId) {
public void setActiveRecipientId(@Nullable RecipientId activeRecipientId) {
}
@@ -229,7 +229,7 @@ object ConversationListSearchModels {
model.thread.query,
true,
false,
0
null
)
}
}
@@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.megaphone.Megaphone
import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.delegate
import org.thoughtcrime.securesms.window.AppScaffoldNavigator
@@ -92,9 +93,9 @@ class MainNavigationViewModel(
private val internalIsFullScreenPane = MutableStateFlow(false)
val isFullScreenPane: StateFlow<Boolean> = internalIsFullScreenPane
val observableActiveChatThreadId: Observable<Long> =
snapshotFlow { chatsBackStack.activeConversationThreadId ?: -1L }
.combine(isFullScreenPane) { id, expanded -> if (expanded) -1L else id }
val observableActiveRecipientId: Observable<Optional<out RecipientId>> =
snapshotFlow { chatsBackStack.activeRecipientId }
.combine(isFullScreenPane) { id, expanded -> if (expanded) Optional.ofNullable(null) else Optional.ofNullable(id) }
.asObservable()
private val internalActiveCallId = MutableStateFlow<CallLogRow.Id?>(null)