From 9c63b37bb4ad27aa8cfea0985b6e931c33c71f6d Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Tue, 28 Jul 2020 10:17:06 -0400 Subject: [PATCH] Refactor use of MessageRecord to increase flexibility of ConversationAdapter. --- .../securesms/BindableConversationItem.java | 17 +-- .../conversation/ConversationAdapter.java | 136 +++++++++--------- .../conversation/ConversationDataSource.java | 23 ++- .../conversation/ConversationFragment.java | 89 ++++++------ .../conversation/ConversationItem.java | 48 ++++--- .../ConversationItemSwipeCallback.java | 10 +- .../conversation/ConversationMessage.java | 44 ++++++ .../conversation/ConversationUpdateItem.java | 43 +++--- .../conversation/ConversationViewModel.java | 30 ++-- .../MessageHeaderViewHolder.java | 3 +- 10 files changed, 253 insertions(+), 190 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 806d67351..da6e68024 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -5,6 +5,7 @@ import androidx.annotation.Nullable; import android.view.View; import org.thoughtcrime.securesms.contactshare.Contact; +import org.thoughtcrime.securesms.conversation.ConversationMessage; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; @@ -21,17 +22,17 @@ import java.util.Locale; import java.util.Set; public interface BindableConversationItem extends Unbindable { - void bind(@NonNull MessageRecord messageRecord, + void bind(@NonNull ConversationMessage messageRecord, @NonNull Optional previousMessageRecord, @NonNull Optional nextMessageRecord, - @NonNull GlideRequests glideRequests, - @NonNull Locale locale, - @NonNull Set batchSelected, - @NonNull Recipient recipients, - @Nullable String searchQuery, - boolean pulseHighlight); + @NonNull GlideRequests glideRequests, + @NonNull Locale locale, + @NonNull Set batchSelected, + @NonNull Recipient recipients, + @Nullable String searchQuery, + boolean pulseHighlight); - MessageRecord getMessageRecord(); + ConversationMessage getConversationMessage(); void setEventListener(@Nullable EventListener listener); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java index a1670d334..cdc19e02c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -65,8 +65,8 @@ import java.util.Set; * manager, so position 0 is at the bottom of the screen. That's why the "header" is at the bottom, * the "footer" is at the top, and we refer to the "next" record as having a lower index. */ -public class ConversationAdapter - extends PagedListAdapter +public class ConversationAdapter + extends PagedListAdapter implements StickyHeaderDecoration.StickyHeaderAdapter { @@ -89,16 +89,16 @@ public class ConversationAdapter private final Locale locale; private final Recipient recipient; - private final Set selected; - private final List fastRecords; - private final Set releasedFastRecords; - private final Calendar calendar; - private final MessageDigest digest; + private final Set selected; + private final List fastRecords; + private final Set releasedFastRecords; + private final Calendar calendar; + private final MessageDigest digest; - private String searchQuery; - private MessageRecord recordToPulseHighlight; - private View headerView; - private View footerView; + private String searchQuery; + private ConversationMessage recordToPulseHighlight; + private View headerView; + private View footerView; ConversationAdapter(@NonNull GlideRequests glideRequests, @NonNull Locale locale, @@ -130,7 +130,8 @@ public class ConversationAdapter return MESSAGE_TYPE_FOOTER; } - MessageRecord messageRecord = getItem(position); + ConversationMessage conversationMessage = getItem(position); + MessageRecord messageRecord = (conversationMessage != null) ? conversationMessage.getMessageRecord() : null; if (messageRecord == null) { return MESSAGE_TYPE_PLACEHOLDER; @@ -153,16 +154,13 @@ public class ConversationAdapter return FOOTER_ID; } - MessageRecord record = getItem(position); + ConversationMessage message = getItem(position); - if (record == null) { + if (message == null) { return -1; } - String unique = (record.isMms() ? "MMS::" : "SMS::") + record.getId(); - byte[] bytes = digest.digest(unique.getBytes()); - - return Conversions.byteArrayToLong(bytes); + return message.getUniqueId(digest); } @Override @@ -175,22 +173,23 @@ public class ConversationAdapter case MESSAGE_TYPE_UPDATE: long start = System.currentTimeMillis(); - V itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false); + View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false); + BindableConversationItem bindable = (BindableConversationItem) itemView; itemView.setOnClickListener(view -> { if (clickListener != null) { - clickListener.onItemClick(itemView.getMessageRecord()); + clickListener.onItemClick(bindable.getConversationMessage()); } }); itemView.setOnLongClickListener(view -> { if (clickListener != null) { - clickListener.onItemLongClick(itemView, itemView.getMessageRecord()); + clickListener.onItemLongClick(itemView, bindable.getConversationMessage()); } return true; }); - itemView.setEventListener(clickListener); + bindable.setEventListener(clickListener); Log.d(TAG, String.format(Locale.US, "Inflate time: %d ms for View type: %d", System.currentTimeMillis() - start, viewType)); return new ConversationViewHolder(itemView); @@ -215,23 +214,23 @@ public class ConversationAdapter case MESSAGE_TYPE_OUTGOING_MULTIMEDIA: case MESSAGE_TYPE_UPDATE: ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder; - MessageRecord messageRecord = Objects.requireNonNull(getItem(position)); + ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position)); int adapterPosition = holder.getAdapterPosition(); - MessageRecord previousRecord = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null; - MessageRecord nextRecord = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null; + ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null; + ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null; - conversationViewHolder.getView().bind(messageRecord, - Optional.fromNullable(previousRecord), - Optional.fromNullable(nextRecord), - glideRequests, - locale, - selected, - recipient, - searchQuery, - messageRecord == recordToPulseHighlight); + conversationViewHolder.getBindable().bind(conversationMessage, + Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null), + Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null), + glideRequests, + locale, + selected, + recipient, + searchQuery, + conversationMessage == recordToPulseHighlight); - if (messageRecord == recordToPulseHighlight) { + if (conversationMessage == recordToPulseHighlight) { recordToPulseHighlight = null; } break; @@ -245,13 +244,13 @@ public class ConversationAdapter } @Override - public void submitList(@Nullable PagedList pagedList) { + public void submitList(@Nullable PagedList pagedList) { cleanFastRecords(); super.submitList(pagedList); } @Override - protected @Nullable MessageRecord getItem(int position) { + protected @Nullable ConversationMessage getItem(int position) { position = hasHeader() ? position - 1 : position; if (position < fastRecords.size()) { @@ -272,7 +271,7 @@ public class ConversationAdapter @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { if (holder instanceof ConversationViewHolder) { - ((ConversationViewHolder) holder).getView().unbind(); + ((ConversationViewHolder) holder).getBindable().unbind(); } else if (holder instanceof HeaderFooterViewHolder) { ((HeaderFooterViewHolder) holder).unbind(); } @@ -285,11 +284,11 @@ public class ConversationAdapter if (position >= getItemCount()) return -1; if (position < 0) return -1; - MessageRecord record = getItem(position); + ConversationMessage conversationMessage = getItem(position); - if (record == null) return -1; + if (conversationMessage == null) return -1; - calendar.setTime(new Date(record.getDateSent())); + calendar.setTime(new Date(conversationMessage.getMessageRecord().getDateSent())); return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR)); } @@ -300,8 +299,8 @@ public class ConversationAdapter @Override public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int position) { - MessageRecord messageRecord = Objects.requireNonNull(getItem(position)); - viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, messageRecord.getDateReceived())); + ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position)); + viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateReceived())); } void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) { @@ -328,12 +327,12 @@ public class ConversationAdapter if (position >= getItemCount()) return 0; if (position < 0) return 0; - MessageRecord messageRecord = getItem(position); + ConversationMessage conversationMessage = getItem(position); - if (messageRecord == null || messageRecord.isOutgoing()) { + if (conversationMessage == null || conversationMessage.getMessageRecord().isOutgoing()) { return 0; } else { - return messageRecord.getDateReceived(); + return conversationMessage.getMessageRecord().getDateReceived(); } } @@ -403,8 +402,8 @@ public class ConversationAdapter * for a database change. */ @MainThread - void addFastRecord(MessageRecord record) { - fastRecords.add(0, record); + void addFastRecord(ConversationMessage conversationMessage) { + fastRecords.add(0, conversationMessage); notifyDataSetChanged(); } @@ -422,7 +421,7 @@ public class ConversationAdapter /** * Returns set of records that are selected in multi-select mode. */ - Set getSelectedItems() { + Set getSelectedItems() { return new HashSet<>(selected); } @@ -436,11 +435,11 @@ public class ConversationAdapter /** * Toggles the selected state of a record in multi-select mode. */ - void toggleSelection(MessageRecord record) { - if (selected.contains(record)) { - selected.remove(record); + void toggleSelection(ConversationMessage conversationMessage) { + if (selected.contains(conversationMessage)) { + selected.remove(conversationMessage); } else { - selected.add(record); + selected.add(conversationMessage); } } @@ -464,11 +463,11 @@ public class ConversationAdapter Util.assertMainThread(); synchronized (releasedFastRecords) { - Iterator recordIterator = fastRecords.iterator(); - while (recordIterator.hasNext()) { - long id = recordIterator.next().getId(); + Iterator messageIterator = fastRecords.iterator(); + while (messageIterator.hasNext()) { + long id = messageIterator.next().getMessageRecord().getId(); if (releasedFastRecords.contains(id)) { - recordIterator.remove(); + messageIterator.remove(); releasedFastRecords.remove(id); } } @@ -510,18 +509,17 @@ public class ConversationAdapter } } - public @Nullable MessageRecord getLastVisibleMessageRecord(int position) { + public @Nullable ConversationMessage getLastVisibleConversationMessage(int position) { return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0)); } static class ConversationViewHolder extends RecyclerView.ViewHolder { - public ConversationViewHolder(final @NonNull V itemView) { + public ConversationViewHolder(final @NonNull View itemView) { super(itemView); } - public V getView() { - //noinspection unchecked - return (V)itemView; + public BindableConversationItem getBindable() { + return (BindableConversationItem) itemView; } } @@ -530,7 +528,7 @@ public class ConversationAdapter StickyHeaderViewHolder(View itemView) { super(itemView); - textView = ViewUtil.findById(itemView, R.id.text); + textView = itemView.findViewById(R.id.text); } StickyHeaderViewHolder(TextView textView) { @@ -571,21 +569,21 @@ public class ConversationAdapter } } - private static class DiffCallback extends DiffUtil.ItemCallback { + private static class DiffCallback extends DiffUtil.ItemCallback { @Override - public boolean areItemsTheSame(@NonNull MessageRecord oldItem, @NonNull MessageRecord newItem) { - return oldItem.isMms() == newItem.isMms() && oldItem.getId() == newItem.getId(); + public boolean areItemsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) { + return oldItem.getMessageRecord().isMms() == newItem.getMessageRecord().isMms() && oldItem.getMessageRecord().getId() == newItem.getMessageRecord().getId(); } @Override - public boolean areContentsTheSame(@NonNull MessageRecord oldItem, @NonNull MessageRecord newItem) { + public boolean areContentsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) { // Corner rounding is not part of the model, so we can't use this yet return false; } } interface ItemClickListener extends BindableConversationItem.EventListener { - void onItemClick(MessageRecord item); - void onItemLongClick(View maskTarget, MessageRecord item); + void onItemClick(ConversationMessage item); + void onItemLongClick(View maskTarget, ConversationMessage item); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java index 40cc00f76..97257fc45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java @@ -7,6 +7,8 @@ import androidx.annotation.NonNull; import androidx.paging.DataSource; import androidx.paging.PositionalDataSource; +import com.annimon.stream.Stream; + import org.thoughtcrime.securesms.database.DatabaseContentProviders; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsSmsDatabase; @@ -24,7 +26,7 @@ import java.util.concurrent.Executor; /** * Core data source for loading an individual conversation. */ -class ConversationDataSource extends PositionalDataSource { +class ConversationDataSource extends PositionalDataSource { private static final String TAG = Log.tag(ConversationDataSource.class); @@ -57,7 +59,7 @@ class ConversationDataSource extends PositionalDataSource { } @Override - public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) { + public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback callback) { long start = System.currentTimeMillis(); MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context); @@ -76,14 +78,18 @@ class ConversationDataSource extends PositionalDataSource { if (!isInvalid()) { SizeFixResult result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount); - callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal()); + List items = Stream.of(result.getItems()) + .map(ConversationMessage::new) + .toList(); + + callback.onResult(items, params.requestedStartPosition, result.getTotal()); } Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.requestedStartPosition + ", size: " + params.requestedLoadSize + (isInvalid() ? " -- invalidated" : "")); } @Override - public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback callback) { + public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback callback) { long start = System.currentTimeMillis(); MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context); @@ -96,12 +102,15 @@ class ConversationDataSource extends PositionalDataSource { } } - callback.onResult(records); + List items = Stream.of(records) + .map(ConversationMessage::new) + .toList(); + callback.onResult(items); Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : "")); } - static class Factory extends DataSource.Factory { + static class Factory extends DataSource.Factory { private final Context context; private final long threadId; @@ -114,7 +123,7 @@ class ConversationDataSource extends PositionalDataSource { } @Override - public @NonNull DataSource create() { + public @NonNull DataSource create() { return new ConversationDataSource(context, threadId, invalidator); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 42ad0835e..5d830cbdd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -55,6 +55,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.OnScrollListener; +import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import com.google.android.collect.Sets; @@ -210,8 +211,8 @@ public class ConversationFragment extends LoggingFragment { typingView = (ConversationTypingView) inflater.inflate(R.layout.conversation_typing_view, container, false); new ConversationItemSwipeCallback( - messageRecord -> actionMode == null && - MenuState.canReplyToMessage(MenuState.isActionMessage(messageRecord), messageRecord, messageRequestViewModel.shouldShowMessageRequest()), + conversationMessage -> actionMode == null && + MenuState.canReplyToMessage(MenuState.isActionMessage(conversationMessage.getMessageRecord()), conversationMessage.getMessageRecord(), messageRequestViewModel.shouldShowMessageRequest()), this::handleReplyMessage ).attachToRecyclerView(list); @@ -288,9 +289,9 @@ public class ConversationFragment extends LoggingFragment { final long lastVisibleMessageTimestamp; if (firstVisiblePosition > 0 && lastVisiblePosition != RecyclerView.NO_POSITION) { - MessageRecord message = getListAdapter().getLastVisibleMessageRecord(lastVisiblePosition); + ConversationMessage message = getListAdapter().getLastVisibleConversationMessage(lastVisiblePosition); - lastVisibleMessageTimestamp = message != null ? message.getDateReceived() : 0; + lastVisibleMessageTimestamp = message != null ? message.getMessageRecord().getDateReceived() : 0; } else { lastVisibleMessageTimestamp = 0; } @@ -519,14 +520,14 @@ public class ConversationFragment extends LoggingFragment { } private void setCorrectMenuVisibility(@NonNull Menu menu) { - Set messageRecords = getListAdapter().getSelectedItems(); + Set messages = getListAdapter().getSelectedItems(); - if (actionMode != null && messageRecords.size() == 0) { + if (actionMode != null && messages.size() == 0) { actionMode.finish(); return; } - MenuState menuState = MenuState.getMenuState(messageRecords, messageRequestViewModel.shouldShowMessageRequest()); + MenuState menuState = MenuState.getMenuState(Stream.of(messages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet()), messageRequestViewModel.shouldShowMessageRequest()); menu.findItem(R.id.menu_context_forward).setVisible(menuState.shouldShowForwardAction()); menu.findItem(R.id.menu_context_reply).setVisible(menuState.shouldShowReplyAction()); @@ -544,8 +545,8 @@ public class ConversationFragment extends LoggingFragment { return (SmoothScrollingLinearLayoutManager) list.getLayoutManager(); } - private MessageRecord getSelectedMessageRecord() { - Set messageRecords = getListAdapter().getSelectedItems(); + private ConversationMessage getSelectedConversationMessage() { + Set messageRecords = getListAdapter().getSelectedItems(); if (messageRecords.size() == 1) return messageRecords.iterator().next(); else throw new AssertionError(); @@ -581,8 +582,8 @@ public class ConversationFragment extends LoggingFragment { list.addItemDecoration(lastSeenDecoration); } - private void handleCopyMessage(final Set messageRecords) { - List messageList = new LinkedList<>(messageRecords); + private void handleCopyMessage(final Set conversationMessages) { + List messageList = Stream.of(conversationMessages).map(ConversationMessage::getMessageRecord).toList(); Collections.sort(messageList, new Comparator() { @Override public int compare(MessageRecord lhs, MessageRecord rhs) { @@ -611,7 +612,8 @@ public class ConversationFragment extends LoggingFragment { clipboard.setText(result); } - private void handleDeleteMessages(final Set messageRecords) { + private void handleDeleteMessages(final Set conversationMessages) { + Set messageRecords = Stream.of(conversationMessages).map(ConversationMessage::getMessageRecord).collect(Collectors.toSet()); if (FeatureFlags.remoteDelete()) { buildRemoteDeleteConfirmationDialog(messageRecords).show(); } else { @@ -725,11 +727,12 @@ public class ConversationFragment extends LoggingFragment { } } - private void handleDisplayDetails(MessageRecord message) { - startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), message, recipient.getId(), threadId)); + private void handleDisplayDetails(ConversationMessage message) { + startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), message.getMessageRecord(), recipient.getId(), threadId)); } - private void handleForwardMessage(MessageRecord message) { + private void handleForwardMessage(ConversationMessage conversationMessage) { + MessageRecord message = conversationMessage.getMessageRecord(); if (message.isViewOnce()) { throw new AssertionError("Cannot forward a view-once message."); } @@ -812,13 +815,13 @@ public class ConversationFragment extends LoggingFragment { }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message); } - private void handleReplyMessage(final MessageRecord message) { + private void handleReplyMessage(final ConversationMessage message) { if (getActivity() != null) { //noinspection ConstantConditions ((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView(); } - listener.handleReplyMessage(message); + listener.handleReplyMessage(message.getMessageRecord()); } private void handleSaveAttachment(final MediaMmsMessageRecord message) { @@ -858,7 +861,7 @@ public class ConversationFragment extends LoggingFragment { if (getListAdapter() != null) { clearHeaderIfNotTyping(getListAdapter()); setLastSeen(0); - getListAdapter().addFastRecord(messageRecord); + getListAdapter().addFastRecord(new ConversationMessage(messageRecord)); list.post(() -> list.scrollToPosition(0)); } @@ -871,7 +874,7 @@ public class ConversationFragment extends LoggingFragment { if (getListAdapter() != null) { clearHeaderIfNotTyping(getListAdapter()); setLastSeen(0); - getListAdapter().addFastRecord(messageRecord); + getListAdapter().addFastRecord(new ConversationMessage(messageRecord)); list.post(() -> list.scrollToPosition(0)); } @@ -1085,9 +1088,9 @@ public class ConversationFragment extends LoggingFragment { private class ConversationFragmentItemClickListener implements ItemClickListener { @Override - public void onItemClick(MessageRecord messageRecord) { + public void onItemClick(ConversationMessage conversationMessage) { if (actionMode != null) { - ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); + ((ConversationAdapter) list.getAdapter()).toggleSelection(conversationMessage); list.getAdapter().notifyDataSetChanged(); if (getListAdapter().getSelectedItems().size() == 0) { @@ -1100,10 +1103,12 @@ public class ConversationFragment extends LoggingFragment { } @Override - public void onItemLongClick(View maskTarget, MessageRecord messageRecord) { + public void onItemLongClick(View maskTarget, ConversationMessage conversationMessage) { if (actionMode != null) return; + MessageRecord messageRecord = conversationMessage.getMessageRecord(); + if (messageRecord.isSecure() && !messageRecord.isRemoteDelete() && !messageRecord.isUpdate() && @@ -1113,12 +1118,12 @@ public class ConversationFragment extends LoggingFragment { { isReacting = true; list.setLayoutFrozen(true); - listener.handleReaction(maskTarget, messageRecord, new ReactionsToolbarListener(messageRecord), () -> { + listener.handleReaction(maskTarget, messageRecord, new ReactionsToolbarListener(conversationMessage), () -> { isReacting = false; list.setLayoutFrozen(false); }); } else { - ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); + ((ConversationAdapter) list.getAdapter()).toggleSelection(conversationMessage); list.getAdapter().notifyDataSetChanged(); actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback); @@ -1287,8 +1292,8 @@ public class ConversationFragment extends LoggingFragment { } } - private void handleEnterMultiSelect(@NonNull MessageRecord messageRecord) { - ((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord); + private void handleEnterMultiSelect(@NonNull ConversationMessage conversationMessage) { + ((ConversationAdapter) list.getAdapter()).toggleSelection(conversationMessage); list.getAdapter().notifyDataSetChanged(); actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback); @@ -1342,23 +1347,23 @@ public class ConversationFragment extends LoggingFragment { private class ReactionsToolbarListener implements Toolbar.OnMenuItemClickListener { - private final MessageRecord messageRecord; + private final ConversationMessage conversationMessage; - private ReactionsToolbarListener(@NonNull MessageRecord messageRecord) { - this.messageRecord = messageRecord; + private ReactionsToolbarListener(@NonNull ConversationMessage conversationMessage) { + this.conversationMessage = conversationMessage; } @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { - case R.id.action_info: handleDisplayDetails(messageRecord); return true; - case R.id.action_delete: handleDeleteMessages(Sets.newHashSet(messageRecord)); return true; - case R.id.action_copy: handleCopyMessage(Sets.newHashSet(messageRecord)); return true; - case R.id.action_reply: handleReplyMessage(messageRecord); return true; - case R.id.action_multiselect: handleEnterMultiSelect(messageRecord); return true; - case R.id.action_forward: handleForwardMessage(messageRecord); return true; - case R.id.action_download: handleSaveAttachment((MediaMmsMessageRecord) messageRecord); return true; - default: return false; + case R.id.action_info: handleDisplayDetails(conversationMessage); return true; + case R.id.action_delete: handleDeleteMessages(Sets.newHashSet(conversationMessage)); return true; + case R.id.action_copy: handleCopyMessage(Sets.newHashSet(conversationMessage)); return true; + case R.id.action_reply: handleReplyMessage(conversationMessage); return true; + case R.id.action_multiselect: handleEnterMultiSelect(conversationMessage); return true; + case R.id.action_forward: handleForwardMessage(conversationMessage); return true; + case R.id.action_download: handleSaveAttachment((MediaMmsMessageRecord) conversationMessage.getMessageRecord()); return true; + default: return false; } } } @@ -1417,24 +1422,24 @@ public class ConversationFragment extends LoggingFragment { actionMode.finish(); return true; case R.id.menu_context_details: - handleDisplayDetails(getSelectedMessageRecord()); + handleDisplayDetails(getSelectedConversationMessage()); actionMode.finish(); return true; case R.id.menu_context_forward: - handleForwardMessage(getSelectedMessageRecord()); + handleForwardMessage(getSelectedConversationMessage()); actionMode.finish(); return true; case R.id.menu_context_resend: - handleResendMessage(getSelectedMessageRecord()); + handleResendMessage(getSelectedConversationMessage().getMessageRecord()); actionMode.finish(); return true; case R.id.menu_context_save_attachment: - handleSaveAttachment((MediaMmsMessageRecord)getSelectedMessageRecord()); + handleSaveAttachment((MediaMmsMessageRecord) getSelectedConversationMessage().getMessageRecord()); actionMode.finish(); return true; case R.id.menu_context_reply: maybeShowSwipeToReplyTooltip(); - handleReplyMessage(getSelectedMessageRecord()); + handleReplyMessage(getSelectedConversationMessage()); actionMode.finish(); return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 40ac31e1c..c4117d4de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -139,11 +139,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati private static final Rect SWIPE_RECT = new Rect(); - private MessageRecord messageRecord; - private Locale locale; - private boolean groupThread; - private LiveRecipient recipient; - private GlideRequests glideRequests; + private ConversationMessage conversationMessage; + private MessageRecord messageRecord; + private Locale locale; + private boolean groupThread; + private LiveRecipient recipient; + private GlideRequests glideRequests; protected ConversationItemBodyBubble bodyBubble; protected View reply; @@ -160,7 +161,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati private ViewGroup container; protected ReactionsConversationView reactionsView; - private @NonNull Set batchSelected = new HashSet<>(); + private @NonNull Set batchSelected = new HashSet<>(); private @NonNull Outliner outliner = new Outliner(); private LiveRecipient conversationRecipient; private Stub mediaThumbnailStub; @@ -234,22 +235,23 @@ public class ConversationItem extends LinearLayout implements BindableConversati } @Override - public void bind(@NonNull MessageRecord messageRecord, + public void bind(@NonNull ConversationMessage conversationMessage, @NonNull Optional previousMessageRecord, @NonNull Optional nextMessageRecord, - @NonNull GlideRequests glideRequests, - @NonNull Locale locale, - @NonNull Set batchSelected, - @NonNull Recipient conversationRecipient, - @Nullable String searchQuery, - boolean pulseHighlight) + @NonNull GlideRequests glideRequests, + @NonNull Locale locale, + @NonNull Set batchSelected, + @NonNull Recipient conversationRecipient, + @Nullable String searchQuery, + boolean pulseHighlight) { if (this.recipient != null) this.recipient.removeForeverObserver(this); if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this); conversationRecipient = conversationRecipient.resolve(); - this.messageRecord = messageRecord; + this.conversationMessage = conversationMessage; + this.messageRecord = conversationMessage.getMessageRecord(); this.locale = locale; this.glideRequests = glideRequests; this.batchSelected = batchSelected; @@ -263,7 +265,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati setGutterSizes(messageRecord, groupThread); setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread); setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread); - setInteractionState(messageRecord, pulseHighlight); + setInteractionState(conversationMessage, pulseHighlight); setBodyText(messageRecord, searchQuery); setBubbleState(messageRecord); setStatusIcons(messageRecord); @@ -381,8 +383,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati } } - public MessageRecord getMessageRecord() { - return messageRecord; + public ConversationMessage getConversationMessage() { + return conversationMessage; } /// MessageRecord Attribute Parsers @@ -424,8 +426,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati } } - private void setInteractionState(MessageRecord messageRecord, boolean pulseHighlight) { - if (batchSelected.contains(messageRecord)) { + private void setInteractionState(ConversationMessage conversationMessage, boolean pulseHighlight) { + if (batchSelected.contains(conversationMessage)) { setBackgroundResource(R.drawable.conversation_item_background); setSelected(true); } else if (pulseHighlight) { @@ -437,19 +439,19 @@ public class ConversationItem extends LinearLayout implements BindableConversati } if (mediaThumbnailStub.resolved()) { - mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); - mediaThumbnailStub.get().setClickable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); + mediaThumbnailStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty()); + mediaThumbnailStub.get().setClickable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty()); mediaThumbnailStub.get().setLongClickable(batchSelected.isEmpty()); } if (audioViewStub.resolved()) { - audioViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); + audioViewStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty()); audioViewStub.get().setClickable(batchSelected.isEmpty()); audioViewStub.get().setEnabled(batchSelected.isEmpty()); } if (documentViewStub.resolved()) { - documentViewStub.get().setFocusable(!shouldInterceptClicks(messageRecord) && batchSelected.isEmpty()); + documentViewStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty()); documentViewStub.get().setClickable(batchSelected.isEmpty()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemSwipeCallback.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemSwipeCallback.java index dfe34a18f..365bd6693 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemSwipeCallback.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemSwipeCallback.java @@ -114,8 +114,8 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback { private void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder) { if (cannotSwipeViewHolder(viewHolder)) return; - ConversationItem item = ((ConversationItem) viewHolder.itemView); - MessageRecord messageRecord = item.getMessageRecord(); + ConversationItem item = ((ConversationItem) viewHolder.itemView); + ConversationMessage messageRecord = item.getConversationMessage(); onSwipeListener.onSwipe(messageRecord); } @@ -169,7 +169,7 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback { if (!(viewHolder.itemView instanceof ConversationItem)) return true; ConversationItem item = ((ConversationItem) viewHolder.itemView); - return !swipeAvailabilityProvider.isSwipeAvailable(item.getMessageRecord()) || + return !swipeAvailabilityProvider.isSwipeAvailable(item.getConversationMessage()) || item.disallowSwipe(latestDownX, latestDownY); } @@ -192,10 +192,10 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback { } interface SwipeAvailabilityProvider { - boolean isSwipeAvailable(MessageRecord messageRecord); + boolean isSwipeAvailable(ConversationMessage conversationMessage); } interface OnSwipeListener { - void onSwipe(MessageRecord messageRecord); + void onSwipe(ConversationMessage conversationMessage); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java new file mode 100644 index 000000000..c0c80be42 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.conversation; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.util.Conversions; + +import java.security.MessageDigest; + +/** + * A view level model used to pass arbitrary message related information needed + * for various presentations. + */ +public class ConversationMessage { + private final MessageRecord messageRecord; + + public ConversationMessage(@NonNull MessageRecord messageRecord) { + this.messageRecord = messageRecord; + } + + public @NonNull MessageRecord getMessageRecord() { + return messageRecord; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final ConversationMessage that = (ConversationMessage) o; + return messageRecord.equals(that.messageRecord); + } + + @Override + public int hashCode() { + return messageRecord.hashCode(); + } + + public long getUniqueId(@NonNull MessageDigest digest) { + String unique = (messageRecord.isMms() ? "MMS::" : "SMS::") + messageRecord.getId(); + byte[] bytes = digest.digest(unique.getBytes()); + + return Conversions.byteArrayToLong(bytes); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 0b11cb8dd..a76b4011e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -50,13 +50,14 @@ public final class ConversationUpdateItem extends LinearLayout { private static final String TAG = ConversationUpdateItem.class.getSimpleName(); - private Set batchSelected; + private Set batchSelected; private ImageView icon; private TextView title; private TextView body; private TextView date; private LiveRecipient sender; + private ConversationMessage conversationMessage; private MessageRecord messageRecord; private Locale locale; private LiveData displayBody; @@ -84,19 +85,19 @@ public final class ConversationUpdateItem extends LinearLayout } @Override - public void bind(@NonNull MessageRecord messageRecord, + public void bind(@NonNull ConversationMessage conversationMessage, @NonNull Optional previousMessageRecord, @NonNull Optional nextMessageRecord, - @NonNull GlideRequests glideRequests, - @NonNull Locale locale, - @NonNull Set batchSelected, - @NonNull Recipient conversationRecipient, - @Nullable String searchQuery, - boolean pulseUpdate) + @NonNull GlideRequests glideRequests, + @NonNull Locale locale, + @NonNull Set batchSelected, + @NonNull Recipient conversationRecipient, + @Nullable String searchQuery, + boolean pulseUpdate) { this.batchSelected = batchSelected; - bind(messageRecord, locale); + bind(conversationMessage, locale); } @Override @@ -111,11 +112,11 @@ public final class ConversationUpdateItem extends LinearLayout } @Override - public MessageRecord getMessageRecord() { - return messageRecord; + public ConversationMessage getConversationMessage() { + return conversationMessage; } - private void bind(@NonNull MessageRecord messageRecord, @NonNull Locale locale) { + private void bind(@NonNull ConversationMessage conversationMessage, @NonNull Locale locale) { if (this.sender != null) { this.sender.removeForeverObserver(this); } @@ -123,9 +124,10 @@ public final class ConversationUpdateItem extends LinearLayout observeDisplayBody(null); setBodyText(null); - this.messageRecord = messageRecord; - this.sender = messageRecord.getIndividualRecipient().live(); - this.locale = locale; + this.conversationMessage = conversationMessage; + this.messageRecord = conversationMessage.getMessageRecord(); + this.sender = messageRecord.getIndividualRecipient().live(); + this.locale = locale; this.sender.observeForever(this); @@ -133,7 +135,7 @@ public final class ConversationUpdateItem extends LinearLayout LiveData liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription); LiveData spannableStringMessage = Transformations.map(liveUpdateMessage, SpannableString::new); - present(messageRecord); + present(conversationMessage); observeDisplayBody(spannableStringMessage); } @@ -162,7 +164,8 @@ public final class ConversationUpdateItem extends LinearLayout } } - private void present(MessageRecord messageRecord) { + private void present(ConversationMessage conversationMessage) { + MessageRecord messageRecord = conversationMessage.getMessageRecord(); if (messageRecord.isGroupAction()) setGroupRecord(); else if (messageRecord.isCallLog()) setCallRecord(messageRecord); else if (messageRecord.isJoined()) setJoinedRecord(); @@ -174,8 +177,8 @@ public final class ConversationUpdateItem extends LinearLayout else if (messageRecord.isProfileChange()) setProfileNameChangeRecord(); else throw new AssertionError("Neither group nor log nor joined."); - if (batchSelected.contains(messageRecord)) setSelected(true); - else setSelected(false); + if (batchSelected.contains(conversationMessage)) setSelected(true); + else setSelected(false); } private void setCallRecord(MessageRecord messageRecord) { @@ -256,7 +259,7 @@ public final class ConversationUpdateItem extends LinearLayout @Override public void onRecipientChanged(@NonNull Recipient recipient) { - present(messageRecord); + present(conversationMessage); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java index 5a6cc400c..dd73eb3bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java @@ -28,14 +28,14 @@ class ConversationViewModel extends ViewModel { private static final String TAG = Log.tag(ConversationViewModel.class); - private final Application context; - private final MediaRepository mediaRepository; - private final ConversationRepository conversationRepository; - private final MutableLiveData> recentMedia; - private final MutableLiveData threadId; - private final LiveData> messages; - private final LiveData conversationMetadata; - private final Invalidator invalidator; + private final Application context; + private final MediaRepository mediaRepository; + private final ConversationRepository conversationRepository; + private final MutableLiveData> recentMedia; + private final MutableLiveData threadId; + private final LiveData> messages; + private final LiveData conversationMetadata; + private final Invalidator invalidator; private int jumpToPosition; @@ -55,12 +55,12 @@ class ConversationViewModel extends ViewModel { return conversationData; }); - LiveData>> messagesForThreadId = Transformations.switchMap(metadata, data -> { - DataSource.Factory factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator); - PagedList.Config config = new PagedList.Config.Builder() - .setPageSize(25) - .setInitialLoadSizeHint(25) - .build(); + LiveData>> messagesForThreadId = Transformations.switchMap(metadata, data -> { + DataSource.Factory factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator); + PagedList.Config config = new PagedList.Config.Builder() + .setPageSize(25) + .setInitialLoadSizeHint(25) + .build(); final int startPosition; if (data.shouldJumpToMessage()) { @@ -109,7 +109,7 @@ class ConversationViewModel extends ViewModel { return conversationMetadata; } - @NonNull LiveData> getMessages() { + @NonNull LiveData> getMessages() { return messages; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java index 23bfb92e1..9f9a61583 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.conversation.ConversationItem; +import org.thoughtcrime.securesms.conversation.ConversationMessage; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.sms.MessageSender; @@ -80,7 +81,7 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder { else if (messageRecord.isOutgoing()) conversationItem = (ConversationItem) sentStub.inflate(); else conversationItem = (ConversationItem) receivedStub.inflate(); } - conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), messageRecord.getRecipient(), null, false); + conversationItem.bind(new ConversationMessage(messageRecord), Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), messageRecord.getRecipient(), null, false); } private void bindErrorState(MessageRecord messageRecord) {