clickable replies
This commit is contained in:
parent
f2012bc7f5
commit
2d92736810
|
@ -164,7 +164,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
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;
|
||||
|
@ -584,11 +586,16 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
}
|
||||
|
||||
public void populateWithMessages(final List<Message> messages) {
|
||||
if (historyPartMessages.size() > 0) {
|
||||
messages.clear();
|
||||
messages.addAll(this.historyPartMessages);
|
||||
} else {
|
||||
synchronized (this.messages) {
|
||||
messages.clear();
|
||||
messages.addAll(this.messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlocked() {
|
||||
|
@ -1144,23 +1151,38 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
String res1 = message.getCounterpart() == null ? null : message.getCounterpart().getResource();
|
||||
String res2 = nextCounterpart == null ? null : nextCounterpart.getResource();
|
||||
|
||||
List<Message> properListToAdd;
|
||||
|
||||
if (!historyPartMessages.isEmpty()) {
|
||||
properListToAdd = historyPartMessages;
|
||||
} else {
|
||||
properListToAdd = this.messages;
|
||||
}
|
||||
|
||||
if (nextCounterpart == null) {
|
||||
if (!message.isPrivateMessage()) {
|
||||
synchronized (this.messages) {
|
||||
this.messages.add(Math.min(offset, this.messages.size()), message);
|
||||
properListToAdd.add(Math.min(offset, properListToAdd.size()), message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (message.isPrivateMessage() && Objects.equals(res1, res2)) {
|
||||
synchronized (this.messages) {
|
||||
this.messages.add(Math.min(offset, this.messages.size()), message);
|
||||
}
|
||||
properListToAdd.add(Math.min(offset, properListToAdd.size()), message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addAll(int index, List<Message> messages) {
|
||||
ArrayList<Message> newM = new ArrayList<>();
|
||||
if (!historyPartMessages.isEmpty() && hasDuplicateMessage(historyPartMessages.get(historyPartMessages.size() - 1))) {
|
||||
messages.addAll(0, historyPartMessages);
|
||||
jumpToLatest();
|
||||
}
|
||||
}
|
||||
|
||||
public void addAll(int index, List<Message> messages, boolean fromPagination) {
|
||||
if (messages.isEmpty()) return;
|
||||
|
||||
List<Message> newM = new ArrayList<>();
|
||||
|
||||
if (nextCounterpart == null) {
|
||||
for(Message m : messages) {
|
||||
|
@ -1181,8 +1203,28 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
synchronized (this.messages) {
|
||||
this.messages.addAll(index, newM);
|
||||
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(newM);
|
||||
}
|
||||
|
@ -1213,6 +1255,43 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
|
|
@ -11,6 +11,8 @@ import android.os.SystemClock;
|
|||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
@ -31,6 +33,7 @@ import java.security.cert.CertificateException;
|
|||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -799,45 +802,86 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
|
||||
public ArrayList<Message> getMessages(Conversation conversations, int limit) {
|
||||
return getMessages(conversations, limit, -1);
|
||||
return getMessages(conversations, limit, -1, false);
|
||||
}
|
||||
|
||||
public ArrayList<Message> getMessages(Conversation conversation, int limit, long timestamp) {
|
||||
|
||||
@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 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) {
|
||||
if (conversation.getNextCounterpart() == null) {
|
||||
String[] selectionArgs = {conversation.getUuid()};
|
||||
cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
|
||||
+ "=?", selectionArgs, null, null, Message.TIME_SENT
|
||||
+ " DESC", String.valueOf(limit));
|
||||
+ sorting, String.valueOf(limit));
|
||||
} else {
|
||||
String[] selectionArgs = {conversation.getUuid(), String.valueOf(Message.TYPE_PRIVATE), String.valueOf(Message.TYPE_PRIVATE_FILE), conversation.getNextCounterpart().toString()};
|
||||
cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
|
||||
+ "=? and (" + Message.TYPE + "=? or " + Message.TYPE + "=?) and " + Message.COUNTERPART + "=?" , selectionArgs, null, null, Message.TIME_SENT
|
||||
+ " DESC", String.valueOf(limit));
|
||||
+ sorting, String.valueOf(limit));
|
||||
}
|
||||
} else {
|
||||
if (conversation.getNextCounterpart() == null) {
|
||||
String[] selectionArgs = {conversation.getUuid(),
|
||||
Long.toString(timestamp)};
|
||||
cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
|
||||
+ "=? and " + Message.TIME_SENT + "<?", selectionArgs,
|
||||
null, null, Message.TIME_SENT + " DESC",
|
||||
+ "=? and " + Message.TIME_SENT + comparsionOperation, selectionArgs,
|
||||
null, null, Message.TIME_SENT + sorting,
|
||||
String.valueOf(limit));
|
||||
} else {
|
||||
String[] selectionArgs = {conversation.getUuid(), String.valueOf(Message.TYPE_PRIVATE), String.valueOf(Message.TYPE_PRIVATE_FILE), conversation.getNextCounterpart().toString(), Long.toString(timestamp)};
|
||||
cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
|
||||
+ "=? and (" + Message.TYPE + "=? or " + Message.TYPE + "=?) and " + Message.COUNTERPART + "=? and " + Message.TIME_SENT + "<?" , selectionArgs, null, null, Message.TIME_SENT
|
||||
+ " DESC", String.valueOf(limit));
|
||||
+ "=? and (" + Message.TYPE + "=? or " + Message.TYPE + "=?) and " + Message.COUNTERPART + "=? and " + Message.TIME_SENT + comparsionOperation, selectionArgs, null, null, Message.TIME_SENT
|
||||
+ sorting, String.valueOf(limit));
|
||||
}
|
||||
}
|
||||
CursorUtils.upgradeCursorWindowSize(cursor);
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
Message m = Message.fromCursor(cursor, conversation);
|
||||
if (isForward) {
|
||||
list.add(m);
|
||||
} else {
|
||||
list.add(0, m);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(Config.LOGTAG, "unable to restore message");
|
||||
}
|
||||
|
|
|
@ -2050,7 +2050,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.findUnreadMessagesAndCalls(mNotificationService::pushFromBacklog);
|
||||
}
|
||||
|
@ -2175,20 +2175,46 @@ public class XmppConnectionService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
|
||||
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().queryInProgress(conversation, callback)) {
|
||||
return;
|
||||
} else if (timestamp == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -2359,7 +2385,7 @@ public class XmppConnectionService extends Service {
|
|||
final Conversation c = conversation;
|
||||
final Runnable runnable = () -> {
|
||||
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);
|
||||
}
|
||||
|
@ -5085,6 +5111,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);
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.app.Activity;
|
|||
import android.app.Fragment;
|
||||
import android.app.FragmentManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
@ -37,6 +38,7 @@ import android.text.TextUtils;
|
|||
import android.text.format.DateUtils;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.util.Range;
|
||||
import android.view.ActionMode;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
|
@ -49,6 +51,7 @@ import android.view.MotionEvent;
|
|||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.CycleInterpolator;
|
||||
import android.view.ViewParent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
@ -59,6 +62,7 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
|
|||
import android.widget.CheckBox;
|
||||
import android.widget.ListView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
@ -71,6 +75,7 @@ import androidx.appcompat.content.res.AppCompatResources;
|
|||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
|
@ -84,10 +89,13 @@ import java.util.Arrays;
|
|||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
@ -218,6 +226,8 @@ public class ConversationFragment extends XmppFragment
|
|||
@ColorInt
|
||||
private int primaryColor = -1;
|
||||
|
||||
private LinkedList<Message> replyJumps = new LinkedList<>();
|
||||
|
||||
private ActionMode selectionActionMode;
|
||||
private final OnClickListener clickToMuc =
|
||||
new OnClickListener() {
|
||||
|
@ -291,20 +301,47 @@ 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) {
|
||||
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(
|
||||
|
@ -370,8 +407,13 @@ public class ConversationFragment extends XmppFragment
|
|||
if (messageLoaderToast != null) {
|
||||
messageLoaderToast.cancel();
|
||||
}
|
||||
|
||||
if (!finalPaginateBackward) {
|
||||
conversation.historyPartLoadedForward.set(true);
|
||||
} else {
|
||||
conversation.messagesLoaded.set(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -398,8 +440,7 @@ public class ConversationFragment extends XmppFragment
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final EditMessage.OnCommitContentListener mEditorContentListener =
|
||||
new EditMessage.OnCommitContentListener() {
|
||||
@Override
|
||||
|
@ -546,6 +587,29 @@ 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.getMergedTimeSent() > lastVisibleMessage.getMergedTimeSent()) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
@ -673,6 +737,8 @@ public class ConversationFragment extends XmppFragment
|
|||
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);
|
||||
if (fragment instanceof ConversationFragment) {
|
||||
|
@ -770,7 +836,7 @@ public class ConversationFragment extends XmppFragment
|
|||
if (conversation == null) {
|
||||
return;
|
||||
}
|
||||
if (scrolledToBottom(listView)) {
|
||||
if (scrolledToBottom(listView) && !conversation.isInHistoryPart()) {
|
||||
lastMessageUuid = null;
|
||||
hideUnreadMessagesCount();
|
||||
} else {
|
||||
|
@ -797,6 +863,27 @@ 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
|
||||
|
@ -1265,6 +1352,9 @@ public class ConversationFragment extends XmppFragment
|
|||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
if (savedInstanceState == null) {
|
||||
conversation.jumpToLatest();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1391,6 +1481,7 @@ public class ConversationFragment extends XmppFragment
|
|||
quoteMessage(message);
|
||||
}
|
||||
});
|
||||
messageListAdapter.setReplyClickListener(this::scrollToReply);
|
||||
|
||||
binding.messagesView.setAdapter(messageListAdapter);
|
||||
|
||||
|
@ -1457,7 +1548,7 @@ public class ConversationFragment extends XmppFragment
|
|||
|
||||
SpannableStringBuilder body = message.getBodyForDisplaying();
|
||||
if (message.isFileOrImage() || message.isOOb()) body.append(" 🖼️");
|
||||
messageListAdapter.handleTextQuotes(body, activity.isDarkTheme(), false);
|
||||
messageListAdapter.handleTextQuotes(body, activity.isDarkTheme(), false, message);
|
||||
binding.contextPreviewText.setText(body);
|
||||
binding.contextPreviewAuthor.setText(message.getAvatarName());
|
||||
binding.contextPreview.setVisibility(View.VISIBLE);
|
||||
|
@ -1470,6 +1561,111 @@ public class ConversationFragment extends XmppFragment
|
|||
}
|
||||
}
|
||||
|
||||
private void scrollToReply(Message message) {
|
||||
Element reply = message.getReply();
|
||||
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.03f)
|
||||
.scaleY(1.03f)
|
||||
.setInterpolator(new CycleInterpolator(0.5f))
|
||||
.setDuration(300L)
|
||||
.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
|
||||
|
@ -2756,6 +2952,8 @@ public class ConversationFragment extends XmppFragment
|
|||
refreshCommands(false);
|
||||
}
|
||||
|
||||
replyJumps.clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -2808,6 +3006,7 @@ public class ConversationFragment extends XmppFragment
|
|||
}
|
||||
this.binding.scrollToBottomButton.setEnabled(false);
|
||||
this.binding.scrollToBottomButton.hide();
|
||||
replyJumps.clear();
|
||||
this.binding.unreadCountCustomView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
|
@ -2819,7 +3018,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) {
|
||||
|
@ -2913,6 +3112,12 @@ public class ConversationFragment extends XmppFragment
|
|||
if (message != null) {
|
||||
startDownloadable(message);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -110,6 +110,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
|||
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(
|
||||
ACTION_VIEW_CONVERSATION,
|
||||
|
|
|
@ -163,7 +163,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);
|
||||
|
|
|
@ -561,6 +561,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, messageUuid);
|
||||
}
|
||||
|
||||
public void switchToConversationAndQuote(Conversation conversation, String text) {
|
||||
switchToConversation(conversation, text, true, null, false, false);
|
||||
}
|
||||
|
@ -575,7 +579,7 @@ public abstract class XmppActivity extends ActionBarActivity {
|
|||
|
||||
protected void switchToConversationDoNotAppend(Contact contact, String body, String postInit) {
|
||||
Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), null, false, false, true, null);
|
||||
switchToConversation(conversation, body, false, null, false, true, postInit);
|
||||
switchToConversation(conversation, body, false, null, false, true, postInit, null);
|
||||
}
|
||||
|
||||
public void highlightInMuc(Conversation conversation, String nick) {
|
||||
|
@ -588,10 +592,10 @@ public abstract class XmppActivity extends ActionBarActivity {
|
|||
}
|
||||
|
||||
public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) {
|
||||
switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, null);
|
||||
switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, null, null);
|
||||
}
|
||||
|
||||
public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend, String postInit) {
|
||||
public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend, String postInit, String messageUuid) {
|
||||
if (conversation == null) return;
|
||||
|
||||
Intent intent = new Intent(this, ConversationsActivity.class);
|
||||
|
@ -610,6 +614,11 @@ 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);
|
||||
|
|
|
@ -14,7 +14,10 @@ import android.preference.PreferenceManager;
|
|||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextPaint;
|
||||
import android.text.format.DateUtils;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
|
@ -33,6 +36,7 @@ import android.widget.TextView;
|
|||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
@ -103,6 +107,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
private MessageEmptyPartClickListener messageEmptyPartClickListener;
|
||||
private SelectionStatusProvider selectionStatusProvider;
|
||||
private MessageBoxSwipedListener messageBoxSwipedListener;
|
||||
private ReplyClickListener replyClickListener;
|
||||
private boolean mUseGreenBackground = false;
|
||||
private final boolean mForceNames;
|
||||
|
||||
|
@ -193,6 +198,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
this.messageBoxSwipedListener = listener;
|
||||
}
|
||||
|
||||
public void setReplyClickListener(ReplyClickListener listener) {
|
||||
this.replyClickListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return 5;
|
||||
|
@ -402,7 +411,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
viewHolder.messageBody.setText(span);
|
||||
}
|
||||
|
||||
private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground, boolean highlightReply) {
|
||||
private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground, boolean highlightReply, Message message) {
|
||||
if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
|
||||
body.insert(start++, "\n");
|
||||
body.setSpan(
|
||||
|
@ -427,13 +436,28 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
|
||||
DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
|
||||
body.setSpan(new QuoteSpan(color, highlightReply ? ContextCompat.getColor(activity, R.color.blue_a100) : -1, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
if (highlightReply) {
|
||||
body.setSpan(new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
if (replyClickListener != null) {
|
||||
replyClickListener.onReplyClick(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
ds.setUnderlineText(false);
|
||||
}
|
||||
}, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies QuoteSpan to group of lines which starts with > or » characters.
|
||||
* Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
|
||||
*/
|
||||
public boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground, boolean highlightReply) {
|
||||
public boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground, boolean highlightReply, Message message) {
|
||||
boolean startsWithQuote = false;
|
||||
int quoteDepth = 1;
|
||||
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
|
||||
|
@ -452,7 +476,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
if (i == 0) startsWithQuote = true;
|
||||
} else if (quoteStart >= 0) {
|
||||
// Line start without quote, apply spans there
|
||||
applyQuoteSpan(body, quoteStart, i - 1, darkBackground, quoteDepth == 1 && highlightReply);
|
||||
applyQuoteSpan(body, quoteStart, i - 1, darkBackground, quoteDepth == 1 && highlightReply, message);
|
||||
quoteStart = -1;
|
||||
}
|
||||
}
|
||||
|
@ -477,7 +501,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
}
|
||||
if (quoteStart >= 0) {
|
||||
// Apply spans to finishing open quote
|
||||
applyQuoteSpan(body, quoteStart, body.length(), darkBackground, quoteDepth == 1 && highlightReply);
|
||||
applyQuoteSpan(body, quoteStart, body.length(), darkBackground, quoteDepth == 1 && highlightReply, message);
|
||||
}
|
||||
quoteDepth++;
|
||||
}
|
||||
|
@ -521,10 +545,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
int start = body.getSpanStart(quote);
|
||||
int end = body.getSpanEnd(quote);
|
||||
body.removeSpan(quote);
|
||||
applyQuoteSpan(body, start, end, darkBackground, message.getReply() != null);
|
||||
applyQuoteSpan(body, start, end, darkBackground, message.getReply() != null, message);
|
||||
}
|
||||
|
||||
boolean startsWithQuote = handleTextQuotes(body, darkBackground, message.getReply() != null);
|
||||
boolean startsWithQuote = handleTextQuotes(body, darkBackground, message.getReply() != null, message);
|
||||
if (!message.isPrivateMessage()) {
|
||||
if (hasMeCommand) {
|
||||
body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
|
||||
|
@ -1123,6 +1147,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
void onMessageBoxSwiped(Message message);
|
||||
}
|
||||
|
||||
public interface ReplyClickListener {
|
||||
void onReplyClick(Message message);
|
||||
}
|
||||
|
||||
private static class ViewHolder {
|
||||
public View root;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue