Fix conversation list jank after returning from calls tab.

This commit is contained in:
Alex Hart 2023-12-12 12:14:28 -04:00 committed by Cody Henthorne
parent 9ed80d46b6
commit 49d6743cbb
7 changed files with 95 additions and 49 deletions

View file

@ -20,4 +20,5 @@ public interface BindableConversationListItem extends Unbindable {
void setSelectedConversations(@NonNull ConversationSet conversations);
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
void updateTimestamp();
}

View file

@ -29,7 +29,7 @@ import java.util.Locale;
import java.util.Objects;
import java.util.Set;
class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.ViewHolder> {
class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.ViewHolder> implements TimestampPayloadSupport {
private static final int TYPE_THREAD = 1;
private static final int TYPE_ACTION = 2;
@ -41,7 +41,8 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
private enum Payload {
TYPING_INDICATOR,
SELECTION
SELECTION,
TIMESTAMP
}
private final LifecycleOwner lifecycleOwner;
@ -129,12 +130,13 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
} else if (holder instanceof ConversationViewHolder) {
for (Object payloadObject : payloads) {
if (payloadObject instanceof Payload) {
Payload payload = (Payload) payloadObject;
Payload payload = (Payload) payloadObject;
ConversationViewHolder vh = (ConversationViewHolder) holder;
if (payload == Payload.SELECTION) {
((ConversationViewHolder) holder).getConversationListItem().setSelectedConversations(selectedConversations);
} else {
((ConversationViewHolder) holder).getConversationListItem().updateTypingIndicator(typingSet);
switch (payload) {
case TYPING_INDICATOR -> vh.getConversationListItem().updateTypingIndicator(typingSet);
case SELECTION -> vh.getConversationListItem().setSelectedConversations(selectedConversations);
case TIMESTAMP -> vh.getConversationListItem().updateTimestamp();
}
}
}
@ -190,6 +192,11 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
return super.getItem(position);
}
@Override
public void notifyTimestampPayloadUpdate() {
notifyItemRangeChanged(0, getItemCount(), Payload.TIMESTAMP);
}
public void setPagingController(@Nullable PagingController pagingController) {
this.pagingController = pagingController;
}

View file

@ -476,8 +476,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
setAdapter(defaultAdapter);
}
if (activeAdapter != null) {
activeAdapter.notifyItemRangeChanged(0, activeAdapter.getItemCount());
if (activeAdapter instanceof TimestampPayloadSupport) {
((TimestampPayloadSupport) activeAdapter).notifyTimestampPayloadUpdate();
}
SignalProxyUtil.startListeningToWebsocket();

View file

@ -141,6 +141,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
private LiveData<SpannableString> displayBody;
private Disposable joinMembersDisposable = Disposable.empty();
private Runnable updateDateView = null;
public ConversationListItem(Context context) {
this(context, null);
@ -249,11 +250,16 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
observeDisplayBody(lifecycleOwner, displayBody);
if (thread.getDate() > 0) {
CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate());
dateView.setText(date);
dateView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE);
dateView.setTextColor(thread.isRead() ? ContextCompat.getColor(getContext(), R.color.signal_text_secondary)
: ContextCompat.getColor(getContext(), R.color.signal_text_primary));
updateDateView = () -> {
CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate());
dateView.setText(date);
};
updateDateView.run();
}
if (thread.isArchived()) {
@ -278,35 +284,6 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
}
}
public void bindContact(@NonNull LifecycleOwner lifecycleOwner,
@NonNull Recipient contact,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable String highlightSubstring)
{
this.glideRequests = glideRequests;
this.locale = locale;
this.highlightSubstring = highlightSubstring;
observeRecipient(lifecycleOwner, contact.live());
observeDisplayBody(null, null);
joinMembersDisposable.dispose();
setSubjectViewText(null);
fromView.setText(contact, SearchUtil.getHighlightedSpan(locale, searchStyleFactory, new SpannableString(contact.getDisplayName(getContext())), highlightSubstring, SearchUtil.MATCH_ALL), true, null);
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, searchStyleFactory, contact.getE164().orElse(""), highlightSubstring, SearchUtil.MATCH_ALL));
dateView.setText("");
archivedView.setVisibility(GONE);
unreadIndicator.setVisibility(GONE);
unreadMentions.setVisibility(GONE);
deliveryStatusIndicator.setNone();
alertView.setNone();
setSelectedConversations(new ConversationSet());
setBadgeFromRecipient(recipient.get());
contactPhotoImage.setAvatar(glideRequests, recipient.get(), !batchMode);
}
public void bindMessage(@NonNull LifecycleOwner lifecycleOwner,
@NonNull MessageResult messageResult,
@NonNull GlideRequests glideRequests,
@ -324,7 +301,10 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
fromView.setText(recipient.get(), recipient.get().getDisplayNameOrUsername(getContext()), false, null, false);
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, searchStyleFactory, messageResult.getBodySnippet(), highlightSubstring, SearchUtil.MATCH_ALL));
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.getReceivedTimestampMs()));
updateDateView = () -> dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.getReceivedTimestampMs()));
updateDateView.run();
archivedView.setVisibility(GONE);
unreadIndicator.setVisibility(GONE);
unreadMentions.setVisibility(GONE);
@ -353,7 +333,9 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
});
fromView.setText(recipient.get(), false);
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, groupWithMembers.getDate()));
updateDateView = () -> dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, groupWithMembers.getDate()));
updateDateView.run();
archivedView.setVisibility(GONE);
unreadIndicator.setVisibility(GONE);
unreadMentions.setVisibility(GONE);
@ -386,6 +368,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
observeDisplayBody(null, null);
joinMembersDisposable.dispose();
updateDateView = null;
}
@Override
@ -429,6 +412,13 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
}
}
@Override
public void updateTimestamp() {
if (updateDateView != null) {
updateDateView.run();
}
}
public Recipient getRecipient() {
return recipient.get();
}

