show avatars in chat

This commit is contained in:
Daniel Gultsch 2023-03-24 12:21:19 +01:00
parent 805d0db486
commit 4fae8d4e11
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
13 changed files with 282 additions and 138 deletions

View file

@ -212,12 +212,12 @@ public abstract class ChatDao {
+ " accountId=c.accountId AND address=c.address AND vCardPhoto NOT NULL LIMIT 1)" + " accountId=c.accountId AND address=c.address AND vCardPhoto NOT NULL LIMIT 1)"
+ " ELSE NULL END) as vCardPhoto,(SELECT thumb_id FROM avatar WHERE" + " ELSE NULL END) as vCardPhoto,(SELECT thumb_id FROM avatar WHERE"
+ " avatar.accountId=c.accountId AND avatar.address=c.address) as avatar,(CASE WHEN" + " 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" + " c.type IN ('MUC','MUC_PM') THEN (SELECT count(distinct(df.feature)) == 2 FROM"
+ " JOIN disco_feature df ON di.discoId = df.discoId WHERE di.address=c.address AND" + " disco_item di JOIN disco_feature df ON di.discoId = df.discoId WHERE"
+ " df.feature IN('muc_membersonly','muc_nonanonymous')) ELSE 0 END) as" + " di.address=c.address AND df.feature IN('muc_membersonly','muc_nonanonymous'))"
+ " membersOnlyNonAnonymous FROM CHAT c LEFT JOIN message m ON (m.id = (SELECT id" + " ELSE 0 END) as membersOnlyNonAnonymous FROM CHAT c LEFT JOIN message m ON (m.id"
+ " FROM message WHERE chatId=c.id ORDER by receivedAt DESC LIMIT 1)) WHERE" + " = (SELECT id FROM message WHERE chatId=c.id ORDER by receivedAt DESC LIMIT 1))"
+ " (:accountId IS NULL OR c.accountId=:accountId) AND (:groupId IS NULL OR" + " 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" + " (c.address IN(SELECT roster.address FROM roster JOIN roster_group ON"
+ " roster.id=roster_group.rosterItemId WHERE roster.accountId=c.accountId AND" + " roster.id=roster_group.rosterItemId WHERE roster.accountId=c.accountId AND"
+ " roster_group.groupId=:groupId) OR c.address IN(SELECT address FROM bookmark" + " 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" + " disco_item.accountId=c.accountId AND disco_item.address=c.address LIMIT 1) as"
+ " discoIdentityName,(SELECT name FROM bookmark WHERE" + " discoIdentityName,(SELECT name FROM bookmark WHERE"
+ " bookmark.accountId=c.accountId AND bookmark.address=c.address) as" + " bookmark.accountId=c.accountId AND bookmark.address=c.address) as"
+ " bookmarkName,(CASE WHEN c.type='MUC' THEN (SELECT count(distinct(df.feature))" + " bookmarkName,(CASE WHEN c.type IN ('MUC','MUC_PM') THEN (SELECT"
+ " == 2 FROM disco_item di JOIN disco_feature df ON di.discoId = df.discoId WHERE" + " count(distinct(df.feature)) == 2 FROM disco_item di JOIN disco_feature df ON"
+ " di.address=c.address AND df.feature IN('muc_membersonly','muc_nonanonymous'))" + " di.discoId = df.discoId WHERE di.address=c.address AND df.feature"
+ " ELSE 0 END) as membersOnlyNonAnonymous FROM chat c WHERE c.id=:chatId") + " IN('muc_membersonly','muc_nonanonymous')) ELSE 0 END) as"
+ " membersOnlyNonAnonymous FROM chat c WHERE c.id=:chatId")
public abstract LiveData<ChatInfo> getChatInfo(final long chatId); public abstract LiveData<ChatInfo> getChatInfo(final long chatId);
public PagingSource<Integer, ChatOverviewItem> getChatOverview(final ChatFilter chatFilter) { public PagingSource<Integer, ChatOverviewItem> getChatOverview(final ChatFilter chatFilter) {

View file

@ -425,29 +425,57 @@ public abstract class MessageDao {
@VisibleForTesting @VisibleForTesting
@Transaction @Transaction
@Query( @Query(
"SELECT message.id as" "SELECT c.accountId,m.id as id,type as"
+ " id,sentAt,outgoing,toBare,toResource,fromBare,fromResource,modification,latestVersion" + " chatType,sentAt,outgoing,toBare,toResource,fromBare,fromResource,senderIdentity"
+ " as version,inReplyToMessageEntityId,encryption,message_version.identityKey,trust" + " as sender,(SELECT name FROM roster WHERE roster.accountId=c.accountId AND"
+ " FROM chat JOIN message on message.chatId=chat.id JOIN message_version ON" + " roster.address=m.senderIdentity) as senderRosterName,(SELECT nick FROM nick"
+ " message.latestVersion=message_version.id LEFT JOIN axolotl_identity ON" + " WHERE nick.accountId=c.accountId AND nick.address=m.senderIdentity) as"
+ " chat.accountId=axolotl_identity.accountId AND" + " senderNick,(SELECT vCardPhoto FROM presence WHERE accountId=c.accountId AND"
+ " message.senderIdentity=axolotl_identity.address AND" + " address=m.senderIdentity AND vCardPhoto NOT NULL LIMIT 1) as"
+ " message_version.identityKey=axolotl_identity.identityKey WHERE chat.id=:chatId" + " senderVcardPhoto,(SELECT thumb_id FROM avatar WHERE"
+ " AND latestVersion IS NOT NULL ORDER BY message.receivedAt") + " 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<MessageWithContentReactions> getMessagesForTesting(long chatId); public abstract List<MessageWithContentReactions> getMessagesForTesting(long chatId);
@Transaction @Transaction
@Query( @Query(
"SELECT message.id as" "SELECT c.accountId,m.id as id,type as"
+ " id,sentAt,outgoing,toBare,toResource,fromBare,fromResource,modification,latestVersion" + " chatType,sentAt,outgoing,toBare,toResource,fromBare,fromResource,senderIdentity"
+ " as version,inReplyToMessageEntityId,encryption,message_version.identityKey,trust" + " as sender,(SELECT name FROM roster WHERE roster.accountId=c.accountId AND"
+ " FROM chat JOIN message on message.chatId=chat.id JOIN message_version ON" + " roster.address=m.senderIdentity) as senderRosterName,(SELECT nick FROM nick"
+ " message.latestVersion=message_version.id LEFT JOIN axolotl_identity ON" + " WHERE nick.accountId=c.accountId AND nick.address=m.senderIdentity) as"
+ " chat.accountId=axolotl_identity.accountId AND" + " senderNick,(SELECT vCardPhoto FROM presence WHERE accountId=c.accountId AND"
+ " message.senderIdentity=axolotl_identity.address AND" + " address=m.senderIdentity AND vCardPhoto NOT NULL LIMIT 1) as"
+ " message_version.identityKey=axolotl_identity.identityKey WHERE chat.id=:chatId" + " senderVcardPhoto,(SELECT thumb_id FROM avatar WHERE"
+ " AND latestVersion IS NOT NULL ORDER BY message.receivedAt DESC") + " avatar.accountId=c.accountId AND avatar.address=m.senderIdentity) as"
public abstract PagingSource<Integer,MessageWithContentReactions> getMessages(long chatId); + " 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<Integer, MessageWithContentReactions> getMessages(long chatId);
public void setInReplyTo( public void setInReplyTo(
ChatIdentifier chat, ChatIdentifier chat,

View file

@ -1,9 +1,10 @@
package im.conversations.android.database.model; package im.conversations.android.database.model;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.impl.JidCreate;
public class ChatInfo { public class ChatInfo implements IndividualName {
public long accountId; public long accountId;
public String address; 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() { private String mucName() {
if (notNullNotEmpty(this.bookmarkName)) { if (notNullNotEmpty(this.bookmarkName)) {
return this.bookmarkName.trim(); return this.bookmarkName.trim();
@ -53,7 +32,29 @@ public class ChatInfo {
if (notNullNotEmpty(this.discoIdentityName)) { if (notNullNotEmpty(this.discoIdentityName)) {
return this.discoIdentityName.trim(); 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) { private static boolean notNullNotEmpty(final String value) {

View file

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

View file

@ -1,7 +1,6 @@
package im.conversations.android.database.model; package im.conversations.android.database.model;
import androidx.room.Relation; import androidx.room.Relation;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables; 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.MessageReactionEntity;
import im.conversations.android.database.entity.MessageStateEntity; import im.conversations.android.database.entity.MessageStateEntity;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Resourcepart;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
public class MessageWithContentReactions { public class MessageWithContentReactions implements IndividualName {
public long accountId;
public long id; public long id;
public ChatType chatType;
public boolean membersOnlyNonAnonymous;
public Instant sentAt; public Instant sentAt;
public boolean outgoing; public boolean outgoing;
public Jid toBare; public BareJid toBare;
public String toResource; public String toResource;
public Jid fromBare; public BareJid fromBare;
public String fromResource; 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 Modification modification;
public long version; public long version;
@ -73,9 +91,62 @@ public class MessageWithContentReactions {
} }
public String textContent() { 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); final var text = Strings.nullToEmpty(content == null ? null : content.body);
return text; 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;
} }
} }

View file

@ -8,7 +8,6 @@ import im.conversations.android.database.model.ChatInfo;
import im.conversations.android.database.model.ChatOverviewItem; import im.conversations.android.database.model.ChatOverviewItem;
import im.conversations.android.database.model.GroupIdentifier; import im.conversations.android.database.model.GroupIdentifier;
import im.conversations.android.database.model.MessageWithContentReactions; import im.conversations.android.database.model.MessageWithContentReactions;
import java.util.List; import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;

View file

@ -64,9 +64,7 @@ public class BindingAdapters {
if (sameDay(instant, now) || now.minus(SIX_HOURS).isBefore(instant)) { if (sameDay(instant, now) || now.minus(SIX_HOURS).isBefore(instant)) {
textView.setText( textView.setText(
DateUtils.formatDateTime( DateUtils.formatDateTime(
context, context, instant.toEpochMilli(), DateUtils.FORMAT_SHOW_TIME));
instant.toEpochMilli(),
DateUtils.FORMAT_SHOW_TIME));
} else if (sameYear(instant, now) || now.minus(THREE_MONTH).isBefore(instant)) { } else if (sameYear(instant, now) || now.minus(THREE_MONTH).isBefore(instant)) {
textView.setText( textView.setText(
DateUtils.formatDateTime( DateUtils.formatDateTime(
@ -90,16 +88,14 @@ public class BindingAdapters {
@BindingAdapter("time") @BindingAdapter("time")
public static void setTime(final TextView textView, final Instant instant) { public static void setTime(final TextView textView, final Instant instant) {
if (instant == null || instant.getEpochSecond() <= 0) { if (instant == null || instant.getEpochSecond() <= 0) {
textView.setVisibility(View.GONE); textView.setVisibility(View.INVISIBLE);
} else { } else {
final Context context = textView.getContext(); final Context context = textView.getContext();
final Instant now = Instant.now(); final Instant now = Instant.now();
textView.setVisibility(View.VISIBLE); textView.setVisibility(View.VISIBLE);
textView.setText( textView.setText(
DateUtils.formatDateTime( DateUtils.formatDateTime(
context, context, instant.toEpochMilli(), DateUtils.FORMAT_SHOW_TIME));
instant.toEpochMilli(),
DateUtils.FORMAT_SHOW_TIME));
} }
} }

View file

@ -3,12 +3,10 @@ package im.conversations.android.ui;
import androidx.paging.PagingDataAdapter; import androidx.paging.PagingDataAdapter;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.Objects; import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RecyclerViewScroller { public class RecyclerViewScroller {
@ -16,7 +14,6 @@ public class RecyclerViewScroller {
private final RecyclerView recyclerView; private final RecyclerView recyclerView;
public RecyclerViewScroller(RecyclerView recyclerView) { public RecyclerViewScroller(RecyclerView recyclerView) {
this.recyclerView = recyclerView; this.recyclerView = recyclerView;
} }
@ -35,7 +32,6 @@ public class RecyclerViewScroller {
private int accumulatedDelay = 0; private int accumulatedDelay = 0;
private ReliableScroller(RecyclerView recyclerView) { private ReliableScroller(RecyclerView recyclerView) {
this.recyclerViewReference = new WeakReference<>(recyclerView); this.recyclerViewReference = new WeakReference<>(recyclerView);
} }
@ -49,8 +45,15 @@ public class RecyclerViewScroller {
final var isSurroundingRendered = isSurroundingRendered(recyclerView, position); final var isSurroundingRendered = isSurroundingRendered(recyclerView, position);
final var doneUpdating = !recyclerView.hasPendingAdapterUpdates(); final var doneUpdating = !recyclerView.hasPendingAdapterUpdates();
final var viewHolder = recyclerView.findViewHolderForAdapterPosition(position); final var viewHolder = recyclerView.findViewHolderForAdapterPosition(position);
LOGGER.info("Item is loaded {}, isSurroundingRendered {}, doneUpdating {} accumulatedDelay {}", isItemLoaded, isSurroundingRendered, doneUpdating, accumulatedDelay); LOGGER.info(
if ((isItemLoaded && isSurroundingRendered && doneUpdating && viewHolder != null) || accumulatedDelay >= MAX_DELAY) { "Item is loaded {}, isSurroundingRendered {}, doneUpdating {} accumulatedDelay"
+ " {}",
isItemLoaded,
isSurroundingRendered,
doneUpdating,
accumulatedDelay);
if ((isItemLoaded && isSurroundingRendered && doneUpdating && viewHolder != null)
|| accumulatedDelay >= MAX_DELAY) {
final var layoutManager = recyclerView.getLayoutManager(); final var layoutManager = recyclerView.getLayoutManager();
if (viewHolder != null && layoutManager instanceof LinearLayoutManager llm) { if (viewHolder != null && layoutManager instanceof LinearLayoutManager llm) {
final var child = viewHolder.itemView; final var child = viewHolder.itemView;
@ -65,12 +68,15 @@ public class RecyclerViewScroller {
} }
recyclerView.scrollToPosition(position); recyclerView.scrollToPosition(position);
accumulatedDelay += INTERVAL; accumulatedDelay += INTERVAL;
recyclerView.postDelayed(()->{ recyclerView.postDelayed(
() -> {
scrollToPosition(position); scrollToPosition(position);
},INTERVAL); },
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(); final var layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager llm) { if (layoutManager instanceof LinearLayoutManager llm) {
final var first = llm.findFirstVisibleItemPosition(); final var first = llm.findFirstVisibleItemPosition();
@ -78,14 +84,23 @@ public class RecyclerViewScroller {
if (first == -1 || last == -1) { if (first == -1 || last == -1) {
return false; return false;
} }
final var isItemLoaded = isItemLoaded(recyclerView, first) && isItemLoaded(recyclerView, last); final var isItemLoaded =
isItemLoaded(recyclerView, first) && isItemLoaded(recyclerView, last);
if (isItemLoaded) { if (isItemLoaded) {
final var requestedIsOnly = first == requestedPosition && last == requestedPosition; final var requestedIsOnly =
first == requestedPosition && last == requestedPosition;
final var firstCompletelyVisible = llm.findFirstCompletelyVisibleItemPosition(); final var firstCompletelyVisible = llm.findFirstCompletelyVisibleItemPosition();
final var lastCompletelyVisible = llm.findLastCompletelyVisibleItemPosition(); final var lastCompletelyVisible = llm.findLastCompletelyVisibleItemPosition();
final var requestedInRange = firstCompletelyVisible <= requestedPosition && requestedPosition <= lastCompletelyVisible; final var requestedInRange =
LOGGER.info("firstComp {} lastComp {} requested {} inRange {}", firstCompletelyVisible, lastCompletelyVisible, requestedPosition, requestedInRange); firstCompletelyVisible <= requestedPosition
&& requestedPosition <= lastCompletelyVisible;
LOGGER.info(
"firstComp {} lastComp {} requested {} inRange {}",
firstCompletelyVisible,
lastCompletelyVisible,
requestedPosition,
requestedInRange);
return requestedIsOnly || requestedInRange; return requestedIsOnly || requestedInRange;
} else { } else {
return false; return false;
@ -97,7 +112,7 @@ public class RecyclerViewScroller {
private static boolean isItemLoaded(final RecyclerView recyclerView, final int position) { private static boolean isItemLoaded(final RecyclerView recyclerView, final int position) {
final var adapter = recyclerView.getAdapter(); final var adapter = recyclerView.getAdapter();
if (adapter instanceof PagingDataAdapter<?,?> pagingDataAdapter) { if (adapter instanceof PagingDataAdapter<?, ?> pagingDataAdapter) {
return Objects.nonNull(pagingDataAdapter.peek(position)); return Objects.nonNull(pagingDataAdapter.peek(position));
} else { } else {
return true; return true;

View file

@ -3,33 +3,34 @@ package im.conversations.android.ui.adapter;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import androidx.paging.PagingDataAdapter; import androidx.paging.PagingDataAdapter;
import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import im.conversations.android.R; import im.conversations.android.R;
import im.conversations.android.database.model.MessageWithContentReactions; import im.conversations.android.database.model.MessageWithContentReactions;
import im.conversations.android.databinding.ItemMessageReceivedBinding; import im.conversations.android.databinding.ItemMessageReceivedBinding;
import im.conversations.android.ui.AvatarFetcher;
public class MessageAdapter extends PagingDataAdapter<MessageWithContentReactions, MessageAdapter.AbstractMessageViewHolder> { public class MessageAdapter
extends PagingDataAdapter<
MessageWithContentReactions, MessageAdapter.AbstractMessageViewHolder> {
public MessageAdapter(@NonNull DiffUtil.ItemCallback<MessageWithContentReactions> diffCallback) { public MessageAdapter(
@NonNull DiffUtil.ItemCallback<MessageWithContentReactions> diffCallback) {
super(diffCallback); super(diffCallback);
} }
@NonNull @NonNull
@Override @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()); final var layoutInflater = LayoutInflater.from(parent.getContext());
if (viewType == 0) { if (viewType == 0) {
return new MessageReceivedViewHolder(DataBindingUtil.inflate( return new MessageReceivedViewHolder(
layoutInflater, DataBindingUtil.inflate(
R.layout.item_message_received, layoutInflater, R.layout.item_message_received, parent, false));
parent,
false));
} }
throw new IllegalArgumentException(String.format("viewType %d not implemented", viewType)); throw new IllegalArgumentException(String.format("viewType %d not implemented", viewType));
} }
@ -41,6 +42,17 @@ public class MessageAdapter extends PagingDataAdapter<MessageWithContentReaction
holder.setMessage(null); holder.setMessage(null);
} }
holder.setMessage(message); holder.setMessage(message);
if (holder instanceof MessageReceivedViewHolder messageReceivedViewHolder) {
final var addressWithName = message == null ? null : message.getAddressWithName();
final var avatar = message == null ? null : message.getAvatar();
if (avatar != null) {
messageReceivedViewHolder.binding.avatar.setVisibility(View.VISIBLE);
AvatarFetcher.fetchInto(messageReceivedViewHolder.binding.avatar, avatar);
} else if (addressWithName != null) {
messageReceivedViewHolder.binding.avatar.setVisibility(View.VISIBLE);
AvatarFetcher.setDefault(messageReceivedViewHolder.binding.avatar, addressWithName);
}
}
} }
protected abstract static class AbstractMessageViewHolder extends RecyclerView.ViewHolder { protected abstract static class AbstractMessageViewHolder extends RecyclerView.ViewHolder {
@ -56,7 +68,6 @@ public class MessageAdapter extends PagingDataAdapter<MessageWithContentReaction
private final ItemMessageReceivedBinding binding; private final ItemMessageReceivedBinding binding;
public MessageReceivedViewHolder(@NonNull ItemMessageReceivedBinding binding) { public MessageReceivedViewHolder(@NonNull ItemMessageReceivedBinding binding) {
super(binding.getRoot()); super(binding.getRoot());
this.binding = binding; this.binding = binding;

View file

@ -2,17 +2,20 @@ package im.conversations.android.ui.adapter;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.DiffUtil;
import im.conversations.android.database.model.MessageWithContentReactions; import im.conversations.android.database.model.MessageWithContentReactions;
public class MessageComparator extends DiffUtil.ItemCallback<MessageWithContentReactions> { public class MessageComparator extends DiffUtil.ItemCallback<MessageWithContentReactions> {
@Override @Override
public boolean areItemsTheSame(@NonNull MessageWithContentReactions oldItem, @NonNull MessageWithContentReactions newItem) { public boolean areItemsTheSame(
@NonNull MessageWithContentReactions oldItem,
@NonNull MessageWithContentReactions newItem) {
return oldItem.id == newItem.id; return oldItem.id == newItem.id;
} }
@Override @Override
public boolean areContentsTheSame(@NonNull MessageWithContentReactions oldItem, @NonNull MessageWithContentReactions newItem) { public boolean areContentsTheSame(
@NonNull MessageWithContentReactions oldItem,
@NonNull MessageWithContentReactions newItem) {
return false; return false;
} }
} }

View file

@ -1,23 +1,14 @@
package im.conversations.android.ui.fragment.main; package im.conversations.android.ui.fragment.main;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment; 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.lifecycle.ViewModelProvider;
import androidx.paging.CombinedLoadStates;
import androidx.paging.LoadState;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import im.conversations.android.R; import im.conversations.android.R;
import im.conversations.android.databinding.FragmentChatBinding; import im.conversations.android.databinding.FragmentChatBinding;
import im.conversations.android.ui.Activities; 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.MessageAdapter;
import im.conversations.android.ui.adapter.MessageComparator; import im.conversations.android.ui.adapter.MessageComparator;
import im.conversations.android.ui.model.ChatViewModel; import im.conversations.android.ui.model.ChatViewModel;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Objects;
public class ChatFragment extends Fragment { public class ChatFragment extends Fragment {
private static final Logger LOGGER = LoggerFactory.getLogger(ChatFragment.class); 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.setChatViewModel(this.chatViewModel);
this.binding.setLifecycleOwner(getViewLifecycleOwner()); this.binding.setLifecycleOwner(getViewLifecycleOwner());
final var linearLayoutManager = new LinearLayoutManager(requireContext()); final var linearLayoutManager = new LinearLayoutManager(requireContext());
//linearLayoutManager.setStackFromEnd(true); // linearLayoutManager.setStackFromEnd(true);
linearLayoutManager.setReverseLayout(true); linearLayoutManager.setReverseLayout(true);
this.binding.messages.setLayoutManager(linearLayoutManager); this.binding.messages.setLayoutManager(linearLayoutManager);
this.recyclerViewScroller = new RecyclerViewScroller(this.binding.messages); this.recyclerViewScroller = new RecyclerViewScroller(this.binding.messages);
@ -76,9 +62,9 @@ public class ChatFragment extends Fragment {
NavControllers.findNavController(requireActivity(), R.id.nav_host_fragment) NavControllers.findNavController(requireActivity(), R.id.nav_host_fragment)
.popBackStack(); .popBackStack();
}); });
this.binding.addContent.setOnClickListener(v ->{ this.binding.addContent.setOnClickListener(
v -> {
scrollToPosition(messageAdapter.getItemCount() - 1); scrollToPosition(messageAdapter.getItemCount() - 1);
}); });
this.binding.messageLayout.setEndIconOnClickListener( this.binding.messageLayout.setEndIconOnClickListener(
v -> { v -> {
@ -89,8 +75,7 @@ public class ChatFragment extends Fragment {
} }
private void scrollToPosition(final int position) { private void scrollToPosition(final int position) {
LOGGER.info("scrollToPosition({})",position); LOGGER.info("scrollToPosition({})", position);
this.recyclerViewScroller.scrollToPosition(position); this.recyclerViewScroller.scrollToPosition(position);
} }
} }

View file

@ -11,13 +11,9 @@ import androidx.paging.Pager;
import androidx.paging.PagingConfig; import androidx.paging.PagingConfig;
import androidx.paging.PagingData; import androidx.paging.PagingData;
import androidx.paging.PagingLiveData; import androidx.paging.PagingLiveData;
import im.conversations.android.database.model.ChatInfo; 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.database.model.MessageWithContentReactions;
import im.conversations.android.repository.ChatRepository; import im.conversations.android.repository.ChatRepository;
import kotlinx.coroutines.CoroutineScope;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -37,7 +33,10 @@ public class ChatViewModel extends AndroidViewModel {
Transformations.switchMap( Transformations.switchMap(
this.chatId, this.chatId,
chatId -> chatId == null ? null : chatRepository.getChatInfo(chatId)); chatId -> chatId == null ? null : chatRepository.getChatInfo(chatId));
final var messages = Transformations.switchMap(this.chatId, chatId -> { final var messages =
Transformations.switchMap(
this.chatId,
chatId -> {
final Pager<Integer, MessageWithContentReactions> pager = final Pager<Integer, MessageWithContentReactions> pager =
new Pager<>( new Pager<>(
new PagingConfig(30), new PagingConfig(30),
@ -46,7 +45,6 @@ public class ChatViewModel extends AndroidViewModel {
}); });
final var viewModelScope = ViewModelKt.getViewModelScope(this); final var viewModelScope = ViewModelKt.getViewModelScope(this);
this.messages = PagingLiveData.cachedIn(messages, viewModelScope); this.messages = PagingLiveData.cachedIn(messages, viewModelScope);
} }
public void setChatId(final long chatId) { public void setChatId(final long chatId) {

View file

@ -157,7 +157,8 @@ public class AvatarManager extends AbstractManager {
final var photo = vcard.getExtension(Photo.class); final var photo = vcard.getExtension(Photo.class);
final var binary = photo == null ? null : photo.getExtension(BinaryValue.class); final var binary = photo == null ? null : photo.getExtension(BinaryValue.class);
if (binary == null) { 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(); return binary.asBytes();
}, },