rudimentary MessageAdapter
This commit is contained in:
parent
da528776db
commit
779e6fa61e
|
@ -59,7 +59,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
originalMessage.getFrom().asBareJid(),
|
originalMessage.getFrom().asBareJid(),
|
||||||
null));
|
null));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
Assert.assertEquals(1, messages.size());
|
Assert.assertEquals(1, messages.size());
|
||||||
final var message = Iterables.getOnlyElement(messages);
|
final var message = Iterables.getOnlyElement(messages);
|
||||||
final var onlyContent = Iterables.getOnlyElement(message.contents);
|
final var onlyContent = Iterables.getOnlyElement(message.contents);
|
||||||
|
@ -103,7 +103,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
MessageTransformation.of(
|
MessageTransformation.of(
|
||||||
reactionC, Instant.now(), REMOTE, "stanza-d", null, "id-user-d"));
|
reactionC, Instant.now(), REMOTE, "stanza-d", null, "id-user-d"));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
Assert.assertEquals(1, messages.size());
|
Assert.assertEquals(1, messages.size());
|
||||||
final var dbMessage = Iterables.getOnlyElement(messages);
|
final var dbMessage = Iterables.getOnlyElement(messages);
|
||||||
Assert.assertEquals(4, dbMessage.reactions.size());
|
Assert.assertEquals(4, dbMessage.reactions.size());
|
||||||
|
@ -136,7 +136,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
null));
|
null));
|
||||||
|
|
||||||
// the correction should not show up as a message
|
// the correction should not show up as a message
|
||||||
Assert.assertEquals(0, database.messageDao().getMessages(1L).size());
|
Assert.assertEquals(0, database.messageDao().getMessagesForTesting(1L).size());
|
||||||
|
|
||||||
final var messageWithTypo = new Message();
|
final var messageWithTypo = new Message();
|
||||||
messageWithTypo.setId("1");
|
messageWithTypo.setId("1");
|
||||||
|
@ -153,7 +153,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
messageWithTypo.getFrom().asBareJid(),
|
messageWithTypo.getFrom().asBareJid(),
|
||||||
null));
|
null));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
|
|
||||||
Assert.assertEquals(1, messages.size());
|
Assert.assertEquals(1, messages.size());
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
messageWithTypo.getFrom().asBareJid(),
|
messageWithTypo.getFrom().asBareJid(),
|
||||||
null));
|
null));
|
||||||
|
|
||||||
Assert.assertEquals(1, database.messageDao().getMessages(1L).size());
|
Assert.assertEquals(1, database.messageDao().getMessagesForTesting(1L).size());
|
||||||
|
|
||||||
final var messageCorrection = new Message();
|
final var messageCorrection = new Message();
|
||||||
messageCorrection.setId("2");
|
messageCorrection.setId("2");
|
||||||
|
@ -199,7 +199,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
messageCorrection.getFrom().asBareJid(),
|
messageCorrection.getFrom().asBareJid(),
|
||||||
null));
|
null));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
|
|
||||||
Assert.assertEquals(1, messages.size());
|
Assert.assertEquals(1, messages.size());
|
||||||
|
|
||||||
|
@ -233,7 +233,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
MessageTransformation.of(
|
MessageTransformation.of(
|
||||||
reactionB, Instant.now(), REMOTE, "stanza-c", null, "id-user-b"));
|
reactionB, Instant.now(), REMOTE, "stanza-c", null, "id-user-b"));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
Assert.assertEquals(1, messages.size());
|
Assert.assertEquals(1, messages.size());
|
||||||
final var dbMessage = Iterables.getOnlyElement(messages);
|
final var dbMessage = Iterables.getOnlyElement(messages);
|
||||||
Assert.assertEquals(1, dbMessage.reactions.size());
|
Assert.assertEquals(1, dbMessage.reactions.size());
|
||||||
|
@ -298,7 +298,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
MessageTransformation.of(
|
MessageTransformation.of(
|
||||||
m4, Instant.ofEpochMilli(1000), REMOTE, ogStanzaId, null, "id-user-a"));
|
m4, Instant.ofEpochMilli(1000), REMOTE, ogStanzaId, null, "id-user-a"));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
Assert.assertEquals(1, messages.size());
|
Assert.assertEquals(1, messages.size());
|
||||||
final var dbMessage = Iterables.getOnlyElement(messages);
|
final var dbMessage = Iterables.getOnlyElement(messages);
|
||||||
Assert.assertEquals(1, dbMessage.reactions.size());
|
Assert.assertEquals(1, dbMessage.reactions.size());
|
||||||
|
@ -363,7 +363,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
MessageTransformation.of(
|
MessageTransformation.of(
|
||||||
m4, Instant.ofEpochMilli(1000), REMOTE, ogStanzaId, null, "id-user-a"));
|
m4, Instant.ofEpochMilli(1000), REMOTE, ogStanzaId, null, "id-user-a"));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
Assert.assertEquals(1, messages.size());
|
Assert.assertEquals(1, messages.size());
|
||||||
final var dbMessage = Iterables.getOnlyElement(messages);
|
final var dbMessage = Iterables.getOnlyElement(messages);
|
||||||
Assert.assertEquals(2, dbMessage.reactions.size());
|
Assert.assertEquals(2, dbMessage.reactions.size());
|
||||||
|
@ -415,7 +415,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
null,
|
null,
|
||||||
"id-user-c"));
|
"id-user-c"));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
Assert.assertEquals(1, messages.size());
|
Assert.assertEquals(1, messages.size());
|
||||||
final var dbMessage = Iterables.getOnlyElement(messages);
|
final var dbMessage = Iterables.getOnlyElement(messages);
|
||||||
Assert.assertEquals(2, dbMessage.reactions.size());
|
Assert.assertEquals(2, dbMessage.reactions.size());
|
||||||
|
@ -451,7 +451,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
MessageTransformation.of(
|
MessageTransformation.of(
|
||||||
m2, Instant.now(), REMOTE, "stanza-b", m2.getFrom().asBareJid(), null));
|
m2, Instant.now(), REMOTE, "stanza-b", m2.getFrom().asBareJid(), null));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
Assert.assertEquals(2, messages.size());
|
Assert.assertEquals(2, messages.size());
|
||||||
final var response = Iterables.get(messages, 1);
|
final var response = Iterables.get(messages, 1);
|
||||||
Assert.assertNotNull(response.inReplyToMessageEntityId);
|
Assert.assertNotNull(response.inReplyToMessageEntityId);
|
||||||
|
@ -486,7 +486,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
MessageTransformation.of(
|
MessageTransformation.of(
|
||||||
m2, Instant.now(), REMOTE, null, m2.getFrom().asBareJid(), null));
|
m2, Instant.now(), REMOTE, null, m2.getFrom().asBareJid(), null));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
final var message = Iterables.getOnlyElement(messages);
|
final var message = Iterables.getOnlyElement(messages);
|
||||||
|
|
||||||
Assert.assertEquals(1L, message.states.size());
|
Assert.assertEquals(1L, message.states.size());
|
||||||
|
@ -513,7 +513,7 @@ public class MessageTransformationTest extends BaseTransformationTest {
|
||||||
MessageTransformation.of(
|
MessageTransformation.of(
|
||||||
m2, Instant.now(), REMOTE, null, m2.getFrom().asBareJid(), null));
|
m2, Instant.now(), REMOTE, null, m2.getFrom().asBareJid(), null));
|
||||||
|
|
||||||
final var messages = database.messageDao().getMessages(1L);
|
final var messages = database.messageDao().getMessagesForTesting(1L);
|
||||||
final var message = Iterables.getOnlyElement(messages);
|
final var message = Iterables.getOnlyElement(messages);
|
||||||
Assert.assertEquals(Modification.RETRACTION, message.modification);
|
Assert.assertEquals(Modification.RETRACTION, message.modification);
|
||||||
Assert.assertEquals(PartType.RETRACTION, Iterables.getOnlyElement(message.contents).type);
|
Assert.assertEquals(PartType.RETRACTION, Iterables.getOnlyElement(message.contents).type);
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package im.conversations.android.database.dao;
|
package im.conversations.android.database.dao;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.paging.PagingSource;
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Insert;
|
import androidx.room.Insert;
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
@ -420,6 +422,7 @@ public abstract class MessageDao {
|
||||||
+ " reactionBy=:fromBare")
|
+ " reactionBy=:fromBare")
|
||||||
protected abstract void deleteReactionsByFromBare(long messageEntityId, BareJid fromBare);
|
protected abstract void deleteReactionsByFromBare(long messageEntityId, BareJid fromBare);
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT message.id as"
|
"SELECT message.id as"
|
||||||
|
@ -431,7 +434,20 @@ public abstract class MessageDao {
|
||||||
+ " message.senderIdentity=axolotl_identity.address AND"
|
+ " message.senderIdentity=axolotl_identity.address AND"
|
||||||
+ " message_version.identityKey=axolotl_identity.identityKey WHERE chat.id=:chatId"
|
+ " message_version.identityKey=axolotl_identity.identityKey WHERE chat.id=:chatId"
|
||||||
+ " AND latestVersion IS NOT NULL ORDER BY message.receivedAt")
|
+ " AND latestVersion IS NOT NULL ORDER BY message.receivedAt")
|
||||||
public abstract List<MessageWithContentReactions> getMessages(long chatId);
|
public abstract List<MessageWithContentReactions> 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<Integer,MessageWithContentReactions> getMessages(long chatId);
|
||||||
|
|
||||||
public void setInReplyTo(
|
public void setInReplyTo(
|
||||||
ChatIdentifier chat,
|
ChatIdentifier chat,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
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.collect.ImmutableSortedSet;
|
import com.google.common.collect.ImmutableSortedSet;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Multimaps;
|
import com.google.common.collect.Multimaps;
|
||||||
import im.conversations.android.database.entity.MessageContentEntity;
|
import im.conversations.android.database.entity.MessageContentEntity;
|
||||||
|
@ -68,4 +71,11 @@ public class MessageWithContentReactions {
|
||||||
(a, b) -> Integer.compare(b.getValue(), a.getValue()),
|
(a, b) -> Integer.compare(b.getValue(), a.getValue()),
|
||||||
aggregatedReactions.entrySet());
|
aggregatedReactions.entrySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String textContent() {
|
||||||
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import im.conversations.android.database.model.ChatFilter;
|
||||||
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.ChatOverviewItem;
|
||||||
import im.conversations.android.database.model.GroupIdentifier;
|
import im.conversations.android.database.model.GroupIdentifier;
|
||||||
|
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;
|
||||||
|
@ -30,4 +32,8 @@ public class ChatRepository extends AbstractRepository {
|
||||||
public LiveData<ChatInfo> getChatInfo(final long chatId) {
|
public LiveData<ChatInfo> getChatInfo(final long chatId) {
|
||||||
return this.database.chatDao().getChatInfo(chatId);
|
return this.database.chatDao().getChatInfo(chatId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PagingSource<Integer, MessageWithContentReactions> getMessages(final long chatId) {
|
||||||
|
return this.database.messageDao().getMessages(chatId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class RecyclerViewScroller {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(RecyclerViewScroller.class);
|
||||||
|
|
||||||
|
private final RecyclerView recyclerView;
|
||||||
|
|
||||||
|
|
||||||
|
public RecyclerViewScroller(RecyclerView recyclerView) {
|
||||||
|
this.recyclerView = recyclerView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scrollToPosition(final int position) {
|
||||||
|
final ReliableScroller reliableScroller = new ReliableScroller(recyclerView);
|
||||||
|
reliableScroller.scrollToPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ReliableScroller {
|
||||||
|
|
||||||
|
private static final int MAX_DELAY = 2000;
|
||||||
|
private static final int INTERVAL = 50;
|
||||||
|
|
||||||
|
private final WeakReference<RecyclerView> recyclerViewReference;
|
||||||
|
|
||||||
|
private int accumulatedDelay = 0;
|
||||||
|
|
||||||
|
|
||||||
|
private ReliableScroller(RecyclerView recyclerView) {
|
||||||
|
this.recyclerViewReference = new WeakReference<>(recyclerView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scrollToPosition(final int position) {
|
||||||
|
final var recyclerView = this.recyclerViewReference.get();
|
||||||
|
if (recyclerView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final var isItemLoaded = isItemLoaded(recyclerView, position);
|
||||||
|
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) {
|
||||||
|
final var layoutManager = recyclerView.getLayoutManager();
|
||||||
|
if (viewHolder != null && layoutManager instanceof LinearLayoutManager llm) {
|
||||||
|
final var child = viewHolder.itemView;
|
||||||
|
final int offset = recyclerView.getHeight() / 2 - child.getHeight() / 2;
|
||||||
|
LOGGER.info("scrollToPositionWithOffset({},{})", position, offset);
|
||||||
|
llm.scrollToPositionWithOffset(position, offset);
|
||||||
|
} else {
|
||||||
|
LOGGER.info("scrollToPosition({})", position);
|
||||||
|
recyclerView.scrollToPosition(position);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
recyclerView.scrollToPosition(position);
|
||||||
|
accumulatedDelay += INTERVAL;
|
||||||
|
recyclerView.postDelayed(()->{
|
||||||
|
scrollToPosition(position);
|
||||||
|
},INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
final var last = llm.findLastVisibleItemPosition();
|
||||||
|
if (first == -1 || last == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final var isItemLoaded = isItemLoaded(recyclerView, first) && isItemLoaded(recyclerView, last);
|
||||||
|
if (isItemLoaded) {
|
||||||
|
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);
|
||||||
|
return requestedIsOnly || requestedInRange;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isItemLoaded(final RecyclerView recyclerView, final int position) {
|
||||||
|
final var adapter = recyclerView.getAdapter();
|
||||||
|
if (adapter instanceof PagingDataAdapter<?,?> pagingDataAdapter) {
|
||||||
|
return Objects.nonNull(pagingDataAdapter.peek(position));
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ 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.ChatOverviewItem;
|
import im.conversations.android.database.model.ChatOverviewItem;
|
||||||
import im.conversations.android.databinding.ItemChatoverviewBinding;
|
import im.conversations.android.databinding.ItemChatOverviewBinding;
|
||||||
import im.conversations.android.ui.AvatarFetcher;
|
import im.conversations.android.ui.AvatarFetcher;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -33,7 +33,7 @@ public class ChatOverviewAdapter
|
||||||
return new ChatOverviewViewHolder(
|
return new ChatOverviewViewHolder(
|
||||||
DataBindingUtil.inflate(
|
DataBindingUtil.inflate(
|
||||||
LayoutInflater.from(parent.getContext()),
|
LayoutInflater.from(parent.getContext()),
|
||||||
R.layout.item_chatoverview,
|
R.layout.item_chat_overview,
|
||||||
parent,
|
parent,
|
||||||
false));
|
false));
|
||||||
}
|
}
|
||||||
|
@ -68,9 +68,9 @@ public class ChatOverviewAdapter
|
||||||
|
|
||||||
public static class ChatOverviewViewHolder extends RecyclerView.ViewHolder {
|
public static class ChatOverviewViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private final ItemChatoverviewBinding binding;
|
private final ItemChatOverviewBinding binding;
|
||||||
|
|
||||||
public ChatOverviewViewHolder(@NonNull ItemChatoverviewBinding binding) {
|
public ChatOverviewViewHolder(@NonNull ItemChatOverviewBinding binding) {
|
||||||
super(binding.getRoot());
|
super(binding.getRoot());
|
||||||
this.binding = binding;
|
this.binding = binding;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class MessageAdapter extends PagingDataAdapter<MessageWithContentReactions, MessageAdapter.AbstractMessageViewHolder> {
|
||||||
|
|
||||||
|
public MessageAdapter(@NonNull DiffUtil.ItemCallback<MessageWithContentReactions> diffCallback) {
|
||||||
|
super(diffCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException(String.format("viewType %d not implemented", viewType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull AbstractMessageViewHolder holder, int position) {
|
||||||
|
final var message = getItem(position);
|
||||||
|
if (message == null) {
|
||||||
|
holder.setMessage(null);
|
||||||
|
}
|
||||||
|
holder.setMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract static class AbstractMessageViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
private AbstractMessageViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void setMessage(final MessageWithContentReactions message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MessageReceivedViewHolder extends AbstractMessageViewHolder {
|
||||||
|
|
||||||
|
private final ItemMessageReceivedBinding binding;
|
||||||
|
|
||||||
|
|
||||||
|
public MessageReceivedViewHolder(@NonNull ItemMessageReceivedBinding binding) {
|
||||||
|
super(binding.getRoot());
|
||||||
|
this.binding = binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setMessage(final MessageWithContentReactions message) {
|
||||||
|
if (message == null) {
|
||||||
|
this.binding.setMessage(null);
|
||||||
|
this.binding.text.setText("(placeholder)");
|
||||||
|
} else {
|
||||||
|
this.binding.setMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package im.conversations.android.ui.adapter;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
|
||||||
|
import im.conversations.android.database.model.MessageWithContentReactions;
|
||||||
|
|
||||||
|
public class MessageComparator extends DiffUtil.ItemCallback<MessageWithContentReactions> {
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull MessageWithContentReactions oldItem, @NonNull MessageWithContentReactions newItem) {
|
||||||
|
return oldItem.id == newItem.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull MessageWithContentReactions oldItem, @NonNull MessageWithContentReactions newItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,50 +1,96 @@
|
||||||
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.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;
|
||||||
import im.conversations.android.ui.NavControllers;
|
import im.conversations.android.ui.NavControllers;
|
||||||
|
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 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);
|
||||||
|
|
||||||
private FragmentChatBinding binding;
|
private FragmentChatBinding binding;
|
||||||
private ChatViewModel chatViewModel;
|
private ChatViewModel chatViewModel;
|
||||||
|
private MessageAdapter messageAdapter;
|
||||||
|
private RecyclerViewScroller recyclerViewScroller;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(
|
public View onCreateView(
|
||||||
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
super.onCreateView(inflater, container, savedInstanceState);
|
super.onCreateView(inflater, container, savedInstanceState);
|
||||||
this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_chat, container, false);
|
this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_chat, container, false);
|
||||||
final long chatId = ChatFragmentArgs.fromBundle(getArguments()).getChat();
|
final long chatId = ChatFragmentArgs.fromBundle(requireArguments()).getChat();
|
||||||
final ViewModelProvider viewModelProvider =
|
final ViewModelProvider viewModelProvider =
|
||||||
new ViewModelProvider(this, getDefaultViewModelProviderFactory());
|
new ViewModelProvider(this, getDefaultViewModelProviderFactory());
|
||||||
this.chatViewModel = viewModelProvider.get(ChatViewModel.class);
|
this.chatViewModel = viewModelProvider.get(ChatViewModel.class);
|
||||||
this.chatViewModel.setChatId(chatId);
|
this.chatViewModel.setChatId(chatId);
|
||||||
this.binding.setChatViewModel(this.chatViewModel);
|
this.binding.setChatViewModel(this.chatViewModel);
|
||||||
this.binding.setLifecycleOwner(getViewLifecycleOwner());
|
this.binding.setLifecycleOwner(getViewLifecycleOwner());
|
||||||
|
final var linearLayoutManager = new LinearLayoutManager(requireContext());
|
||||||
|
//linearLayoutManager.setStackFromEnd(true);
|
||||||
|
linearLayoutManager.setReverseLayout(true);
|
||||||
|
this.binding.messages.setLayoutManager(linearLayoutManager);
|
||||||
|
this.recyclerViewScroller = new RecyclerViewScroller(this.binding.messages);
|
||||||
|
this.messageAdapter = new MessageAdapter(new MessageComparator());
|
||||||
|
this.binding.messages.setAdapter(this.messageAdapter);
|
||||||
|
|
||||||
|
this.chatViewModel
|
||||||
|
.getMessages()
|
||||||
|
.observe(
|
||||||
|
getViewLifecycleOwner(),
|
||||||
|
pagingData -> {
|
||||||
|
LOGGER.info("submitData()");
|
||||||
|
messageAdapter.submitData(getLifecycle(), pagingData);
|
||||||
|
});
|
||||||
this.binding.materialToolbar.setNavigationOnClickListener(
|
this.binding.materialToolbar.setNavigationOnClickListener(
|
||||||
view -> {
|
view -> {
|
||||||
NavControllers.findNavController(requireActivity(), R.id.nav_host_fragment)
|
NavControllers.findNavController(requireActivity(), R.id.nav_host_fragment)
|
||||||
.popBackStack();
|
.popBackStack();
|
||||||
});
|
});
|
||||||
|
this.binding.addContent.setOnClickListener(v ->{
|
||||||
|
scrollToPosition(messageAdapter.getItemCount() - 1);
|
||||||
|
|
||||||
|
});
|
||||||
this.binding.messageLayout.setEndIconOnClickListener(
|
this.binding.messageLayout.setEndIconOnClickListener(
|
||||||
v -> {
|
v -> {
|
||||||
LOGGER.info("On send pressed");
|
scrollToPosition(0);
|
||||||
});
|
});
|
||||||
Activities.setStatusAndNavigationBarColors(requireActivity(), binding.getRoot(), true);
|
Activities.setStatusAndNavigationBarColors(requireActivity(), binding.getRoot(), true);
|
||||||
return this.binding.getRoot();
|
return this.binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void scrollToPosition(final int position) {
|
||||||
|
LOGGER.info("scrollToPosition({})",position);
|
||||||
|
this.recyclerViewScroller.scrollToPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,18 @@ import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.Transformations;
|
import androidx.lifecycle.Transformations;
|
||||||
|
import androidx.lifecycle.ViewModelKt;
|
||||||
|
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.ChatInfo;
|
||||||
|
import im.conversations.android.database.model.ChatOverviewItem;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ -18,6 +28,7 @@ public class ChatViewModel extends AndroidViewModel {
|
||||||
private final ChatRepository chatRepository;
|
private final ChatRepository chatRepository;
|
||||||
private final MutableLiveData<Long> chatId = new MutableLiveData<>();
|
private final MutableLiveData<Long> chatId = new MutableLiveData<>();
|
||||||
private final LiveData<ChatInfo> chatInfo;
|
private final LiveData<ChatInfo> chatInfo;
|
||||||
|
private final LiveData<PagingData<MessageWithContentReactions>> messages;
|
||||||
|
|
||||||
public ChatViewModel(@NonNull Application application) {
|
public ChatViewModel(@NonNull Application application) {
|
||||||
super(application);
|
super(application);
|
||||||
|
@ -26,6 +37,16 @@ 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 Pager<Integer, MessageWithContentReactions> 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) {
|
public void setChatId(final long chatId) {
|
||||||
|
@ -36,4 +57,8 @@ public class ChatViewModel extends AndroidViewModel {
|
||||||
return Transformations.map(
|
return Transformations.map(
|
||||||
this.chatInfo, chatInfo -> chatInfo == null ? null : chatInfo.name());
|
this.chatInfo, chatInfo -> chatInfo == null ? null : chatInfo.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<PagingData<MessageWithContentReactions>> getMessages() {
|
||||||
|
return this.messages;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="?colorSecondaryContainer"/>
|
||||||
|
<corners android:radius="16dp"/>
|
||||||
|
</shape>
|
|
@ -37,7 +37,7 @@
|
||||||
app:menu="@menu/fragment_chat"/>
|
app:menu="@menu/fragment_chat"/>
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<RelativeLayout
|
||||||
android:id="@+id/chat"
|
android:id="@+id/chat"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@ -45,64 +45,73 @@
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/chats"
|
android:id="@+id/messages"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/message_layout"
|
android:layout_above="@+id/compose_box"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_message_received"/>
|
||||||
|
|
||||||
<Button
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_marginStart="8dp"
|
android:id="@+id/compose_box"
|
||||||
android:id="@+id/add_content"
|
android:layout_alignParentBottom="true"
|
||||||
style="?attr/materialIconButtonStyle"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:icon="@drawable/ic_add_circle_outline_24dp"
|
|
||||||
app:iconTint="?colorOnSurface"
|
|
||||||
app:iconSize="24dp"
|
|
||||||
android:padding="8dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/message_layout"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/message_layout"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<Button
|
||||||
android:id="@+id/message_layout"
|
android:id="@+id/add_content"
|
||||||
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
style="?attr/materialIconButtonStyle"
|
||||||
android:theme="@style/ThemeOverlay.C3.TextSelection.Secondary"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
app:boxBackgroundColor="?colorTertiaryContainer"
|
|
||||||
app:boxCornerRadiusBottomEnd="24dp"
|
|
||||||
app:boxCornerRadiusBottomStart="24dp"
|
|
||||||
app:boxCornerRadiusTopEnd="24dp"
|
|
||||||
app:boxCornerRadiusTopStart="24dp"
|
|
||||||
app:boxStrokeWidth="0dp"
|
|
||||||
app:boxStrokeWidthFocused="0dp"
|
|
||||||
app:endIconDrawable="@drawable/ic_send_24dp"
|
|
||||||
app:endIconMode="custom"
|
|
||||||
app:endIconTint="?colorOnTertiaryContainer"
|
|
||||||
app:expandedHintEnabled="false"
|
|
||||||
app:helperTextEnabled="false"
|
|
||||||
app:hintEnabled="false"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/add_content">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
style="@style/Widget.C3.TextInputEditText.FilledBox.Tertiary"
|
|
||||||
android:theme="@style/ThemeOverlay.C3.TextSelection.Secondary"
|
|
||||||
android:id="@+id/message"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/send_message"
|
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:maxHeight="112dp" />
|
app:icon="@drawable/ic_add_circle_outline_24dp"
|
||||||
|
app:iconSize="24dp"
|
||||||
|
app:iconTint="?colorOnSurface"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/message_layout"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/message_layout"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
||||||
|
android:theme="@style/ThemeOverlay.C3.TextSelection.Secondary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
app:boxBackgroundColor="?colorTertiaryContainer"
|
||||||
|
app:boxCornerRadiusBottomEnd="24dp"
|
||||||
|
app:boxCornerRadiusBottomStart="24dp"
|
||||||
|
app:boxCornerRadiusTopEnd="24dp"
|
||||||
|
app:boxCornerRadiusTopStart="24dp"
|
||||||
|
app:boxStrokeWidth="0dp"
|
||||||
|
app:boxStrokeWidthFocused="0dp"
|
||||||
|
app:endIconDrawable="@drawable/ic_send_24dp"
|
||||||
|
app:endIconMode="custom"
|
||||||
|
app:endIconTint="?colorOnTertiaryContainer"
|
||||||
|
app:expandedHintEnabled="false"
|
||||||
|
app:helperTextEnabled="false"
|
||||||
|
app:hintEnabled="false"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/add_content">
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
style="@style/Widget.C3.TextInputEditText.FilledBox.Tertiary"
|
||||||
|
android:theme="@style/ThemeOverlay.C3.TextSelection.Secondary"
|
||||||
|
android:id="@+id/message"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/send_message"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:maxHeight="112dp" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/item_chatoverview"
|
tools:listitem="@layout/item_chat_overview"
|
||||||
android:fillViewport="true"
|
android:fillViewport="true"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
|
|
56
app/src/main/res/layout/item_message_received.xml
Normal file
56
app/src/main/res/layout/item_message_received.xml
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout 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">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/avatar"
|
||||||
|
android:layout_width="@dimen/avatar_chat_overview_size"
|
||||||
|
android:layout_height="@dimen/avatar_chat_overview_size"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:background="@drawable/background_message_received"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/avatar"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="?colorOnSecondaryContainer"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:text="@{message.textContent}"
|
||||||
|
tools:text="Fusce vitae vehicula risus, nec ornare lorem. Quisque facilisis mattis velit ac porttitor. Aenean aliquet pretium varius. Quisque neque felis, mattis sit amet leo ac, tempus dapibus nibh." />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<data>
|
||||||
|
<import type="android.view.View" />
|
||||||
|
<variable
|
||||||
|
name="message"
|
||||||
|
type="im.conversations.android.database.model.MessageWithContentReactions" />
|
||||||
|
</data>
|
||||||
|
</layout>
|
Loading…
Reference in a new issue