introduced type private_file_message to handle attachments in PMs. fixes #3372

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java  |  2 
src/main/java/eu/siacs/conversations/entities/Conversation.java          |  4 
src/main/java/eu/siacs/conversations/entities/Message.java               | 36 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java     |  7 
src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java    |  3 
src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java      |  4 
src/main/java/eu/siacs/conversations/parser/MessageParser.java           |  6 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java    | 10 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java        |  3 
src/main/java/eu/siacs/conversations/services/NotificationService.java   |  2 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 22 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        |  9 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java      | 70 
src/main/res/layout/message_content.xml                                  | 16 
14 files changed, 123 insertions(+), 71 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -1326,7 +1326,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		}
 
 		final boolean success;
-		if (message.getType() == Message.TYPE_PRIVATE) {
+		if (message.isPrivateMessage()) {
 			success = buildHeader(axolotlMessage, message.getTrueCounterpart());
 		} else {
 			success = buildHeader(axolotlMessage, (Conversation) message.getConversation());

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -486,7 +486,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
 				final Message message = messages.get(i);
 				if (message.getStatus() <= Message.STATUS_RECEIVED
 						&& (message.markable || isPrivateAndNonAnonymousMuc)
-						&& message.getType() != Message.TYPE_PRIVATE) {
+						&& !message.isPrivateMessage()) {
 					return message;
 				}
 			}
@@ -748,7 +748,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
 		synchronized (this.messages) {
 			for (int i = this.messages.size() - 1; i >= 0; --i) {
 				final Message message = this.messages.get(i);
-				if (message.getType() == Message.TYPE_PRIVATE) {
+				if (message.isPrivateMessage()) {
 					continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
 				}
 				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {

src/main/java/eu/siacs/conversations/entities/Message.java 🔗

@@ -57,6 +57,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
 	public static final int TYPE_FILE = 2;
 	public static final int TYPE_STATUS = 3;
 	public static final int TYPE_PRIVATE = 4;
+	public static final int TYPE_PRIVATE_FILE = 5;
 
 	public static final String CONVERSATION = "conversationUuid";
 	public static final String COUNTERPART = "counterpart";
@@ -495,7 +496,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
 	}
 
 	boolean similar(Message message) {
-		if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
+		if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
 			return this.serverMsgId.equals(message.getServerMsgId()) || Edited.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
 		} else if (Edited.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
 			return true;
@@ -837,8 +838,12 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
 		this.mPreviousMessage = null;
 	}
 
+	public boolean isPrivateMessage() {
+		return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
+	}
+
 	public boolean isFileOrImage() {
-		return type == TYPE_FILE || type == TYPE_IMAGE;
+		return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
 	}
 
 	public boolean hasFileOnRemoteHost() {
@@ -915,4 +920,31 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
 		}
 		return encryption;
 	}
+
+	public static boolean configurePrivateMessage(final Message message) {
+		return configurePrivateMessage(message, false);
+	}
+
+	public static boolean configurePrivateFileMessage(final Message message) {
+		return configurePrivateMessage(message, true);
+	}
+
+	private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
+		final Conversation conversation;
+		if (message.conversation instanceof Conversation) {
+			conversation = (Conversation) message.conversation;
+		} else {
+			return false;
+		}
+		if (conversation.getMode() == Conversation.MODE_MULTI) {
+			final Jid nextCounterpart = conversation.getNextCounterpart();
+			if (nextCounterpart != null) {
+				message.setCounterpart(nextCounterpart);
+				message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
+				message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
+				return true;
+			}
+		}
+		return false;
+	}
 }

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -1,7 +1,5 @@
 package eu.siacs.conversations.generator;
 
-import android.util.Log;
-
 import java.net.URL;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -13,7 +11,6 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.http.P1S3UrlStreamHandler;
@@ -43,7 +40,7 @@ public class MessageGenerator extends AbstractGenerator {
 			if (this.mXmppConnectionService.indicateReceived() && !isWithSelf) {
 				packet.addChild("request", "urn:xmpp:receipts");
 			}
-		} else if (message.getType() == Message.TYPE_PRIVATE) { //TODO files and images might be private as well
+		} else if (message.isPrivateMessage()) {
 			packet.setTo(message.getCounterpart());
 			packet.setType(MessagePacket.TYPE_CHAT);
 			packet.addChild("x", "http://jabber.org/protocol/muc#user");
@@ -54,7 +51,7 @@ public class MessageGenerator extends AbstractGenerator {
 			packet.setTo(message.getCounterpart().asBareJid());
 			packet.setType(MessagePacket.TYPE_GROUPCHAT);
 		}
-		if (conversation.isSingleOrPrivateAndNonAnonymous() && message.getType() != Message.TYPE_PRIVATE) {
+		if (conversation.isSingleOrPrivateAndNonAnonymous() && !message.isPrivateMessage()) {
 			packet.addChild("markable", "urn:xmpp:chat-markers:0");
 		}
 		packet.setFrom(account.getJid());

src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java 🔗

@@ -442,7 +442,8 @@ public class HttpDownloadConnection implements Transferable {
 		}
 
 		private void updateImageBounds() {
-			message.setType(Message.TYPE_FILE);
+			final boolean privateMessage = message.isPrivateMessage();
+			message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
 			final URL url;
 			final String ref = mUrl.getRef();
 			if (method == Method.P1_S3) {

src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java 🔗

@@ -216,7 +216,9 @@ public class HttpUploadConnection implements Transferable {
 				mXmppConnectionService.getFileBackend().updateFileParams(message, get);
 				mXmppConnectionService.getFileBackend().updateMediaScanner(file);
 				finish();
-				message.setCounterpart(message.getConversation().getJid().asBareJid());
+				if (!message.isPrivateMessage()) {
+					message.setCounterpart(message.getConversation().getJid().asBareJid());
+				}
 				mXmppConnectionService.resendMessage(message, delayed);
 			} else {
 				Log.d(Config.LOGTAG,"http upload failed because response code was "+code);

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -551,7 +551,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                             mXmppConnectionService.updateMessage(replacedMessage, uuid);
                             if (mXmppConnectionService.confirmMessages()
                                     && replacedMessage.getStatus() == Message.STATUS_RECEIVED
-                                    && (replacedMessage.trusted() || replacedMessage.getType() == Message.TYPE_PRIVATE)
+                                    && (replacedMessage.trusted() || replacedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs?
                                     && remoteMsgId != null
                                     && !selfAddressed
                                     && !isTypeGroupChat) {
@@ -577,7 +577,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             }
 
             boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay"))
-                    || message.getType() == Message.TYPE_PRIVATE
+                    || message.isPrivateMessage()
                     || message.getServerMsgId() != null
                     || (query == null && mXmppConnectionService.getMessageArchiveService().isCatchupInProgress(conversation));
             if (checkForDuplicates) {
@@ -637,7 +637,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
 
             if (mXmppConnectionService.confirmMessages()
                     && message.getStatus() == Message.STATUS_RECEIVED
-                    && (message.trusted() || message.getType() == Message.TYPE_PRIVATE)
+                    && (message.trusted() || message.isPrivateMessage())
                     && remoteMsgId != null
                     && !selfAddressed
                     && !isTypeGroupChat) {

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -812,14 +812,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         if (internal) {
             final String name = file.getName();
             if (name.endsWith(".pgp")) {
-                selection = "(" + Message.RELATIVE_FILE_PATH + " IN(?,?) OR (" + Message.RELATIVE_FILE_PATH + "=? and encryption in(1,4))) and type in (1,2)";
+                selection = "(" + Message.RELATIVE_FILE_PATH + " IN(?,?) OR (" + Message.RELATIVE_FILE_PATH + "=? and encryption in(1,4))) and type in (1,2,5)";
                 selectionArgs = new String[]{file.getAbsolutePath(), name, name.substring(0, name.length() - 4)};
             } else {
-                selection = Message.RELATIVE_FILE_PATH + " IN(?,?) and type in (1,2)";
+                selection = Message.RELATIVE_FILE_PATH + " IN(?,?) and type in (1,2,5)";
                 selectionArgs = new String[]{file.getAbsolutePath(), name};
             }
         } else {
-            selection = Message.RELATIVE_FILE_PATH + "=? and type in (1,2)";
+            selection = Message.RELATIVE_FILE_PATH + "=? and type in (1,2,5)";
             selectionArgs = new String[]{file.getAbsolutePath()};
         }
         final List<String> uuids = new ArrayList<>();
@@ -862,7 +862,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 
     public List<FilePathInfo> getFilePathInfo() {
         final SQLiteDatabase db = this.getReadableDatabase();
-        final Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED}, "type in (1,2) and "+Message.RELATIVE_FILE_PATH+" is not null", null, null, null, null);
+        final Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED}, "type in (1,2,5) and "+Message.RELATIVE_FILE_PATH+" is not null", null, null, null, null);
         final List<FilePathInfo> list = new ArrayList<>();
         while (cursor != null && cursor.moveToNext()) {
             list.add(new FilePathInfo(cursor.getString(0), cursor.getString(1), cursor.getInt(2) > 0));
@@ -875,7 +875,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 
     public List<FilePath> getRelativeFilePaths(String account, Jid jid, int limit) {
         SQLiteDatabase db = this.getReadableDatabase();
-        final String SQL = "select uuid,relativeFilePath from messages where type in (1,2) and deleted=0 and "+Message.RELATIVE_FILE_PATH+" is not null and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc";
+        final String SQL = "select uuid,relativeFilePath from messages where type in (1,2,5) and deleted=0 and "+Message.RELATIVE_FILE_PATH+" is not null and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc";
         final String[] args = {account, jid.toEscapedString(), jid.toEscapedString() + "/%"};
         Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + String.valueOf(limit) : ""), args);
         List<FilePath> filesPaths = new ArrayList<>();

src/main/java/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -1178,6 +1178,7 @@ public class FileBackend {
     public void updateFileParams(Message message, URL url) {
         DownloadableFile file = getFile(message);
         final String mime = file.getMimeType();
+        final boolean privateMessage = message.isPrivateMessage();
         final boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/"));
         final boolean video = mime != null && mime.startsWith("video/");
         final boolean audio = mime != null && mime.startsWith("audio/");
@@ -1201,7 +1202,7 @@ public class FileBackend {
         }
         message.setBody(body.toString());
         message.setDeleted(false);
-        message.setType(image ? Message.TYPE_IMAGE : Message.TYPE_FILE);
+        message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE));
     }
 
 

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -487,9 +487,7 @@ public class XmppConnectionService extends Service {
             encryption = Message.ENCRYPTION_DECRYPTED;
         }
         Message message = new Message(conversation, uri.toString(), encryption);
-        if (conversation.getNextCounterpart() != null) {
-            message.setCounterpart(conversation.getNextCounterpart());
-        }
+        Message.configurePrivateMessage(message);
         if (encryption == Message.ENCRYPTION_DECRYPTED) {
             getPgpEngine().encrypt(message, callback);
         } else {
@@ -505,8 +503,12 @@ public class XmppConnectionService extends Service {
         } else {
             message = new Message(conversation, "", conversation.getNextEncryption());
         }
-        message.setCounterpart(conversation.getNextCounterpart());
-        message.setType(Message.TYPE_FILE);
+        if (!Message.configurePrivateFileMessage(message)) {
+            message.setCounterpart(conversation.getNextCounterpart());
+            message.setType(Message.TYPE_FILE);
+        }
+        Log.d(Config.LOGTAG,"attachFile: type="+message.getType());
+        Log.d(Config.LOGTAG,"counterpart="+message.getCounterpart());
         final AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this, uri, type, message, callback);
         if (runnable.isVideoMessage()) {
             mVideoCompressionExecutor.execute(runnable);
@@ -533,8 +535,11 @@ public class XmppConnectionService extends Service {
         } else {
             message = new Message(conversation, "", conversation.getNextEncryption());
         }
-        message.setCounterpart(conversation.getNextCounterpart());
-        message.setType(Message.TYPE_IMAGE);
+        if (!Message.configurePrivateFileMessage(message)) {
+            message.setCounterpart(conversation.getNextCounterpart());
+            message.setType(Message.TYPE_IMAGE);
+        }
+        Log.d(Config.LOGTAG,"attachImage: type="+message.getType());
         mFileAddingExecutor.execute(() -> {
             try {
                 getFileBackend().copyImageToPrivateStorage(message, uri);
@@ -1431,7 +1436,7 @@ public class XmppConnectionService extends Service {
         }
 
 
-        boolean mucMessage = conversation.getMode() == Conversation.MODE_MULTI && message.getType() != Message.TYPE_PRIVATE;
+        boolean mucMessage = conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage();
         if (mucMessage) {
             message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid());
         }
@@ -1466,6 +1471,7 @@ public class XmppConnectionService extends Service {
                     packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
                 }
             }
+            Log.d(Config.LOGTAG,packet.toString());
             sendMessagePacket(account, packet);
         }
     }

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -728,14 +728,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
         final Message message;
         if (conversation.getCorrectingMessage() == null) {
             message = new Message(conversation, body, conversation.getNextEncryption());
-            if (conversation.getMode() == Conversation.MODE_MULTI) {
-                final Jid nextCounterpart = conversation.getNextCounterpart();
-                if (nextCounterpart != null) {
-                    message.setCounterpart(nextCounterpart);
-                    message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
-                    message.setType(Message.TYPE_PRIVATE);
-                }
-            }
+            Message.configurePrivateMessage(message);
         } else {
             message = conversation.getCorrectingMessage();
             message.setBody(body);

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -32,6 +32,8 @@ import android.widget.RelativeLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.google.common.base.Strings;
+
 import java.net.URL;
 import java.util.List;
 import java.util.regex.Matcher;
@@ -70,6 +72,7 @@ import eu.siacs.conversations.utils.GeoHelper;
 import eu.siacs.conversations.utils.StylingHelper;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xmpp.mam.MamReference;
+import rocks.xmpp.addr.Jid;
 
 public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextView.CopyHandler {
 
@@ -439,7 +442,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 				body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 			}
 			boolean startsWithQuote = handleTextQuotes(body, darkBackground);
-			if (message.getType() != Message.TYPE_PRIVATE) {
+			if (!message.isPrivateMessage()) {
 				if (hasMeCommand) {
 					body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
 							Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -449,13 +452,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 				if (message.getStatus() <= Message.STATUS_RECEIVED) {
 					privateMarker = activity.getString(R.string.private_message);
 				} else {
-					final String to;
-					if (message.getCounterpart() != null) {
-						to = message.getCounterpart().getResource();
-					} else {
-						to = "";
-					}
-					privateMarker = activity.getString(R.string.private_message_to, to);
+					Jid cp = message.getCounterpart();
+					privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource()));
 				}
 				body.insert(0, privateMarker);
 				int privateMarkerIndex = privateMarker.length();
@@ -506,27 +504,27 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 		}
 	}
 
-	private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text) {
+	private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text, final boolean darkBackground) {
+		toggleWhisperInfo(viewHolder, message, darkBackground);
 		viewHolder.image.setVisibility(View.GONE);
-		viewHolder.messageBody.setVisibility(View.GONE);
 		viewHolder.audioPlayer.setVisibility(View.GONE);
 		viewHolder.download_button.setVisibility(View.VISIBLE);
 		viewHolder.download_button.setText(text);
 		viewHolder.download_button.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
 	}
 
-	private void displayOpenableMessage(ViewHolder viewHolder, final Message message) {
+	private void displayOpenableMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) {
+		toggleWhisperInfo(viewHolder, message, darkBackground);
 		viewHolder.image.setVisibility(View.GONE);
-		viewHolder.messageBody.setVisibility(View.GONE);
 		viewHolder.audioPlayer.setVisibility(View.GONE);
 		viewHolder.download_button.setVisibility(View.VISIBLE);
 		viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message)));
 		viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
 	}
 
-	private void displayLocationMessage(ViewHolder viewHolder, final Message message) {
+	private void displayLocationMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) {
+		toggleWhisperInfo(viewHolder, message, darkBackground);
 		viewHolder.image.setVisibility(View.GONE);
-		viewHolder.messageBody.setVisibility(View.GONE);
 		viewHolder.audioPlayer.setVisibility(View.GONE);
 		viewHolder.download_button.setVisibility(View.VISIBLE);
 		viewHolder.download_button.setText(R.string.show_location);
@@ -534,8 +532,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 	}
 
 	private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground) {
+		toggleWhisperInfo(viewHolder, message, darkBackground);
 		viewHolder.image.setVisibility(View.GONE);
-		viewHolder.messageBody.setVisibility(View.GONE);
 		viewHolder.download_button.setVisibility(View.GONE);
 		final RelativeLayout audioPlayer = viewHolder.audioPlayer;
 		audioPlayer.setVisibility(View.VISIBLE);
@@ -543,9 +541,9 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 		this.audioPlayer.init(audioPlayer, message);
 	}
 
