From d9641128a83fd160fda54c5c6d27cd8bf7d794f9 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Tue, 16 Jun 2020 16:00:24 -0400 Subject: [PATCH] Refresh Message Details screen. --- app/src/main/AndroidManifest.xml | 6 + .../conversation/ConversationFragment.java | 2 +- .../conversation/ConversationItem.java | 2 +- .../database/model/DisplayRecord.java | 4 + .../messagedetails/MessageDetails.java | 85 +++++++ .../MessageDetailsActivity.java | 147 ++++++++++++ .../messagedetails/MessageDetailsAdapter.java | 150 +++++++++++++ .../MessageDetailsRepository.java | 132 +++++++++++ .../MessageDetailsViewModel.java | 55 +++++ .../MessageHeaderViewHolder.java | 210 ++++++++++++++++++ .../messagedetails/MessageRecordLiveData.java | 107 +++++++++ .../RecipientDeliveryStatus.java | 62 ++++++ .../messagedetails/RecipientHeader.java | 64 ++++++ .../RecipientHeaderViewHolder.java | 42 ++++ .../messagedetails/RecipientViewHolder.java | 68 ++++++ .../res/layout/message_details_2_activity.xml | 8 + .../res/layout/message_details_2_header.xml | 189 ++++++++++++++++ .../layout/message_details_2_recipient.xml | 97 ++++++++ .../message_details_2_recipient_header.xml | 34 +++ app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/values/strings.xml | 8 + 21 files changed, 1471 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsViewModel.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageRecordLiveData.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientHeader.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientHeaderViewHolder.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientViewHolder.java create mode 100644 app/src/main/res/layout/message_details_2_activity.xml create mode 100644 app/src/main/res/layout/message_details_2_header.xml create mode 100644 app/src/main/res/layout/message_details_2_recipient.xml create mode 100644 app/src/main/res/layout/message_details_2_recipient_header.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 05a237b932..45571e0080 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -249,6 +249,12 @@ android:launchMode="singleTask" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + 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 99eed8bf51..3539433ecd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -690,7 +690,7 @@ public class ConversationFragment extends Fragment { private void handleDisplayDetails(MessageRecord message) { - Intent intent = new Intent(getActivity(), MessageDetailsActivity.class); + Intent intent = new Intent(getActivity(), org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity.class); intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId()); intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, threadId); intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT); 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 2a75a7224c..2742a54394 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -1377,7 +1377,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati if (!shouldInterceptClicks(messageRecord) && parent != null) { parent.onClick(v); } else if (messageRecord.isFailed()) { - Intent intent = new Intent(context, MessageDetailsActivity.class); + Intent intent = new Intent(context, org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity.class); intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, messageRecord.getId()); intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, messageRecord.getThreadId()); intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index 45222fe1ba..7a4765905e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -78,6 +78,10 @@ public abstract class DisplayRecord { !MmsSmsColumns.Types.isIdentityDefault(type); } + public boolean isSent() { + return MmsSmsColumns.Types.isSentType(type); + } + public boolean isOutgoing() { return MmsSmsColumns.Types.isOutgoingMessageType(type); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java new file mode 100644 index 0000000000..1525309399 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java @@ -0,0 +1,85 @@ +package org.thoughtcrime.securesms.messagedetails; + +import androidx.annotation.NonNull; + +import com.annimon.stream.ComparatorCompat; + +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.TreeSet; + +final class MessageDetails { + private static final Comparator HAS_DISPLAY_NAME = (r1, r2) -> Boolean.compare(r2.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication()), r1.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication())); + private static final Comparator ALPHABETICAL = (r1, r2) -> r1.getRecipient().getDisplayName(ApplicationDependencies.getApplication()).compareToIgnoreCase(r2.getRecipient().getDisplayName(ApplicationDependencies.getApplication())); + private static final Comparator RECIPIENT_COMPARATOR = ComparatorCompat.chain(HAS_DISPLAY_NAME).thenComparing(ALPHABETICAL); + + private final MessageRecord messageRecord; + + private final Collection pending; + private final Collection sent; + private final Collection delivered; + private final Collection read; + private final Collection notSent; + + MessageDetails(MessageRecord messageRecord, List recipients) { + this.messageRecord = messageRecord; + + pending = new TreeSet<>(RECIPIENT_COMPARATOR); + sent = new TreeSet<>(RECIPIENT_COMPARATOR); + delivered = new TreeSet<>(RECIPIENT_COMPARATOR); + read = new TreeSet<>(RECIPIENT_COMPARATOR); + notSent = new TreeSet<>(RECIPIENT_COMPARATOR); + + if (messageRecord.isOutgoing()) { + for (RecipientDeliveryStatus status : recipients) { + switch (status.getDeliveryStatus()) { + case UNKNOWN: + notSent.add(status); + break; + case PENDING: + pending.add(status); + break; + case SENT: + sent.add(status); + break; + case DELIVERED: + delivered.add(status); + break; + case READ: + read.add(status); + break; + } + } + } else { + sent.addAll(recipients); + } + } + + @NonNull MessageRecord getMessageRecord() { + return messageRecord; + } + + @NonNull Collection getPending() { + return pending; + } + + @NonNull Collection getSent() { + return sent; + } + + @NonNull Collection getDelivered() { + return delivered; + } + + @NonNull Collection getRead() { + return read; + } + + @NonNull Collection getNotSent() { + return notSent; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java new file mode 100644 index 0000000000..2b8711f3e2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java @@ -0,0 +1,147 @@ +package org.thoughtcrime.securesms.messagedetails; + +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.color.MaterialColor; +import org.thoughtcrime.securesms.messagedetails.MessageDetailsAdapter.MessageDetailsViewState; +import org.thoughtcrime.securesms.messagedetails.MessageDetailsViewModel.Factory; +import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme; +import org.thoughtcrime.securesms.util.DynamicTheme; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class MessageDetailsActivity extends PassphraseRequiredActionBarActivity { + + public static final String MESSAGE_ID_EXTRA = "message_id"; + public static final String THREAD_ID_EXTRA = "thread_id"; + public static final String TYPE_EXTRA = "type"; + public static final String RECIPIENT_EXTRA = "recipient_id"; + + private GlideRequests glideRequests; + private MessageDetailsViewModel viewModel; + private MessageDetailsAdapter adapter; + + private DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme(); + + @Override + protected void onPreCreate() { + dynamicTheme.onCreate(this); + } + + @Override + protected void onCreate(Bundle savedInstanceState, boolean ready) { + super.onCreate(savedInstanceState, ready); + setContentView(R.layout.message_details_2_activity); + + glideRequests = GlideApp.with(this); + + initializeList(); + initializeViewModel(); + initializeActionBar(); + } + + @Override + protected void onResume() { + super.onResume(); + dynamicTheme.onResume(this); + adapter.resumeMessageExpirationTimer(); + } + + @Override + protected void onPause() { + super.onPause(); + adapter.pauseMessageExpirationTimer(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void initializeList() { + RecyclerView list = findViewById(R.id.message_details_list); + adapter = new MessageDetailsAdapter(glideRequests); + + list.setAdapter(adapter); + } + + private void initializeViewModel() { + final RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_EXTRA); + final String type = getIntent().getStringExtra(TYPE_EXTRA); + final Long messageId = getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1); + final Factory factory = new Factory(recipientId, type, messageId); + + viewModel = ViewModelProviders.of(this, factory).get(MessageDetailsViewModel.class); + viewModel.getMessageDetails().observe(this, details -> { + if (details == null) { + finish(); + } else { + adapter.submitList(convertToRows(details)); + } + }); + } + + private void initializeActionBar() { + assert getSupportActionBar() != null; + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + viewModel.getRecipientColor().observe(this, this::setActionBarColor); + } + + private void setActionBarColor(MaterialColor color) { + assert getSupportActionBar() != null; + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this))); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(color.toStatusBarColor(this)); + } + } + + private List> convertToRows(MessageDetails details) { + List> list = new ArrayList<>(); + + list.add(new MessageDetailsViewState<>(details.getMessageRecord(), MessageDetailsViewState.MESSAGE_HEADER)); + + int headerOrder = 0; + if (details.getMessageRecord().isOutgoing()) { + if (addRecipients(list, RecipientHeader.notSent(headerOrder), details.getNotSent())) headerOrder++; + if (addRecipients(list, RecipientHeader.read(headerOrder), details.getRead())) headerOrder++; + if (addRecipients(list, RecipientHeader.delivered(headerOrder), details.getDelivered())) headerOrder++; + if (addRecipients(list, RecipientHeader.sentTo(headerOrder), details.getSent())) headerOrder++; + addRecipients(list, RecipientHeader.pending(headerOrder), details.getPending()); + } else { + addRecipients(list, RecipientHeader.sentFrom(headerOrder), details.getSent()); + } + + return list; + } + + private boolean addRecipients(List> list, RecipientHeader header, Collection recipients) { + if (recipients.isEmpty()) { + return false; + } + + list.add(new MessageDetailsViewState<>(header, MessageDetailsViewState.RECIPIENT_HEADER)); + for (RecipientDeliveryStatus status : recipients) { + list.add(new MessageDetailsViewState<>(status, MessageDetailsViewState.RECIPIENT)); + } + return true; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java new file mode 100644 index 0000000000..4662fce859 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java @@ -0,0 +1,150 @@ +package org.thoughtcrime.securesms.messagedetails; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.mms.GlideRequests; + +import java.util.List; + +final class MessageDetailsAdapter extends ListAdapter, RecyclerView.ViewHolder> { + + private static final Object EXPIRATION_TIMER_CHANGE_PAYLOAD = new Object(); + + private final GlideRequests glideRequests; + private boolean running; + + MessageDetailsAdapter(GlideRequests glideRequests) { + super(new MessageDetailsDiffer()); + this.glideRequests = glideRequests; + running = true; + } + + @Override + public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case MessageDetailsViewState.MESSAGE_HEADER: + return new MessageHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_2_header, parent, false), glideRequests); + case MessageDetailsViewState.RECIPIENT_HEADER: + return new RecipientHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_2_recipient_header, parent, false)); + case MessageDetailsViewState.RECIPIENT: + return new RecipientViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_2_recipient, parent, false)); + default: + throw new AssertionError("unknown view type"); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (holder instanceof MessageHeaderViewHolder) { + ((MessageHeaderViewHolder) holder).bind((MessageRecord) getItem(position).data, running); + } else if (holder instanceof RecipientHeaderViewHolder) { + ((RecipientHeaderViewHolder) holder).bind((RecipientHeader) getItem(position).data); + } else if (holder instanceof RecipientViewHolder) { + ((RecipientViewHolder) holder).bind((RecipientDeliveryStatus) getItem(position).data); + } else { + throw new AssertionError("unknown view holder"); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads); + } else if (holder instanceof MessageHeaderViewHolder) { + ((MessageHeaderViewHolder) holder).partialBind((MessageRecord) getItem(position).data, running); + } + } + + @Override + public int getItemViewType(int position) { + return getItem(position).itemType; + } + + void resumeMessageExpirationTimer() { + running = true; + if (getItemCount() > 0) { + notifyItemChanged(0, EXPIRATION_TIMER_CHANGE_PAYLOAD); + } + } + + void pauseMessageExpirationTimer() { + running = false; + if (getItemCount() > 0) { + notifyItemChanged(0, EXPIRATION_TIMER_CHANGE_PAYLOAD); + } + } + + private static class MessageDetailsDiffer extends DiffUtil.ItemCallback> { + @Override + public boolean areItemsTheSame(@NonNull MessageDetailsViewState oldItem, @NonNull MessageDetailsViewState newItem) { + Object oldData = oldItem.data; + Object newData = newItem.data; + + if (oldData.getClass() == newData.getClass() && oldItem.itemType == newItem.itemType) { + switch (oldItem.itemType) { + case MessageDetailsViewState.MESSAGE_HEADER: + return true; + case MessageDetailsViewState.RECIPIENT_HEADER: + return ((RecipientHeader) oldData).getHeaderOrder() == ((RecipientHeader) newData).getHeaderOrder(); + case MessageDetailsViewState.RECIPIENT: + return ((RecipientDeliveryStatus) oldData).getRecipient().getId().equals(((RecipientDeliveryStatus) newData).getRecipient().getId()); + } + } + + return false; + } + + @SuppressLint("DiffUtilEquals") + @Override + public boolean areContentsTheSame(@NonNull MessageDetailsViewState oldItem, @NonNull MessageDetailsViewState newItem) { + Object oldData = oldItem.data; + Object newData = newItem.data; + + if (oldData.getClass() == newData.getClass() && oldItem.itemType == newItem.itemType) { + switch (oldItem.itemType) { + case MessageDetailsViewState.MESSAGE_HEADER: + return areMessageRecordContentsTheSame((MessageRecord) oldData, (MessageRecord) newData); + case MessageDetailsViewState.RECIPIENT_HEADER: + return ((RecipientHeader) oldData).getHeader() == ((RecipientHeader) newData).getHeader(); + case MessageDetailsViewState.RECIPIENT: + return true; + } + } + + return false; + } + + private boolean areMessageRecordContentsTheSame(MessageRecord oldData, MessageRecord newData) { + return oldData.equals(newData) && + oldData.getDateSent() == newData.getDateSent() && + oldData.getDateReceived() == newData.getDateReceived() && + oldData.getType() == newData.getType() && + oldData.getExpiresIn() == newData.getExpiresIn() && + oldData.getExpireStarted() == newData.getExpireStarted() && + oldData.getReactions().equals(newData.getReactions()); + } + } + + static final class MessageDetailsViewState { + public static final int MESSAGE_HEADER = 0; + public static final int RECIPIENT_HEADER = 1; + public static final int RECIPIENT = 2; + + private final T data; + private int itemType; + + MessageDetailsViewState(T t, int itemType) { + this.data = t; + this.itemType = itemType; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java new file mode 100644 index 0000000000..377e420f06 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java @@ -0,0 +1,132 @@ +package org.thoughtcrime.securesms.messagedetails; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.GroupReceiptDatabase; +import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; +import org.thoughtcrime.securesms.database.documents.NetworkFailure; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; + +import java.util.LinkedList; +import java.util.List; + +final class MessageDetailsRepository { + + private final Context context = ApplicationDependencies.getApplication(); + + @NonNull LiveData getMessageRecord(String type, Long messageId) { + return new MessageRecordLiveData(context, type, messageId); + } + + @NonNull LiveData getMessageDetails(@Nullable MessageRecord messageRecord) { + final MutableLiveData liveData = new MutableLiveData<>(); + + if (messageRecord != null) { + SignalExecutors.BOUNDED.execute(() -> liveData.postValue(getRecipientDeliveryStatusesInternal(messageRecord))); + } else { + liveData.setValue(null); + } + + return liveData; + } + + @WorkerThread + private @NonNull MessageDetails getRecipientDeliveryStatusesInternal(@NonNull MessageRecord messageRecord) { + List recipients = new LinkedList<>(); + + if (!messageRecord.getRecipient().isGroup()) { + recipients.add(new RecipientDeliveryStatus(messageRecord, + messageRecord.getRecipient(), + getStatusFor(messageRecord), + messageRecord.isUnidentified(), + -1, + getNetworkFailure(messageRecord, messageRecord.getRecipient()), + getKeyMismatchFailure(messageRecord, messageRecord.getRecipient()))); + } else { + List receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId()); + + if (receiptInfoList.isEmpty()) { + List group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); + + for (Recipient recipient : group) { + recipients.add(new RecipientDeliveryStatus(messageRecord, + recipient, + RecipientDeliveryStatus.Status.UNKNOWN, + false, + -1, + getNetworkFailure(messageRecord, recipient), + getKeyMismatchFailure(messageRecord, recipient))); + } + } else { + for (GroupReceiptDatabase.GroupReceiptInfo info : receiptInfoList) { + Recipient recipient = Recipient.resolved(info.getRecipientId()); + NetworkFailure failure = getNetworkFailure(messageRecord, recipient); + IdentityKeyMismatch mismatch = getKeyMismatchFailure(messageRecord, recipient); + boolean recipientFailure = failure != null || mismatch != null; + + recipients.add(new RecipientDeliveryStatus(messageRecord, + recipient, + getStatusFor(info.getStatus(), messageRecord.isPending(), recipientFailure), + info.isUnidentified(), + info.getTimestamp(), + failure, + mismatch)); + } + } + } + + return new MessageDetails(messageRecord, recipients); + } + + private @Nullable NetworkFailure getNetworkFailure(MessageRecord messageRecord, Recipient recipient) { + if (messageRecord.hasNetworkFailures()) { + for (final NetworkFailure failure : messageRecord.getNetworkFailures()) { + if (failure.getRecipientId(context).equals(recipient.getId())) { + return failure; + } + } + } + return null; + } + + private @Nullable IdentityKeyMismatch getKeyMismatchFailure(MessageRecord messageRecord, Recipient recipient) { + if (messageRecord.isIdentityMismatchFailure()) { + for (final IdentityKeyMismatch mismatch : messageRecord.getIdentityKeyMismatches()) { + if (mismatch.getRecipientId(context).equals(recipient.getId())) { + return mismatch; + } + } + } + return null; + } + + private @NonNull RecipientDeliveryStatus.Status getStatusFor(MessageRecord messageRecord) { + if (messageRecord.isRemoteRead()) return RecipientDeliveryStatus.Status.READ; + if (messageRecord.isDelivered()) return RecipientDeliveryStatus.Status.DELIVERED; + if (messageRecord.isSent()) return RecipientDeliveryStatus.Status.SENT; + if (messageRecord.isPending()) return RecipientDeliveryStatus.Status.PENDING; + + return RecipientDeliveryStatus.Status.UNKNOWN; + } + + private @NonNull RecipientDeliveryStatus.Status getStatusFor(int groupStatus, boolean pending, boolean failed) { + if (groupStatus == GroupReceiptDatabase.STATUS_READ) return RecipientDeliveryStatus.Status.READ; + else if (groupStatus == GroupReceiptDatabase.STATUS_DELIVERED) return RecipientDeliveryStatus.Status.DELIVERED; + else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && failed) return RecipientDeliveryStatus.Status.UNKNOWN; + else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && !pending) return RecipientDeliveryStatus.Status.SENT; + else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED) return RecipientDeliveryStatus.Status.PENDING; + else if (groupStatus == GroupReceiptDatabase.STATUS_UNKNOWN) return RecipientDeliveryStatus.Status.UNKNOWN; + throw new AssertionError(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsViewModel.java new file mode 100644 index 0000000000..3c9affc28c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsViewModel.java @@ -0,0 +1,55 @@ +package org.thoughtcrime.securesms.messagedetails; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Transformations; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import org.thoughtcrime.securesms.color.MaterialColor; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; + +import java.util.Objects; + +final class MessageDetailsViewModel extends ViewModel { + + private final LiveData recipient; + private final LiveData messageDetails; + + private MessageDetailsViewModel(RecipientId recipientId, String type, Long messageId) { + recipient = Recipient.live(recipientId).getLiveData(); + + MessageDetailsRepository repository = new MessageDetailsRepository(); + LiveData messageRecord = repository.getMessageRecord(type, messageId); + + messageDetails = Transformations.switchMap(messageRecord, repository::getMessageDetails); + } + + @NonNull LiveData getRecipientColor() { + return Transformations.distinctUntilChanged(Transformations.map(recipient, Recipient::getColor)); + } + + @NonNull LiveData getMessageDetails() { + return messageDetails; + } + + static final class Factory implements ViewModelProvider.Factory { + + private final RecipientId recipientId; + private final String type; + private final Long messageId; + + Factory(RecipientId recipientId, String type, Long messageId) { + this.recipientId = recipientId; + this.type = type; + this.messageId = messageId; + } + + @Override + public @NonNull T create(@NonNull Class modelClass) { + return Objects.requireNonNull(modelClass.cast(new MessageDetailsViewModel(recipientId, type, messageId))); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java new file mode 100644 index 0000000000..9a605586b9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageHeaderViewHolder.java @@ -0,0 +1,210 @@ +package org.thoughtcrime.securesms.messagedetails; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.view.View; +import android.view.ViewStub; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.conversation.ConversationItem; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.sms.MessageSender; +import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.ExpirationUtil; +import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; +import org.whispersystems.libsignal.util.guava.Optional; + +import java.sql.Date; +import java.text.SimpleDateFormat; +import java.util.HashSet; +import java.util.Locale; + +final class MessageHeaderViewHolder extends RecyclerView.ViewHolder { + private final TextView sentDate; + private final TextView receivedDate; + private final TextView expiresIn; + private final TextView transport; + private final View expiresGroup; + private final View receivedGroup; + private final TextView errorText; + private final View resendButton; + private final View messageMetadata; + private final ViewStub updateStub; + private final ViewStub sentStub; + private final ViewStub receivedStub; + + private GlideRequests glideRequests; + private ConversationItem conversationItem; + private ExpiresUpdater expiresUpdater; + + MessageHeaderViewHolder(@NonNull View itemView, GlideRequests glideRequests) { + super(itemView); + this.glideRequests = glideRequests; + + sentDate = itemView.findViewById(R.id.sent_time); + receivedDate = itemView.findViewById(R.id.received_time); + receivedGroup = itemView.findViewById(R.id.received_group); + expiresIn = itemView.findViewById(R.id.expires_in); + expiresGroup = itemView.findViewById(R.id.expires_group); + transport = itemView.findViewById(R.id.transport); + errorText = itemView.findViewById(R.id.error_text); + resendButton = itemView.findViewById(R.id.resend_button); + messageMetadata = itemView.findViewById(R.id.message_metadata); + updateStub = itemView.findViewById(R.id.message_view_update); + sentStub = itemView.findViewById(R.id.message_view_sent_multimedia); + receivedStub = itemView.findViewById(R.id.message_view_received_multimedia); + } + + void bind(MessageRecord messageRecord, boolean running) { + bindMessageView(messageRecord); + bindErrorState(messageRecord); + bindSentReceivedDates(messageRecord); + bindExpirationTime(messageRecord, running); + bindTransport(messageRecord); + } + + void partialBind(MessageRecord messageRecord, boolean running) { + bindExpirationTime(messageRecord, running); + } + + private void bindMessageView(MessageRecord messageRecord) { + if (conversationItem == null) { + if (messageRecord.isGroupAction()) conversationItem = (ConversationItem) updateStub.inflate(); + 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); + } + + private void bindErrorState(MessageRecord messageRecord) { + boolean isPushGroup = messageRecord.getRecipient().isPushGroup(); + boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty(); + boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty(); + + if (isGroupNetworkFailure || isIndividualNetworkFailure) { + errorText.setVisibility(View.VISIBLE); + resendButton.setVisibility(View.VISIBLE); + resendButton.setOnClickListener(unused -> { + resendButton.setOnClickListener(null); + SignalExecutors.BOUNDED.execute(() -> MessageSender.resend(itemView.getContext().getApplicationContext(), messageRecord)); + }); + messageMetadata.setVisibility(View.GONE); + } else if (messageRecord.isFailed()) { + errorText.setVisibility(View.VISIBLE); + resendButton.setVisibility(View.GONE); + resendButton.setOnClickListener(null); + messageMetadata.setVisibility(View.GONE); + } else { + errorText.setVisibility(View.GONE); + resendButton.setVisibility(View.GONE); + resendButton.setOnClickListener(null); + messageMetadata.setVisibility(View.VISIBLE); + } + } + + private void bindSentReceivedDates(MessageRecord messageRecord) { + sentDate.setOnLongClickListener(null); + receivedDate.setOnLongClickListener(null); + + if (messageRecord.isPending() || messageRecord.isFailed()) { + sentDate.setText("-"); + receivedGroup.setVisibility(View.GONE); + } else { + Locale dateLocale = Locale.getDefault(); + SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(itemView.getContext(), dateLocale); + sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent()))); + sentDate.setOnLongClickListener(v -> { + copyToClipboard(String.valueOf(messageRecord.getDateSent())); + return true; + }); + + if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) { + receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived()))); + receivedDate.setOnLongClickListener(v -> { + copyToClipboard(String.valueOf(messageRecord.getDateReceived())); + return true; + }); + receivedGroup.setVisibility(View.VISIBLE); + } else { + receivedGroup.setVisibility(View.GONE); + } + } + } + + private void bindExpirationTime(final MessageRecord messageRecord, boolean running) { + if (expiresUpdater != null) { + expiresUpdater.stop(); + expiresUpdater = null; + } + + if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) { + expiresGroup.setVisibility(View.GONE); + return; + } + + expiresGroup.setVisibility(View.VISIBLE); + if (running) { + expiresUpdater = new ExpiresUpdater(messageRecord); + Util.runOnMain(expiresUpdater); + } + } + + private void bindTransport(MessageRecord messageRecord) { + final String transportText; + if (messageRecord.isOutgoing() && messageRecord.isFailed()) { + transportText = "-"; + } else if (messageRecord.isPending()) { + transportText = itemView.getContext().getString(R.string.ConversationFragment_pending); + } else if (messageRecord.isPush()) { + transportText = itemView.getContext().getString(R.string.ConversationFragment_push); + } else if (messageRecord.isMms()) { + transportText = itemView.getContext().getString(R.string.ConversationFragment_mms); + } else { + transportText = itemView.getContext().getString(R.string.ConversationFragment_sms); + } + + transport.setText(transportText); + } + + private void copyToClipboard(String text) { + ((ClipboardManager) itemView.getContext().getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text)); + } + + private class ExpiresUpdater implements Runnable { + + private final long expireStartedTimestamp; + private final long expiresInTimestamp; + private boolean running; + + ExpiresUpdater(MessageRecord messageRecord) { + expireStartedTimestamp = messageRecord.getExpireStarted(); + expiresInTimestamp = messageRecord.getExpiresIn(); + running = true; + } + + @Override + public void run() { + long elapsed = System.currentTimeMillis() - expireStartedTimestamp; + long remaining = expiresInTimestamp - elapsed; + int expirationTime = Math.max((int) (remaining / 1000), 1); + String duration = ExpirationUtil.getExpirationDisplayValue(itemView.getContext(), expirationTime); + + expiresIn.setText(duration); + + if (running && expirationTime > 1) { + Util.runOnMainDelayed(this, 500); + } + } + + void stop() { + running = false; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageRecordLiveData.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageRecordLiveData.java new file mode 100644 index 0000000000..c838c2426b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageRecordLiveData.java @@ -0,0 +1,107 @@ +package org.thoughtcrime.securesms.messagedetails; + +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; + +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.lifecycle.LiveData; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.MmsSmsDatabase; +import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; + +final class MessageRecordLiveData extends LiveData { + + private final Context context; + private final String type; + private final Long messageId; + private final ContentObserver obs; + + private @Nullable Cursor cursor; + + MessageRecordLiveData(Context context, String type, Long messageId) { + this.context = context; + this.type = type; + this.messageId = messageId; + + obs = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + SignalExecutors.BOUNDED.execute(() -> resetCursor()); + } + }; + } + + @Override + protected void onActive() { + retrieveMessageRecord(); + } + + @Override + protected void onInactive() { + SignalExecutors.BOUNDED.execute(this::destroyCursor); + } + + private void retrieveMessageRecord() { + SignalExecutors.BOUNDED.execute(this::retrieveMessageRecordActual); + } + + @WorkerThread + private synchronized void destroyCursor() { + if (cursor != null) { + cursor.unregisterContentObserver(obs); + cursor.close(); + cursor = null; + } + } + + @WorkerThread + private synchronized void resetCursor() { + destroyCursor(); + retrieveMessageRecord(); + } + + @WorkerThread + private synchronized void retrieveMessageRecordActual() { + if (cursor != null) { + return; + } + switch (type) { + case MmsSmsDatabase.SMS_TRANSPORT: + handleSms(); + break; + case MmsSmsDatabase.MMS_TRANSPORT: + handleMms(); + break; + default: + throw new AssertionError("no valid message type specified"); + } + } + + @WorkerThread + private synchronized void handleSms() { + final SmsDatabase db = DatabaseFactory.getSmsDatabase(context); + final Cursor cursor = db.getVerboseMessageCursor(messageId); + final MessageRecord record = db.readerFor(cursor).getNext(); + + postValue(record); + cursor.registerContentObserver(obs); + this.cursor = cursor; + } + + @WorkerThread + private synchronized void handleMms() { + final MmsDatabase db = DatabaseFactory.getMmsDatabase(context); + final Cursor cursor = db.getVerboseMessage(messageId); + final MessageRecord record = db.readerFor(cursor).getNext(); + + postValue(record); + cursor.registerContentObserver(obs); + this.cursor = cursor; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java new file mode 100644 index 0000000000..49a8d493f0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientDeliveryStatus.java @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms.messagedetails; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; +import org.thoughtcrime.securesms.database.documents.NetworkFailure; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.recipients.Recipient; + +final class RecipientDeliveryStatus { + + enum Status { + UNKNOWN, PENDING, SENT, DELIVERED, READ + } + + private final MessageRecord messageRecord; + private final Recipient recipient; + private final Status deliveryStatus; + private final boolean isUnidentified; + private final long timestamp; + private final NetworkFailure networkFailure; + private final IdentityKeyMismatch keyMismatchFailure; + + RecipientDeliveryStatus(@NonNull MessageRecord messageRecord, @NonNull Recipient recipient, @NonNull Status deliveryStatus, boolean isUnidentified, long timestamp, @Nullable NetworkFailure networkFailure, @Nullable IdentityKeyMismatch keyMismatchFailure) { + this.messageRecord = messageRecord; + this.recipient = recipient; + this.deliveryStatus = deliveryStatus; + this.isUnidentified = isUnidentified; + this.timestamp = timestamp; + this.networkFailure = networkFailure; + this.keyMismatchFailure = keyMismatchFailure; + } + + @NonNull MessageRecord getMessageRecord() { + return messageRecord; + } + + @NonNull Status getDeliveryStatus() { + return deliveryStatus; + } + + boolean isUnidentified() { + return isUnidentified; + } + + long getTimestamp() { + return timestamp; + } + + @NonNull Recipient getRecipient() { + return recipient; + } + + @Nullable NetworkFailure getNetworkFailure() { + return networkFailure; + } + + @Nullable IdentityKeyMismatch getKeyMismatchFailure() { + return keyMismatchFailure; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientHeader.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientHeader.java new file mode 100644 index 0000000000..1459fd6e64 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientHeader.java @@ -0,0 +1,64 @@ +package org.thoughtcrime.securesms.messagedetails; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import org.thoughtcrime.securesms.R; + +final class RecipientHeader { + + private final int headerOrder; + private final int headerText; + private final HeaderStatus status; + + private RecipientHeader(int headerOrder, @StringRes int headerText, @NonNull HeaderStatus headerStatus) { + this.headerOrder = headerOrder; + this.headerText = headerText; + this.status = headerStatus; + } + + int getHeaderOrder() { + return headerOrder; + } + + @StringRes int getHeader() { + return headerText; + } + + @NonNull HeaderStatus getHeaderStatus() { + return status; + } + + static RecipientHeader pending(int idx) { + return new RecipientHeader(idx, R.string.message_details_recipient_header__pending_send, HeaderStatus.PENDING); + } + + static RecipientHeader sentTo(int idx) { + return new RecipientHeader(idx, R.string.message_details_recipient_header__sent_to, HeaderStatus.SENT_TO); + } + + static RecipientHeader sentFrom(int idx) { + return new RecipientHeader(idx, R.string.message_details_recipient_header__sent_from, HeaderStatus.SENT_FROM); + } + + static RecipientHeader delivered(int idx) { + return new RecipientHeader(idx, R.string.message_details_recipient_header__delivered_to, HeaderStatus.DELIVERED); + } + + static RecipientHeader read(int idx) { + return new RecipientHeader(idx, R.string.message_details_recipient_header__read_by, HeaderStatus.READ); + } + + static RecipientHeader notSent(int idx) { + return new RecipientHeader(idx, R.string.message_details_recipient_header__not_sent, HeaderStatus.NOT_SENT); + } + + enum HeaderStatus { + PENDING, + SENT_TO, + SENT_FROM, + DELIVERED, + READ, + NOT_SENT + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientHeaderViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientHeaderViewHolder.java new file mode 100644 index 0000000000..ec46fabd44 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientHeaderViewHolder.java @@ -0,0 +1,42 @@ +package org.thoughtcrime.securesms.messagedetails; + +import android.view.View; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.DeliveryStatusView; + +final class RecipientHeaderViewHolder extends RecyclerView.ViewHolder { + private final TextView header; + private final DeliveryStatusView deliveryStatus; + + RecipientHeaderViewHolder(View itemView) { + super(itemView); + + header = itemView.findViewById(R.id.recipient_header_text); + deliveryStatus = itemView.findViewById(R.id.recipient_header_delivery_status); + } + + void bind(RecipientHeader recipientHeader) { + header.setText(recipientHeader.getHeader()); + switch (recipientHeader.getHeaderStatus()) { + case PENDING: + deliveryStatus.setPending(); + break; + case SENT_TO: + deliveryStatus.setSent(); + break; + case DELIVERED: + deliveryStatus.setDelivered(); + break; + case READ: + deliveryStatus.setRead(); + break; + default: + deliveryStatus.setNone(); + break; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientViewHolder.java new file mode 100644 index 0000000000..ac0b6dd343 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/RecipientViewHolder.java @@ -0,0 +1,68 @@ +package org.thoughtcrime.securesms.messagedetails; + +import android.view.View; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.ConfirmIdentityDialog; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.AvatarImageView; +import org.thoughtcrime.securesms.components.FromTextView; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.TextSecurePreferences; + +import java.sql.Date; +import java.text.SimpleDateFormat; +import java.util.Locale; + +final class RecipientViewHolder extends RecyclerView.ViewHolder { + private final AvatarImageView avatar; + private final FromTextView fromView; + private final TextView timestamp; + private final TextView error; + private final View conflictButton; + private final View unidentifiedDeliveryIcon; + + RecipientViewHolder(View itemView) { + super(itemView); + + fromView = itemView.findViewById(R.id.recipient_name); + avatar = itemView.findViewById(R.id.recipient_avatar); + timestamp = itemView.findViewById(R.id.recipient_timestamp); + error = itemView.findViewById(R.id.error_description); + conflictButton = itemView.findViewById(R.id.conflict_button); + unidentifiedDeliveryIcon = itemView.findViewById(R.id.ud_indicator); + } + + void bind(RecipientDeliveryStatus data) { + unidentifiedDeliveryIcon.setVisibility(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(itemView.getContext()) && data.isUnidentified() ? View.VISIBLE : View.GONE); + fromView.setText(data.getRecipient()); + avatar.setRecipient(data.getRecipient()); + + if (data.getKeyMismatchFailure() != null) { + timestamp.setVisibility(View.GONE); + error.setVisibility(View.VISIBLE); + conflictButton.setVisibility(View.VISIBLE); + error.setText(itemView.getContext().getString(R.string.MessageDetailsRecipient_new_safety_number)); + conflictButton.setOnClickListener(unused -> new ConfirmIdentityDialog(itemView.getContext(), data.getMessageRecord(), data.getKeyMismatchFailure()).show()); + } else if ((data.getNetworkFailure() != null && !data.getMessageRecord().isPending()) || (!data.getMessageRecord().getRecipient().isPushGroup() && data.getMessageRecord().isFailed())) { + timestamp.setVisibility(View.GONE); + error.setVisibility(View.VISIBLE); + conflictButton.setVisibility(View.GONE); + error.setText(itemView.getContext().getString(R.string.MessageDetailsRecipient_failed_to_send)); + } else { + timestamp.setVisibility(View.VISIBLE); + error.setVisibility(View.GONE); + conflictButton.setVisibility(View.GONE); + + if (data.getTimestamp() > 0) { + Locale dateLocale = Locale.getDefault(); + timestamp.setText(DateUtils.getTimeString(itemView.getContext(), dateLocale, data.getTimestamp())); + } else { + timestamp.setText(""); + } + } + } +} diff --git a/app/src/main/res/layout/message_details_2_activity.xml b/app/src/main/res/layout/message_details_2_activity.xml new file mode 100644 index 0000000000..05004eb824 --- /dev/null +++ b/app/src/main/res/layout/message_details_2_activity.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/message_details_2_header.xml b/app/src/main/res/layout/message_details_2_header.xml new file mode 100644 index 0000000000..1e8fae2b1a --- /dev/null +++ b/app/src/main/res/layout/message_details_2_header.xml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + +