jump to message id
This commit is contained in:
parent
4bfcf209d7
commit
4d5445d123
|
@ -11,6 +11,7 @@ import androidx.room.Update;
|
|||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import im.conversations.android.database.entity.MessageContentEntity;
|
||||
import im.conversations.android.database.entity.MessageEntity;
|
||||
import im.conversations.android.database.entity.MessageReactionEntity;
|
||||
|
@ -480,9 +481,17 @@ public abstract class MessageDao {
|
|||
+ " 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")
|
||||
+ " latestVersion IS NOT NULL ORDER BY m.receivedAt DESC,m.id DESC")
|
||||
public abstract PagingSource<Integer, MessageWithContentReactions> getMessages(long chatId);
|
||||
|
||||
@Query(
|
||||
"SELECT CASE WHEN (SELECT EXISTS(SELECT id FROM message WHERE chatId=:chatId AND"
|
||||
+ " id=:messageId)) THEN (SELECT count(id) FROM message WHERE chatId=:chatId AND"
|
||||
+ " (receivedAt > (SELECT receivedAt FROM message WHERE id=:messageId) OR"
|
||||
+ " (receivedAt=(SELECT receivedAt FROM message WHERE id=:messageId) AND id >"
|
||||
+ " :messageId))) ELSE NULL END")
|
||||
public abstract ListenableFuture<Integer> getPosition(final long chatId, final long messageId);
|
||||
|
||||
public void setInReplyTo(
|
||||
ChatIdentifier chat,
|
||||
MessageIdentifier messageIdentifier,
|
||||
|
|
|
@ -3,6 +3,7 @@ package im.conversations.android.repository;
|
|||
import android.content.Context;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.paging.PagingSource;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import im.conversations.android.database.model.ChatFilter;
|
||||
import im.conversations.android.database.model.ChatInfo;
|
||||
import im.conversations.android.database.model.ChatOverviewItem;
|
||||
|
@ -35,4 +36,8 @@ public class ChatRepository extends AbstractRepository {
|
|||
public PagingSource<Integer, MessageWithContentReactions> getMessages(final long chatId) {
|
||||
return this.database.messageDao().getMessages(chatId);
|
||||
}
|
||||
|
||||
public ListenableFuture<Integer> getMessagePosition(final long chatId, final long messageId) {
|
||||
return this.database.messageDao().getPosition(chatId, messageId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,12 @@ public class RecyclerViewScroller {
|
|||
}
|
||||
|
||||
public void scrollToPosition(final int position) {
|
||||
this.scrollToPosition(position, null);
|
||||
}
|
||||
|
||||
public void scrollToPosition(final int position, final Runnable onScrolledRunnable) {
|
||||
final ReliableScroller reliableScroller = new ReliableScroller(recyclerView);
|
||||
reliableScroller.scrollToPosition(position);
|
||||
reliableScroller.scrollToPosition(position, onScrolledRunnable);
|
||||
}
|
||||
|
||||
private static class ReliableScroller {
|
||||
|
@ -36,7 +40,7 @@ public class RecyclerViewScroller {
|
|||
this.recyclerViewReference = new WeakReference<>(recyclerView);
|
||||
}
|
||||
|
||||
private void scrollToPosition(final int position) {
|
||||
private void scrollToPosition(final int position, final Runnable onScrolledRunnable) {
|
||||
final var recyclerView = this.recyclerViewReference.get();
|
||||
if (recyclerView == null) {
|
||||
return;
|
||||
|
@ -64,13 +68,16 @@ public class RecyclerViewScroller {
|
|||
LOGGER.info("scrollToPosition({})", position);
|
||||
recyclerView.scrollToPosition(position);
|
||||
}
|
||||
if (onScrolledRunnable != null) {
|
||||
recyclerView.post(onScrolledRunnable);
|
||||
}
|
||||
return;
|
||||
}
|
||||
recyclerView.scrollToPosition(position);
|
||||
accumulatedDelay += INTERVAL;
|
||||
recyclerView.postDelayed(
|
||||
() -> {
|
||||
scrollToPosition(position);
|
||||
scrollToPosition(position, onScrolledRunnable);
|
||||
},
|
||||
INTERVAL);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package im.conversations.android.ui.adapter;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.paging.PagingData;
|
||||
import androidx.paging.PagingDataTransforms;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import im.conversations.android.database.model.MessageAdapterItem;
|
||||
import im.conversations.android.database.model.MessageWithContentReactions;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public final class MessageAdapterItems {
|
||||
|
||||
private MessageAdapterItems() {}
|
||||
|
||||
public static PagingData<MessageAdapterItem> insertSeparators(
|
||||
final PagingData<MessageWithContentReactions> pagingData) {
|
||||
return PagingDataTransforms.insertSeparators(
|
||||
pagingData,
|
||||
MoreExecutors.directExecutor(),
|
||||
(before, after) -> {
|
||||
final var dayBefore = zonedDay(before);
|
||||
final var dayAfter = zonedDay(after);
|
||||
if (dayAfter == null && dayBefore != null) {
|
||||
return new MessageAdapterItem.MessageDateSeparator(dayBefore.toInstant());
|
||||
} else if (dayBefore == null || dayBefore.equals(dayAfter)) {
|
||||
return null;
|
||||
} else {
|
||||
return new MessageAdapterItem.MessageDateSeparator(dayBefore.toInstant());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static ZonedDateTime zonedDay(@Nullable final MessageWithContentReactions message) {
|
||||
return message == null ? null : zonedDay(message.sentAt);
|
||||
}
|
||||
|
||||
private static ZonedDateTime zonedDay(final Instant instant) {
|
||||
return instant.atZone(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS);
|
||||
}
|
||||
|
||||
public static PagingData<MessageAdapterItem> of(
|
||||
final PagingData<MessageWithContentReactions> pagingData) {
|
||||
return PagingDataTransforms.map(pagingData, MoreExecutors.directExecutor(), m -> m);
|
||||
}
|
||||
}
|
|
@ -9,19 +9,20 @@ import androidx.databinding.DataBindingUtil;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.paging.PagingData;
|
||||
import androidx.paging.PagingDataTransforms;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import im.conversations.android.R;
|
||||
import im.conversations.android.database.model.MessageAdapterItem;
|
||||
import im.conversations.android.database.model.MessageWithContentReactions;
|
||||
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.MessageAdapterItems;
|
||||
import im.conversations.android.ui.adapter.MessageComparator;
|
||||
import im.conversations.android.ui.model.ChatViewModel;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import im.conversations.android.util.MainThreadExecutor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -54,39 +55,10 @@ public class ChatFragment extends Fragment {
|
|||
this.messageAdapter = new MessageAdapter(new MessageComparator());
|
||||
this.binding.messages.setAdapter(this.messageAdapter);
|
||||
|
||||
this.chatViewModel.getMessages().observe(getViewLifecycleOwner(), this::submitPagingData);
|
||||
this.chatViewModel
|
||||
.getMessages()
|
||||
.observe(
|
||||
getViewLifecycleOwner(),
|
||||
pagingData -> {
|
||||
final PagingData<MessageAdapterItem> foo =
|
||||
PagingDataTransforms.insertSeparators(
|
||||
pagingData,
|
||||
MoreExecutors.directExecutor(),
|
||||
(before, after) -> {
|
||||
final var dayBefore =
|
||||
before == null
|
||||
? null
|
||||
: before.sentAt.truncatedTo(
|
||||
ChronoUnit.DAYS);
|
||||
final var dayAfter =
|
||||
after == null
|
||||
? null
|
||||
: after.sentAt.truncatedTo(
|
||||
ChronoUnit.DAYS);
|
||||
if (dayAfter == null && dayBefore != null) {
|
||||
return new MessageAdapterItem
|
||||
.MessageDateSeparator(dayBefore);
|
||||
} else if (dayBefore == null
|
||||
|| dayBefore.equals(dayAfter)) {
|
||||
return null;
|
||||
} else {
|
||||
return new MessageAdapterItem
|
||||
.MessageDateSeparator(dayBefore);
|
||||
}
|
||||
});
|
||||
messageAdapter.submitData(getLifecycle(), foo);
|
||||
});
|
||||
.isShowDateSeparators()
|
||||
.observe(getViewLifecycleOwner(), this::submitPagingData);
|
||||
this.binding.materialToolbar.setNavigationOnClickListener(
|
||||
view -> {
|
||||
NavControllers.findNavController(requireActivity(), R.id.nav_host_fragment)
|
||||
|
@ -94,18 +66,65 @@ public class ChatFragment extends Fragment {
|
|||
});
|
||||
this.binding.addContent.setOnClickListener(
|
||||
v -> {
|
||||
scrollToPosition(messageAdapter.getItemCount() - 1);
|
||||
scrollToMessageId(1039);
|
||||
});
|
||||
this.binding.messageLayout.setEndIconOnClickListener(
|
||||
v -> {
|
||||
scrollToPosition(0);
|
||||
this.scrollToPositionToEnd();
|
||||
});
|
||||
Activities.setStatusAndNavigationBarColors(requireActivity(), binding.getRoot(), true);
|
||||
return this.binding.getRoot();
|
||||
}
|
||||
|
||||
private void scrollToPosition(final int position) {
|
||||
LOGGER.info("scrollToPosition({})", position);
|
||||
this.recyclerViewScroller.scrollToPosition(position);
|
||||
private void submitPagingData(final Boolean isShowDateSeparators) {
|
||||
final var pagingData = this.chatViewModel.getMessages().getValue();
|
||||
if (pagingData == null) {
|
||||
LOGGER.info("PagingData not ready");
|
||||
return;
|
||||
}
|
||||
this.submitPagingData(pagingData, Boolean.TRUE.equals(isShowDateSeparators));
|
||||
}
|
||||
|
||||
private void submitPagingData(final PagingData<MessageWithContentReactions> pagingData) {
|
||||
submitPagingData(
|
||||
pagingData,
|
||||
Boolean.TRUE.equals(this.chatViewModel.isShowDateSeparators().getValue()));
|
||||
}
|
||||
|
||||
private void submitPagingData(
|
||||
final PagingData<MessageWithContentReactions> pagingData,
|
||||
final boolean insertSeparators) {
|
||||
if (insertSeparators) {
|
||||
messageAdapter.submitData(
|
||||
getLifecycle(), MessageAdapterItems.insertSeparators(pagingData));
|
||||
} else {
|
||||
messageAdapter.submitData(getLifecycle(), MessageAdapterItems.of(pagingData));
|
||||
}
|
||||
}
|
||||
|
||||
private void scrollToPositionToEnd() {
|
||||
this.recyclerViewScroller.scrollToPosition(0);
|
||||
}
|
||||
|
||||
private void scrollToMessageId(final long messageId) {
|
||||
LOGGER.info("scrollToMessageId({})", messageId);
|
||||
this.chatViewModel.setShowDateSeparators(false);
|
||||
final var future = this.chatViewModel.getMessagePosition(messageId);
|
||||
Futures.addCallback(
|
||||
future,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(final @NonNull Integer position) {
|
||||
recyclerViewScroller.scrollToPosition(
|
||||
position, () -> chatViewModel.setShowDateSeparators(true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull final Throwable throwable) {
|
||||
LOGGER.info("Could not scroll to {}", messageId, throwable);
|
||||
chatViewModel.setShowDateSeparators(true);
|
||||
}
|
||||
},
|
||||
MainThreadExecutor.getInstance());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,9 @@ import androidx.paging.Pager;
|
|||
import androidx.paging.PagingConfig;
|
||||
import androidx.paging.PagingData;
|
||||
import androidx.paging.PagingLiveData;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import im.conversations.android.database.model.ChatInfo;
|
||||
import im.conversations.android.database.model.MessageWithContentReactions;
|
||||
import im.conversations.android.repository.ChatRepository;
|
||||
|
@ -25,6 +28,7 @@ public class ChatViewModel extends AndroidViewModel {
|
|||
private final MutableLiveData<Long> chatId = new MutableLiveData<>();
|
||||
private final LiveData<ChatInfo> chatInfo;
|
||||
private final LiveData<PagingData<MessageWithContentReactions>> messages;
|
||||
private final MutableLiveData<Boolean> showDateSeparators = new MutableLiveData<>(true);
|
||||
|
||||
public ChatViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
|
@ -59,4 +63,31 @@ public class ChatViewModel extends AndroidViewModel {
|
|||
public LiveData<PagingData<MessageWithContentReactions>> getMessages() {
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> isShowDateSeparators() {
|
||||
return this.showDateSeparators;
|
||||
}
|
||||
|
||||
public void setShowDateSeparators(final boolean showDateSeparators) {
|
||||
this.showDateSeparators.postValue(showDateSeparators);
|
||||
}
|
||||
|
||||
public ListenableFuture<Integer> getMessagePosition(final long messageId) {
|
||||
final Long chatId = this.chatId.getValue();
|
||||
if (chatId == null) {
|
||||
return Futures.immediateFailedFuture(
|
||||
new IllegalStateException("Chat id has not been configured yet"));
|
||||
}
|
||||
return Futures.transform(
|
||||
this.chatRepository.getMessagePosition(chatId, messageId),
|
||||
position -> {
|
||||
if (position == null) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"messageId %s is not part of chat %s", messageId, chatId));
|
||||
}
|
||||
return position;
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue