From 4fae8d4e11052b56efa868aed3d15ac49340af76 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 24 Mar 2023 12:21:19 +0100 Subject: [PATCH] show avatars in chat --- .../android/database/dao/ChatDao.java | 21 ++--- .../android/database/dao/MessageDao.java | 66 +++++++++----- .../android/database/model/ChatInfo.java | 49 +++++------ .../database/model/IndividualName.java | 35 ++++++++ .../model/MessageWithContentReactions.java | 85 +++++++++++++++++-- .../android/repository/ChatRepository.java | 1 - .../android/ui/BindingAdapters.java | 14 ++- .../android/ui/RecyclerViewScroller.java | 51 +++++++---- .../android/ui/adapter/MessageAdapter.java | 33 ++++--- .../android/ui/adapter/MessageComparator.java | 9 +- .../ui/fragment/main/ChatFragment.java | 27 ++---- .../android/ui/model/ChatViewModel.java | 26 +++--- .../android/xmpp/manager/AvatarManager.java | 3 +- 13 files changed, 282 insertions(+), 138 deletions(-) create mode 100644 app/src/main/java/im/conversations/android/database/model/IndividualName.java diff --git a/app/src/main/java/im/conversations/android/database/dao/ChatDao.java b/app/src/main/java/im/conversations/android/database/dao/ChatDao.java index 3b7129eb1..93ffd14ac 100644 --- a/app/src/main/java/im/conversations/android/database/dao/ChatDao.java +++ b/app/src/main/java/im/conversations/android/database/dao/ChatDao.java @@ -212,12 +212,12 @@ public abstract class ChatDao { + " accountId=c.accountId AND address=c.address AND vCardPhoto NOT NULL LIMIT 1)" + " ELSE NULL END) as vCardPhoto,(SELECT thumb_id FROM avatar WHERE" + " avatar.accountId=c.accountId AND avatar.address=c.address) as avatar,(CASE WHEN" - + " c.type='MUC' THEN (SELECT count(distinct(df.feature)) == 2 FROM disco_item di" - + " JOIN disco_feature df ON di.discoId = df.discoId WHERE di.address=c.address AND" - + " df.feature IN('muc_membersonly','muc_nonanonymous')) ELSE 0 END) as" - + " membersOnlyNonAnonymous FROM CHAT c LEFT JOIN message m ON (m.id = (SELECT id" - + " FROM message WHERE chatId=c.id ORDER by receivedAt DESC LIMIT 1)) WHERE" - + " (:accountId IS NULL OR c.accountId=:accountId) AND (:groupId IS NULL OR" + + " c.type IN ('MUC','MUC_PM') THEN (SELECT count(distinct(df.feature)) == 2 FROM" + + " disco_item di JOIN disco_feature df ON di.discoId = df.discoId WHERE" + + " di.address=c.address AND df.feature IN('muc_membersonly','muc_nonanonymous'))" + + " ELSE 0 END) as membersOnlyNonAnonymous FROM CHAT c LEFT JOIN message m ON (m.id" + + " = (SELECT id FROM message WHERE chatId=c.id ORDER by receivedAt DESC LIMIT 1))" + + " WHERE (:accountId IS NULL OR c.accountId=:accountId) AND (:groupId IS NULL OR" + " (c.address IN(SELECT roster.address FROM roster JOIN roster_group ON" + " roster.id=roster_group.rosterItemId WHERE roster.accountId=c.accountId AND" + " roster_group.groupId=:groupId) OR c.address IN(SELECT address FROM bookmark" @@ -236,10 +236,11 @@ public abstract class ChatDao { + " disco_item.accountId=c.accountId AND disco_item.address=c.address LIMIT 1) as" + " discoIdentityName,(SELECT name FROM bookmark WHERE" + " bookmark.accountId=c.accountId AND bookmark.address=c.address) as" - + " bookmarkName,(CASE WHEN c.type='MUC' THEN (SELECT count(distinct(df.feature))" - + " == 2 FROM disco_item di JOIN disco_feature df ON di.discoId = df.discoId WHERE" - + " di.address=c.address AND df.feature IN('muc_membersonly','muc_nonanonymous'))" - + " ELSE 0 END) as membersOnlyNonAnonymous FROM chat c WHERE c.id=:chatId") + + " bookmarkName,(CASE WHEN c.type IN ('MUC','MUC_PM') THEN (SELECT" + + " count(distinct(df.feature)) == 2 FROM disco_item di JOIN disco_feature df ON" + + " di.discoId = df.discoId WHERE di.address=c.address AND df.feature" + + " IN('muc_membersonly','muc_nonanonymous')) ELSE 0 END) as" + + " membersOnlyNonAnonymous FROM chat c WHERE c.id=:chatId") public abstract LiveData getChatInfo(final long chatId); public PagingSource getChatOverview(final ChatFilter chatFilter) { 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 0b1e36c78..6817033ae 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 @@ -425,29 +425,57 @@ public abstract class MessageDao { @VisibleForTesting @Transaction @Query( - "SELECT message.id as" - + " id,sentAt,outgoing,toBare,toResource,fromBare,fromResource,modification,latestVersion" - + " as version,inReplyToMessageEntityId,encryption,message_version.identityKey,trust" - + " FROM chat JOIN message on message.chatId=chat.id JOIN message_version ON" - + " message.latestVersion=message_version.id LEFT JOIN axolotl_identity ON" - + " chat.accountId=axolotl_identity.accountId AND" - + " message.senderIdentity=axolotl_identity.address AND" - + " message_version.identityKey=axolotl_identity.identityKey WHERE chat.id=:chatId" - + " AND latestVersion IS NOT NULL ORDER BY message.receivedAt") + "SELECT c.accountId,m.id as id,type as" + + " chatType,sentAt,outgoing,toBare,toResource,fromBare,fromResource,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" + + " senderNick,(SELECT vCardPhoto FROM presence WHERE accountId=c.accountId AND" + + " address=m.senderIdentity AND vCardPhoto NOT NULL LIMIT 1) as" + + " senderVcardPhoto,(SELECT thumb_id FROM avatar WHERE" + + " avatar.accountId=c.accountId AND avatar.address=m.senderIdentity) as" + + " senderAvatar,(CASE WHEN c.type IN ('MUC','MUC_PM') THEN (SELECT vCardPhoto FROM" + + " presence WHERE accountId=c.accountId AND address=c.address AND" + + " occupantId=m.occupantId AND vCardPhoto NOT NULL LIMIT 1) ELSE NULL END) as" + + " occupantVcardPhoto,modification,latestVersion as" + + " version,inReplyToMessageEntityId,encryption,message_version.identityKey,trust,(CASE" + + " WHEN c.type IN ('MUC','MUC_PM') THEN (SELECT count(distinct(df.feature)) == 2" + + " FROM disco_item di JOIN disco_feature df ON di.discoId = df.discoId WHERE" + + " di.address=c.address AND df.feature IN('muc_membersonly','muc_nonanonymous'))" + + " ELSE 0 END) as membersOnlyNonAnonymous FROM chat c JOIN message m on" + + " m.chatId=c.id JOIN message_version ON m.latestVersion=message_version.id LEFT" + + " JOIN axolotl_identity ON c.accountId=axolotl_identity.accountId AND" + + " m.senderIdentity=axolotl_identity.address AND" + + " message_version.identityKey=axolotl_identity.identityKey WHERE c.id=:chatId AND" + + " latestVersion IS NOT NULL ORDER BY m.receivedAt DESC") public abstract List getMessagesForTesting(long chatId); @Transaction @Query( - "SELECT message.id as" - + " id,sentAt,outgoing,toBare,toResource,fromBare,fromResource,modification,latestVersion" - + " as version,inReplyToMessageEntityId,encryption,message_version.identityKey,trust" - + " FROM chat JOIN message on message.chatId=chat.id JOIN message_version ON" - + " message.latestVersion=message_version.id LEFT JOIN axolotl_identity ON" - + " chat.accountId=axolotl_identity.accountId AND" - + " message.senderIdentity=axolotl_identity.address AND" - + " message_version.identityKey=axolotl_identity.identityKey WHERE chat.id=:chatId" - + " AND latestVersion IS NOT NULL ORDER BY message.receivedAt DESC") - public abstract PagingSource getMessages(long chatId); + "SELECT c.accountId,m.id as id,type as" + + " chatType,sentAt,outgoing,toBare,toResource,fromBare,fromResource,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" + + " senderNick,(SELECT vCardPhoto FROM presence WHERE accountId=c.accountId AND" + + " address=m.senderIdentity AND vCardPhoto NOT NULL LIMIT 1) as" + + " senderVcardPhoto,(SELECT thumb_id FROM avatar WHERE" + + " avatar.accountId=c.accountId AND avatar.address=m.senderIdentity) as" + + " senderAvatar,(CASE WHEN c.type IN ('MUC','MUC_PM') THEN (SELECT vCardPhoto FROM" + + " presence WHERE accountId=c.accountId AND address=c.address AND" + + " occupantId=m.occupantId AND vCardPhoto NOT NULL LIMIT 1) ELSE NULL END) as" + + " occupantVcardPhoto,modification,latestVersion as" + + " version,inReplyToMessageEntityId,encryption,message_version.identityKey,trust,(CASE" + + " WHEN c.type IN ('MUC','MUC_PM') THEN (SELECT count(distinct(df.feature)) == 2" + + " FROM disco_item di JOIN disco_feature df ON di.discoId = df.discoId WHERE" + + " di.address=c.address AND df.feature IN('muc_membersonly','muc_nonanonymous'))" + + " ELSE 0 END) as membersOnlyNonAnonymous FROM chat c JOIN message m on" + + " m.chatId=c.id JOIN message_version ON m.latestVersion=message_version.id LEFT" + + " JOIN axolotl_identity ON c.accountId=axolotl_identity.accountId AND" + + " m.senderIdentity=axolotl_identity.address AND" + + " message_version.identityKey=axolotl_identity.identityKey WHERE c.id=:chatId AND" + + " latestVersion IS NOT NULL ORDER BY m.receivedAt DESC") + public abstract PagingSource getMessages(long chatId); public void setInReplyTo( ChatIdentifier chat, diff --git a/app/src/main/java/im/conversations/android/database/model/ChatInfo.java b/app/src/main/java/im/conversations/android/database/model/ChatInfo.java index f200e01ad..a0b8c9b90 100644 --- a/app/src/main/java/im/conversations/android/database/model/ChatInfo.java +++ b/app/src/main/java/im/conversations/android/database/model/ChatInfo.java @@ -1,9 +1,10 @@ package im.conversations.android.database.model; +import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.impl.JidCreate; -public class ChatInfo { +public class ChatInfo implements IndividualName { public long accountId; public String address; @@ -24,28 +25,6 @@ public class ChatInfo { }; } - private String individualName() { - if (notNullNotEmpty(rosterName)) { - return rosterName.trim(); - } - if (notNullNotEmpty(nick)) { - return nick.trim(); - } - return fallbackName(); - } - - private String fallbackName() { - final Jid jid = getJidAddress(); - if (jid == null) { - return this.address; - } - if (jid.hasLocalpart()) { - return jid.getLocalpartOrThrow().toString(); - } else { - return jid.toString(); - } - } - private String mucName() { if (notNullNotEmpty(this.bookmarkName)) { return this.bookmarkName.trim(); @@ -53,7 +32,29 @@ public class ChatInfo { if (notNullNotEmpty(this.discoIdentityName)) { return this.discoIdentityName.trim(); } - return fallbackName(); + final var jid = getJidAddress(); + if (jid == null) { + return this.address; + } else if (jid.hasLocalpart()) { + return jid.getLocalpartOrThrow().toString(); + } else { + return jid.toString(); + } + } + + @Override + public String individualRosterName() { + return this.rosterName; + } + + @Override + public String individualNick() { + return nick; + } + + @Override + public BareJid individualAddress() { + return address == null ? null : JidCreate.fromOrNull(address).asBareJid(); } private static boolean notNullNotEmpty(final String value) { diff --git a/app/src/main/java/im/conversations/android/database/model/IndividualName.java b/app/src/main/java/im/conversations/android/database/model/IndividualName.java new file mode 100644 index 000000000..3701e7807 --- /dev/null +++ b/app/src/main/java/im/conversations/android/database/model/IndividualName.java @@ -0,0 +1,35 @@ +package im.conversations.android.database.model; + +import org.jxmpp.jid.BareJid; + +public interface IndividualName { + + default String individualName() { + final var rosterName = individualRosterName(); + if (notNullNotEmpty(rosterName)) { + return rosterName.trim(); + } + final var nick = individualNick(); + if (notNullNotEmpty(nick)) { + return nick.trim(); + } + final var address = individualAddress(); + if (address == null) { + return null; + } else if (address.hasLocalpart()) { + return address.getLocalpartOrThrow().toString(); + } else { + return address.toString(); + } + } + + String individualRosterName(); + + String individualNick(); + + BareJid individualAddress(); + + private static boolean notNullNotEmpty(final String value) { + return value != null && !value.trim().isEmpty(); + } +} 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 a5a03a34c..3b15f0d37 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 @@ -1,7 +1,6 @@ package im.conversations.android.database.model; import androidx.room.Relation; - import com.google.common.base.Strings; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; @@ -12,25 +11,44 @@ import im.conversations.android.database.entity.MessageEntity; import im.conversations.android.database.entity.MessageReactionEntity; import im.conversations.android.database.entity.MessageStateEntity; import java.time.Instant; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.jid.parts.Resourcepart; import org.whispersystems.libsignal.IdentityKey; -public class MessageWithContentReactions { +public class MessageWithContentReactions implements IndividualName { + + public long accountId; public long id; + public ChatType chatType; + public boolean membersOnlyNonAnonymous; + public Instant sentAt; public boolean outgoing; - public Jid toBare; + public BareJid toBare; public String toResource; - public Jid fromBare; - public String fromResource; + public BareJid fromBare; + public Resourcepart fromResource; + + // TODO retrieve occupantResource (current resource inferred by occupant id) + + public BareJid sender; + public String senderVcardPhoto; + public String senderAvatar; + public String senderRosterName; + public String senderNick; + + public String occupantVcardPhoto; public Modification modification; public long version; @@ -73,9 +91,62 @@ public class MessageWithContentReactions { } public String textContent() { - final var content = Iterables.getFirst(this.contents,null); + final var content = Iterables.getFirst(this.contents, null); final var text = Strings.nullToEmpty(content == null ? null : content.body); return text; - //return text.substring(0,Math.min(text.length(),20)); + // return text.substring(0,Math.min(text.length(),20)); + } + + public AddressWithName getAddressWithName() { + if (isIndividual()) { + return new AddressWithName(individualAddress(), individualName()); + } else { + final Jid address = JidCreate.fullFrom(fromBare, fromResource); + final String name = fromResource.toString(); + return new AddressWithName(address, name); + } + } + + public AvatarWithAccount getAvatar() { + final var address = getAddressWithName(); + if (address == null) { + return null; + } + if (isIndividual()) { + if (this.senderAvatar != null) { + return new AvatarWithAccount(accountId, address, AvatarType.PEP, this.senderAvatar); + } + if (this.senderVcardPhoto != null) { + return new AvatarWithAccount( + accountId, address, AvatarType.VCARD, this.senderVcardPhoto); + } + } else if (occupantVcardPhoto != null) { + return new AvatarWithAccount( + accountId, address, AvatarType.VCARD, this.occupantVcardPhoto); + } + + return null; + } + + private boolean isIndividual() { + return chatType == ChatType.INDIVIDUAL + || (Arrays.asList(ChatType.MUC, ChatType.MUC_PM).contains(chatType) + && membersOnlyNonAnonymous + && sender != null); + } + + @Override + public String individualRosterName() { + return senderRosterName; + } + + @Override + public String individualNick() { + return senderNick; + } + + @Override + public BareJid individualAddress() { + return sender; } } diff --git a/app/src/main/java/im/conversations/android/repository/ChatRepository.java b/app/src/main/java/im/conversations/android/repository/ChatRepository.java index 1068ffbcd..8ee348de2 100644 --- a/app/src/main/java/im/conversations/android/repository/ChatRepository.java +++ b/app/src/main/java/im/conversations/android/repository/ChatRepository.java @@ -8,7 +8,6 @@ import im.conversations.android.database.model.ChatInfo; import im.conversations.android.database.model.ChatOverviewItem; import im.conversations.android.database.model.GroupIdentifier; import im.conversations.android.database.model.MessageWithContentReactions; - import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 3e217dbe4..de780cc3a 100644 --- a/app/src/main/java/im/conversations/android/ui/BindingAdapters.java +++ b/app/src/main/java/im/conversations/android/ui/BindingAdapters.java @@ -64,9 +64,7 @@ public class BindingAdapters { if (sameDay(instant, now) || now.minus(SIX_HOURS).isBefore(instant)) { textView.setText( DateUtils.formatDateTime( - context, - instant.toEpochMilli(), - DateUtils.FORMAT_SHOW_TIME)); + context, instant.toEpochMilli(), DateUtils.FORMAT_SHOW_TIME)); } else if (sameYear(instant, now) || now.minus(THREE_MONTH).isBefore(instant)) { textView.setText( DateUtils.formatDateTime( @@ -90,16 +88,14 @@ public class BindingAdapters { @BindingAdapter("time") public static void setTime(final TextView textView, final Instant instant) { if (instant == null || instant.getEpochSecond() <= 0) { - textView.setVisibility(View.GONE); + textView.setVisibility(View.INVISIBLE); } else { final Context context = textView.getContext(); final Instant now = Instant.now(); textView.setVisibility(View.VISIBLE); - textView.setText( - DateUtils.formatDateTime( - context, - instant.toEpochMilli(), - DateUtils.FORMAT_SHOW_TIME)); + textView.setText( + DateUtils.formatDateTime( + context, instant.toEpochMilli(), DateUtils.FORMAT_SHOW_TIME)); } } diff --git a/app/src/main/java/im/conversations/android/ui/RecyclerViewScroller.java b/app/src/main/java/im/conversations/android/ui/RecyclerViewScroller.java index 914ea2cfe..f36e121d8 100644 --- a/app/src/main/java/im/conversations/android/ui/RecyclerViewScroller.java +++ b/app/src/main/java/im/conversations/android/ui/RecyclerViewScroller.java @@ -3,12 +3,10 @@ package im.conversations.android.ui; import androidx.paging.PagingDataAdapter; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.lang.ref.WeakReference; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RecyclerViewScroller { @@ -16,7 +14,6 @@ public class RecyclerViewScroller { private final RecyclerView recyclerView; - public RecyclerViewScroller(RecyclerView recyclerView) { this.recyclerView = recyclerView; } @@ -35,7 +32,6 @@ public class RecyclerViewScroller { private int accumulatedDelay = 0; - private ReliableScroller(RecyclerView recyclerView) { this.recyclerViewReference = new WeakReference<>(recyclerView); } @@ -49,8 +45,15 @@ public class RecyclerViewScroller { final var isSurroundingRendered = isSurroundingRendered(recyclerView, position); final var doneUpdating = !recyclerView.hasPendingAdapterUpdates(); final var viewHolder = recyclerView.findViewHolderForAdapterPosition(position); - LOGGER.info("Item is loaded {}, isSurroundingRendered {}, doneUpdating {} accumulatedDelay {}", isItemLoaded, isSurroundingRendered, doneUpdating, accumulatedDelay); - if ((isItemLoaded && isSurroundingRendered && doneUpdating && viewHolder != null) || accumulatedDelay >= MAX_DELAY) { + LOGGER.info( + "Item is loaded {}, isSurroundingRendered {}, doneUpdating {} accumulatedDelay" + + " {}", + isItemLoaded, + isSurroundingRendered, + doneUpdating, + accumulatedDelay); + if ((isItemLoaded && isSurroundingRendered && doneUpdating && viewHolder != null) + || accumulatedDelay >= MAX_DELAY) { final var layoutManager = recyclerView.getLayoutManager(); if (viewHolder != null && layoutManager instanceof LinearLayoutManager llm) { final var child = viewHolder.itemView; @@ -58,19 +61,22 @@ public class RecyclerViewScroller { LOGGER.info("scrollToPositionWithOffset({},{})", position, offset); llm.scrollToPositionWithOffset(position, offset); } else { - LOGGER.info("scrollToPosition({})", position); + LOGGER.info("scrollToPosition({})", position); recyclerView.scrollToPosition(position); } return; } recyclerView.scrollToPosition(position); accumulatedDelay += INTERVAL; - recyclerView.postDelayed(()->{ - scrollToPosition(position); - },INTERVAL); + recyclerView.postDelayed( + () -> { + scrollToPosition(position); + }, + INTERVAL); } - private static boolean isSurroundingRendered(final RecyclerView recyclerView, final int requestedPosition) { + private static boolean isSurroundingRendered( + final RecyclerView recyclerView, final int requestedPosition) { final var layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof LinearLayoutManager llm) { final var first = llm.findFirstVisibleItemPosition(); @@ -78,14 +84,23 @@ public class RecyclerViewScroller { if (first == -1 || last == -1) { return false; } - final var isItemLoaded = isItemLoaded(recyclerView, first) && isItemLoaded(recyclerView, last); + final var isItemLoaded = + isItemLoaded(recyclerView, first) && isItemLoaded(recyclerView, last); if (isItemLoaded) { - final var requestedIsOnly = first == requestedPosition && last == requestedPosition; + final var requestedIsOnly = + first == requestedPosition && last == requestedPosition; final var firstCompletelyVisible = llm.findFirstCompletelyVisibleItemPosition(); final var lastCompletelyVisible = llm.findLastCompletelyVisibleItemPosition(); - final var requestedInRange = firstCompletelyVisible <= requestedPosition && requestedPosition <= lastCompletelyVisible; - LOGGER.info("firstComp {} lastComp {} requested {} inRange {}", firstCompletelyVisible, lastCompletelyVisible, requestedPosition, requestedInRange); + final var requestedInRange = + firstCompletelyVisible <= requestedPosition + && requestedPosition <= lastCompletelyVisible; + LOGGER.info( + "firstComp {} lastComp {} requested {} inRange {}", + firstCompletelyVisible, + lastCompletelyVisible, + requestedPosition, + requestedInRange); return requestedIsOnly || requestedInRange; } else { return false; @@ -97,7 +112,7 @@ public class RecyclerViewScroller { private static boolean isItemLoaded(final RecyclerView recyclerView, final int position) { final var adapter = recyclerView.getAdapter(); - if (adapter instanceof PagingDataAdapter pagingDataAdapter) { + if (adapter instanceof PagingDataAdapter pagingDataAdapter) { return Objects.nonNull(pagingDataAdapter.peek(position)); } else { return true; 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 c16de2bc5..4d3451ab4 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 @@ -3,33 +3,34 @@ package im.conversations.android.ui.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; import androidx.paging.PagingDataAdapter; import androidx.recyclerview.widget.DiffUtil; 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.ui.AvatarFetcher; -public class MessageAdapter extends PagingDataAdapter { +public class MessageAdapter + extends PagingDataAdapter< + MessageWithContentReactions, MessageAdapter.AbstractMessageViewHolder> { - public MessageAdapter(@NonNull DiffUtil.ItemCallback diffCallback) { + public MessageAdapter( + @NonNull DiffUtil.ItemCallback diffCallback) { super(diffCallback); } @NonNull @Override - public AbstractMessageViewHolder onCreateViewHolder(final @NonNull ViewGroup parent, final int viewType) { + public AbstractMessageViewHolder onCreateViewHolder( + final @NonNull ViewGroup parent, final int viewType) { final var layoutInflater = LayoutInflater.from(parent.getContext()); if (viewType == 0) { - return new MessageReceivedViewHolder(DataBindingUtil.inflate( - layoutInflater, - R.layout.item_message_received, - parent, - false)); + return new MessageReceivedViewHolder( + DataBindingUtil.inflate( + layoutInflater, R.layout.item_message_received, parent, false)); } throw new IllegalArgumentException(String.format("viewType %d not implemented", viewType)); } @@ -41,6 +42,17 @@ public class MessageAdapter extends PagingDataAdapter { @Override - public boolean areItemsTheSame(@NonNull MessageWithContentReactions oldItem, @NonNull MessageWithContentReactions newItem) { + public boolean areItemsTheSame( + @NonNull MessageWithContentReactions oldItem, + @NonNull MessageWithContentReactions newItem) { return oldItem.id == newItem.id; } @Override - public boolean areContentsTheSame(@NonNull MessageWithContentReactions oldItem, @NonNull MessageWithContentReactions newItem) { + public boolean areContentsTheSame( + @NonNull MessageWithContentReactions oldItem, + @NonNull MessageWithContentReactions newItem) { return false; } } diff --git a/app/src/main/java/im/conversations/android/ui/fragment/main/ChatFragment.java b/app/src/main/java/im/conversations/android/ui/fragment/main/ChatFragment.java index cd86deb65..9eb5690f1 100644 --- a/app/src/main/java/im/conversations/android/ui/fragment/main/ChatFragment.java +++ b/app/src/main/java/im/conversations/android/ui/fragment/main/ChatFragment.java @@ -1,23 +1,14 @@ package im.conversations.android.ui.fragment.main; import android.os.Bundle; -import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; import androidx.fragment.app.Fragment; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Observer; -import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModelProvider; -import androidx.paging.CombinedLoadStates; -import androidx.paging.LoadState; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - import im.conversations.android.R; import im.conversations.android.databinding.FragmentChatBinding; import im.conversations.android.ui.Activities; @@ -26,14 +17,9 @@ import im.conversations.android.ui.RecyclerViewScroller; import im.conversations.android.ui.adapter.MessageAdapter; import im.conversations.android.ui.adapter.MessageComparator; import im.conversations.android.ui.model.ChatViewModel; -import kotlin.Unit; -import kotlin.jvm.functions.Function1; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Objects; - public class ChatFragment extends Fragment { private static final Logger LOGGER = LoggerFactory.getLogger(ChatFragment.class); @@ -56,7 +42,7 @@ public class ChatFragment extends Fragment { this.binding.setChatViewModel(this.chatViewModel); this.binding.setLifecycleOwner(getViewLifecycleOwner()); final var linearLayoutManager = new LinearLayoutManager(requireContext()); - //linearLayoutManager.setStackFromEnd(true); + // linearLayoutManager.setStackFromEnd(true); linearLayoutManager.setReverseLayout(true); this.binding.messages.setLayoutManager(linearLayoutManager); this.recyclerViewScroller = new RecyclerViewScroller(this.binding.messages); @@ -76,10 +62,10 @@ public class ChatFragment extends Fragment { NavControllers.findNavController(requireActivity(), R.id.nav_host_fragment) .popBackStack(); }); - this.binding.addContent.setOnClickListener(v ->{ - scrollToPosition(messageAdapter.getItemCount() - 1); - - }); + this.binding.addContent.setOnClickListener( + v -> { + scrollToPosition(messageAdapter.getItemCount() - 1); + }); this.binding.messageLayout.setEndIconOnClickListener( v -> { scrollToPosition(0); @@ -89,8 +75,7 @@ public class ChatFragment extends Fragment { } private void scrollToPosition(final int position) { - LOGGER.info("scrollToPosition({})",position); + LOGGER.info("scrollToPosition({})", position); this.recyclerViewScroller.scrollToPosition(position); } - } diff --git a/app/src/main/java/im/conversations/android/ui/model/ChatViewModel.java b/app/src/main/java/im/conversations/android/ui/model/ChatViewModel.java index 2846e5a8a..3b128d486 100644 --- a/app/src/main/java/im/conversations/android/ui/model/ChatViewModel.java +++ b/app/src/main/java/im/conversations/android/ui/model/ChatViewModel.java @@ -11,13 +11,9 @@ import androidx.paging.Pager; import androidx.paging.PagingConfig; import androidx.paging.PagingData; import androidx.paging.PagingLiveData; - import im.conversations.android.database.model.ChatInfo; -import im.conversations.android.database.model.ChatOverviewItem; import im.conversations.android.database.model.MessageWithContentReactions; import im.conversations.android.repository.ChatRepository; -import kotlinx.coroutines.CoroutineScope; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,16 +33,18 @@ public class ChatViewModel extends AndroidViewModel { Transformations.switchMap( this.chatId, chatId -> chatId == null ? null : chatRepository.getChatInfo(chatId)); - final var messages = Transformations.switchMap(this.chatId, chatId -> { - final Pager pager = - new Pager<>( - new PagingConfig(30), - () -> chatRepository.getMessages(chatId)); - return PagingLiveData.getLiveData(pager); - }); - final var viewModelScope = ViewModelKt.getViewModelScope(this); - this.messages = PagingLiveData.cachedIn(messages, viewModelScope); - + final var messages = + Transformations.switchMap( + this.chatId, + chatId -> { + final Pager pager = + new Pager<>( + new PagingConfig(30), + () -> chatRepository.getMessages(chatId)); + return PagingLiveData.getLiveData(pager); + }); + final var viewModelScope = ViewModelKt.getViewModelScope(this); + this.messages = PagingLiveData.cachedIn(messages, viewModelScope); } public void setChatId(final long chatId) { diff --git a/app/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java index 2b24b1fc8..e4ac7a459 100644 --- a/app/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java +++ b/app/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java @@ -157,7 +157,8 @@ public class AvatarManager extends AbstractManager { final var photo = vcard.getExtension(Photo.class); final var binary = photo == null ? null : photo.getExtension(BinaryValue.class); if (binary == null) { - throw new IllegalStateException("vCard did not have embedded photo"); + throw new IllegalStateException( + String.format("vCard for %s did not have embedded photo", address)); } return binary.asBytes(); },