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_NEXT_ENCRYPTION = "next_encryption";
|
||||||
private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message";
|
private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message";
|
||||||
protected final ArrayList<Message> messages = new ArrayList<>();
|
protected final ArrayList<Message> messages = new ArrayList<>();
|
||||||
|
protected final ArrayList<Message> historyPartMessages = new ArrayList<>();
|
||||||
public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
|
public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
|
||||||
|
public AtomicBoolean historyPartLoadedForward = new AtomicBoolean(true);
|
||||||
protected Account account = null;
|
protected Account account = null;
|
||||||
private String draftMessage;
|
private String draftMessage;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@ -584,9 +586,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
}
|
}
|
||||||
|
|
||||||
public void populateWithMessages(final List<Message> messages) {
|
public void populateWithMessages(final List<Message> messages) {
|
||||||
synchronized (this.messages) {
|
if (historyPartMessages.size() > 0) {
|
||||||
messages.clear();
|
messages.clear();
|
||||||
messages.addAll(this.messages);
|
messages.addAll(this.historyPartMessages);
|
||||||
|
} else {
|
||||||
|
synchronized (this.messages) {
|
||||||
|
messages.clear();
|
||||||
|
messages.addAll(this.messages);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1144,23 +1151,38 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
String res1 = message.getCounterpart() == null ? null : message.getCounterpart().getResource();
|
String res1 = message.getCounterpart() == null ? null : message.getCounterpart().getResource();
|
||||||
String res2 = nextCounterpart == null ? null : nextCounterpart.getResource();
|
String res2 = nextCounterpart == null ? null : nextCounterpart.getResource();
|
||||||
|
|
||||||
|
List<Message> properListToAdd;
|
||||||
|
|
||||||
|
if (!historyPartMessages.isEmpty()) {
|
||||||
|
properListToAdd = historyPartMessages;
|
||||||
|
} else {
|
||||||
|
properListToAdd = this.messages;
|
||||||
|
}
|
||||||
|
|
||||||
if (nextCounterpart == null) {
|
if (nextCounterpart == null) {
|
||||||
if (!message.isPrivateMessage()) {
|
if (!message.isPrivateMessage()) {
|
||||||
synchronized (this.messages) {
|
synchronized (this.messages) {
|
||||||
this.messages.add(Math.min(offset, this.messages.size()), message);
|
properListToAdd.add(Math.min(offset, properListToAdd.size()), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (message.isPrivateMessage() && Objects.equals(res1, res2)) {
|
if (message.isPrivateMessage() && Objects.equals(res1, res2)) {
|
||||||
synchronized (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) {
|
||||||
ArrayList<Message> newM = new ArrayList<>();
|
if (messages.isEmpty()) return;
|
||||||
|
|
||||||
|
List<Message> newM = new ArrayList<>();
|
||||||
|
|
||||||
if (nextCounterpart == null) {
|
if (nextCounterpart == null) {
|
||||||
for(Message m : messages) {
|
for(Message m : messages) {
|
||||||
|
@ -1181,8 +1203,28 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (this.messages) {
|
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);
|
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() {
|
private void untieMessages() {
|
||||||
for (Message message : this.messages) {
|
for (Message message : this.messages) {
|
||||||
message.untie();
|
message.untie();
|
||||||
|
|
|
@ -11,6 +11,8 @@ import android.os.SystemClock;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.common.base.Stopwatch;
|
import com.google.common.base.Stopwatch;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
@ -31,6 +33,7 @@ import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -799,45 +802,86 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArrayList<Message> getMessages(Conversation conversations, int limit) {
|
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<>();
|
ArrayList<Message> list = new ArrayList<>();
|
||||||
SQLiteDatabase db = this.getReadableDatabase();
|
SQLiteDatabase db = this.getReadableDatabase();
|
||||||
Cursor cursor;
|
Cursor cursor;
|
||||||
|
String comparsionOperation = isForward ? ">?" : "<?";
|
||||||
|
String sorting = isForward ? " ASC" : " DESC";
|
||||||
if (timestamp == -1) {
|
if (timestamp == -1) {
|
||||||
if (conversation.getNextCounterpart() == null) {
|
if (conversation.getNextCounterpart() == null) {
|
||||||
String[] selectionArgs = {conversation.getUuid()};
|
String[] selectionArgs = {conversation.getUuid()};
|
||||||
cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
|
cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
|
||||||
+ "=?", selectionArgs, null, null, Message.TIME_SENT
|
+ "=?", selectionArgs, null, null, Message.TIME_SENT
|
||||||
+ " DESC", String.valueOf(limit));
|
+ sorting, String.valueOf(limit));
|
||||||
} else {
|
} else {
|
||||||
String[] selectionArgs = {conversation.getUuid(), String.valueOf(Message.TYPE_PRIVATE), String.valueOf(Message.TYPE_PRIVATE_FILE), conversation.getNextCounterpart().toString()};
|
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
|
cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
|
||||||
+ "=? and (" + Message.TYPE + "=? or " + Message.TYPE + "=?) and " + Message.COUNTERPART + "=?" , selectionArgs, null, null, Message.TIME_SENT
|
+ "=? and (" + Message.TYPE + "=? or " + Message.TYPE + "=?) and " + Message.COUNTERPART + "=?" , selectionArgs, null, null, Message.TIME_SENT
|
||||||
+ " DESC", String.valueOf(limit));
|
+ sorting, String.valueOf(limit));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (conversation.getNextCounterpart() == null) {
|
if (conversation.getNextCounterpart() == null) {
|
||||||
String[] selectionArgs = {conversation.getUuid(),
|
String[] selectionArgs = {conversation.getUuid(),
|
||||||
Long.toString(timestamp)};
|
Long.toString(timestamp)};
|
||||||
cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
|
cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
|
||||||
+ "=? and " + Message.TIME_SENT + "<?", selectionArgs,
|
+ "=? and " + Message.TIME_SENT + comparsionOperation, selectionArgs,
|
||||||
null, null, Message.TIME_SENT + " DESC",
|
null, null, Message.TIME_SENT + sorting,
|
||||||
String.valueOf(limit));
|
String.valueOf(limit));
|
||||||
} else {
|
} else {
|
||||||
String[] selectionArgs = {conversation.getUuid(), String.valueOf(Message.TYPE_PRIVATE), String.valueOf(Message.TYPE_PRIVATE_FILE), conversation.getNextCounterpart().toString(), Long.toString(timestamp)};
|
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
|
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
|
+ "=? and (" + Message.TYPE + "=? or " + Message.TYPE + "=?) and " + Message.COUNTERPART + "=? and " + Message.TIME_SENT + comparsionOperation, selectionArgs, null, null, Message.TIME_SENT
|
||||||
+ " DESC", String.valueOf(limit));
|
+ sorting, String.valueOf(limit));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CursorUtils.upgradeCursorWindowSize(cursor);
|
CursorUtils.upgradeCursorWindowSize(cursor);
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
try {
|
try {
|
||||||
Message m = Message.fromCursor(cursor, conversation);
|
Message m = Message.fromCursor(cursor, conversation);
|
||||||
list.add(0, m);
|
if (isForward) {
|
||||||
|
list.add(m);
|
||||||
|
} else {
|
||||||
|
list.add(0, m);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(Config.LOGTAG, "unable to restore message");
|
Log.e(Config.LOGTAG, "unable to restore message");
|
||||||
}
|
}
|
||||||
|
|
|
@ -2050,7 +2050,7 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restoreMessages(Conversation conversation) {
|
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.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
|
||||||
conversation.findUnreadMessagesAndCalls(mNotificationService::pushFromBacklog);
|
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)) {
|
if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) {
|
||||||
return;
|
return;
|
||||||
} else if (timestamp == 0) {
|
} else if (timestamp == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG, "load more messages for " + 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 Runnable runnable = () -> {
|
||||||
final Account account = conversation.getAccount();
|
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) {
|
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);
|
callback.onMoreMessagesLoaded(messages.size(), conversation);
|
||||||
} else if (conversation.hasMessagesLeftOnServer()
|
} else if (!isForward &&
|
||||||
|
conversation.hasMessagesLeftOnServer()
|
||||||
&& account.isOnlineAndConnected()
|
&& account.isOnlineAndConnected()
|
||||||
&& conversation.getLastClearHistory().getTimestamp() == 0) {
|
&& conversation.getLastClearHistory().getTimestamp() == 0) {
|
||||||
final boolean mamAvailable;
|
final boolean mamAvailable;
|
||||||
|
@ -2359,7 +2385,7 @@ public class XmppConnectionService extends Service {
|
||||||
final Conversation c = conversation;
|
final Conversation c = conversation;
|
||||||
final Runnable runnable = () -> {
|
final Runnable runnable = () -> {
|
||||||
if (loadMessagesFromDb) {
|
if (loadMessagesFromDb) {
|
||||||
c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
|
c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE), false);
|
||||||
updateConversationUi();
|
updateConversationUi();
|
||||||
c.messagesLoaded.set(true);
|
c.messagesLoaded.set(true);
|
||||||
}
|
}
|
||||||
|
@ -5085,6 +5111,11 @@ public class XmppConnectionService extends Service {
|
||||||
void informUser(int r);
|
void informUser(int r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface JumpToMessageListener {
|
||||||
|
void onSuccess();
|
||||||
|
void onNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
public interface OnMoreMessagesLoaded {
|
public interface OnMoreMessagesLoaded {
|
||||||
void onMoreMessagesLoaded(int count, Conversation conversation);
|
void onMoreMessagesLoaded(int count, Conversation conversation);
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import android.app.Activity;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.app.FragmentManager;
|
import android.app.FragmentManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
@ -37,6 +38,7 @@ import android.text.TextUtils;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
|
import android.util.Range;
|
||||||
import android.view.ActionMode;
|
import android.view.ActionMode;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
|
@ -49,6 +51,7 @@ import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.CycleInterpolator;
|
||||||
import android.view.ViewParent;
|
import android.view.ViewParent;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
@ -59,6 +62,7 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.PopupMenu;
|
import android.widget.PopupMenu;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.TextView.OnEditorActionListener;
|
import android.widget.TextView.OnEditorActionListener;
|
||||||
import android.widget.Toast;
|
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.InputConnectionCompat;
|
||||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||||
import androidx.databinding.DataBindingUtil;
|
import androidx.databinding.DataBindingUtil;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
|
@ -84,10 +89,13 @@ import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
@ -218,6 +226,8 @@ public class ConversationFragment extends XmppFragment
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private int primaryColor = -1;
|
private int primaryColor = -1;
|
||||||
|
|
||||||
|
private LinkedList<Message> replyJumps = new LinkedList<>();
|
||||||
|
|
||||||
private ActionMode selectionActionMode;
|
private ActionMode selectionActionMode;
|
||||||
private final OnClickListener clickToMuc =
|
private final OnClickListener clickToMuc =
|
||||||
new OnClickListener() {
|
new OnClickListener() {
|
||||||
|
@ -291,115 +301,146 @@ public class ConversationFragment extends XmppFragment
|
||||||
int totalItemCount) {
|
int totalItemCount) {
|
||||||
toggleScrollDownButton(view);
|
toggleScrollDownButton(view);
|
||||||
synchronized (ConversationFragment.this.messageList) {
|
synchronized (ConversationFragment.this.messageList) {
|
||||||
if (firstVisibleItem < 5
|
boolean paginateBackward = firstVisibleItem < 5;
|
||||||
&& conversation != null
|
boolean paginationForward = conversation.isInHistoryPart() && firstVisibleItem + visibleItemCount + 5 > totalItemCount;
|
||||||
&& conversation.messagesLoaded.compareAndSet(true, false)
|
loadMoreMessages(paginateBackward, paginationForward, view);
|
||||||
&& messageList.size() > 0) {
|
|
||||||
long timestamp;
|
|
||||||
if (messageList.get(0).getType() == Message.TYPE_STATUS
|
|
||||||
&& messageList.size() >= 2) {
|
|
||||||
timestamp = messageList.get(1).getTimeSent();
|
|
||||||
} else {
|
|
||||||
timestamp = messageList.get(0).getTimeSent();
|
|
||||||
}
|
|
||||||
activity.xmppConnectionService.loadMoreMessages(
|
|
||||||
conversation,
|
|
||||||
timestamp,
|
|
||||||
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);
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 =
|
private final EditMessage.OnCommitContentListener mEditorContentListener =
|
||||||
new EditMessage.OnCommitContentListener() {
|
new EditMessage.OnCommitContentListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -546,6 +587,29 @@ public class ConversationFragment extends XmppFragment
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
stopScrolling();
|
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);
|
setSelection(binding.messagesView.getCount() - 1, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -673,6 +737,8 @@ public class ConversationFragment extends XmppFragment
|
||||||
private boolean firstWord = false;
|
private boolean firstWord = false;
|
||||||
private Message mPendingDownloadableMessage;
|
private Message mPendingDownloadableMessage;
|
||||||
|
|
||||||
|
private ProgressDialog fetchHistoryDialog;
|
||||||
|
|
||||||
private static ConversationFragment findConversationFragment(Activity activity) {
|
private static ConversationFragment findConversationFragment(Activity activity) {
|
||||||
Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment);
|
Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment);
|
||||||
if (fragment instanceof ConversationFragment) {
|
if (fragment instanceof ConversationFragment) {
|
||||||
|
@ -770,7 +836,7 @@ public class ConversationFragment extends XmppFragment
|
||||||
if (conversation == null) {
|
if (conversation == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (scrolledToBottom(listView)) {
|
if (scrolledToBottom(listView) && !conversation.isInHistoryPart()) {
|
||||||
lastMessageUuid = null;
|
lastMessageUuid = null;
|
||||||
hideUnreadMessagesCount();
|
hideUnreadMessagesCount();
|
||||||
} else {
|
} else {
|
||||||
|
@ -797,6 +863,27 @@ public class ConversationFragment extends XmppFragment
|
||||||
return -1;
|
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() {
|
private ScrollState getScrollPosition() {
|
||||||
final ListView listView = this.binding == null ? null : this.binding.messagesView;
|
final ListView listView = this.binding == null ? null : this.binding.messagesView;
|
||||||
if (listView == null
|
if (listView == null
|
||||||
|
@ -1265,6 +1352,9 @@ public class ConversationFragment extends XmppFragment
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
conversation.jumpToLatest();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1391,6 +1481,7 @@ public class ConversationFragment extends XmppFragment
|
||||||
quoteMessage(message);
|
quoteMessage(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
messageListAdapter.setReplyClickListener(this::scrollToReply);
|
||||||
|
|
||||||
binding.messagesView.setAdapter(messageListAdapter);
|
binding.messagesView.setAdapter(messageListAdapter);
|
||||||
|
|
||||||
|
@ -1457,7 +1548,7 @@ public class ConversationFragment extends XmppFragment
|
||||||
|
|
||||||
SpannableStringBuilder body = message.getBodyForDisplaying();
|
SpannableStringBuilder body = message.getBodyForDisplaying();
|
||||||
if (message.isFileOrImage() || message.isOOb()) body.append(" 🖼️");
|
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.contextPreviewText.setText(body);
|
||||||
binding.contextPreviewAuthor.setText(message.getAvatarName());
|
binding.contextPreviewAuthor.setText(message.getAvatarName());
|
||||||
binding.contextPreview.setVisibility(View.VISIBLE);
|
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
|
@Override
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
// This should cancel any remaining click events that would otherwise trigger links
|
// This should cancel any remaining click events that would otherwise trigger links
|
||||||
|
@ -2756,6 +2952,8 @@ public class ConversationFragment extends XmppFragment
|
||||||
refreshCommands(false);
|
refreshCommands(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replyJumps.clear();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2808,6 +3006,7 @@ public class ConversationFragment extends XmppFragment
|
||||||
}
|
}
|
||||||
this.binding.scrollToBottomButton.setEnabled(false);
|
this.binding.scrollToBottomButton.setEnabled(false);
|
||||||
this.binding.scrollToBottomButton.hide();
|
this.binding.scrollToBottomButton.hide();
|
||||||
|
replyJumps.clear();
|
||||||
this.binding.unreadCountCustomView.setVisibility(View.GONE);
|
this.binding.unreadCountCustomView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2819,7 +3018,7 @@ public class ConversationFragment extends XmppFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean scrolledToBottom() {
|
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) {
|
private void processExtras(final Bundle extras) {
|
||||||
|
@ -2913,6 +3112,12 @@ public class ConversationFragment extends XmppFragment
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
startDownloadable(message);
|
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) {
|
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_TYPE = "type";
|
||||||
public static final String EXTRA_NODE = "node";
|
public static final String EXTRA_NODE = "node";
|
||||||
public static final String EXTRA_JID = "jid";
|
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(
|
private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
|
||||||
ACTION_VIEW_CONVERSATION,
|
ACTION_VIEW_CONVERSATION,
|
||||||
|
|
|
@ -163,7 +163,7 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.open_conversation:
|
case R.id.open_conversation:
|
||||||
switchToConversation(wrap(message.getConversation()));
|
switchToConversationOnMessage(wrap(message.getConversation()), message.getUuid());
|
||||||
break;
|
break;
|
||||||
case R.id.share_with:
|
case R.id.share_with:
|
||||||
ShareUtil.share(this, message);
|
ShareUtil.share(this, message);
|
||||||
|
|
|
@ -561,6 +561,10 @@ public abstract class XmppActivity extends ActionBarActivity {
|
||||||
switchToConversation(conversation, null);
|
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) {
|
public void switchToConversationAndQuote(Conversation conversation, String text) {
|
||||||
switchToConversation(conversation, text, true, null, false, false);
|
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) {
|
protected void switchToConversationDoNotAppend(Contact contact, String body, String postInit) {
|
||||||
Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), null, false, false, true, null);
|
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) {
|
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) {
|
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;
|
if (conversation == null) return;
|
||||||
|
|
||||||
Intent intent = new Intent(this, ConversationsActivity.class);
|
Intent intent = new Intent(this, ConversationsActivity.class);
|
||||||
|
@ -610,6 +614,11 @@ public abstract class XmppActivity extends ActionBarActivity {
|
||||||
if (doNotAppend) {
|
if (doNotAppend) {
|
||||||
intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true);
|
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.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, postInit);
|
||||||
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
|
|
|
@ -14,7 +14,10 @@ import android.preference.PreferenceManager;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.TextPaint;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
|
import android.text.style.CharacterStyle;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.RelativeSizeSpan;
|
import android.text.style.RelativeSizeSpan;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
|
@ -33,6 +36,7 @@ import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
@ -103,6 +107,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
private MessageEmptyPartClickListener messageEmptyPartClickListener;
|
private MessageEmptyPartClickListener messageEmptyPartClickListener;
|
||||||
private SelectionStatusProvider selectionStatusProvider;
|
private SelectionStatusProvider selectionStatusProvider;
|
||||||
private MessageBoxSwipedListener messageBoxSwipedListener;
|
private MessageBoxSwipedListener messageBoxSwipedListener;
|
||||||
|
private ReplyClickListener replyClickListener;
|
||||||
private boolean mUseGreenBackground = false;
|
private boolean mUseGreenBackground = false;
|
||||||
private final boolean mForceNames;
|
private final boolean mForceNames;
|
||||||
|
|
||||||
|
@ -193,6 +198,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
this.messageBoxSwipedListener = listener;
|
this.messageBoxSwipedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setReplyClickListener(ReplyClickListener listener) {
|
||||||
|
this.replyClickListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getViewTypeCount() {
|
public int getViewTypeCount() {
|
||||||
return 5;
|
return 5;
|
||||||
|
@ -402,7 +411,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
viewHolder.messageBody.setText(span);
|
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())) {
|
if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
|
||||||
body.insert(start++, "\n");
|
body.insert(start++, "\n");
|
||||||
body.setSpan(
|
body.setSpan(
|
||||||
|
@ -427,13 +436,28 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
|
|
||||||
DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
|
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);
|
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.
|
* 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.
|
* 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;
|
boolean startsWithQuote = false;
|
||||||
int quoteDepth = 1;
|
int quoteDepth = 1;
|
||||||
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
|
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
|
||||||
|
@ -452,7 +476,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
if (i == 0) startsWithQuote = true;
|
if (i == 0) startsWithQuote = true;
|
||||||
} else if (quoteStart >= 0) {
|
} else if (quoteStart >= 0) {
|
||||||
// Line start without quote, apply spans there
|
// 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;
|
quoteStart = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -477,7 +501,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
}
|
}
|
||||||
if (quoteStart >= 0) {
|
if (quoteStart >= 0) {
|
||||||
// Apply spans to finishing open quote
|
// 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++;
|
quoteDepth++;
|
||||||
}
|
}
|
||||||
|
@ -521,10 +545,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
int start = body.getSpanStart(quote);
|
int start = body.getSpanStart(quote);
|
||||||
int end = body.getSpanEnd(quote);
|
int end = body.getSpanEnd(quote);
|
||||||
body.removeSpan(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 (!message.isPrivateMessage()) {
|
||||||
if (hasMeCommand) {
|
if (hasMeCommand) {
|
||||||
body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
|
body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
|
||||||
|
@ -1123,6 +1147,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
void onMessageBoxSwiped(Message message);
|
void onMessageBoxSwiped(Message message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface ReplyClickListener {
|
||||||
|
void onReplyClick(Message message);
|
||||||
|
}
|
||||||
|
|
||||||
private static class ViewHolder {
|
private static class ViewHolder {
|
||||||
public View root;
|
public View root;
|
||||||
|
|
||||||
|
|
|
@ -53,5 +53,17 @@ public class ListViewUtils {
|
||||||
listView.setSelection(pos);
|
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