View file

@ -64,4 +64,9 @@ public class ConversationListItemAction extends FrameLayout implements BindableC
public void updateTypingIndicator(@NonNull Set<Long> typingThreads) {
}
@Override
public void updateTimestamp() {
// Intentionally left blank.
}
}

View file

@ -33,7 +33,11 @@ class ConversationListSearchAdapter(
callButtonClickCallbacks: CallButtonClickCallbacks,
lifecycleOwner: LifecycleOwner,
glideRequests: GlideRequests
) : ContactSearchAdapter(context, fixedContacts, displayOptions, onClickedCallbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks) {
) : ContactSearchAdapter(context, fixedContacts, displayOptions, onClickedCallbacks, longClickCallbacks, storyContextMenuCallbacks, callButtonClickCallbacks), TimestampPayloadSupport {
companion object {
private const val PAYLOAD_TIMESTAMP = 0
}
init {
registerFactory(
@ -62,6 +66,27 @@ class ConversationListSearchAdapter(
)
}
override fun notifyTimestampPayloadUpdate() {
notifyItemRangeChanged(0, itemCount, PAYLOAD_TIMESTAMP)
}
private abstract class ConversationListItemViewHolder<M : MappingModel<M>>(
itemView: View
) : MappingViewHolder<M>(itemView) {
private val conversationListItem: ConversationListItem = itemView as ConversationListItem
override fun bind(model: M) {
if (payload.contains(PAYLOAD_TIMESTAMP)) {
conversationListItem.updateTimestamp()
return
}
fullBind(model)
}
abstract fun fullBind(model: M)
}
private class EmptyViewHolder(
itemView: View
) : MappingViewHolder<EmptyModel>(itemView) {
@ -69,6 +94,10 @@ class ConversationListSearchAdapter(
private val noResults = itemView.findViewById<TextView>(R.id.search_no_results)
override fun bind(model: EmptyModel) {
if (payload.isNotEmpty()) {
return
}
noResults.text = context.getString(R.string.SearchFragment_no_results, model.empty.query ?: "")
}
}
@ -78,8 +107,8 @@ class ConversationListSearchAdapter(
private val lifecycleOwner: LifecycleOwner,
private val glideRequests: GlideRequests,
itemView: View
) : MappingViewHolder<ThreadModel>(itemView) {
override fun bind(model: ThreadModel) {
) : ConversationListItemViewHolder<ThreadModel>(itemView) {
override fun fullBind(model: ThreadModel) {
itemView.setOnClickListener {
threadListener.onClicked(itemView, model.thread, false)
}
@ -101,8 +130,8 @@ class ConversationListSearchAdapter(
private val lifecycleOwner: LifecycleOwner,
private val glideRequests: GlideRequests,
itemView: View
) : MappingViewHolder<MessageModel>(itemView) {
override fun bind(model: MessageModel) {
) : ConversationListItemViewHolder<MessageModel>(itemView) {
override fun fullBind(model: MessageModel) {
itemView.setOnClickListener {
messageListener.onClicked(itemView, model.message, false)
}
@ -122,8 +151,8 @@ class ConversationListSearchAdapter(
private val lifecycleOwner: LifecycleOwner,
private val glideRequests: GlideRequests,
itemView: View
) : MappingViewHolder<GroupWithMembersModel>(itemView) {
override fun bind(model: GroupWithMembersModel) {
) : ConversationListItemViewHolder<GroupWithMembersModel>(itemView) {
override fun fullBind(model: GroupWithMembersModel) {
itemView.setOnClickListener {
groupWithMembersListener.onClicked(itemView, model.groupWithMembers, false)
}

View file

@ -0,0 +1,14 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversationlist
/**
* Generic interface for the adapters to support updating the
* timestamp in a given row as opposed to rebinding every item.
*/
interface TimestampPayloadSupport {
fun notifyTimestampPayloadUpdate()
}