From 66a6bda6a18bce413dde86520199f782ca7cbec1 Mon Sep 17 00:00:00 2001 From: Arne Date: Mon, 20 Apr 2026 16:34:33 -0400 Subject: [PATCH] Initial jump to searched message --- .../conversations/entities/Conversation.java | 113 ++++- .../persistance/DatabaseBackend.java | 60 ++- .../services/XmppConnectionService.java | 43 +- .../ui/ConferenceDetailsActivity.java | 2 +- .../ui/ContactDetailsActivity.java | 9 + .../ui/ConversationFragment.java | 444 +++++++++++++----- .../ui/ConversationsActivity.java | 1 + .../conversations/ui/SearchActivity.java | 2 +- .../siacs/conversations/ui/XmppActivity.java | 12 +- .../ui/adapter/MessageAdapter.java | 19 +- .../conversations/ui/util/ListViewUtils.java | 12 + 11 files changed, 582 insertions(+), 135 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index f66315df91051abb70fc6ac1b96d16a2f37b33d2..ab0ff37699b6a0c5863a5e4cd52cc238692c5067 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -223,7 +223,9 @@ public class Conversation extends AbstractEntity private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message"; protected final ArrayList messages = new ArrayList<>(); + protected final ArrayList historyPartMessages = new ArrayList<>(); public AtomicBoolean messagesLoaded = new AtomicBoolean(true); + public AtomicBoolean historyPartLoadedForward = new AtomicBoolean(true); protected Account account = null; private String draftMessage; private final String name; @@ -770,11 +772,18 @@ public class Conversation extends AbstractEntity } public void populateWithMessages(final List messages, XmppConnectionService xmppConnectionService) { - synchronized (this.messages) { + if (historyPartMessages.size() > 0) { messages.clear(); - messages.addAll(this.messages); + messages.addAll(this.historyPartMessages); threads.clear(); reactions.clear(); + } else { + synchronized (this.messages) { + messages.clear(); + messages.addAll(this.messages); + threads.clear(); + reactions.clear(); + } } Set extraIds = new HashSet<>(); for (ListIterator iterator = messages.listIterator(messages.size()); iterator.hasPrevious(); ) { @@ -1583,15 +1592,72 @@ public class Conversation extends AbstractEntity public void prepend(int offset, Message message) { checkSpam(message); + + List properListToAdd; + + if (!historyPartMessages.isEmpty()) { + properListToAdd = historyPartMessages; + } else { + properListToAdd = this.messages; + } + synchronized (this.messages) { - this.messages.add(Math.min(offset, this.messages.size()), message); + properListToAdd.add(Math.min(offset, properListToAdd.size()), message); + } + + if (!historyPartMessages.isEmpty() && hasDuplicateMessage(historyPartMessages.get(historyPartMessages.size() - 1))) { + messages.addAll(0, historyPartMessages); + jumpToLatest(); } } - public void addAll(int index, List messages) { + public void addAll(int index, List messages, boolean fromPagination) { + if (messages.isEmpty()) return; checkSpam(messages.toArray(new Message[0])); + + List newM = new ArrayList<>(); + + if (nextCounterpart == null) { + for(Message m : messages) { + if (!m.isPrivateMessage() && m.encryption != Message.ENCRYPTION_OTR) { + newM.add(m); + } + } + + } else { + for(Message m : messages) { + String res1 = m.getCounterpart() == null ? null : m.getCounterpart().getResource(); + String res2 = nextCounterpart == null ? null : nextCounterpart.getResource(); + + + if ((m.isPrivateMessage() || m.encryption == Message.ENCRYPTION_OTR) && Objects.equals(res1, res2)) { + newM.add(m); + } + } + + } + synchronized (this.messages) { - this.messages.addAll(index, messages); + List properListToAdd; + + if (fromPagination && !historyPartMessages.isEmpty() && checkIsMergeable(newM)) { + historyPartMessages.addAll(newM); + newM = filterExisted(historyPartMessages); + index = 0; + jumpToLatest(); + } + + if (fromPagination && !historyPartMessages.isEmpty()) { + properListToAdd = historyPartMessages; + } else { + properListToAdd = this.messages; + } + + if (index == -1) { + properListToAdd.addAll(newM); + } else { + properListToAdd.addAll(index, newM); + } } account.getPgpDecryptionService().decrypt(messages); } @@ -1625,6 +1691,43 @@ public class Conversation extends AbstractEntity } } + public void jumpToHistoryPart(List messages) { + historyPartMessages.clear(); + + if (checkIsMergeable(messages)) { + addAll(0, filterExisted(messages), false); + } else { + historyPartMessages.addAll(messages); + } + } + + public void jumpToLatest() { + historyPartMessages.clear(); + } + + public boolean isInHistoryPart() { + return !historyPartMessages.isEmpty(); + } + + private boolean checkIsMergeable(List messages) { + if (messages.isEmpty()) return true; + return findDuplicateMessage(messages.get(messages.size() - 1)) != null; + } + + private List filterExisted(List messages) { + if (messages.isEmpty()) return Collections.emptyList(); + + List result = new ArrayList<>(); + + for (Message m : messages) { + if (findDuplicateMessage(m) == null) { + result.add(m); + } + } + + return result; + } + private void untieMessages() { for (Message message : this.messages) { message.untie(); diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 1d78675362f288d2091c79655bb9638451de5841..ecf9f948272a1745e3bfe79b8939761b1ba60f02 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -16,6 +16,8 @@ import android.util.Base64; import android.util.Log; import com.cheogram.android.WebxdcUpdate; +import androidx.annotation.Nullable; + import com.google.common.base.Stopwatch; import com.google.common.collect.Multimap; import com.google.common.collect.HashMultimap; @@ -1728,9 +1730,45 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public ArrayList getMessages(Conversation conversations, int limit) { - return getMessages(conversations, limit, -1); + return getMessages(conversations, limit, -1, false); } + + @Nullable + public ArrayList getMessagesNearUuid(Conversation conversation, int limit, String uuid) { + SQLiteDatabase db = this.getReadableDatabase(); + + String[] selectionArgs = {conversation.getUuid(), uuid, uuid, uuid}; + Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=? and (" + Message.SERVER_MSG_ID + "=? or " + Message.REMOTE_MSG_ID + "=? or " + Message.UUID + "=?)", selectionArgs, null, null, Message.TIME_SENT + + " DESC", String.valueOf(1)); + CursorUtils.upgradeCursorWindowSize(cursor); + Message anchorMessage = null; + while (cursor.moveToNext()) { + try { + anchorMessage = Message.fromCursor(cursor, conversation); + } catch (Exception e) { + Log.e(Config.LOGTAG, "unable to restore message"); + } + } + + cursor.close(); + + if (anchorMessage == null) { + return null; + } + + List prev = getMessages(conversation, limit / 2, anchorMessage.getTimeSent(), false); + List next = getMessages(conversation, limit / 2, anchorMessage.getTimeSent(), true); + + ArrayList list = new ArrayList<>(prev); + list.add(anchorMessage); + list.addAll(next); + + return list; + } + + public Map getMessageFuzzyIds(Conversation conversation, Collection ids) { final var result = new Hashtable(); if (ids.size() < 1) return result; @@ -1767,10 +1805,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { return result; } - public ArrayList getMessages(Conversation conversation, int limit, long timestamp) { + public ArrayList getMessages(Conversation conversation, int limit, long timestamp, boolean isForward) { ArrayList list = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor; + String comparsionOperation = isForward ? ">? " : " markMessage(message, Message.STATUS_WAITING)); conversation.findMessagesAndCallsToNotify(mNotificationService::pushFromBacklog); } @@ -2598,9 +2598,24 @@ public class XmppConnectionService extends Service { } } + public void jumpToMessage(final Conversation conversation, final String uuid, JumpToMessageListener listener) { + final Runnable runnable = () -> { + List messages = databaseBackend.getMessagesNearUuid(conversation, 30, uuid); + if (messages != null && !messages.isEmpty()) { + conversation.jumpToHistoryPart(messages); + listener.onSuccess(); + } else { + listener.onNotFound(); + } + }; + + mDatabaseReaderExecutor.execute(runnable); + } + public void loadMoreMessages( final Conversation conversation, final long timestamp, + boolean isForward, final OnMoreMessagesLoaded callback) { if (XmppConnectionService.this .getMessageArchiveService() @@ -2615,15 +2630,26 @@ public class XmppConnectionService extends Service { + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); + + if (isForward) { + Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " after " + MessageGenerator.getTimestamp(timestamp)); + } else { + Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); + } + final Runnable runnable = () -> { final Account account = conversation.getAccount(); - List messages = - databaseBackend.getMessages(conversation, 50, timestamp); + List messages = databaseBackend.getMessages(conversation, Config.PAGE_SIZE, timestamp, isForward); if (messages.size() > 0) { - conversation.addAll(0, messages); + if (isForward) { + conversation.addAll(-1, messages, true); + } else { + conversation.addAll(0, messages, true); + } callback.onMoreMessagesLoaded(messages.size(), conversation); - } else if (conversation.hasMessagesLeftOnServer() + } else if (!isForward && + conversation.hasMessagesLeftOnServer() && account.isOnlineAndConnected() && conversation.getLastClearHistory().getTimestamp() == 0) { final boolean mamAvailable; @@ -2927,7 +2953,7 @@ public class XmppConnectionService extends Service { final var singleMode = c.getMode() == Conversational.MODE_SINGLE; final var account = c.getAccount(); if (loadMessagesFromDb) { - c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE)); + c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE), false); updateConversationUi(); c.messagesLoaded.set(true); } @@ -5144,6 +5170,11 @@ public class XmppConnectionService extends Service { void informUser(int r); } + public interface JumpToMessageListener { + void onSuccess(); + void onNotFound(); + } + public interface OnMoreMessagesLoaded { void onMoreMessagesLoaded(int count, Conversation conversation); diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 26a79446d385a0c979517fd1855a93b527193030..709e1c381e2f3c86a2cd15baf83112e2d6623432 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -345,7 +345,7 @@ public class ConferenceDetailsActivity extends XmppActivity GridManager.setupLayoutManager(this, this.binding.users, R.dimen.media_size); this.binding.recentThreads.setOnItemClickListener((a0, v, pos, a3) -> { final Conversation.Thread thread = (Conversation.Thread) binding.recentThreads.getAdapter().getItem(pos); - switchToConversation(mConversation, null, false, null, false, true, null, thread.getThreadId()); + switchToConversation(mConversation, null, false, null, false, true, null, thread.getThreadId(), null); }); this.binding.invite.setOnClickListener(v -> inviteToConversation(mConversation)); this.binding.showUsers.setOnClickListener( diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 49d50627247441aa7c0080e1d24c6b77481d61b5..66b049b16a8970ba475fa208bd741cd86caa7659 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -300,6 +300,15 @@ public class ContactDetailsActivity extends OmemoActivity mMediaAdapter = new MediaAdapter(this, R.dimen.media_size); this.binding.media.setAdapter(mMediaAdapter); GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size); + this.binding.recentThreads.setOnItemClickListener((a0, v, pos, a3) -> { + Account thisAccount = xmppConnectionService.findAccountByJid(accountJid); + if (thisAccount == null) { + return; + } + final var conversation = xmppConnectionService.findOrCreateConversation(thisAccount, contact.getJid(), false, true); + final Conversation.Thread thread = (Conversation.Thread) binding.recentThreads.getAdapter().getItem(pos); + switchToConversation(conversation, null, false, null, false, true, null, thread.getThreadId(), null); + }); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 320e7aad33ca625f3f9d3741130c33114af1284d..f927f830f1d43bf4caf1ee38457b268fc93ee0e1 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -16,6 +16,7 @@ import android.app.DatePickerDialog; import android.app.Fragment; import android.app.FragmentManager; import android.app.PendingIntent; +import android.app.ProgressDialog; import android.app.TimePickerDialog; import android.content.ActivityNotFoundException; import android.content.Context; @@ -56,6 +57,9 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.CycleInterpolator; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.view.WindowManager; @@ -125,6 +129,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -293,6 +298,30 @@ public class ConversationFragment extends XmppFragment private int identiconWidth = -1; private File savingAsSticker = null; private EmojiSearch emojiSearch = null; + File dirStickers = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/monocles chat" + File.separator + "Stickers"); + private String[] StickerfilesPaths; + private String[] StickerfilesNames; + private String[] GifsfilesPaths; + private String[] GifsfilesNames; + + private LinkedList replyJumps = new LinkedList<>(); + + private PinnedMessageRepository pinnedMessageRepository; + private String currentDisplayedPinnedMessageUuid = null; + + private final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor(); + + private KeyboardHeightProvider.KeyboardHeightListener keyboardHeightListener = null; + private KeyboardHeightProvider keyboardHeightProvider = null; + private static final String PINNED_MESSAGE_KEY_PREFIX = "pinned_message_"; + + protected OnClickListener clickToVerify = new OnClickListener() { + @Override + public void onClick(View v) { + activity.verifyOtrSessionDialog(conversation, v); + } + }; + private final OnClickListener clickToMuc = new OnClickListener() { @@ -353,7 +382,6 @@ public class ConversationFragment extends XmppFragment @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) { - updateThreadFromLastMessage(); fireReadEvent(); } } @@ -366,113 +394,146 @@ public class ConversationFragment extends XmppFragment int totalItemCount) { toggleScrollDownButton(view); synchronized (ConversationFragment.this.messageList) { - if (firstVisibleItem < 5 - && conversation != null - && conversation.messagesLoaded.compareAndSet(true, false) - && messageList.size() > 0 - && activity != null) { - long timestamp = conversation.loadMoreTimestamp(); - activity.xmppConnectionService.loadMoreMessages( - conversation, - timestamp, - new XmppConnectionService.OnMoreMessagesLoaded() { - @Override - public void onMoreMessagesLoaded( - final int c, final Conversation conversation) { - if (ConversationFragment.this.conversation - != conversation) { + boolean paginateBackward = firstVisibleItem < 5; + boolean paginationForward = conversation.isInHistoryPart() && firstVisibleItem + visibleItemCount + 5 > totalItemCount; + loadMoreMessages(paginateBackward, paginationForward, view); + } + } + }; + + private void loadMoreMessages(boolean paginateBackward, boolean paginationForward, AbsListView view) { + if (paginateBackward && !conversation.messagesLoaded.get()) { + paginateBackward = false; + } + + if ( + conversation != null && + messageList.size() > 0 && + ((paginateBackward && conversation.messagesLoaded.compareAndSet(true, false)) || + (paginationForward && conversation.historyPartLoadedForward.compareAndSet(true, false))) + ) { + long timestamp; + + if (paginateBackward) { + if (messageList.get(0).getType() == Message.TYPE_STATUS + && messageList.size() >= 2) { + timestamp = messageList.get(1).getTimeSent(); + } else { + timestamp = messageList.get(0).getTimeSent(); + } + } else { + if (messageList.get(messageList.size() - 1).getType() == Message.TYPE_STATUS + && messageList.size() >= 2) { + timestamp = messageList.get(messageList.size() - 2).getTimeSent(); + } else { + timestamp = messageList.get(messageList.size() - 1).getTimeSent(); + } + } + + boolean finalPaginateBackward = paginateBackward; + activity.xmppConnectionService.loadMoreMessages( + conversation, + timestamp, + !paginateBackward, + new XmppConnectionService.OnMoreMessagesLoaded() { + @Override + public void onMoreMessagesLoaded( + final int c, final Conversation conversation) { + if (ConversationFragment.this.conversation + != conversation) { + conversation.messagesLoaded.set(true); + return; + } + runOnUiThread( + () -> { + synchronized (messageList) { + final int oldPosition = + binding.messagesView + .getFirstVisiblePosition(); + Message message = null; + int childPos; + for (childPos = 0; + childPos + oldPosition + < messageList.size(); + ++childPos) { + message = + messageList.get( + oldPosition + + childPos); + if (message.getType() + != Message.TYPE_STATUS) { + break; + } + } + final String uuid = + message != null + ? message.getUuid() + : null; + View v = + binding.messagesView.getChildAt( + childPos); + final int pxOffset = + (v == null) ? 0 : v.getTop(); + ConversationFragment.this.conversation + .populateWithMessages( + ConversationFragment + .this + .messageList, + activity == null ? null : activity.xmppConnectionService); + try { + updateStatusMessages(); + } catch (IllegalStateException e) { + Log.d( + Config.LOGTAG, + "caught illegal state exception while updating status messages"); + } + messageListAdapter + .notifyDataSetChanged(); + int pos = + Math.max( + getIndexOf( + uuid, + messageList), + 0); + binding.messagesView + .setSelectionFromTop( + pos, pxOffset); + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + + if (!finalPaginateBackward) { + conversation.historyPartLoadedForward.set(true); + } else { conversation.messagesLoaded.set(true); - return; } - runOnUiThread( - () -> { - synchronized (messageList) { - final int oldPosition = - binding.messagesView - .getFirstVisiblePosition(); - Message message = null; - int childPos; - for (childPos = 0; - childPos + oldPosition - < messageList.size(); - ++childPos) { - message = - messageList.get( - oldPosition - + childPos); - if (message.getType() - != Message.TYPE_STATUS) { - break; - } - } - final String uuid = - message != null - ? message.getUuid() - : null; - View v = - binding.messagesView.getChildAt( - childPos); - final int pxOffset = - (v == null) ? 0 : v.getTop(); - ConversationFragment.this.conversation - .populateWithMessages( - ConversationFragment - .this - .messageList, activity == null ? null : activity.xmppConnectionService); - try { - updateStatusMessages(); - } catch (IllegalStateException e) { - Log.d( - Config.LOGTAG, - "caught illegal state" - + " exception while" - + " updating status" - + " messages"); - } - messageListAdapter - .notifyDataSetChanged(); - int pos = - Math.max( - getIndexOf( - uuid, - messageList), - 0); - binding.messagesView - .setSelectionFromTop( - pos, pxOffset); - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - conversation.messagesLoaded.set(true); - } - }); } + }); + } - @Override - public void informUser(final int resId) { - - runOnUiThread( - () -> { - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - if (ConversationFragment.this.conversation - != conversation) { - return; - } - messageLoaderToast = - Toast.makeText( - view.getContext(), - resId, - Toast.LENGTH_LONG); - messageLoaderToast.show(); - }); + @Override + public void informUser(final int resId) { + + runOnUiThread( + () -> { + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); } + if (ConversationFragment.this.conversation + != conversation) { + return; + } + messageLoaderToast = + Toast.makeText( + view.getContext(), + resId, + Toast.LENGTH_LONG); + messageLoaderToast.show(); }); } - } - } - }; + }); + } + } private final EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() { @Override @@ -619,6 +680,28 @@ public class ConversationFragment extends XmppFragment @Override public void onClick(View v) { stopScrolling(); + + if (!replyJumps.isEmpty()) { + int lastVisiblePosition = binding.messagesView.getLastVisiblePosition(); + Message lastVisibleMessage = messageListAdapter.getItem(lastVisiblePosition); + if (lastVisibleMessage == null) { + replyJumps.clear(); + } else { + while (!replyJumps.isEmpty()) { + Message jump = replyJumps.pop(); + if (jump.getTimeSent() > lastVisibleMessage.getTimeSent()) { + Runnable postSelectionRunnable = () -> highlightMessage(jump.getUuid()); + updateSelection(jump.getUuid(), binding.messagesView.getHeight() / 2, postSelectionRunnable, false, false); + return; + } + } + } + } + + if (conversation.isInHistoryPart()) { + conversation.jumpToLatest(); + refresh(false); + } setSelection(binding.messagesView.getCount() - 1, true); } }; @@ -682,6 +765,7 @@ public class ConversationFragment extends XmppFragment private int lastCompletionCursor; private boolean firstWord = false; private Message mPendingDownloadableMessage; + private ProgressDialog fetchHistoryDialog; private static ConversationFragment findConversationFragment(Activity activity) { Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment); @@ -780,7 +864,7 @@ public class ConversationFragment extends XmppFragment if (conversation == null) { return; } - if (scrolledToBottom(listView)) { + if (scrolledToBottom(listView) && !conversation.isInHistoryPart()) { lastMessageUuid = null; hideUnreadMessagesCount(); } else { @@ -807,6 +891,26 @@ public class ConversationFragment extends XmppFragment return -1; } + private int getIndexOfExtended(String uuid, List messages) { + if (uuid == null) { + return messages.size() - 1; + } + for (int i = 0; i < messages.size(); ++i) { + if (uuid.equals(messages.get(i).getServerMsgId())) { + return i; + } + + if (uuid.equals(messages.get(i).getRemoteMsgId())) { + return i; + } + + if (uuid.equals(messages.get(i).getUuid())) { + return i; + } + } + return -1; + } + private ScrollState getScrollPosition() { final ListView listView = this.binding == null ? null : this.binding.messagesView; if (listView == null @@ -1394,6 +1498,9 @@ public class ConversationFragment extends XmppFragment super.onCreate(savedInstanceState); setHasOptionsMenu(true); activity.getOnBackPressedDispatcher().addCallback(this, backPressedLeaveSingleThread); + if (savedInstanceState == null) { + conversation.jumpToLatest(); + } } @Override @@ -1500,6 +1607,7 @@ public class ConversationFragment extends XmppFragment messageListAdapter.setOnContactPictureLongClicked(this); messageListAdapter.setOnInlineImageLongClicked(this); messageListAdapter.setConversationFragment(this); + // messageListAdapter.setReplyClickListener(this::scrollToReply); //TODO add a better scrol to reply later binding.messagesView.setAdapter(messageListAdapter); binding.textinput.addTextChangedListener( @@ -1805,6 +1913,114 @@ public class ConversationFragment extends XmppFragment updateSendButton(); } + + private void scrollToReply(Message message) { + Element reply = message.getReply(); + if (reply == null) return; + + String replyId = reply.getAttribute("id"); + + if (replyId != null) { + Runnable postSelectionRunnable = () -> highlightMessage(replyId); + replyJumps.push(message); + updateSelection(replyId, binding.messagesView.getHeight() / 2, postSelectionRunnable, true, false); + } + } + + private void highlightMessage(String uuid) { + binding.messagesView.postDelayed(() -> { + int actualIndex = getIndexOfExtended(uuid, messageList); + + if (actualIndex == -1) { + return; + } + + View view = ListViewUtils.getViewByPosition(actualIndex, binding.messagesView); + View messageBox = view.findViewById(R.id.message_box); + if (messageBox != null) { + messageBox.animate() + .scaleX(1.14f) + .scaleY(1.14f) + .setInterpolator(new CycleInterpolator(0.5f)) + .setDuration(400L) + .start(); + } + }, 300L); + } + + private void updateSelection(String uuid, Integer offsetFormTop, Runnable selectionUpdatedRunnable, boolean populateFromMam, boolean recursiveFetch) { + if (recursiveFetch && (fetchHistoryDialog == null || !fetchHistoryDialog.isShowing())) return; + + int pos = getIndexOfExtended(uuid, messageList); + + Runnable updateSelectionRunnable = () -> { + FragmentConversationBinding binding = ConversationFragment.this.binding; + + Runnable performRunnable = () -> { + if (offsetFormTop != null) { + binding.messagesView.setSelectionFromTop(pos, offsetFormTop); + return; + } + + binding.messagesView.setSelection(pos); + }; + + performRunnable.run(); + binding.messagesView.post(performRunnable); + + if (selectionUpdatedRunnable != null) { + selectionUpdatedRunnable.run(); + } + }; + + if (pos != -1) { + hideFetchHistoryDialog(); + updateSelectionRunnable.run(); + } else { + activity.xmppConnectionService.jumpToMessage(conversation, uuid, new XmppConnectionService.JumpToMessageListener() { + @Override + public void onSuccess() { + activity.runOnUiThread(() -> { + refresh(false); + conversation.messagesLoaded.set(true); + conversation.historyPartLoadedForward.set(true); + toggleScrollDownButton(); + updateSelection(uuid, binding.messagesView.getHeight() / 2, selectionUpdatedRunnable, populateFromMam, false); + }); + } + + @Override + public void onNotFound() { + activity.runOnUiThread(() -> { + if (populateFromMam && conversation.hasMessagesLeftOnServer()) { + showFetchHistoryDialog(); + loadMoreMessages(true, false, binding.messagesView); + binding.messagesView.postDelayed(() -> updateSelection(uuid, binding.messagesView.getHeight() / 2, selectionUpdatedRunnable, populateFromMam, true), 500L); + } else { + hideFetchHistoryDialog(); + } + }); + } + }); + } + } + + private void showFetchHistoryDialog() { + if (fetchHistoryDialog != null && fetchHistoryDialog.isShowing()) return; + + fetchHistoryDialog = new ProgressDialog(getActivity()); + fetchHistoryDialog.setIndeterminate(true); + fetchHistoryDialog.setMessage(getString(R.string.please_wait)); + fetchHistoryDialog.setCancelable(true); + fetchHistoryDialog.show(); + } + + private void hideFetchHistoryDialog() { + if (fetchHistoryDialog != null && fetchHistoryDialog.isShowing()) { + fetchHistoryDialog.hide(); + } + } + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { // This should cancel any remaining click events that would otherwise trigger links @@ -3341,7 +3557,7 @@ public class ConversationFragment extends XmppFragment super.onStart(); if (this.reInitRequiredOnStart && this.conversation != null) { final Bundle extras = pendingExtras.pop(); - reInit(this.conversation, extras != null); + reInit(this.conversation, extras != null, extras != null && extras.getString(ConversationsActivity.EXTRA_MESSAGE_UUID) != null); if (extras != null) { processExtras(extras); } @@ -3407,7 +3623,7 @@ public class ConversationFragment extends XmppFragment this.saveMessageDraftStopAudioPlayer(); } this.clearPending(); - if (this.reInit(conversation, extras != null)) { + if (this.reInit(conversation, extras != null, extras != null && extras.getString(ConversationsActivity.EXTRA_MESSAGE_UUID) != null)) { if (extras != null) { processExtras(extras); } @@ -3420,10 +3636,13 @@ public class ConversationFragment extends XmppFragment } private void reInit(Conversation conversation) { - reInit(conversation, false); + reInit(conversation, false, false); + if (activity != null) { + activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); + } } - private boolean reInit(final Conversation conversation, final boolean hasExtras) { + private boolean reInit(final Conversation conversation, final boolean hasExtras, final boolean hasMessageUUID) { if (conversation == null) { return false; } @@ -3504,7 +3723,7 @@ public class ConversationFragment extends XmppFragment this.conversation.messagesLoaded.set(true); Log.d(Config.LOGTAG, "scrolledToBottomAndNoPending=" + scrolledToBottomAndNoPending); - if (hasExtras || scrolledToBottomAndNoPending) { + if (!hasMessageUUID && (hasExtras || scrolledToBottomAndNoPending)) { resetUnreadMessagesCount(); synchronized (this.messageList) { Log.d(Config.LOGTAG, "jump to first unread message"); @@ -3547,9 +3766,8 @@ public class ConversationFragment extends XmppFragment }); refreshCommands(false); } - binding.commandsNote.setVisibility(activity.xmppConnectionService.isOnboarding() ? View.VISIBLE : View.GONE); - + replyJumps.clear(); return true; } @@ -3650,6 +3868,7 @@ public class ConversationFragment extends XmppFragment } this.binding.scrollToBottomButton.setEnabled(false); this.binding.scrollToBottomButton.hide(); + replyJumps.clear(); this.binding.unreadCountCustomView.setVisibility(View.GONE); } @@ -3661,7 +3880,7 @@ public class ConversationFragment extends XmppFragment } private boolean scrolledToBottom() { - return this.binding != null && scrolledToBottom(this.binding.messagesView); + return !conversation.isInHistoryPart() && this.binding != null && scrolledToBottom(this.binding.messagesView); } private void processExtras(final Bundle extras) { @@ -3783,6 +4002,11 @@ public class ConversationFragment extends XmppFragment conversation.startCommand(commandFor(Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register"), "jabber:iq:register"), activity.xmppConnectionService); } } + String messageUuid = extras.getString(ConversationsActivity.EXTRA_MESSAGE_UUID); + if (messageUuid != null) { + Runnable postSelectionRunnable = () -> highlightMessage(messageUuid); + updateSelection(messageUuid, binding.messagesView.getHeight() / 2, postSelectionRunnable, false, false); + } } private Element commandFor(final Jid jid, final String node) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 75232fb680e25087899101bd626e942ed4cb31a5..e44e701151175e954a517c599707038cb46294d6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -148,6 +148,7 @@ public class ConversationsActivity extends XmppActivity public static final String EXTRA_TYPE = "type"; public static final String EXTRA_NODE = "node"; public static final String EXTRA_JID = "jid"; + public static final String EXTRA_MESSAGE_UUID = "messageUuid"; private static final List VIEW_AND_SHARE_ACTIONS = Arrays.asList( diff --git a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java index 5803a20e5876d694ae37c56453b9ce1f21bbe549..6fd0cd34ef211cc24535d5ad14a545a4c3e5efab 100644 --- a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java @@ -165,7 +165,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc if (message != null) { switch (item.getItemId()) { case R.id.open_conversation: - switchToConversation(wrap(message.getConversation())); + switchToConversationOnMessage(wrap(message.getConversation()), message.getUuid()); break; case R.id.share_with: ShareUtil.share(this, message); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 4b708ebcb5db6e019bada5631fedbba8ce906ea9..206e1e79fb0f657c0e95d0c1c37c1c2db4399c95 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -793,6 +793,10 @@ public abstract class XmppActivity extends ActionBarActivity { switchToConversation(conversation, null); } + public void switchToConversationOnMessage(Conversation conversation, String messageUuid) { + switchToConversation(conversation, null, false, null, false, false, null, null, messageUuid); + } + public void switchToConversationAndQuote(Conversation conversation, String text) { switchToConversation(conversation, text, true, null, false, false); } @@ -818,7 +822,7 @@ public abstract class XmppActivity extends ActionBarActivity { } public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend, String postInit) { - switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, postInit, null); + switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, postInit, null, null); } public void switchToConversation( @@ -829,7 +833,8 @@ public abstract class XmppActivity extends ActionBarActivity { boolean pm, boolean doNotAppend, String postInit, - String thread) { + String thread, + String messageUuid) { if (conversation == null) return; Intent intent = new Intent(this, ConversationsActivity.class); intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); @@ -848,6 +853,9 @@ public abstract class XmppActivity extends ActionBarActivity { if (doNotAppend) { intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true); } + if (messageUuid != null) { + intent.putExtra(ConversationsActivity.EXTRA_MESSAGE_UUID, messageUuid); + } intent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, postInit); intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 01057198dbb1bd65bc87d9be66684423e5661b7c..6f88f6720617e738687688d0489aca5ec1ca13a9 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -124,6 +124,7 @@ import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.NotificationService; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.Activities; import eu.siacs.conversations.ui.BindingAdapters; import eu.siacs.conversations.ui.ConversationFragment; @@ -1515,8 +1516,22 @@ public class MessageAdapter extends ArrayAdapter { } else { viewHolder.inReplyToBox().setVisibility(View.VISIBLE); viewHolder.inReplyTo().setText(UIHelper.getMessageDisplayName(message.getInReplyTo())); - viewHolder.inReplyTo().setOnClickListener((v) -> mConversationFragment.jumpTo(message.getInReplyTo())); - viewHolder.inReplyToQuote().setOnClickListener((v) -> mConversationFragment.jumpTo(message.getInReplyTo())); + final var replyToClickListener = (View.OnClickListener) (v) -> { + final Message inReplyTo = message.getInReplyTo(); + if (inReplyTo == null || inReplyTo.getUuid() == null) return; + final var replyConversation = mConversationFragment.getConversation(); + activity.xmppConnectionService.jumpToMessage(replyConversation, inReplyTo.getUuid(), new XmppConnectionService.JumpToMessageListener() { + @Override + public void onSuccess() { + activity.runOnUiThread(() -> mConversationFragment.refresh()); + } + + @Override + public void onNotFound() {} + }); + }; + viewHolder.inReplyTo().setOnClickListener(replyToClickListener); + viewHolder.inReplyToQuote().setOnClickListener(replyToClickListener); setTextColor(viewHolder.inReplyTo(), bubbleColor); } diff --git a/src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java b/src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java index 4fae7f13a9af4b155f8a8b6bf9b96a590dbd736a..b24362dc967437ecb96a1982f4b4c4bf3db05f9d 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java @@ -53,5 +53,17 @@ public class ListViewUtils { listView.setSelection(pos); } + public static View getViewByPosition(int pos, ListView listView) { + final int firstListItemPosition = listView.getFirstVisiblePosition(); + final int lastListItemPosition = firstListItemPosition + listView.getChildCount() - 1; + + if (pos < firstListItemPosition || pos > lastListItemPosition ) { + return listView.getAdapter().getView(pos, null, listView); + } else { + final int childIndex = pos - firstListItemPosition; + return listView.getChildAt(childIndex); + } + } + }