mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Implement activated state for conversation list items.
This commit is contained in:
committed by
Cody Henthorne
parent
dac3a332d7
commit
64239962fc
@@ -17,9 +17,11 @@ public interface BindableConversationListItem extends Unbindable {
|
||||
@NonNull ThreadRecord thread,
|
||||
@NonNull RequestManager requestManager, @NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull ConversationSet selectedConversations);
|
||||
@NonNull ConversationSet selectedConversations,
|
||||
long activeThreadId);
|
||||
|
||||
void setSelectedConversations(@NonNull ConversationSet conversations);
|
||||
void setActiveThreadId(long activeThreadId);
|
||||
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
|
||||
void updateTimestamp();
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
|
||||
setContent {
|
||||
val listHostState = rememberFragmentState()
|
||||
val detailLocation by mainNavigationViewModel.detailLocation.collectAsStateWithLifecycle(MainNavigationDetailLocation.Empty)
|
||||
val detailLocation by mainNavigationViewModel.detailLocationRequests.collectAsStateWithLifecycle(MainNavigationDetailLocation.Empty)
|
||||
val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle()
|
||||
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
|
||||
val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -44,7 +44,8 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
||||
private enum Payload {
|
||||
TYPING_INDICATOR,
|
||||
SELECTION,
|
||||
TIMESTAMP
|
||||
TIMESTAMP,
|
||||
ACTIVE
|
||||
}
|
||||
|
||||
private final LifecycleOwner lifecycleOwner;
|
||||
@@ -55,6 +56,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
||||
private final Set<Long> typingSet = new HashSet<>();
|
||||
|
||||
private ConversationSet selectedConversations = new ConversationSet();
|
||||
private long activeThreadId = 0;
|
||||
private PagingController pagingController;
|
||||
|
||||
protected ConversationListAdapter(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@@ -148,6 +150,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,7 +168,8 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
||||
requestManager,
|
||||
Locale.getDefault(),
|
||||
typingSet,
|
||||
selectedConversations);
|
||||
selectedConversations,
|
||||
activeThreadId);
|
||||
} else if (holder.getItemViewType() == TYPE_HEADER) {
|
||||
HeaderViewHolder casted = (HeaderViewHolder) holder;
|
||||
Conversation conversation = Objects.requireNonNull(getItem(position));
|
||||
@@ -224,6 +228,11 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
||||
notifyItemRangeChanged(0, getItemCount(), Payload.SELECTION);
|
||||
}
|
||||
|
||||
void setActiveThreadId(long activeThreadId) {
|
||||
this.activeThreadId = activeThreadId;
|
||||
notifyItemRangeChanged(0, getItemCount(), Payload.ACTIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
Conversation conversation = getItem(position);
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
@@ -117,6 +118,7 @@ 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.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest;
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource;
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFilterPullView;
|
||||
@@ -132,6 +134,7 @@ 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;
|
||||
@@ -160,6 +163,7 @@ import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
@@ -403,6 +407,22 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
}));
|
||||
|
||||
if (WindowSizeClass.Companion.getWindowSizeClass(getResources()).isSplitPane()) {
|
||||
lifecycleDisposable.add(mainNavigationViewModel.getDetailLocationObservable()
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(location -> {
|
||||
if (location instanceof MainNavigationDetailLocation.Conversation) {
|
||||
Intent intent = ((MainNavigationDetailLocation.Conversation) location).getIntent();
|
||||
ConversationIntents.Args args = ConversationIntents.Args.from(Objects.requireNonNull(intent.getExtras()));
|
||||
long threadId = args.getThreadId();
|
||||
|
||||
defaultAdapter.setActiveThreadId(threadId);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
defaultAdapter.setActiveThreadId(0);
|
||||
}
|
||||
|
||||
requireCallback().bindScrollHelper(list, getViewLifecycleOwner(), chatFolderList, color -> {
|
||||
for (int i = 0; i < chatFolderList.getChildCount(); i++) {
|
||||
View child = chatFolderList.getChildAt(i);
|
||||
|
||||
@@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.database.MessageTypes;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
@@ -213,9 +214,10 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||
@NonNull RequestManager glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull ConversationSet selectedConversations)
|
||||
@NonNull ConversationSet selectedConversations,
|
||||
long activeThreadId)
|
||||
{
|
||||
bindThread(lifecycleOwner, thread, glideRequests, locale, typingThreads, selectedConversations, null, false, true);
|
||||
bindThread(lifecycleOwner, thread, glideRequests, locale, typingThreads, selectedConversations, null, false, true, activeThreadId);
|
||||
}
|
||||
|
||||
public void bindThread(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@@ -226,7 +228,8 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||
@NonNull ConversationSet selectedConversations,
|
||||
@Nullable String highlightSubstring,
|
||||
boolean appendSystemContactIcon,
|
||||
boolean showPinned)
|
||||
boolean showPinned,
|
||||
long activeThreadId)
|
||||
{
|
||||
this.threadId = thread.getThreadId();
|
||||
this.requestManager = requestManager;
|
||||
@@ -282,6 +285,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||
this.archivedView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
setActiveThreadId(activeThreadId);
|
||||
setStatusIcons(thread);
|
||||
setSelectedConversations(selectedConversations);
|
||||
setBadgeFromRecipient(recipient.get());
|
||||
@@ -326,6 +330,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||
alertView.setNone();
|
||||
|
||||
setSelectedConversations(new ConversationSet());
|
||||
setActiveThreadId(0);
|
||||
setBadgeFromRecipient(recipient.get());
|
||||
contactPhotoImage.setAvatar(requestManager, recipient.get(), !batchMode, false);
|
||||
}
|
||||
@@ -363,6 +368,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||
alertView.setNone();
|
||||
|
||||
setSelectedConversations(new ConversationSet());
|
||||
setActiveThreadId(0);
|
||||
setBadgeFromRecipient(recipient.get());
|
||||
contactPhotoImage.setAvatar(requestManager, recipient.get(), !batchMode);
|
||||
}
|
||||
@@ -383,6 +389,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||
if (this.recipient != null) {
|
||||
observeRecipient(null, null);
|
||||
setSelectedConversations(new ConversationSet());
|
||||
setActiveThreadId(0);
|
||||
contactPhotoImage.setAvatar(requestManager, null, !batchMode);
|
||||
}
|
||||
|
||||
@@ -391,6 +398,11 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
||||
updateDateView = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActiveThreadId(long activeThreadId) {
|
||||
setActivated(activeThreadId > 0 && this.threadId == activeThreadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectedConversations(@NonNull ConversationSet conversations) {
|
||||
this.batchMode = !conversations.isEmpty();
|
||||
|
||||
@@ -46,7 +46,8 @@ public class ConversationListItemAction extends FrameLayout implements BindableC
|
||||
@NonNull RequestManager requestManager,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull ConversationSet selectedConversations)
|
||||
@NonNull ConversationSet selectedConversations,
|
||||
long activeThreadId)
|
||||
{
|
||||
this.description.setText(getContext().getString(R.string.ConversationListItemAction_archived_conversations_d, thread.getUnreadCount()));
|
||||
}
|
||||
@@ -56,6 +57,11 @@ public class ConversationListItemAction extends FrameLayout implements BindableC
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActiveThreadId(long activeThreadId) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectedConversations(@NonNull ConversationSet conversations) {
|
||||
|
||||
|
||||
@@ -122,7 +122,8 @@ class ConversationListSearchAdapter(
|
||||
ConversationSet(),
|
||||
model.thread.query,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,18 @@ import org.thoughtcrime.securesms.stories.Stories
|
||||
class MainNavigationViewModel(initialListLocation: MainNavigationListLocation = MainNavigationListLocation.CHATS) : ViewModel() {
|
||||
private val megaphoneRepository = AppDependencies.megaphoneRepository
|
||||
|
||||
private val detailLocationFlow = MutableSharedFlow<MainNavigationDetailLocation>()
|
||||
val detailLocation: SharedFlow<MainNavigationDetailLocation> = detailLocationFlow
|
||||
/**
|
||||
* A shared flow of detail location requests that the MainActivity will service.
|
||||
*/
|
||||
private val detailLocationRequestFlow = MutableSharedFlow<MainNavigationDetailLocation>()
|
||||
val detailLocationRequests: SharedFlow<MainNavigationDetailLocation> = detailLocationRequestFlow
|
||||
|
||||
/**
|
||||
* The latest detail location that has been requested, for consumption by other components.
|
||||
*/
|
||||
private val detailLocationFlow = MutableStateFlow<MainNavigationDetailLocation>(MainNavigationDetailLocation.Empty)
|
||||
val detailLocation: StateFlow<MainNavigationDetailLocation> = detailLocationFlow
|
||||
val detailLocationObservable: Observable<MainNavigationDetailLocation> = detailLocationFlow.asObservable()
|
||||
|
||||
private val internalMegaphone = MutableStateFlow(Megaphone.NONE)
|
||||
val megaphone: StateFlow<Megaphone> = internalMegaphone
|
||||
@@ -73,6 +83,7 @@ class MainNavigationViewModel(initialListLocation: MainNavigationListLocation =
|
||||
|
||||
fun goTo(location: MainNavigationDetailLocation) {
|
||||
viewModelScope.launch {
|
||||
detailLocationRequestFlow.emit(location)
|
||||
detailLocationFlow.emit(location)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:enterFadeDuration="100"
|
||||
android:exitFadeDuration="100">
|
||||
|
||||
<item android:state_selected="true">
|
||||
<inset
|
||||
android:insetLeft="12dp"
|
||||
android:insetRight="12dp"
|
||||
android:insetTop="2dp"
|
||||
android:insetBottom="2dp">
|
||||
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/conversation_list_selected_color" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</inset>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<ripple android:color="@color/conversation_list_selected_color">
|
||||
<item android:id="@android:id/mask">
|
||||
<inset
|
||||
android:insetLeft="12dp"
|
||||
android:insetRight="12dp"
|
||||
android:insetTop="2dp"
|
||||
android:insetBottom="2dp">
|
||||
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/transparent_black_60" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</inset>
|
||||
</item>
|
||||
<item android:drawable="@drawable/conversation_list_item_background_default" />
|
||||
</ripple>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -1,7 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/signal_colorSurface2" android:state_selected="true" />
|
||||
<item android:drawable="@color/signal_colorSurface2" android:state_pressed="true" />
|
||||
<item android:drawable="@color/signal_colorSurface3" android:state_focused="true" />
|
||||
<item android:drawable="@drawable/conversation_list_item_background_default" />
|
||||
<!--
|
||||
~ Copyright 2025 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:enterFadeDuration="100"
|
||||
android:exitFadeDuration="100">
|
||||
|
||||
<item android:state_activated="true">
|
||||
<inset
|
||||
android:insetLeft="12dp"
|
||||
android:insetRight="12dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp">
|
||||
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/signal_colorSecondaryContainer" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</inset>
|
||||
</item>
|
||||
|
||||
<item android:state_selected="true">
|
||||
<inset
|
||||
android:insetLeft="12dp"
|
||||
android:insetRight="12dp"
|
||||
android:insetTop="2dp"
|
||||
android:insetBottom="2dp">
|
||||
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/conversation_list_selected_color" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</inset>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<ripple android:color="@color/conversation_list_selected_color">
|
||||
<item android:id="@android:id/mask">
|
||||
<inset
|
||||
android:insetLeft="12dp"
|
||||
android:insetRight="12dp"
|
||||
android:insetTop="2dp"
|
||||
android:insetBottom="2dp">
|
||||
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/transparent_black_60" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</inset>
|
||||
</item>
|
||||
<item android:drawable="@drawable/conversation_list_item_background_default" />
|
||||
</ripple>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
Reference in New Issue
Block a user