mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 09:18:49 +00:00
Refresh Message Details screen.
This commit is contained in:
parent
dfb5562142
commit
d9641128a8
@ -249,6 +249,12 @@
|
|||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
|
<activity android:name=".messagedetails.MessageDetailsActivity"
|
||||||
|
android:label="@string/AndroidManifest__message_details"
|
||||||
|
android:windowSoftInputMode="stateHidden"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".GroupCreateActivity"
|
<activity android:name=".GroupCreateActivity"
|
||||||
android:windowSoftInputMode="stateVisible"
|
android:windowSoftInputMode="stateVisible"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
@ -690,7 +690,7 @@ public class ConversationFragment extends Fragment {
|
|||||||
|
|
||||||
|
|
||||||
private void handleDisplayDetails(MessageRecord message) {
|
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.MESSAGE_ID_EXTRA, message.getId());
|
||||||
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, threadId);
|
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, threadId);
|
||||||
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
||||||
|
@ -1377,7 +1377,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||||||
if (!shouldInterceptClicks(messageRecord) && parent != null) {
|
if (!shouldInterceptClicks(messageRecord) && parent != null) {
|
||||||
parent.onClick(v);
|
parent.onClick(v);
|
||||||
} else if (messageRecord.isFailed()) {
|
} 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.MESSAGE_ID_EXTRA, messageRecord.getId());
|
||||||
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, messageRecord.getThreadId());
|
intent.putExtra(MessageDetailsActivity.THREAD_ID_EXTRA, messageRecord.getThreadId());
|
||||||
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
||||||
|
@ -78,6 +78,10 @@ public abstract class DisplayRecord {
|
|||||||
!MmsSmsColumns.Types.isIdentityDefault(type);
|
!MmsSmsColumns.Types.isIdentityDefault(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSent() {
|
||||||
|
return MmsSmsColumns.Types.isSentType(type);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isOutgoing() {
|
public boolean isOutgoing() {
|
||||||
return MmsSmsColumns.Types.isOutgoingMessageType(type);
|
return MmsSmsColumns.Types.isOutgoingMessageType(type);
|
||||||
}
|
}
|
||||||
|
@ -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<RecipientDeliveryStatus> HAS_DISPLAY_NAME = (r1, r2) -> Boolean.compare(r2.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication()), r1.getRecipient().hasAUserSetDisplayName(ApplicationDependencies.getApplication()));
|
||||||
|
private static final Comparator<RecipientDeliveryStatus> ALPHABETICAL = (r1, r2) -> r1.getRecipient().getDisplayName(ApplicationDependencies.getApplication()).compareToIgnoreCase(r2.getRecipient().getDisplayName(ApplicationDependencies.getApplication()));
|
||||||
|
private static final Comparator<RecipientDeliveryStatus> RECIPIENT_COMPARATOR = ComparatorCompat.chain(HAS_DISPLAY_NAME).thenComparing(ALPHABETICAL);
|
||||||
|
|
||||||
|
private final MessageRecord messageRecord;
|
||||||
|
|
||||||
|
private final Collection<RecipientDeliveryStatus> pending;
|
||||||
|
private final Collection<RecipientDeliveryStatus> sent;
|
||||||
|
private final Collection<RecipientDeliveryStatus> delivered;
|
||||||
|
private final Collection<RecipientDeliveryStatus> read;
|
||||||
|
private final Collection<RecipientDeliveryStatus> notSent;
|
||||||
|
|
||||||
|
MessageDetails(MessageRecord messageRecord, List<RecipientDeliveryStatus> 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<RecipientDeliveryStatus> getPending() {
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Collection<RecipientDeliveryStatus> getSent() {
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Collection<RecipientDeliveryStatus> getDelivered() {
|
||||||
|
return delivered;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Collection<RecipientDeliveryStatus> getRead() {
|
||||||
|
return read;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Collection<RecipientDeliveryStatus> getNotSent() {
|
||||||
|
return notSent;
|
||||||
|
}
|
||||||
|
}
|
@ -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<MessageDetailsViewState<?>> convertToRows(MessageDetails details) {
|
||||||
|
List<MessageDetailsViewState<?>> 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<MessageDetailsViewState<?>> list, RecipientHeader header, Collection<RecipientDeliveryStatus> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<MessageDetailsAdapter.MessageDetailsViewState<?>, 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<Object> 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<MessageDetailsViewState<?>> {
|
||||||
|
@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<T> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<MessageRecord> getMessageRecord(String type, Long messageId) {
|
||||||
|
return new MessageRecordLiveData(context, type, messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull LiveData<MessageDetails> getMessageDetails(@Nullable MessageRecord messageRecord) {
|
||||||
|
final MutableLiveData<MessageDetails> 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<RecipientDeliveryStatus> 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<GroupReceiptDatabase.GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
|
||||||
|
|
||||||
|
if (receiptInfoList.isEmpty()) {
|
||||||
|
List<Recipient> 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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> recipient;
|
||||||
|
private final LiveData<MessageDetails> messageDetails;
|
||||||
|
|
||||||
|
private MessageDetailsViewModel(RecipientId recipientId, String type, Long messageId) {
|
||||||
|
recipient = Recipient.live(recipientId).getLiveData();
|
||||||
|
|
||||||
|
MessageDetailsRepository repository = new MessageDetailsRepository();
|
||||||
|
LiveData<MessageRecord> messageRecord = repository.getMessageRecord(type, messageId);
|
||||||
|
|
||||||
|
messageDetails = Transformations.switchMap(messageRecord, repository::getMessageDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull LiveData<MaterialColor> getRecipientColor() {
|
||||||
|
return Transformations.distinctUntilChanged(Transformations.map(recipient, Recipient::getColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull LiveData<MessageDetails> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||||
|
return Objects.requireNonNull(modelClass.cast(new MessageDetailsViewModel(recipientId, type, messageId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<MessageRecord> {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
app/src/main/res/layout/message_details_2_activity.xml
Normal file
8
app/src/main/res/layout/message_details_2_activity.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/message_details_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?pref_divider"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
189
app/src/main/res/layout/message_details_2_header.xml
Normal file
189
app/src/main/res/layout/message_details_2_header.xml
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/group_media_card"
|
||||||
|
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingTop="24dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="24dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/message_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/message_view_update"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout="@layout/conversation_item_update"/>
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/message_view_sent_multimedia"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout="@layout/conversation_item_sent_multimedia"/>
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/message_view_received_multimedia"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout="@layout/conversation_item_received_multimedia"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/error_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:padding="5dp"
|
||||||
|
tools:visibility="visible"
|
||||||
|
android:text="@string/message_details_header__issues_need_your_attention"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
app:drawableStartCompat="@drawable/ic_info_outline_message_details_24"
|
||||||
|
android:gravity="center_vertical" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/resend_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="38sp"
|
||||||
|
style="@style/InfoButton"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingEnd="10dp"
|
||||||
|
android:paddingTop="5dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:drawableStart="@drawable/ic_refresh_white_18dp"
|
||||||
|
android:text="@string/message_recipients_list_item__resend"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/message_metadata"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sent_time_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/message_details_header__sent"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sent_time"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/message_details_table_row_pad"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/sent_time_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/label_barrier"
|
||||||
|
tools:text="Jan 18, 2015, 12:29:37 AM GMT-08:00" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/received_time_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/message_details_header__received"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/sent_time_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/received_time"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/message_details_table_row_pad"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/received_time_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/label_barrier"
|
||||||
|
tools:text="Jan 18, 2015, 12:31:15 AM GMT-08:00" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/received_group"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:constraint_referenced_ids="received_time_label,received_time" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/expires_in_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/message_details_header__disappears"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/received_time_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/expires_in"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/message_details_table_row_pad"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/expires_in_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/label_barrier"
|
||||||
|
tools:text="1 week" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/expires_group"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:constraint_referenced_ids="expires_in_label,expires_in" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/transport_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/message_details_header__via"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/expires_in_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/transport"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/message_details_table_row_pad"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/transport_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/label_barrier"
|
||||||
|
tools:text="Push (TextSecure)" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/label_barrier"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="end"
|
||||||
|
app:constraint_referenced_ids="sent_time_label,received_time_label,expires_in_label" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
97
app/src/main/res/layout/message_details_2_recipient.xml
Normal file
97
app/src/main/res/layout/message_details_2_recipient.xml
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/group_media_card"
|
||||||
|
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
|
android:id="@+id/recipient_avatar"
|
||||||
|
android:foreground="@drawable/contact_photo_background"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:cropToPadding="true"
|
||||||
|
tools:src="@drawable/ic_contact_picture"
|
||||||
|
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/recipient_name"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.FromTextView
|
||||||
|
android:id="@+id/recipient_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="?attr/conversation_list_item_contact_color"
|
||||||
|
android:singleLine="true"
|
||||||
|
tools:text="Jules Bonnot"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/recipient_timestamp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/error_description"/>
|
||||||
|
|
||||||
|
<TextView android:id="@+id/error_description"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="#FFF44336"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:text="New identity"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/recipient_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/recipient_name"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/recipient_timestamp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="05/27/20 1:32 PM"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/recipient_name"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/conflict_button"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/conflict_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
style="@style/ErrorButton"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
android:drawableStart="@drawable/ic_error_white_18dp"
|
||||||
|
android:text="@string/message_recipients_list_item__view"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/recipient_timestamp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/ud_indicator"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<ImageView android:id="@+id/ud_indicator"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:src="@drawable/ic_unidentified_delivery"
|
||||||
|
android:tint="?attr/conversation_item_sent_text_secondary_color"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/conflict_button"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/group_media_card"
|
||||||
|
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="4dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/recipient_header_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
tools:text="Read by" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.DeliveryStatusView
|
||||||
|
android:id="@+id/recipient_header_delivery_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:iconColor="?attr/conversation_item_sent_text_secondary_color"
|
||||||
|
android:layout_gravity="end|center_vertical" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
@ -85,7 +85,7 @@
|
|||||||
<dimen name="quote_thumb_size">60dp</dimen>
|
<dimen name="quote_thumb_size">60dp</dimen>
|
||||||
|
|
||||||
<integer name="media_overview_cols">3</integer>
|
<integer name="media_overview_cols">3</integer>
|
||||||
<dimen name="message_details_table_row_pad">10dp</dimen>
|
<dimen name="message_details_table_row_pad">8dp</dimen>
|
||||||
|
|
||||||
<dimen name="sticker_page_item_padding">8dp</dimen>
|
<dimen name="sticker_page_item_padding">8dp</dimen>
|
||||||
<dimen name="sticker_page_item_divisor">88dp</dimen>
|
<dimen name="sticker_page_item_divisor">88dp</dimen>
|
||||||
|
@ -1686,6 +1686,14 @@
|
|||||||
<string name="message_details_header__from">From:</string>
|
<string name="message_details_header__from">From:</string>
|
||||||
<string name="message_details_header__with">With:</string>
|
<string name="message_details_header__with">With:</string>
|
||||||
|
|
||||||
|
<!-- message_details_recipient_header -->
|
||||||
|
<string name="message_details_recipient_header__pending_send">Pending</string>
|
||||||
|
<string name="message_details_recipient_header__sent_to">Sent to</string>
|
||||||
|
<string name="message_details_recipient_header__sent_from">Sent from</string>
|
||||||
|
<string name="message_details_recipient_header__delivered_to">Delivered to</string>
|
||||||
|
<string name="message_details_recipient_header__read_by">Read by</string>
|
||||||
|
<string name="message_details_recipient_header__not_sent">Not sent</string>
|
||||||
|
|
||||||
<!-- AndroidManifest.xml -->
|
<!-- AndroidManifest.xml -->
|
||||||
<string name="AndroidManifest__create_passphrase">Create passphrase</string>
|
<string name="AndroidManifest__create_passphrase">Create passphrase</string>
|
||||||
<string name="AndroidManifest__select_contacts">Select contacts</string>
|
<string name="AndroidManifest__select_contacts">Select contacts</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user