improve replies on file or image messages

This commit is contained in:
kosyak 2024-05-22 20:30:58 +02:00
parent 350b36a2d6
commit 8c78febaa8
6 changed files with 213 additions and 17 deletions

View file

@ -1,10 +1,13 @@
package eu.siacs.conversations.entities; package eu.siacs.conversations.entities;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -16,6 +19,7 @@ import com.google.common.primitives.Longs;
import org.json.JSONException; import org.json.JSONException;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
@ -27,10 +31,12 @@ import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.http.URL; import eu.siacs.conversations.http.URL;
import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.ui.util.PresenceSelector;
import eu.siacs.conversations.ui.util.QuoteHelper; import eu.siacs.conversations.ui.util.QuoteHelper;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
@ -38,6 +44,7 @@ import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.StringUtils;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.Tag;
@ -839,9 +846,17 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
} }
public SpannableStringBuilder getBodyForDisplaying() { public SpannableStringBuilder getBodyForDisplaying() {
return getBodyForDisplaying(false);
}
public SpannableStringBuilder getBodyForDisplaying(boolean omitReplyText) {
if (replyMessage != null) { if (replyMessage != null) {
if (omitReplyText) {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(removeReplyFallback(this).toString()).trim());
}
try { try {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getReplyText(replyMessage) + "\n" + removeReplyFallback(this, replyMessage)).trim()); return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getReplyText(replyMessage) + "\n" + removeReplyFallback(this)).trim());
} catch (IndexOutOfBoundsException e) { } catch (IndexOutOfBoundsException e) {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); return new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
} }
@ -850,16 +865,20 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
} }
} }
public SpannableStringBuilder getBodyForReplyPreview() { public SpannableStringBuilder getBodyForReplyPreview(XmppConnectionService xmppConnectionService) {
try { if (isFileOrImage()) {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getReplyText(this).toString()).trim()); return SpannableStringBuilder.valueOf(StringUtils.capitalize(UIHelper.getFileDescriptionString(xmppConnectionService, this)));
} catch (IndexOutOfBoundsException e) { } else {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); try {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getReplyText(this).toString()).trim());
} catch (IndexOutOfBoundsException e) {
return new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
}
} }
} }
private StringBuilder getReplyText(Message message) { private StringBuilder getReplyText(Message message) {
StringBuilder reply = removeReplyFallback(message, message.replyMessage); StringBuilder reply = removeReplyFallback(message);
reply.insert(0, '>'); reply.insert(0, '>');
for (int i=0;i<reply.length();i++) { for (int i=0;i<reply.length();i++) {
char c = reply.charAt(i); char c = reply.charAt(i);
@ -872,7 +891,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return reply; return reply;
} }
private StringBuilder removeReplyFallback(Message message, Message replyMessage) { private StringBuilder removeReplyFallback(Message message) {
StringBuilder sb = new StringBuilder(message.body); StringBuilder sb = new StringBuilder(message.body);
List<Element> replyFallback = message.getFallbacks("urn:xmpp:reply:0"); List<Element> replyFallback = message.getFallbacks("urn:xmpp:reply:0");

View file

@ -38,6 +38,7 @@ import android.os.Vibrator;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.Editable; import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.format.DateUtils; import android.text.format.DateUtils;
@ -62,6 +63,7 @@ import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.ListView; import android.widget.ListView;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.TextView.OnEditorActionListener; import android.widget.TextView.OnEditorActionListener;
@ -73,6 +75,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
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;
@ -126,6 +129,7 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.adapter.CommandAdapter; import eu.siacs.conversations.ui.adapter.CommandAdapter;
import eu.siacs.conversations.ui.adapter.MediaPreviewAdapter; import eu.siacs.conversations.ui.adapter.MediaPreviewAdapter;
import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter;
import eu.siacs.conversations.ui.text.QuoteSpan;
import eu.siacs.conversations.ui.util.ActivityResult; import eu.siacs.conversations.ui.util.ActivityResult;
import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.ui.util.Attachment;
import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
@ -1572,9 +1576,33 @@ public class ConversationFragment extends XmppFragment
return; return;
} }
SpannableStringBuilder body = message.getBodyForReplyPreview(); SpannableStringBuilder body = message.getBodyForReplyPreview(activity.xmppConnectionService);
if (message.isFileOrImage() || message.isOOb()) body.append(" 🖼️");
messageListAdapter.handleTextQuotes(body, activity.isDarkTheme(), true, message); if (message.isFileOrImage() && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
if (message.getFileParams().width > 0 && message.getFileParams().height > 0) {
binding.contextPreviewImage.setVisibility(View.VISIBLE);
binding.contextPreviewDoc.setVisibility(View.GONE);
binding.contextPreviewAudio.setVisibility(View.GONE);
activity.loadBitmap(message, binding.contextPreviewImage);
} else if (message.getFileParams().runtime > 0) {
binding.contextPreviewImage.setVisibility(View.GONE);
binding.contextPreviewDoc.setVisibility(View.GONE);
binding.contextPreviewAudio.setVisibility(View.VISIBLE);
} else {
binding.contextPreviewImage.setVisibility(View.GONE);
binding.contextPreviewDoc.setVisibility(View.VISIBLE);
binding.contextPreviewAudio.setVisibility(View.GONE);
}
} else if (message.isOOb()) {
messageListAdapter.handleTextQuotes(body, activity.isDarkTheme(), true, message);
binding.contextPreviewImage.setVisibility(View.GONE);
binding.contextPreviewDoc.setVisibility(View.GONE);
body.append(" 🖼️");
} else {
messageListAdapter.handleTextQuotes(body, activity.isDarkTheme(), true, message);
binding.contextPreviewImage.setVisibility(View.GONE);
binding.contextPreviewDoc.setVisibility(View.GONE);
}
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

@ -314,7 +314,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
return this.getItemViewType(getItem(position)); return this.getItemViewType(getItem(position));
} }
private int getMessageTextColor(boolean onDark, boolean primary) { public int getMessageTextColor(boolean onDark, boolean primary) {
if (onDark) { if (onDark) {
return ContextCompat.getColor(activity, primary ? R.color.white : R.color.white70); return ContextCompat.getColor(activity, primary ? R.color.white : R.color.white70);
} else { } else {
@ -520,8 +520,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
int color = this.getMessageTextColor(darkBackground, false); int color = this.getMessageTextColor(darkBackground, false);
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 && start == 0 ? ContextCompat.getColor(activity, R.color.blue_a100) : -1, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if (highlightReply) { if (highlightReply && start == 0) {
body.setSpan(new ReplyClickableSpan(new WeakReference(replyClickListener), new WeakReference(message)), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); body.setSpan(new ReplyClickableSpan(new WeakReference(replyClickListener), new WeakReference(message)), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
} }
@ -633,15 +633,22 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
if (message.getBody() != null) { if (message.getBody() != null) {
final String nick = UIHelper.getMessageDisplayName(message); final String nick = UIHelper.getMessageDisplayName(message);
SpannableStringBuilder body = message.getBodyForDisplaying();
Message replyMessage = message.getReplyMessage();
Boolean fileOrImageReply = replyMessage != null && replyMessage.isFileOrImage();
SpannableStringBuilder body = message.getBodyForDisplaying(fileOrImageReply);
boolean hasMeCommand = message.hasMeCommand(); boolean hasMeCommand = message.hasMeCommand();
if (hasMeCommand) { if (hasMeCommand) {
body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
} }
if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
body.append("\u2026"); body.append("\u2026");
} }
Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class); Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class);
for (Message.MergeSeparator mergeSeparator : mergeSeparators) { for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
int start = body.getSpanStart(mergeSeparator); int start = body.getSpanStart(mergeSeparator);
@ -656,7 +663,46 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
applyQuoteSpan(body, start, end, darkBackground, message.getReplyMessage() != null, message); applyQuoteSpan(body, start, end, darkBackground, message.getReplyMessage() != null, message);
} }
handleTextQuotes(body, darkBackground, message.getReplyMessage() != null, message); if (fileOrImageReply) {
viewHolder.nonTextReplyContent.setVisibility(View.VISIBLE);
WeakReference<ReplyClickListener> listener = new WeakReference<>(replyClickListener);
viewHolder.nonTextReplyContent.setOnClickListener(v -> {
ReplyClickListener l = listener.get();
if (l != null) {
l.onReplyClick(message);
}
});
TextView text = viewHolder.nonTextReplyContent.findViewById(R.id.reply_body);
TextView author = viewHolder.nonTextReplyContent.findViewById(R.id.context_preview_author);
ImageView contextPreviewImage = viewHolder.nonTextReplyContent.findViewById(R.id.context_preview_image);
View contextPreviewDoc = viewHolder.nonTextReplyContent.findViewById(R.id.context_preview_doc);
View contextPreviewAudio = viewHolder.nonTextReplyContent.findViewById(R.id.context_preview_audio);
text.setText(replyMessage.getBodyForReplyPreview(activity.xmppConnectionService));
author.setText(replyMessage.getAvatarName());
if (replyMessage.getFileParams().width > 0 && replyMessage.getFileParams().height > 0) {
contextPreviewImage.setVisibility(View.VISIBLE);
contextPreviewDoc.setVisibility(View.GONE);
contextPreviewAudio.setVisibility(View.GONE);
activity.loadBitmap(replyMessage, contextPreviewImage);
} else if (replyMessage.getFileParams().runtime > 0) {
contextPreviewImage.setVisibility(View.GONE);
contextPreviewDoc.setVisibility(View.GONE);
contextPreviewAudio.setVisibility(View.VISIBLE);
} else {
contextPreviewImage.setVisibility(View.GONE);
contextPreviewDoc.setVisibility(View.VISIBLE);
contextPreviewAudio.setVisibility(View.GONE);
}
} else {
viewHolder.nonTextReplyContent.setVisibility(View.GONE);
}
handleTextQuotes(body, darkBackground, message.getReplyMessage() != null && !fileOrImageReply, 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(),
@ -859,6 +905,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
viewHolder.image = view.findViewById(R.id.message_image); viewHolder.image = view.findViewById(R.id.message_image);
viewHolder.messageBody = view.findViewById(R.id.message_body); viewHolder.messageBody = view.findViewById(R.id.message_body);
viewHolder.nonTextReplyContent = view.findViewById(R.id.non_text_reply_content);
viewHolder.time = view.findViewById(R.id.message_time); viewHolder.time = view.findViewById(R.id.message_time);
viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
viewHolder.audioPlayer = view.findViewById(R.id.audio_player); viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
@ -875,6 +922,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
viewHolder.image = view.findViewById(R.id.message_image); viewHolder.image = view.findViewById(R.id.message_image);
viewHolder.messageBody = view.findViewById(R.id.message_body); viewHolder.messageBody = view.findViewById(R.id.message_body);
viewHolder.nonTextReplyContent = view.findViewById(R.id.non_text_reply_content);
viewHolder.time = view.findViewById(R.id.message_time); viewHolder.time = view.findViewById(R.id.message_time);
viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
viewHolder.encryption = view.findViewById(R.id.message_encryption); viewHolder.encryption = view.findViewById(R.id.message_encryption);
@ -1302,6 +1350,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements DraggableLi
protected TextView status_message; protected TextView status_message;
protected TextView encryption; protected TextView encryption;
protected View nonTextReplyContent;
protected View clicksInterceptor; protected View clicksInterceptor;
int position; int position;

View file

@ -47,4 +47,8 @@ public class StringUtils {
return input == null || input.trim().isEmpty() ? null : input; return input == null || input.trim().isEmpty() ? null : input;
} }
public static String capitalize(String str) {
if(str == null || str.length()<=1) return str;
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
} }

View file

@ -70,6 +70,34 @@
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
android:contentDescription="Reply to" /> android:contentDescription="Reply to" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/context_preview_image"
android:layout_width="32dp"
android:layout_height="32dp"
android:scaleType="centerCrop"
android:layout_marginEnd="16dp"/>
<ImageView
android:id="@+id/context_preview_doc"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="16dp"
android:src="?attr/ic_attach_document" />
<ImageView
android:id="@+id/context_preview_audio"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="16dp"
android:src="?attr/ic_attach_record" />
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_weight="1" android:layout_weight="1"
android:layout_width="0dp" android:layout_width="0dp"
@ -87,7 +115,6 @@
android:id="@+id/context_preview_text" android:id="@+id/context_preview_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLines="5"
android:ellipsize="end" /> android:ellipsize="end" />
</LinearLayout> </LinearLayout>

View file

@ -1,6 +1,74 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"> <merge xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:id="@+id/non_text_reply_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<View
android:id="@+id/divider"
android:layout_width="2sp"
android:layout_height="32dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="1dp"
android:layout_centerVertical="true"
android:background="@color/blue_a100" />
<FrameLayout
android:id="@+id/icons_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_toEndOf="@id/divider"
android:layout_centerVertical="true">
<ImageView
android:id="@+id/context_preview_image"
android:layout_width="32dp"
android:layout_height="32dp"
android:scaleType="centerCrop"
android:visibility="gone"/>
<ImageView
android:id="@+id/context_preview_doc"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="?attr/ic_attach_document"
android:layout_toEndOf="@id/divider"
android:layout_below="@id/reply_body"
android:visibility="gone"/>
<ImageView
android:id="@+id/context_preview_audio"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="?attr/ic_attach_record"
android:layout_toEndOf="@id/divider"
android:layout_below="@id/reply_body"
android:visibility="gone"/>
</FrameLayout>
<TextView
android:id="@+id/context_preview_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/icons_container"
android:fontFamily="sans-serif-medium" />
<TextView
android:id="@+id/reply_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-2dp"
android:longClickable="false"
android:layout_toEndOf="@id/icons_container"
android:layout_below="@id/context_preview_author"/>
</RelativeLayout>
<TextView <TextView
android:id="@+id/message_body" android:id="@+id/message_body"
android:layout_width="wrap_content" android:layout_width="wrap_content"