From 366e5aa389a306387ecc51487d053ace5aba68bd Mon Sep 17 00:00:00 2001 From: kosyak Date: Sat, 20 Jan 2024 00:56:22 +0100 Subject: [PATCH] show real reply text instead of fallback --- .../conversations/entities/Conversation.java | 85 ++++++++++++++++- .../siacs/conversations/entities/Message.java | 62 +++++++++++- .../conversations/parser/MessageParser.java | 3 + .../persistance/DatabaseBackend.java | 40 ++++++++ .../services/XmppConnectionService.java | 95 ++++++++++++++++++- .../ui/ConversationFragment.java | 4 +- .../ui/adapter/MessageAdapter.java | 8 +- .../jingle/JingleFileTransferConnection.java | 1 + 8 files changed, 286 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 874dff415..5536911c7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -780,6 +780,45 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl 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() { synchronized (this.messages) { if (this.messages.size() == 0) { @@ -1190,12 +1229,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (!message.isPrivateMessage()) { synchronized (this.messages) { this.messages.add(message); + actualizeReplyMessages(this.messages, List.of(message)); } } } else { if (message.isPrivateMessage() && Objects.equals(res1, res2)) { synchronized (this.messages) { 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()) { synchronized (this.messages) { properListToAdd.add(Math.min(offset, properListToAdd.size()), message); + actualizeReplyMessages(properListToAdd, List.of(message)); } } } else { if (message.isPrivateMessage() && Objects.equals(res1, res2)) { synchronized (this.messages) { properListToAdd.add(Math.min(offset, properListToAdd.size()), message); + actualizeReplyMessages(properListToAdd, List.of(message)); } } } - if (!historyPartMessages.isEmpty() && hasDuplicateMessage(historyPartMessages.get(historyPartMessages.size() - 1))) { - messages.addAll(0, historyPartMessages); - jumpToLatest(); + synchronized (this.messages) { + if (!historyPartMessages.isEmpty() && hasDuplicateMessage(historyPartMessages.get(historyPartMessages.size() - 1))) { + messages.addAll(0, historyPartMessages); + actualizeReplyMessages(messages, List.of(message)); + jumpToLatest(); + } } } @@ -1279,10 +1325,43 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } else { properListToAdd.addAll(index, newM); } + + actualizeReplyMessages(properListToAdd, messages); } account.getPgpDecryptionService().decrypt(newM); } + private void actualizeReplyMessages(List mainList, List 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) { synchronized (this.messages) { for (ListIterator iterator = this.messages.listIterator(); iterator.hasNext(); ) { diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 5a2e7b6c2..9fb20e9e3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -4,7 +4,11 @@ import android.content.ContentValues; import android.database.Cursor; import android.graphics.Color; import android.text.SpannableStringBuilder; +import android.text.TextUtils; import android.util.Log; + +import androidx.annotation.Nullable; + import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteSource; @@ -122,6 +126,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable protected Transferable transferable = null; private Message mNextMessage = null; private Message mPreviousMessage = null; + private Message replyMessage = null; + private boolean replyRestoredFromDb = false; private String axolotlFingerprint = null; private String errorMessage = null; private Set readByMarkers = new CopyOnWriteArraySet<>(); @@ -411,6 +417,25 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable 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() { return conversationUuid; } @@ -815,7 +840,42 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } 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 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() { diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index c3fcdbff1..2ec1bacf2 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -758,6 +758,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } + mXmppConnectionService.restoreReplyForMessage(conversation, message); if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { conversation.prepend(query.getActualInThisQuery(), message); } else { @@ -946,6 +947,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece message.setServerMsgId(serverMsgId); message.setTime(timestamp); message.setBody(new RtpSessionStatus(false, 0).toString()); + mXmppConnectionService.restoreReplyForMessage(conversation, message); c.add(message); mXmppConnectionService.databaseBackend.createMessage(message); } @@ -988,6 +990,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece message.setServerMsgId(serverMsgId); message.setTime(timestamp); message.setBody(new RtpSessionStatus(true, 0).toString()); + mXmppConnectionService.restoreReplyForMessage(conversation, message); if (query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { c.prepend(query.getActualInThisQuery(), message); } else { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index fdcd0f17c..41ab36dff 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -8,6 +8,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Environment; import android.os.SystemClock; +import android.text.TextUtils; import android.util.Base64; import android.util.Log; @@ -840,6 +841,45 @@ public class DatabaseBackend extends SQLiteOpenHelper { return list; } + public ArrayList getMessagesByIds(Conversation conversation, Set ids) { + SQLiteDatabase db = this.getReadableDatabase(); + List 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 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 getMessages(Conversation conversation, int limit, long timestamp, boolean isForward) { ArrayList list = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index da18976b5..0c53e7ccf 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1821,6 +1821,7 @@ public class XmppConnectionService extends Service { } } else { if (addToConversation) { + restoreReplyForMessage(conversation, message); conversation.add(message); } if (saveInDb) { @@ -2182,7 +2183,9 @@ public class XmppConnectionService extends Service { } private void restoreMessages(Conversation conversation) { - conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE), false); + List messages = databaseBackend.getMessages(conversation, Config.PAGE_SIZE); + restoreRepliesForMessages(conversation, messages); + conversation.addAll(0, messages, false); conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING)); conversation.findUnreadMessagesAndCalls(mNotificationService::pushFromBacklog); } @@ -2310,6 +2313,7 @@ public class XmppConnectionService extends Service { public void jumpToMessage(final Conversation conversation, final String uuid, JumpToMessageListener listener) { final Runnable runnable = () -> { List messages = databaseBackend.getMessagesNearUuid(conversation, 30, uuid); + restoreRepliesForMessages(conversation, messages); if (messages != null && !messages.isEmpty()) { conversation.jumpToHistoryPart(messages); listener.onSuccess(); @@ -2321,6 +2325,88 @@ public class XmppConnectionService extends Service { mDatabaseReaderExecutor.execute(runnable); } + public void restoreReplyForMessage(final Conversation conversation, Message message) { + restoreRepliesForMessages(conversation, List.of(message)); + } + + public void restoreRepliesForMessages(final Conversation conversation, List messages) { + Map> 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 list = notFoundReplies.computeIfAbsent(replyId, id -> new ArrayList<>()); + list.add(m); + } + } + + if (notFoundReplies != null) { + List 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) { if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) { return; @@ -2339,11 +2425,14 @@ public class XmppConnectionService extends Service { List messages = databaseBackend.getMessages(conversation, Config.PAGE_SIZE, timestamp, isForward); if (messages.size() > 0) { + restoreRepliesForMessages(conversation, messages); + if (isForward) { conversation.addAll(-1, messages, true); } else { conversation.addAll(0, messages, true); } + callback.onMoreMessagesLoaded(messages.size(), conversation); } else if (!isForward && conversation.hasMessagesLeftOnServer() @@ -2517,7 +2606,9 @@ public class XmppConnectionService extends Service { final Conversation c = conversation; final Runnable runnable = () -> { if (loadMessagesFromDb) { - c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE), false); + List messages = databaseBackend.getMessages(c, Config.PAGE_SIZE); + restoreRepliesForMessages(c, messages); + c.addAll(0, messages, false); updateConversationUi(); c.messagesLoaded.set(true); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index fdd6d4712..34e48122d 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1554,9 +1554,9 @@ public class ConversationFragment extends XmppFragment return; } - SpannableStringBuilder body = message.getBodyForDisplaying(); + SpannableStringBuilder body = message.getBodyForReplyPreview(); 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.contextPreviewAuthor.setText(message.getAvatarName()); binding.contextPreview.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 16bf0e228..2b86f9e8a 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -37,6 +37,7 @@ import android.widget.Toast; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; @@ -462,7 +463,7 @@ public class MessageAdapter extends ArrayAdapter { * 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, Message message) { + public void handleTextQuotes(SpannableStringBuilder body, boolean darkBackground, boolean highlightReply, Message message) { boolean startsWithQuote = false; int quoteDepth = 0; while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) { @@ -546,7 +547,6 @@ public class MessageAdapter extends ArrayAdapter { applyQuoteSpan(body, start, end, darkBackground, true, message); } - return startsWithQuote; } private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) { @@ -586,10 +586,10 @@ public class MessageAdapter extends ArrayAdapter { int start = body.getSpanStart(quote); int end = body.getSpanEnd(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 (hasMeCommand) { body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 7c6892136..2543a3280 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -508,6 +508,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } long size = parseLong(fileSize, 0); message.setBody(Long.toString(size)); + xmppConnectionService.restoreReplyForMessage(conversation, message); conversation.add(message); jingleConnectionManager.updateConversationUi(true); this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);