-	private void displayImageMessage(ViewHolder viewHolder, final Message message) {
+	private void displayImageMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) {
+		toggleWhisperInfo(viewHolder, message, darkBackground);
 		viewHolder.download_button.setVisibility(View.GONE);
-		viewHolder.messageBody.setVisibility(View.GONE);
 		viewHolder.audioPlayer.setVisibility(View.GONE);
 		viewHolder.image.setVisibility(View.VISIBLE);
 		FileParams params = message.getFileParams();
@@ -572,6 +570,25 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 		viewHolder.image.setOnClickListener(v -> openDownloadable(message));
 	}
 
+	private void toggleWhisperInfo(ViewHolder viewHolder, final Message message, final boolean darkBackground) {
+		if (message.isPrivateMessage()) {
+			final String privateMarker;
+			if (message.getStatus() <= Message.STATUS_RECEIVED) {
+				privateMarker = activity.getString(R.string.private_message);
+			} else {
+				Jid cp = message.getCounterpart();
+				privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource()));
+			}
+			final SpannableString body = new SpannableString(privateMarker);
+			body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+			body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+			viewHolder.messageBody.setText(body);
+			viewHolder.messageBody.setVisibility(View.VISIBLE);
+		} else {
+			viewHolder.messageBody.setVisibility(View.GONE);
+		}
+	}
+
 	private void loadMoreMessages(Conversation conversation) {
 		conversation.setLastClearHistory(0, null);
 		activity.xmppConnectionService.updateConversation(conversation);
@@ -722,19 +739,19 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 		final Transferable transferable = message.getTransferable();
 		if (message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
 			if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) {
-				displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)));
+				displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), darkBackground);
 			} else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
