Refactor use of MessageRecord to increase flexibility of ConversationAdapter.

master
Cody Henthorne 2020-07-28 10:17:06 -04:00 committed by Greyson Parrelli
parent 5c110ca359
commit 9c63b37bb4
10 changed files with 253 additions and 190 deletions

View File

@ -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<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient recipients,
@Nullable String searchQuery,
boolean pulseHighlight);
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient recipients,
@Nullable String searchQuery,
boolean pulseHighlight);
MessageRecord getMessageRecord();
ConversationMessage getConversationMessage();
void setEventListener(@Nullable EventListener listener);

View File

@ -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<V extends View & BindableConversationItem>
extends PagedListAdapter<MessageRecord, RecyclerView.ViewHolder>
public class ConversationAdapter
extends PagedListAdapter<ConversationMessage, RecyclerView.ViewHolder>
implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationAdapter.StickyHeaderViewHolder>
{
@ -89,16 +89,16 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
private final Locale locale;
private final Recipient recipient;
private final Set<MessageRecord> selected;
private final List<MessageRecord> fastRecords;
private final Set<Long> releasedFastRecords;
private final Calendar calendar;
private final MessageDigest digest;
private final Set<ConversationMessage> selected;
private final List<ConversationMessage> fastRecords;
private final Set<Long> 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<V extends View & BindableConversationItem>
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<V extends View & BindableConversationItem>
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<V extends View & BindableConversationItem>
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<V extends View & BindableConversationItem>
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<V extends View & BindableConversationItem>
}
@Override
public void submitList(@Nullable PagedList<MessageRecord> pagedList) {
public void submitList(@Nullable PagedList<ConversationMessage> 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<V extends View & BindableConversationItem>
@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<V extends View & BindableConversationItem>
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<V extends View & BindableConversationItem>
@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<V extends View & BindableConversationItem>
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<V extends View & BindableConversationItem>
* 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<V extends View & BindableConversationItem>
/**
* Returns set of records that are selected in multi-select mode.
*/
Set<MessageRecord> getSelectedItems() {
Set<ConversationMessage> getSelectedItems() {
return new HashSet<>(selected);
}
@ -436,11 +435,11 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
/**
* 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<V extends View & BindableConversationItem>
Util.assertMainThread();
synchronized (releasedFastRecords) {
Iterator<MessageRecord> recordIterator = fastRecords.iterator();
while (recordIterator.hasNext()) {
long id = recordIterator.next().getId();
Iterator<ConversationMessage> 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<V extends View & BindableConversationItem>
}
}
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 <V extends View & BindableConversationItem> ConversationViewHolder(final @NonNull V itemView) {
public ConversationViewHolder(final @NonNull View itemView) {
super(itemView);
}
public <V extends View & BindableConversationItem> V getView() {
//noinspection unchecked
return (V)itemView;
public BindableConversationItem getBindable() {
return (BindableConversationItem) itemView;
}
}
@ -530,7 +528,7 @@ public class ConversationAdapter<V extends View & BindableConversationItem>
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<V extends View & BindableConversationItem>
}
}
private static class DiffCallback extends DiffUtil.ItemCallback<MessageRecord> {
private static class DiffCallback extends DiffUtil.ItemCallback<ConversationMessage> {
@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);
}
}

View File

@ -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<MessageRecord> {
class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
private static final String TAG = Log.tag(ConversationDataSource.class);
@ -57,7 +59,7 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
}
@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<MessageRecord> callback) {
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<ConversationMessage> callback) {
long start = System.currentTimeMillis();
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
@ -76,14 +78,18 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
if (!isInvalid()) {
SizeFixResult<MessageRecord> result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
List<ConversationMessage> 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<MessageRecord> callback) {
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<ConversationMessage> callback) {
long start = System.currentTimeMillis();
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
@ -96,12 +102,15 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
}
}
callback.onResult(records);
List<ConversationMessage> 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<Integer, MessageRecord> {
static class Factory extends DataSource.Factory<Integer, ConversationMessage> {
private final Context context;
private final long threadId;
@ -114,7 +123,7 @@ class ConversationDataSource extends PositionalDataSource<MessageRecord> {
}
@Override
public @NonNull DataSource<Integer, MessageRecord> create() {
public @NonNull DataSource<Integer, ConversationMessage> create() {
return new ConversationDataSource(context, threadId, invalidator);
}
}

View File

@ -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<MessageRecord> messageRecords = getListAdapter().getSelectedItems();
Set<ConversationMessage> 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<MessageRecord> messageRecords = getListAdapter().getSelectedItems();
private ConversationMessage getSelectedConversationMessage() {
Set<ConversationMessage> 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<MessageRecord> messageRecords) {
List<MessageRecord> messageList = new LinkedList<>(messageRecords);
private void handleCopyMessage(final Set<ConversationMessage> conversationMessages) {
List<MessageRecord> messageList = Stream.of(conversationMessages).map(ConversationMessage::getMessageRecord).toList();
Collections.sort(messageList, new Comparator<MessageRecord>() {
@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<MessageRecord> messageRecords) {
private void handleDeleteMessages(final Set<ConversationMessage> conversationMessages) {
Set<MessageRecord> 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;
}

View File

@ -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<MessageRecord> batchSelected = new HashSet<>();
private @NonNull Set<ConversationMessage> batchSelected = new HashSet<>();
private @NonNull Outliner outliner = new Outliner();
private LiveRecipient conversationRecipient;
private Stub<ConversationItemThumbnail> 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<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulseHighlight)
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<ConversationMessage> 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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -50,13 +50,14 @@ public final class ConversationUpdateItem extends LinearLayout
{
private static final String TAG = ConversationUpdateItem.class.getSimpleName();
private Set<MessageRecord> batchSelected;
private Set<ConversationMessage> 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<SpannableString> 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<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<MessageRecord> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulseUpdate)
@NonNull GlideRequests glideRequests,
@NonNull Locale locale,
@NonNull Set<ConversationMessage> 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<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription);
LiveData<SpannableString> 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

View File

@ -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<List<Media>> recentMedia;
private final MutableLiveData<Long> threadId;
private final LiveData<PagedList<MessageRecord>> messages;
private final LiveData<ConversationData> conversationMetadata;
private final Invalidator invalidator;
private final Application context;
private final MediaRepository mediaRepository;
private final ConversationRepository conversationRepository;
private final MutableLiveData<List<Media>> recentMedia;
private final MutableLiveData<Long> threadId;
private final LiveData<PagedList<ConversationMessage>> messages;
private final LiveData<ConversationData> conversationMetadata;
private final Invalidator invalidator;
private int jumpToPosition;
@ -55,12 +55,12 @@ class ConversationViewModel extends ViewModel {
return conversationData;
});
LiveData<Pair<Long, PagedList<MessageRecord>>> messagesForThreadId = Transformations.switchMap(metadata, data -> {
DataSource.Factory<Integer, MessageRecord> factory = new ConversationDataSource.Factory(context, data.getThreadId(), invalidator);
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(25)
.setInitialLoadSizeHint(25)
.build();
LiveData<Pair<Long, PagedList<ConversationMessage>>> messagesForThreadId = Transformations.switchMap(metadata, data -> {
DataSource.Factory<Integer, ConversationMessage> 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<PagedList<MessageRecord>> getMessages() {
@NonNull LiveData<PagedList<ConversationMessage>> getMessages() {
return messages;
}

View File

@ -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) {