Detailed changes
@@ -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<Message> messages = new ArrayList<>();
+ protected final ArrayList<Message> 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<Message> 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<String> extraIds = new HashSet<>();
for (ListIterator<Message> 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<Message> 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<Message> messages) {
+ public void addAll(int index, List<Message> messages, boolean fromPagination) {
+ if (messages.isEmpty()) return;
checkSpam(messages.toArray(new Message[0]));
+
+ List<Message> 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<Message> 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<Message> 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<Message> messages) {
+ if (messages.isEmpty()) return true;
+ return findDuplicateMessage(messages.get(messages.size() - 1)) != null;
+ }
+
+ private List<Message> filterExisted(List<Message> messages) {
+ if (messages.isEmpty()) return Collections.emptyList();
+
+ List<Message> 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();
@@ -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<Message> getMessages(Conversation conversations, int limit) {
- return getMessages(conversations, limit, -1);
+ return getMessages(conversations, limit, -1, false);
}
+
+ @Nullable
+ public ArrayList<Message> 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<Message> prev = getMessages(conversation, limit / 2, anchorMessage.getTimeSent(), false);
+ List<Message> next = getMessages(conversation, limit / 2, anchorMessage.getTimeSent(), true);
+
+ ArrayList<Message> list = new ArrayList<>(prev);
+ list.add(anchorMessage);
+ list.addAll(next);
+
+ return list;
+ }
+
+
public Map<String, Message> getMessageFuzzyIds(Conversation conversation, Collection<String> ids) {
final var result = new Hashtable<String, Message>();
if (ids.size() < 1) return result;
@@ -1767,10 +1805,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return result;
}
- public ArrayList<Message> getMessages(Conversation conversation, int limit, long timestamp) {
+ public ArrayList<Message> getMessages(Conversation conversation, int limit, long timestamp, boolean isForward) {
ArrayList<Message> list = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor;
+ String comparsionOperation = isForward ? ">? " : "<? ";
+ String sorting = isForward ? " ASC " : " DESC ";
if (timestamp == -1) {
String[] selectionArgs = {conversation.getUuid()};
cursor = db.rawQuery(
@@ -1780,9 +1820,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
" WHERE " + Message.UUID + " IN (" +
"SELECT " + Message.UUID + " FROM " + Message.TABLENAME +
" WHERE " + Message.CONVERSATION + "=? " +
- "ORDER BY " + Message.TIME_SENT + " DESC " +
+ "ORDER BY " + Message.TIME_SENT + sorting +
"LIMIT " + String.valueOf(limit) + ") " +
- "ORDER BY " + Message.TIME_SENT + " DESC ",
+ "ORDER BY " + Message.TIME_SENT + sorting,
selectionArgs
);
} else {
@@ -1795,10 +1835,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
" WHERE " + Message.UUID + " IN (" +
"SELECT " + Message.UUID + " FROM " + Message.TABLENAME +
" WHERE " + Message.CONVERSATION + "=? AND " +
- Message.TIME_SENT + "<? " +
- "ORDER BY " + Message.TIME_SENT + " DESC " +
+ Message.TIME_SENT + comparsionOperation +
+ "ORDER BY " + Message.TIME_SENT + sorting +
"LIMIT " + String.valueOf(limit) + ") " +
- "ORDER BY " + Message.TIME_SENT + " DESC ",
+ "ORDER BY " + Message.TIME_SENT + sorting,
selectionArgs
);
}
@@ -1813,7 +1853,11 @@ public class DatabaseBackend extends SQLiteOpenHelper {
replyIds.add(reply.getAttribute("id"));
waitingForReplies.put(reply.getAttribute("id"), m);
}
- list.add(0, m);
+ if (isForward) {
+ list.add(m);
+ } else {
+ list.add(0, m);
+ }
} catch (Exception e) {
Log.e(Config.LOGTAG, "unable to restore message", e);
}
@@ -2457,7 +2457,7 @@ public class XmppConnectionService extends Service {
}
private void restoreMessages(Conversation conversation) {
- conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
+ conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE), false);
conversation.findUnsentTextMessages(message -> 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<Message> 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<Message> messages =
- databaseBackend.getMessages(conversation, 50, timestamp);
+ List<Message> 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);
@@ -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(
@@ -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
@@ -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<Message> 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<Message> 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) {
@@ -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<String> VIEW_AND_SHARE_ACTIONS =
Arrays.asList(
@@ -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);
@@ -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);
@@ -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<Message> {
} 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);
}
@@ -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);
+ }
+ }
+
}