diff --git a/app/src/main/java/im/conversations/android/database/dao/MessageDao.java b/app/src/main/java/im/conversations/android/database/dao/MessageDao.java index 92055ecde..670890022 100644 --- a/app/src/main/java/im/conversations/android/database/dao/MessageDao.java +++ b/app/src/main/java/im/conversations/android/database/dao/MessageDao.java @@ -426,7 +426,7 @@ public abstract class MessageDao { @Transaction @Query( "SELECT c.accountId,m.id as id,type as" - + " chatType,sentAt,outgoing,toBare,toResource,fromBare,fromResource,senderIdentity" + + " chatType,sentAt,outgoing,toBare,toResource,fromBare,fromResource,acknowledged,senderIdentity" + " as sender,(SELECT name FROM roster WHERE roster.accountId=c.accountId AND" + " roster.address=m.senderIdentity) as senderRosterName,(SELECT nick FROM nick" + " WHERE nick.accountId=c.accountId AND nick.address=m.senderIdentity) as" @@ -456,7 +456,7 @@ public abstract class MessageDao { @Transaction @Query( "SELECT c.accountId,m.id as id,type as" - + " chatType,sentAt,outgoing,toBare,toResource,fromBare,fromResource,senderIdentity" + + " chatType,sentAt,outgoing,toBare,toResource,fromBare,fromResource,acknowledged,senderIdentity" + " as sender,(SELECT name FROM roster WHERE roster.accountId=c.accountId AND" + " roster.address=m.senderIdentity) as senderRosterName,(SELECT nick FROM nick" + " WHERE nick.accountId=c.accountId AND nick.address=m.senderIdentity) as" diff --git a/app/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java b/app/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java index 854845276..81ae1d6e5 100644 --- a/app/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java +++ b/app/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java @@ -3,6 +3,7 @@ package im.conversations.android.database.model; import androidx.room.Relation; import com.google.common.base.Objects; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; @@ -53,6 +54,7 @@ public class MessageWithContentReactions implements IndividualName, KnownSender public Modification modification; public long version; + public boolean acknowledged; public Long inReplyToMessageEntityId; public Encryption encryption; public IdentityKey identityKey; @@ -176,6 +178,27 @@ public class MessageWithContentReactions implements IndividualName, KnownSender return new EncryptionTuple(this.encryption, this.trust); } + public State getState() { + if (outgoing) { + final var status = + Collections2.transform( + Collections2.filter( + this.states, s -> s != null && s.fromBare.equals(toBare)), + s -> s.type); + if (status.contains(StateType.DISPLAYED)) { + return State.READ; + } else if (status.contains(StateType.DELIVERED)) { + return State.DELIVERED; + } else if (acknowledged) { + return State.DELIVERED_TO_SERVER; + } else { + return State.NONE; + } + } else { + return State.NONE; + } + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -251,4 +274,12 @@ public class MessageWithContentReactions implements IndividualName, KnownSender this.trust = trust; } } + + public enum State { + NONE, + DELIVERED_TO_SERVER, + DELIVERED, + READ, + ERROR + } } diff --git a/app/src/main/java/im/conversations/android/ui/BindingAdapters.java b/app/src/main/java/im/conversations/android/ui/BindingAdapters.java index 20e75bd74..f059dc696 100644 --- a/app/src/main/java/im/conversations/android/ui/BindingAdapters.java +++ b/app/src/main/java/im/conversations/android/ui/BindingAdapters.java @@ -6,9 +6,11 @@ import android.view.KeyEvent; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.databinding.BindingAdapter; import androidx.lifecycle.LiveData; +import com.google.android.material.color.MaterialColors; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import com.google.common.base.Supplier; @@ -144,4 +146,37 @@ public class BindingAdapters { throw new IllegalArgumentException(String.format("Unknown encryption %s", encryption)); } } + + @BindingAdapter("state") + public static void setState( + final ImageView imageView, final MessageWithContentReactions.State state) { + if (state == null || state == MessageWithContentReactions.State.NONE) { + imageView.setVisibility(View.INVISIBLE); + } else { + @DrawableRes + final var drawableRes = + switch (state) { + case DELIVERED_TO_SERVER -> R.drawable.ic_check_24dp; + case DELIVERED, READ -> R.drawable.ic_done_all_24dp; + case ERROR -> R.drawable.ic_error_outline_24dp; + default -> throw new IllegalArgumentException( + String.format("State %s not implemented", state)); + }; + imageView.setImageResource(drawableRes); + if (state == MessageWithContentReactions.State.READ) { + // the two color candidates are colorTertiary and colorPrimary + // depending on the exact color scheme one might 'pop' more than the other + imageView.setImageTintList( + MaterialColors.getColorStateListOrNull( + imageView.getContext(), + com.google.android.material.R.attr.colorPrimary)); + } else { + imageView.setImageTintList( + MaterialColors.getColorStateListOrNull( + imageView.getContext(), + com.google.android.material.R.attr.colorOnSurface)); + } + imageView.setVisibility(View.VISIBLE); + } + } } diff --git a/app/src/main/java/im/conversations/android/ui/adapter/MessageAdapter.java b/app/src/main/java/im/conversations/android/ui/adapter/MessageAdapter.java index a450cb75b..84298dc27 100644 --- a/app/src/main/java/im/conversations/android/ui/adapter/MessageAdapter.java +++ b/app/src/main/java/im/conversations/android/ui/adapter/MessageAdapter.java @@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView; import im.conversations.android.R; import im.conversations.android.database.model.MessageWithContentReactions; import im.conversations.android.databinding.ItemMessageReceivedBinding; +import im.conversations.android.databinding.ItemMessageSentBinding; import im.conversations.android.ui.AvatarFetcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,6 +20,9 @@ public class MessageAdapter extends PagingDataAdapter< MessageWithContentReactions, MessageAdapter.AbstractMessageViewHolder> { + private static final int VIEW_TYPE_RECEIVED = 0; + private static final int VIEW_TYPE_SENT = 1; + private static final Logger LOGGER = LoggerFactory.getLogger(MessageAdapter.class); public MessageAdapter( @@ -26,15 +30,29 @@ public class MessageAdapter super(diffCallback); } + @Override + public int getItemViewType(final int position) { + final var message = getItem(position); + if (message != null && message.outgoing) { + return VIEW_TYPE_SENT; + } else { + return VIEW_TYPE_RECEIVED; + } + } + @NonNull @Override public AbstractMessageViewHolder onCreateViewHolder( final @NonNull ViewGroup parent, final int viewType) { final var layoutInflater = LayoutInflater.from(parent.getContext()); - if (viewType == 0) { + if (viewType == VIEW_TYPE_RECEIVED) { return new MessageReceivedViewHolder( DataBindingUtil.inflate( layoutInflater, R.layout.item_message_received, parent, false)); + } else if (viewType == VIEW_TYPE_SENT) { + return new MessageSentViewHolder( + DataBindingUtil.inflate( + layoutInflater, R.layout.item_message_sent, parent, false)); } throw new IllegalArgumentException(String.format("viewType %d not implemented", viewType)); } @@ -83,4 +101,19 @@ public class MessageAdapter this.binding.setMessage(message); } } + + public static class MessageSentViewHolder extends AbstractMessageViewHolder { + + private final ItemMessageSentBinding binding; + + public MessageSentViewHolder(@NonNull ItemMessageSentBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + @Override + protected void setMessage(MessageWithContentReactions message) { + this.binding.setMessage(message); + } + } } diff --git a/app/src/main/res/drawable/ic_done_all_24dp.xml b/app/src/main/res/drawable/ic_done_all_24dp.xml new file mode 100644 index 000000000..a363e6616 --- /dev/null +++ b/app/src/main/res/drawable/ic_done_all_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_error_outline_24dp.xml b/app/src/main/res/drawable/ic_error_outline_24dp.xml new file mode 100644 index 000000000..d712eaad1 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_outline_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/item_message_sent.xml b/app/src/main/res/layout/item_message_sent.xml new file mode 100644 index 000000000..27dad1898 --- /dev/null +++ b/app/src/main/res/layout/item_message_sent.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file