show real reply text instead of fallback

This commit is contained in:
kosyak 2024-01-20 00:56:22 +01:00
parent 869f92169d
commit 366e5aa389
8 changed files with 286 additions and 12 deletions

View file

@ -780,6 +780,45 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return unread; return unread;
} }
@Nullable
public Message getMessageWithAnyMatchingId(String uuid) {
if (uuid == null) {
return null;
}
synchronized (this.messages) {
for (int i = 0; i < messages.size(); ++i) {
if (uuid.equals(messages.get(i).getServerMsgId())) {
return messages.get(i);
}
if (uuid.equals(messages.get(i).getRemoteMsgId())) {
return messages.get(i);
}
if (uuid.equals(messages.get(i).getUuid())) {
return messages.get(i);
}
}
for (int i = 0; i < historyPartMessages.size(); ++i) {
if (uuid.equals(historyPartMessages.get(i).getServerMsgId())) {
return historyPartMessages.get(i);
}
if (uuid.equals(historyPartMessages.get(i).getRemoteMsgId())) {
return historyPartMessages.get(i);
}
if (uuid.equals(historyPartMessages.get(i).getUuid())) {
return historyPartMessages.get(i);
}
}
}
return null;
}
public Message getLatestMessage() { public Message getLatestMessage() {
synchronized (this.messages) { synchronized (this.messages) {
if (this.messages.size() == 0) { if (this.messages.size() == 0) {
@ -1190,12 +1229,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (!message.isPrivateMessage()) { if (!message.isPrivateMessage()) {
synchronized (this.messages) { synchronized (this.messages) {
this.messages.add(message); this.messages.add(message);
actualizeReplyMessages(this.messages, List.of(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(message); this.messages.add(message);
actualizeReplyMessages(this.messages, List.of(message));
} }
} }
} }
@ -1217,19 +1258,24 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (!message.isPrivateMessage()) { if (!message.isPrivateMessage()) {
synchronized (this.messages) { synchronized (this.messages) {
properListToAdd.add(Math.min(offset, properListToAdd.size()), message); properListToAdd.add(Math.min(offset, properListToAdd.size()), message);
actualizeReplyMessages(properListToAdd, List.of(message));
} }
} }
} else { } else {
if (message.isPrivateMessage() && Objects.equals(res1, res2)) { if (message.isPrivateMessage() && Objects.equals(res1, res2)) {
synchronized (this.messages) { synchronized (this.messages) {
properListToAdd.add(Math.min(offset, properListToAdd.size()), message); properListToAdd.add(Math.min(offset, properListToAdd.size()), message);
actualizeReplyMessages(properListToAdd, List.of(message));
} }
} }
} }
if (!historyPartMessages.isEmpty() && hasDuplicateMessage(historyPartMessages.get(historyPartMessages.size() - 1))) { synchronized (this.messages) {
messages.addAll(0, historyPartMessages); if (!historyPartMessages.isEmpty() && hasDuplicateMessage(historyPartMessages.get(historyPartMessages.size() - 1))) {
jumpToLatest(); messages.addAll(0, historyPartMessages);
actualizeReplyMessages(messages, List.of(message));
jumpToLatest();
}
} }
} }
@ -1279,10 +1325,43 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
} else { } else {
properListToAdd.addAll(index, newM); properListToAdd.addAll(index, newM);
} }
actualizeReplyMessages(properListToAdd, messages);
} }
account.getPgpDecryptionService().decrypt(newM); account.getPgpDecryptionService().decrypt(newM);
} }
private void actualizeReplyMessages(List<Message> mainList, List<Message> messages) {
for (Message m : mainList) {
if (m.isReplyRestoredFromDb()) {
Element reply = m.getReply();
if (reply == null) {
continue;
}
String replyId = reply.getAttribute("id");
for (Message rep : messages) {
if (replyId.equals(rep.getServerMsgId())) {
m.setReplyMessage(rep, false);
break;
}
if (replyId.equals(rep.getRemoteMsgId())) {
m.setReplyMessage(rep, false);
break;
}
if (replyId.equals(rep.getUuid())) {
m.setReplyMessage(rep, false);
break;
}
}
}
}
}
public void expireOldMessages(long timestamp) { public void expireOldMessages(long timestamp) {
synchronized (this.messages) { synchronized (this.messages) {
for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) { for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {

View file

@ -4,7 +4,11 @@ import android.content.ContentValues;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Color; import android.graphics.Color;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteSource; import com.google.common.io.ByteSource;
@ -122,6 +126,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
protected Transferable transferable = null; protected Transferable transferable = null;
private Message mNextMessage = null; private Message mNextMessage = null;
private Message mPreviousMessage = null; private Message mPreviousMessage = null;
private Message replyMessage = null;
private boolean replyRestoredFromDb = false;
private String axolotlFingerprint = null; private String axolotlFingerprint = null;
private String errorMessage = null; private String errorMessage = null;
private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>(); private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
@ -411,6 +417,25 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return null; return null;
} }
@Nullable
public Message getReplyMessage() {
if (replyMessage != null && replyMessage.deleted) {
replyMessage = null;
replyRestoredFromDb = false;
}
return replyMessage;
}
public boolean isReplyRestoredFromDb() {
return replyRestoredFromDb;
}
public void setReplyMessage(Message replyMessage, boolean restoredFromDb) {
this.replyMessage = replyMessage;
this.replyRestoredFromDb = restoredFromDb;
}
public String getConversationUuid() { public String getConversationUuid() {
return conversationUuid; return conversationUuid;
} }
@ -815,7 +840,42 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
} }
public SpannableStringBuilder getBodyForDisplaying() { public SpannableStringBuilder getBodyForDisplaying() {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); if (replyMessage != null) {
try {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(">" + removeReplyFallback(replyMessage) + "\n" + removeReplyFallback(this)).trim());
} catch (IndexOutOfBoundsException e) {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
}
} else {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
}
}
public SpannableStringBuilder getBodyForReplyPreview() {
try {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(">" + removeReplyFallback(this)).trim());
} catch (IndexOutOfBoundsException e) {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
}
}
private StringBuilder removeReplyFallback(Message message) {
StringBuilder sb = new StringBuilder(message.body);
List<Element> replyFallback = message.getFallbacks("urn:xmpp:reply:0");
if (replyFallback.size() == 0) {
return sb;
}
Element bodyFallback = replyFallback.get(0).findChild("body");
int startCodePoint = Integer.parseInt(bodyFallback.getAttribute("start"));
int endCodePoint = Integer.parseInt(bodyFallback.getAttribute("end"));
if (startCodePoint < 0) return sb;
if (endCodePoint > sb.length()) return sb;
sb.replace(message.body.offsetByCodePoints(0, startCodePoint), message.body.offsetByCodePoints(0, endCodePoint), "");
return sb;
} }
public boolean hasMeCommand() { public boolean hasMeCommand() {

View file

@ -758,6 +758,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} }
} }
mXmppConnectionService.restoreReplyForMessage(conversation, message);
if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
conversation.prepend(query.getActualInThisQuery(), message); conversation.prepend(query.getActualInThisQuery(), message);
} else { } else {
@ -946,6 +947,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
message.setServerMsgId(serverMsgId); message.setServerMsgId(serverMsgId);
message.setTime(timestamp); message.setTime(timestamp);
message.setBody(new RtpSessionStatus(false, 0).toString()); message.setBody(new RtpSessionStatus(false, 0).toString());
mXmppConnectionService.restoreReplyForMessage(conversation, message);
c.add(message); c.add(message);
mXmppConnectionService.databaseBackend.createMessage(message); mXmppConnectionService.databaseBackend.createMessage(message);
} }
@ -988,6 +990,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
message.setServerMsgId(serverMsgId); message.setServerMsgId(serverMsgId);
message.setTime(timestamp); message.setTime(timestamp);
message.setBody(new RtpSessionStatus(true, 0).toString()); message.setBody(new RtpSessionStatus(true, 0).toString());
mXmppConnectionService.restoreReplyForMessage(conversation, message);
if (query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { if (query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
c.prepend(query.getActualInThisQuery(), message); c.prepend(query.getActualInThisQuery(), message);
} else { } else {

View file

@ -8,6 +8,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment; import android.os.Environment;
import android.os.SystemClock; import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
@ -840,6 +841,45 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return list; return list;
} }
public ArrayList<Message> getMessagesByIds(Conversation conversation, Set<String> ids) {
SQLiteDatabase db = this.getReadableDatabase();
List<String> parameters = new ArrayList<>();
for (String id : ids) {
parameters.add("?");
}
String parametersString = TextUtils.join(",", parameters);
String[] selectionArgs = new String[ids.size() * 3 + 1];
selectionArgs[0] = conversation.getUuid();
int ind = 1;
for (int i=0;i<3;i++) {
for (String id : ids) {
selectionArgs[ind] = id;
ind++;
}
}
Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
+ "=? and (" + Message.SERVER_MSG_ID + " in (" + parametersString + ") or " + Message.REMOTE_MSG_ID + " in (" + parametersString + ") or " + Message.UUID + " in (" + parametersString + "))", selectionArgs, null, null, Message.TIME_SENT
+ " DESC", String.valueOf(ids.size()));
CursorUtils.upgradeCursorWindowSize(cursor);
ArrayList<Message> list = new ArrayList<>();
while (cursor.moveToNext()) {
try {
Message m = Message.fromCursor(cursor, conversation);
list.add(m);
} catch (Exception e) {
Log.e(Config.LOGTAG, "unable to restore message");
}
}
return list;
}
public ArrayList<Message> getMessages(Conversation conversation, int limit, long timestamp, boolean isForward) { 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();

View file

@ -1821,6 +1821,7 @@ public class XmppConnectionService extends Service {
} }
} else { } else {
if (addToConversation) { if (addToConversation) {
restoreReplyForMessage(conversation, message);
conversation.add(message); conversation.add(message);
} }
if (saveInDb) { if (saveInDb) {
@ -2182,7 +2183,9 @@ public class XmppConnectionService extends Service {
} }
private void restoreMessages(Conversation conversation) { private void restoreMessages(Conversation conversation) {
conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE), false); List<Message> messages = databaseBackend.getMessages(conversation, Config.PAGE_SIZE);
restoreRepliesForMessages(conversation, messages);
conversation.addAll(0, messages, 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);
} }
@ -2310,6 +2313,7 @@ public class XmppConnectionService extends Service {
public void jumpToMessage(final Conversation conversation, final String uuid, JumpToMessageListener listener) { public void jumpToMessage(final Conversation conversation, final String uuid, JumpToMessageListener listener) {
final Runnable runnable = () -> { final Runnable runnable = () -> {
List<Message> messages = databaseBackend.getMessagesNearUuid(conversation, 30, uuid); List<Message> messages = databaseBackend.getMessagesNearUuid(conversation, 30, uuid);
restoreRepliesForMessages(conversation, messages);
if (messages != null && !messages.isEmpty()) { if (messages != null && !messages.isEmpty()) {
conversation.jumpToHistoryPart(messages); conversation.jumpToHistoryPart(messages);
listener.onSuccess(); listener.onSuccess();
@ -2321,6 +2325,88 @@ public class XmppConnectionService extends Service {
mDatabaseReaderExecutor.execute(runnable); mDatabaseReaderExecutor.execute(runnable);
} }
public void restoreReplyForMessage(final Conversation conversation, Message message) {
restoreRepliesForMessages(conversation, List.of(message));
}
public void restoreRepliesForMessages(final Conversation conversation, List<Message> messages) {
Map<String, ArrayList<Message>> notFoundReplies = null;
for (Message m : messages) {
Element reply = m.getReply();
if (reply == null) {
continue;
}
String replyId = reply.getAttribute("id");
Message replyMessage = null;
for (Message rep : messages) {
if (replyId.equals(rep.getServerMsgId())) {
replyMessage = rep;
break;
}
if (replyId.equals(rep.getRemoteMsgId())) {
replyMessage = rep;
break;
}
if (replyId.equals(rep.getUuid())) {
replyMessage = rep;
break;
}
}
if (replyMessage == null) {
replyMessage = conversation.getMessageWithAnyMatchingId(replyId);
}
if (replyMessage != null) {
m.setReplyMessage(replyMessage, false);
} else {
if (notFoundReplies == null) {
notFoundReplies = new HashMap<>();
}
ArrayList<Message> list = notFoundReplies.computeIfAbsent(replyId, id -> new ArrayList<>());
list.add(m);
}
}
if (notFoundReplies != null) {
List<Message> restored = databaseBackend.getMessagesByIds(conversation, notFoundReplies.keySet());
for (String id : notFoundReplies.keySet()) {
for (Message m : restored) {
if (id.equals(m.getServerMsgId())) {
for (Message rm : notFoundReplies.get(id)) {
rm.setReplyMessage(m, true);
}
break;
}
if (id.equals(m.getRemoteMsgId())) {
for (Message rm : notFoundReplies.get(id)) {
rm.setReplyMessage(m, true);
}
break;
}
if (id.equals(m.getUuid())) {
for (Message rm : notFoundReplies.get(id)) {
rm.setReplyMessage(m, true);
}
break;
}
}
}
}
}
public void loadMoreMessages(final Conversation conversation, final long timestamp, boolean isForward, final OnMoreMessagesLoaded callback) { 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;
@ -2339,11 +2425,14 @@ public class XmppConnectionService extends Service {
List<Message> messages = databaseBackend.getMessages(conversation, Config.PAGE_SIZE, timestamp, isForward); List<Message> messages = databaseBackend.getMessages(conversation, Config.PAGE_SIZE, timestamp, isForward);
if (messages.size() > 0) { if (messages.size() > 0) {
restoreRepliesForMessages(conversation, messages);
if (isForward) { if (isForward) {
conversation.addAll(-1, messages, true); conversation.addAll(-1, messages, true);
} else { } else {
conversation.addAll(0, messages, true); conversation.addAll(0, messages, true);
} }
callback.onMoreMessagesLoaded(messages.size(), conversation); callback.onMoreMessagesLoaded(messages.size(), conversation);
} else if (!isForward && } else if (!isForward &&
conversation.hasMessagesLeftOnServer() conversation.hasMessagesLeftOnServer()
@ -2517,7 +2606,9 @@ 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), false); List<Message> messages = databaseBackend.getMessages(c, Config.PAGE_SIZE);
restoreRepliesForMessages(c, messages);
c.addAll(0, messages, false);
updateConversationUi(); updateConversationUi();
c.messagesLoaded.set(true); c.messagesLoaded.set(true);
} }

View file

@ -1554,9 +1554,9 @@ public class ConversationFragment extends XmppFragment
return; return;
} }
SpannableStringBuilder body = message.getBodyForDisplaying(); SpannableStringBuilder body = message.getBodyForReplyPreview();
if (message.isFileOrImage() || message.isOOb()) body.append(" 🖼️"); if (message.isFileOrImage() || message.isOOb()) body.append(" 🖼️");
messageListAdapter.handleTextQuotes(body, activity.isDarkTheme(), false, message); messageListAdapter.handleTextQuotes(body, activity.isDarkTheme(), true, 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);

View file

@ -37,6 +37,7 @@ import android.widget.Toast;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
@ -462,7 +463,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
* 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, Message message) { public void handleTextQuotes(SpannableStringBuilder body, boolean darkBackground, boolean highlightReply, Message message) {
boolean startsWithQuote = false; boolean startsWithQuote = false;
int quoteDepth = 0; int quoteDepth = 0;
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) { while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
@ -546,7 +547,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
applyQuoteSpan(body, start, end, darkBackground, true, message); applyQuoteSpan(body, start, end, darkBackground, true, message);
} }
return startsWithQuote;
} }
private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) { private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) {
@ -586,10 +586,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, message); applyQuoteSpan(body, start, end, darkBackground, message.getReplyMessage() != null, message);
} }
boolean startsWithQuote = handleTextQuotes(body, darkBackground, message.getReply() != null, message); handleTextQuotes(body, darkBackground, message.getReplyMessage() != 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(),

View file

@ -508,6 +508,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
} }
long size = parseLong(fileSize, 0); long size = parseLong(fileSize, 0);
message.setBody(Long.toString(size)); message.setBody(Long.toString(size));
xmppConnectionService.restoreReplyForMessage(conversation, message);
conversation.add(message); conversation.add(message);
jingleConnectionManager.updateConversationUi(true); jingleConnectionManager.updateConversationUi(true);
this.file = this.xmppConnectionService.getFileBackend().getFile(message, false); this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);