From 779e6fa61ed23f446d0cc04e37503140b64d6d39 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 23 Mar 2023 12:36:59 +0100 Subject: [PATCH] rudimentary MessageAdapter --- .../xmpp/MessageTransformationTest.java | 26 ++-- .../android/database/dao/MessageDao.java | 18 ++- .../model/MessageWithContentReactions.java | 10 ++ .../android/repository/ChatRepository.java | 6 + .../android/ui/RecyclerViewScroller.java | 107 +++++++++++++++++ .../ui/adapter/ChatOverviewAdapter.java | 8 +- .../android/ui/adapter/MessageAdapter.java | 75 ++++++++++++ .../android/ui/adapter/MessageComparator.java | 18 +++ .../ui/fragment/main/ChatFragment.java | 50 +++++++- .../android/ui/model/ChatViewModel.java | 25 ++++ .../drawable/background_message_received.xml | 5 + app/src/main/res/layout/fragment_chat.xml | 113 ++++++++++-------- app/src/main/res/layout/fragment_overview.xml | 2 +- ...hatoverview.xml => item_chat_overview.xml} | 0 .../main/res/layout/item_message_received.xml | 56 +++++++++ 15 files changed, 446 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/im/conversations/android/ui/RecyclerViewScroller.java create mode 100644 app/src/main/java/im/conversations/android/ui/adapter/MessageAdapter.java create mode 100644 app/src/main/java/im/conversations/android/ui/adapter/MessageComparator.java create mode 100644 app/src/main/res/drawable/background_message_received.xml rename app/src/main/res/layout/{item_chatoverview.xml => item_chat_overview.xml} (100%) create mode 100644 app/src/main/res/layout/item_message_received.xml diff --git a/app/src/androidTest/java/im/conversations/android/xmpp/MessageTransformationTest.java b/app/src/androidTest/java/im/conversations/android/xmpp/MessageTransformationTest.java index ee17d63b5..88fde52c1 100644 --- a/app/src/androidTest/java/im/conversations/android/xmpp/MessageTransformationTest.java +++ b/app/src/androidTest/java/im/conversations/android/xmpp/MessageTransformationTest.java @@ -59,7 +59,7 @@ public class MessageTransformationTest extends BaseTransformationTest { originalMessage.getFrom().asBareJid(), null)); - final var messages = database.messageDao().getMessages(1L); + final var messages = database.messageDao().getMessagesForTesting(1L); Assert.assertEquals(1, messages.size()); final var message = Iterables.getOnlyElement(messages); final var onlyContent = Iterables.getOnlyElement(message.contents); @@ -103,7 +103,7 @@ public class MessageTransformationTest extends BaseTransformationTest { MessageTransformation.of( 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()); final var dbMessage = Iterables.getOnlyElement(messages); Assert.assertEquals(4, dbMessage.reactions.size()); @@ -136,7 +136,7 @@ public class MessageTransformationTest extends BaseTransformationTest { null)); // 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(); messageWithTypo.setId("1"); @@ -153,7 +153,7 @@ public class MessageTransformationTest extends BaseTransformationTest { messageWithTypo.getFrom().asBareJid(), null)); - final var messages = database.messageDao().getMessages(1L); + final var messages = database.messageDao().getMessagesForTesting(1L); Assert.assertEquals(1, messages.size()); @@ -181,7 +181,7 @@ public class MessageTransformationTest extends BaseTransformationTest { messageWithTypo.getFrom().asBareJid(), null)); - Assert.assertEquals(1, database.messageDao().getMessages(1L).size()); + Assert.assertEquals(1, database.messageDao().getMessagesForTesting(1L).size()); final var messageCorrection = new Message(); messageCorrection.setId("2"); @@ -199,7 +199,7 @@ public class MessageTransformationTest extends BaseTransformationTest { messageCorrection.getFrom().asBareJid(), null)); - final var messages = database.messageDao().getMessages(1L); + final var messages = database.messageDao().getMessagesForTesting(1L); Assert.assertEquals(1, messages.size()); @@ -233,7 +233,7 @@ public class MessageTransformationTest extends BaseTransformationTest { MessageTransformation.of( 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()); final var dbMessage = Iterables.getOnlyElement(messages); Assert.assertEquals(1, dbMessage.reactions.size()); @@ -298,7 +298,7 @@ public class MessageTransformationTest extends BaseTransformationTest { MessageTransformation.of( 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()); final var dbMessage = Iterables.getOnlyElement(messages); Assert.assertEquals(1, dbMessage.reactions.size()); @@ -363,7 +363,7 @@ public class MessageTransformationTest extends BaseTransformationTest { MessageTransformation.of( 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()); final var dbMessage = Iterables.getOnlyElement(messages); Assert.assertEquals(2, dbMessage.reactions.size()); @@ -415,7 +415,7 @@ public class MessageTransformationTest extends BaseTransformationTest { null, "id-user-c")); - final var messages = database.messageDao().getMessages(1L); + final var messages = database.messageDao().getMessagesForTesting(1L); Assert.assertEquals(1, messages.size()); final var dbMessage = Iterables.getOnlyElement(messages); Assert.assertEquals(2, dbMessage.reactions.size()); @@ -451,7 +451,7 @@ public class MessageTransformationTest extends BaseTransformationTest { MessageTransformation.of( 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()); final var response = Iterables.get(messages, 1); Assert.assertNotNull(response.inReplyToMessageEntityId); @@ -486,7 +486,7 @@ public class MessageTransformationTest extends BaseTransformationTest { MessageTransformation.of( 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); Assert.assertEquals(1L, message.states.size()); @@ -513,7 +513,7 @@ public class MessageTransformationTest extends BaseTransformationTest { MessageTransformation.of( 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); Assert.assertEquals(Modification.RETRACTION, message.modification); Assert.assertEquals(PartType.RETRACTION, Iterables.getOnlyElement(message.contents).type); 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 115520776..0b1e36c78 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 @@ -1,6 +1,8 @@ package im.conversations.android.database.dao; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.paging.PagingSource; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.Query; @@ -420,6 +422,7 @@ public abstract class MessageDao { + " reactionBy=:fromBare") protected abstract void deleteReactionsByFromBare(long messageEntityId, BareJid fromBare); + @VisibleForTesting @Transaction @Query( "SELECT message.id as" @@ -431,7 +434,20 @@ public abstract class MessageDao { + " 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") - public abstract List getMessages(long chatId); + 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); public void setInReplyTo( ChatIdentifier chat, 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 7b00c307d..a5a03a34c 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,10 @@ 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; import com.google.common.collect.Maps; import com.google.common.collect.Multimaps; import im.conversations.android.database.entity.MessageContentEntity; @@ -68,4 +71,11 @@ public class MessageWithContentReactions { (a, b) -> Integer.compare(b.getValue(), a.getValue()), 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)); + } } 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 f1fad6fd3..1068ffbcd 100644 --- a/app/src/main/java/im/conversations/android/repository/ChatRepository.java +++ b/app/src/main/java/im/conversations/android/repository/ChatRepository.java @@ -7,6 +7,8 @@ import im.conversations.android.database.model.ChatFilter; 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; @@ -30,4 +32,8 @@ public class ChatRepository extends AbstractRepository { public LiveData getChatInfo(final long chatId) { return this.database.chatDao().getChatInfo(chatId); } + + public PagingSource getMessages(final long chatId) { + return this.database.messageDao().getMessages(chatId); + } } diff --git a/app/src/main/java/im/conversations/android/ui/RecyclerViewScroller.java b/app/src/main/java/im/conversations/android/ui/RecyclerViewScroller.java new file mode 100644 index 000000000..914ea2cfe --- /dev/null +++ b/app/src/main/java/im/conversations/android/ui/RecyclerViewScroller.java @@ -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 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; + } + } + } +} diff --git a/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewAdapter.java b/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewAdapter.java index c9b7f7535..1455974a3 100644 --- a/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewAdapter.java +++ b/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewAdapter.java @@ -10,7 +10,7 @@ import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import im.conversations.android.R; 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 java.util.function.Consumer; import org.slf4j.Logger; @@ -33,7 +33,7 @@ public class ChatOverviewAdapter return new ChatOverviewViewHolder( DataBindingUtil.inflate( LayoutInflater.from(parent.getContext()), - R.layout.item_chatoverview, + R.layout.item_chat_overview, parent, false)); } @@ -68,9 +68,9 @@ public class ChatOverviewAdapter 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()); this.binding = binding; } 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 new file mode 100644 index 000000000..7f3a118b4 --- /dev/null +++ b/app/src/main/java/im/conversations/android/ui/adapter/MessageAdapter.java @@ -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 { + + public MessageAdapter(@NonNull DiffUtil.ItemCallback 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); + } + } + } +} diff --git a/app/src/main/java/im/conversations/android/ui/adapter/MessageComparator.java b/app/src/main/java/im/conversations/android/ui/adapter/MessageComparator.java new file mode 100644 index 000000000..477c35033 --- /dev/null +++ b/app/src/main/java/im/conversations/android/ui/adapter/MessageComparator.java @@ -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 { + @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; + } +} 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 c981fce10..cd86deb65 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,50 +1,96 @@ 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; 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 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); private FragmentChatBinding binding; private ChatViewModel chatViewModel; + private MessageAdapter messageAdapter; + private RecyclerViewScroller recyclerViewScroller; @Override public View onCreateView( @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); 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 = new ViewModelProvider(this, getDefaultViewModelProviderFactory()); this.chatViewModel = viewModelProvider.get(ChatViewModel.class); this.chatViewModel.setChatId(chatId); this.binding.setChatViewModel(this.chatViewModel); 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( view -> { NavControllers.findNavController(requireActivity(), R.id.nav_host_fragment) .popBackStack(); }); + this.binding.addContent.setOnClickListener(v ->{ + scrollToPosition(messageAdapter.getItemCount() - 1); + + }); this.binding.messageLayout.setEndIconOnClickListener( v -> { - LOGGER.info("On send pressed"); + scrollToPosition(0); }); Activities.setStatusAndNavigationBarColors(requireActivity(), binding.getRoot(), true); return this.binding.getRoot(); } + + private void scrollToPosition(final int 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 33c44b0b0..2846e5a8a 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 @@ -6,8 +6,18 @@ import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; 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.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; @@ -18,6 +28,7 @@ public class ChatViewModel extends AndroidViewModel { private final ChatRepository chatRepository; private final MutableLiveData chatId = new MutableLiveData<>(); private final LiveData chatInfo; + private final LiveData> messages; public ChatViewModel(@NonNull Application application) { super(application); @@ -26,6 +37,16 @@ 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); + } public void setChatId(final long chatId) { @@ -36,4 +57,8 @@ public class ChatViewModel extends AndroidViewModel { return Transformations.map( this.chatInfo, chatInfo -> chatInfo == null ? null : chatInfo.name()); } + + public LiveData> getMessages() { + return this.messages; + } } diff --git a/app/src/main/res/drawable/background_message_received.xml b/app/src/main/res/drawable/background_message_received.xml new file mode 100644 index 000000000..a65d9e4cf --- /dev/null +++ b/app/src/main/res/drawable/background_message_received.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat.xml b/app/src/main/res/layout/fragment_chat.xml index 749a2bd30..8b335f046 100644 --- a/app/src/main/res/layout/fragment_chat.xml +++ b/app/src/main/res/layout/fragment_chat.xml @@ -37,7 +37,7 @@ app:menu="@menu/fragment_chat"/> - + android:layout_height="match_parent" + android:layout_above="@+id/compose_box" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_message_received"/> -