-				displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)));
+				displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground);
 			} else {
 				displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first, darkBackground);
 			}
 		} else if (message.isFileOrImage() && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
 			if (message.getFileParams().width > 0 && message.getFileParams().height > 0) {
-				displayImageMessage(viewHolder, message);
+				displayImageMessage(viewHolder, message, darkBackground);
 			} else if (message.getFileParams().runtime > 0) {
 				displayAudioMessage(viewHolder, message, darkBackground);
 			} else {
-				displayOpenableMessage(viewHolder, message);
+				displayOpenableMessage(viewHolder, message, darkBackground);
 			}
 		} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 			if (account.isPgpDecryptionServiceConnected()) {
@@ -756,7 +773,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 			displayInfoMessage(viewHolder, activity.getString(R.string.omemo_decryption_failed), darkBackground);
 		} else {
 			if (message.isGeoUri()) {
-				displayLocationMessage(viewHolder, message);
+				displayLocationMessage(viewHolder, message, darkBackground);
 			} else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) {
 				displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground);
 			} else if (message.treatAsDownloadable()) {
@@ -766,19 +783,22 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 						displayDownloadableMessage(viewHolder,
 								message,
 								activity.getString(R.string.check_x_filesize,
-										UIHelper.getFileDescriptionString(activity, message)));
+										UIHelper.getFileDescriptionString(activity, message)),
+								darkBackground);
 					} else {
 						displayDownloadableMessage(viewHolder,
 								message,
 								activity.getString(R.string.check_x_filesize_on_host,
 										UIHelper.getFileDescriptionString(activity, message),
-										url.getHost()));
+										url.getHost()),
+								darkBackground);
 					}
 				} catch (Exception e) {
 					displayDownloadableMessage(viewHolder,
 							message,
 							activity.getString(R.string.check_x_filesize,
-									UIHelper.getFileDescriptionString(activity, message)));
+									UIHelper.getFileDescriptionString(activity, message)),
+							darkBackground);
 				}
 			} else {
 				displayTextMessage(viewHolder, message, darkBackground, type);

src/main/res/layout/message_content.xml 🔗

@@ -1,6 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
+    <eu.siacs.conversations.ui.widget.CopyTextView
+        android:id="@+id/message_body"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:autoLink="web"
+        android:longClickable="true"
+        android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+
     <ImageView
         android:id="@+id/message_image"
         android:layout_width="wrap_content"
@@ -12,14 +20,6 @@
         android:longClickable="true"
         android:scaleType="centerCrop"/>
 
-    <eu.siacs.conversations.ui.widget.CopyTextView
-        android:id="@+id/message_body"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:autoLink="web"
-        android:longClickable="true"
-        android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
-
     <Button
         android:id="@+id/download_button"
         style="?android:attr/buttonStyleSmall"