Rework how ConversationFragment RecyclerView responds to data updates.

master
Alex Hart 2020-06-07 16:25:21 -03:00 committed by Greyson Parrelli
parent 3a479d7eef
commit 7d06e2395f
11 changed files with 151 additions and 72 deletions

View File

@ -56,8 +56,8 @@ public class MainNavigator {
return false;
}
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition, boolean highlightStartPosition) {
Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, startingPosition, highlightStartPosition);
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition) {
Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, startingPosition);
activity.startActivity(intent);
activity.overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);

View File

@ -289,7 +289,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public static final String STICKER_EXTRA = "sticker_extra";
public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type";
public static final String STARTING_POSITION_EXTRA = "starting_position";
public static final String HIGHLIGHT_STARTING_POSITION_EXTRA = "highlight_starting_position";
private static final int PICK_GALLERY = 1;
private static final int PICK_DOCUMENT = 2;
@ -358,15 +357,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@NonNull RecipientId recipientId,
long threadId,
int distributionType,
int startingPosition,
boolean highlightStartingPosition)
int startingPosition)
{
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, startingPosition);
intent.putExtra(ConversationActivity.HIGHLIGHT_STARTING_POSITION_EXTRA, highlightStartingPosition);
return intent;
}

View File

@ -81,8 +81,8 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
private static final int MESSAGE_TYPE_FOOTER = 6;
private static final int MESSAGE_TYPE_PLACEHOLDER = 7;
private static final long HEADER_ID = Long.MIN_VALUE;
private static final long FOOTER_ID = Long.MIN_VALUE + 1;
private static final long HEADER_ID = Long.MIN_VALUE;
private static final long FOOTER_ID = Long.MIN_VALUE + 1;
private final ItemClickListener clickListener;
private final GlideRequests glideRequests;
@ -100,7 +100,6 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
private View headerView;
private View footerView;
ConversationAdapter(@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@Nullable ItemClickListener clickListener,
@ -248,7 +247,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
@Override
public void submitList(@Nullable PagedList<MessageRecord> pagedList) {
cleanFastRecords();
super.submitList(pagedList, this::notifyDataSetChanged);
super.submitList(pagedList);
}
@Override
@ -341,15 +340,35 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
/**
* Sets the view the appears at the top of the list (because the list is reversed).
*/
void setFooterView(View view) {
void setFooterView(@Nullable View view) {
boolean hadFooter = hasFooter();
this.footerView = view;
if (view == null && hadFooter) {
notifyItemRemoved(getItemCount());
} else if (view != null && hadFooter) {
notifyItemChanged(getItemCount() - 1);
} else if (view != null) {
notifyItemInserted(getItemCount() - 1);
}
}
/**
* Sets the view that appears at the bottom of the list (because the list is reversed).
*/
void setHeaderView(View view) {
void setHeaderView(@Nullable View view) {
boolean hadHeader = hasHeader();
this.headerView = view;
if (view == null && hadHeader) {
notifyItemRemoved(0);
} else if (view != null && hadHeader) {
notifyItemChanged(0);
} else if (view != null) {
notifyItemInserted(0);
}
}
/**

View File

@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.conversation;
* Represents metadata about a conversation.
*/
final class ConversationData {
private final long threadId;
private final long lastSeen;
private final int lastSeenPosition;
private final boolean hasSent;
@ -11,13 +12,15 @@ final class ConversationData {
private final boolean hasPreMessageRequestMessages;
private final int jumpToPosition;
ConversationData(long lastSeen,
ConversationData(long threadId,
long lastSeen,
int lastSeenPosition,
boolean hasSent,
boolean isMessageRequestAccepted,
boolean hasPreMessageRequestMessages,
int jumpToPosition)
{
this.threadId = threadId;
this.lastSeen = lastSeen;
this.lastSeenPosition = lastSeenPosition;
this.hasSent = hasSent;
@ -26,6 +29,10 @@ final class ConversationData {
this.jumpToPosition = jumpToPosition;
}
public long getThreadId() {
return threadId;
}
long getLastSeen() {
return lastSeen;
}

View File

@ -103,7 +103,10 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
}
callback.onResult(records);
Util.runOnMain(dataUpdateCallback::onDataUpdated);
if (!isInvalid()) {
Util.runOnMain(dataUpdateCallback::onDataUpdated);
}
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms" + (isInvalid() ? " -- invalidated" : ""));
}

View File

@ -21,6 +21,7 @@ import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Rect;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@ -63,9 +64,6 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.ConversationTypingView;
import org.thoughtcrime.securesms.components.TooltipPopup;
@ -73,8 +71,8 @@ import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearL
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
@ -83,6 +81,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
@ -100,8 +99,10 @@ import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity;
import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator;
@ -163,6 +164,8 @@ public class ConversationFragment extends Fragment {
private MessageRequestViewModel messageRequestViewModel;
private ConversationViewModel conversationViewModel;
private Deferred deferred = new Deferred();
public static void prepare(@NonNull Context context) {
FrameLayout parent = new FrameLayout(context);
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
@ -221,7 +224,7 @@ public class ConversationFragment extends Fragment {
getListAdapter().submitList(list);
}
});
conversationViewModel.getConversationMetadata().observe(this, this::presentConversationMetadata);
conversationViewModel.getConversationMetadata().observe(this, data -> deferred.defer(() -> presentConversationMetadata(data)));
return view;
}
@ -279,16 +282,6 @@ public class ConversationFragment extends Fragment {
initializeTypingObserver();
}
@Override
public void onResume() {
super.onResume();
if (list.getAdapter() != null) {
Log.i(TAG, "onResume notifyDataSetChanged");
list.getAdapter().notifyDataSetChanged();
}
}
@Override
public void onStop() {
super.onStop();
@ -411,6 +404,7 @@ public class ConversationFragment extends Fragment {
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
this.unknownSenderView = new UnknownSenderView(getActivity(), recipient.get(), threadId, () -> clearHeaderIfNotTyping(getListAdapter()));
deferred.setDeferred(true);
conversationViewModel.onConversationDataAvailable(threadId, startingPosition);
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
@ -429,6 +423,8 @@ public class ConversationFragment extends Fragment {
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
ConversationAdapter.initializePool(list.getRecycledViewPool());
adapter.registerAdapterDataObserver(new DataObserver());
setLastSeen(conversationViewModel.getLastSeen());
emptyConversationBanner.setVisibility(View.GONE);
@ -482,14 +478,11 @@ public class ConversationFragment extends Fragment {
});
list.postDelayed(() -> list.setVerticalScrollBarEnabled(true), 300);
adapter.setHeaderView(typingView);
adapter.notifyItemInserted(0);
} else {
if (isTypingIndicatorShowing()) {
adapter.setHeaderView(typingView);
adapter.notifyItemChanged(0);
} else {
adapter.setHeaderView(typingView);
adapter.notifyItemInserted(0);
}
}
} else {
@ -500,12 +493,10 @@ public class ConversationFragment extends Fragment {
list.setVerticalScrollBarEnabled(false);
list.postDelayed(() -> {
adapter.setHeaderView(null);
adapter.notifyItemRemoved(0);
list.post(() -> list.setVerticalScrollBarEnabled(true));
}, 200);
} else if (!replacedByIncomingMessage) {
adapter.setHeaderView(null);
adapter.notifyItemRemoved(0);
} else {
adapter.setHeaderView(null);
}
@ -552,6 +543,8 @@ public class ConversationFragment extends Fragment {
if (this.threadId != threadId) {
this.threadId = threadId;
messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
deferred.setDeferred(true);
conversationViewModel.onConversationDataAvailable(threadId, -1);
initializeListAdapter();
}
@ -866,8 +859,6 @@ public class ConversationFragment extends Fragment {
}
private void presentConversationMetadata(@NonNull ConversationData conversation) {
Log.d(TAG, "presentConversationMetadata()");
ConversationAdapter adapter = getListAdapter();
if (adapter == null) {
return;
@ -907,17 +898,10 @@ public class ConversationFragment extends Fragment {
private void scrollToStartingPosition(int startingPosition) {
list.post(() -> {
list.getLayoutManager().scrollToPosition(startingPosition);
if (shouldHighlightStartingPosition()) {
getListAdapter().pulseHighlightItem(startingPosition);
}
getListAdapter().pulseHighlightItem(startingPosition);
});
}
private boolean shouldHighlightStartingPosition() {
return requireActivity().getIntent().getBooleanExtra(ConversationActivity.HIGHLIGHT_STARTING_POSITION_EXTRA, false);
}
private void scrollToLastSeenPosition(int lastSeenPosition) {
if (lastSeenPosition > 0) {
list.post(() -> getListLayoutManager().scrollToPositionWithOffset(lastSeenPosition, list.getHeight()));
@ -1072,6 +1056,44 @@ public class ConversationFragment extends Fragment {
}
}
private class DataObserver extends RecyclerView.AdapterDataObserver {
private final Rect rect = new Rect();
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
if (deferred.isDeferred()) {
deferred.setDeferred(false);
return;
}
if (positionStart == 0 && itemCount == 1 && isTypingIndicatorShowing()) {
return;
}
if (list.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
int firstVisibleItem = getListLayoutManager().findFirstVisibleItemPosition();
if (firstVisibleItem == 0) {
View view = getListLayoutManager().findViewByPosition(0);
if (view == null) {
return;
}
view.getDrawingRect(rect);
list.offsetDescendantRectToMyCoords(view, rect);
int bottom = rect.bottom;
list.getDrawingRect(rect);
if (bottom <= rect.bottom) {
getListLayoutManager().scrollToPosition(0);
}
}
}
}
}
private class ConversationFragmentItemClickListener implements ItemClickListener {
@Override
@ -1424,4 +1446,34 @@ public class ConversationFragment extends Fragment {
}, 400);
}
}
private static class Deferred {
private Runnable deferred;
private boolean isDeferred;
public void defer(@Nullable Runnable deferred) {
this.deferred = deferred;
executeIfNecessary();
}
public void setDeferred(boolean isDeferred) {
this.isDeferred = isDeferred;
executeIfNecessary();
}
public boolean isDeferred() {
return isDeferred;
}
private void executeIfNecessary() {
if (deferred != null && !isDeferred) {
Runnable local = deferred;
deferred = null;
local.run();
}
}
}
}

View File

@ -52,6 +52,6 @@ class ConversationRepository {
lastSeen = 0;
}
return new ConversationData(lastSeen, lastSeenPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition);
return new ConversationData(threadId, lastSeen, lastSeenPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition);
}
}

View File

@ -54,17 +54,30 @@ class ConversationViewModel extends ViewModel {
this.onNextMessageLoad = new CopyOnWriteArrayList<>();
this.invalidator = new Invalidator();
LiveData<Pair<Long, PagedList<MessageRecord>>> messagesForThreadId = Transformations.switchMap(threadId, thread -> {
DataSource.Factory<Integer, MessageRecord> factory = new ConversationDataSource.Factory(context, thread, invalidator, this::onMessagesUpdated);
LiveData<ConversationData> conversationDataForRequestedThreadId = Transformations.switchMap(threadId, thread -> {
return conversationRepository.getConversationData(thread, jumpToPosition);
});
LiveData<Pair<Long, PagedList<MessageRecord>>> messagesForThreadId = Transformations.switchMap(conversationDataForRequestedThreadId, data -> {
DataSource.Factory<Integer, MessageRecord> factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator, this::onMessagesUpdated);
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(25)
.setInitialLoadSizeHint(25)
.build();
final int startPosition;
if (jumpToPosition > 0) {
startPosition = jumpToPosition;
} else {
startPosition = data.getLastSeenPosition();
}
Log.d(TAG, "Starting at position " + startPosition + " :: " + jumpToPosition + " :: " + data.getLastSeenPosition());
return Transformations.map(new LivePagedListBuilder<>(factory, config).setFetchExecutor(ConversationDataSource.EXECUTOR)
.setInitialLoadKey(Math.max(jumpToPosition, 0))
.setInitialLoadKey(Math.max(startPosition, 0))
.build(),
input -> new Pair<>(thread, input));
input -> new Pair<>(data.getThreadId(), input));
});
this.messages = Transformations.map(messagesForThreadId, Pair::second);

View File

@ -353,24 +353,19 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
getNavigator().goToConversation(threadRecord.getRecipient().getId(),
threadRecord.getThreadId(),
threadRecord.getDistributionType(),
threadRecord.getUnreadCount(),
false);
-1);
}
@Override
public void onContactClicked(@NonNull Recipient contact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact);
int unreadCount = DatabaseFactory.getMmsSmsDatabase(getContext()).getUnreadCount(threadId);
return new Pair<>(threadId, unreadCount);
}, pair -> {
return DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact);
}, threadId -> {
hideKeyboard();
getNavigator().goToConversation(contact.getId(),
pair.first(),
threadId,
ThreadDatabase.DistributionTypes.DEFAULT,
pair.second(),
false);
-1);
});
}
@ -384,8 +379,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
getNavigator().goToConversation(message.conversationRecipient.getId(),
message.threadId,
ThreadDatabase.DistributionTypes.DEFAULT,
startingPosition,
true);
startingPosition);
});
}
@ -735,8 +729,8 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size()));
}
private void handleCreateConversation(long threadId, Recipient recipient, int distributionType, int unreadCount) {
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, unreadCount, false);
private void handleCreateConversation(long threadId, Recipient recipient, int distributionType) {
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
}
@Override
@ -770,7 +764,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
@Override
public void onItemClick(ConversationListItem item) {
if (actionMode == null) {
handleCreateConversation(item.getThreadId(), item.getRecipient(), item.getDistributionType(), item.getUnreadCount());
handleCreateConversation(item.getThreadId(), item.getRecipient(), item.getDistributionType());
} else {
ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter();
adapter.toggleThreadInBatchSet(item.getThread());

View File

@ -63,8 +63,7 @@ public class AddGroupDetailsActivity extends PassphraseRequiredActionBarActivity
recipientId,
threadId,
ThreadDatabase.DistributionTypes.DEFAULT,
-1,
false);
-1);
startActivity(intent);
setResult(RESULT_OK);

View File

@ -81,12 +81,7 @@ public class NotificationItem {
Recipient recipient = threadRecipient != null ? threadRecipient : conversationRecipient;
int startingPosition = jumpToMessage ? getStartingPosition(context, threadId, messageReceivedTimestamp) : -1;
if (!jumpToMessage) {
int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId);
startingPosition = unreadCount > 0 ? unreadCount : -1;
}
Intent intent = ConversationActivity.buildIntent(context, recipient.getId(), threadId, 0, startingPosition, jumpToMessage);
Intent intent = ConversationActivity.buildIntent(context, recipient.getId(), threadId, 0, startingPosition);
makeIntentUniqueToPreventMerging(intent);