diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/:w b/src/main/java/eu/siacs/conversations/ui/adapter/:w deleted file mode 100644 index b033660fd7480ce0b845e4d65ed89f8cc6efc978..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/:w +++ /dev/null @@ -1,1701 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import android.Manifest; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.content.res.ColorStateList; -import android.graphics.Typeface; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.preference.PreferenceManager; -import android.text.Editable; -import android.text.Spanned; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.style.ImageSpan; -import android.text.style.ClickableSpan; -import android.text.format.DateUtils; -import android.text.style.ForegroundColorSpan; -import android.text.style.RelativeSizeSpan; -import android.text.style.StyleSpan; -import android.text.style.URLSpan; -import android.util.DisplayMetrics; -import android.util.LruCache; -import android.view.accessibility.AccessibilityEvent; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.AttrRes; -import androidx.annotation.ColorInt; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.core.content.res.ResourcesCompat; -import androidx.core.widget.ImageViewCompat; - -import com.google.android.material.imageview.ShapeableImageView; -import com.google.android.material.shape.CornerFamily; -import com.google.android.material.shape.ShapeAppearanceModel; - -import com.cheogram.android.BobTransfer; -import com.cheogram.android.MessageTextActionModeCallback; -import com.cheogram.android.SwipeDetector; -import com.cheogram.android.WebxdcPage; -import com.cheogram.android.WebxdcUpdate; - -import com.google.android.material.button.MaterialButton; -import com.google.android.material.color.MaterialColors; -import com.google.common.base.Joiner; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; - -import com.lelloman.identicon.view.GithubIdenticonView; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.NoSuchAlgorithmException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.ipfs.cid.Cid; - -import me.saket.bettermovementmethod.BetterLinkMovementMethod; - -import eu.siacs.conversations.AppSettings; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message.FileParams; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Roster; -import eu.siacs.conversations.entities.RtpSessionStatus; -import eu.siacs.conversations.entities.Transferable; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.MessageArchiveService; -import eu.siacs.conversations.services.NotificationService; -import eu.siacs.conversations.ui.Activities; -import eu.siacs.conversations.ui.ConversationFragment; -import eu.siacs.conversations.ui.ConversationsActivity; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.service.AudioPlayer; -import eu.siacs.conversations.ui.text.DividerSpan; -import eu.siacs.conversations.ui.text.QuoteSpan; -import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.MyLinkify; -import eu.siacs.conversations.ui.util.QuoteHelper; -import eu.siacs.conversations.ui.util.ShareUtil; -import eu.siacs.conversations.ui.util.ViewUtil; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.Emoticons; -import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.utils.MessageUtils; -import eu.siacs.conversations.utils.StylingHelper; -import eu.siacs.conversations.utils.TimeFrameUtils; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.mam.MamReference; -import eu.siacs.conversations.xml.Element; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class MessageAdapter extends ArrayAdapter { - - public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; - private static final int SENT = 0; - private static final int RECEIVED = 1; - private static final int STATUS = 2; - private static final int DATE_SEPARATOR = 3; - private static final int RTP_SESSION = 4; - private final XmppActivity activity; - private final AudioPlayer audioPlayer; - private List highlightedTerm = null; - private final DisplayMetrics metrics; - private ConversationFragment mConversationFragment = null; - private OnContactPictureClicked mOnContactPictureClickedListener; - private OnContactPictureClicked mOnMessageBoxClickedListener; - private OnContactPictureClicked mOnMessageBoxSwipedListener; - private OnContactPictureLongClicked mOnContactPictureLongClickedListener; - private OnInlineImageLongClicked mOnInlineImageLongClickedListener; - private boolean mUseGreenBackground = false; - private BubbleDesign bubbleDesign = new BubbleDesign(false, false); - private final boolean mForceNames; - private final Map lastWebxdcUpdate = new HashMap<>(); - private String selectionUuid = null; - - public MessageAdapter( - final XmppActivity activity, final List messages, final boolean forceNames) { - super(activity, 0, messages); - this.audioPlayer = new AudioPlayer(this); - this.activity = activity; - metrics = getContext().getResources().getDisplayMetrics(); - updatePreferences(); - this.mForceNames = forceNames; - } - - public MessageAdapter(final XmppActivity activity, final List messages) { - this(activity, messages, false); - } - - private static void resetClickListener(View... views) { - for (View view : views) { - if (view != null) view.setOnClickListener(null); - } - } - - public void flagScreenOn() { - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - public void flagScreenOff() { - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - public void setVolumeControl(final int stream) { - activity.setVolumeControlStream(stream); - } - - public void setOnContactPictureClicked(OnContactPictureClicked listener) { - this.mOnContactPictureClickedListener = listener; - } - - public void setOnMessageBoxClicked(OnContactPictureClicked listener) { - this.mOnMessageBoxClickedListener = listener; - } - - public void setOnMessageBoxSwiped(OnContactPictureClicked listener) { - this.mOnMessageBoxSwipedListener = listener; - } - - public void setConversationFragment(ConversationFragment frag) { - mConversationFragment = frag; - } - - public void quoteText(String text) { - if (mConversationFragment != null) mConversationFragment.quoteText(text); - } - - public boolean hasSelection() { - return selectionUuid != null; - } - - public Activity getActivity() { - return activity; - } - - public void setOnContactPictureLongClicked(OnContactPictureLongClicked listener) { - this.mOnContactPictureLongClickedListener = listener; - } - - public void setOnInlineImageLongClicked(OnInlineImageLongClicked listener) { - this.mOnInlineImageLongClickedListener = listener; - } - - @Override - public int getViewTypeCount() { - return 5; - } - - private int getItemViewType(Message message) { - if (message.getType() == Message.TYPE_STATUS) { - if (DATE_SEPARATOR_BODY.equals(message.getBody())) { - return DATE_SEPARATOR; - } else { - return STATUS; - } - } else if (message.getType() == Message.TYPE_RTP_SESSION) { - return RTP_SESSION; - } else if (message.getStatus() <= Message.STATUS_RECEIVED) { - return RECEIVED; - } else { - return SENT; - } - } - - @Override - public int getItemViewType(int position) { - return this.getItemViewType(getItem(position)); - } - - private void displayStatus( - final ViewHolder viewHolder, - final Message message, - final int type, - final BubbleColor bubbleColor) { - final int mergedStatus = message.getMergedStatus(); - final boolean error; - if (viewHolder.indicatorReceived != null) { - viewHolder.indicatorReceived.setVisibility(View.GONE); - } - final Transferable transferable = message.getTransferable(); - final boolean multiReceived = - message.getConversation().getMode() == Conversation.MODE_MULTI - && mergedStatus <= Message.STATUS_RECEIVED; - final String fileSize; - if (message.isFileOrImage() - || transferable != null - || MessageUtils.unInitiatedButKnownSize(message)) { - final FileParams params = message.getFileParams(); - fileSize = params.size != null ? UIHelper.filesizeToString(params.size) : null; - if (message.getStatus() == Message.STATUS_SEND_FAILED - || (transferable != null - && (transferable.getStatus() == Transferable.STATUS_FAILED - || transferable.getStatus() - == Transferable.STATUS_CANCELLED))) { - error = true; - } else { - error = message.getStatus() == Message.STATUS_SEND_FAILED; - } - } else { - fileSize = null; - error = message.getStatus() == Message.STATUS_SEND_FAILED; - } - if (type == SENT) { - final @DrawableRes Integer receivedIndicator = - getMessageStatusAsDrawable(message, mergedStatus); - if (receivedIndicator == null) { - viewHolder.indicatorReceived.setVisibility(View.INVISIBLE); - } else { - viewHolder.indicatorReceived.setImageResource(receivedIndicator); - if (mergedStatus == Message.STATUS_SEND_FAILED) { - setImageTintError(viewHolder.indicatorReceived); - } else { - setImageTint(viewHolder.indicatorReceived, bubbleColor); - } - viewHolder.indicatorReceived.setVisibility(View.VISIBLE); - } - } - final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus); - - if (error && type == SENT) { - viewHolder.time.setTextColor( - MaterialColors.getColor( - viewHolder.time, com.google.android.material.R.attr.colorError)); - } else { - setTextColor(viewHolder.time, bubbleColor); - } - setTextColor(viewHolder.subject, bubbleColor); - if (message.getEncryption() == Message.ENCRYPTION_NONE) { - viewHolder.indicator.setVisibility(View.GONE); - } else { - boolean verified = false; - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - final FingerprintStatus status = - message.getConversation() - .getAccount() - .getAxolotlService() - .getFingerprintTrust(message.getFingerprint()); - if (status != null && status.isVerified()) { - verified = true; - } - } - if (verified) { - viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp); - } else { - viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp); - } - if (error && type == SENT) { - setImageTintError(viewHolder.indicator); - } else { - setImageTint(viewHolder.indicator, bubbleColor); - } - viewHolder.indicator.setVisibility(View.VISIBLE); - } - - if (viewHolder.edit_indicator != null) { - if (message.edited()) { - viewHolder.edit_indicator.setVisibility(View.VISIBLE); - if (error && type == SENT) { - setImageTintError(viewHolder.edit_indicator); - } else { - setImageTint(viewHolder.edit_indicator, bubbleColor); - } - } else { - viewHolder.edit_indicator.setVisibility(View.GONE); - } - } - - final String formattedTime = - UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); - final String bodyLanguage = message.getBodyLanguage(); - final ImmutableList.Builder timeInfoBuilder = new ImmutableList.Builder<>(); - if (message.getStatus() <= Message.STATUS_RECEIVED) { - timeInfoBuilder.add(formattedTime); - if (fileSize != null) { - timeInfoBuilder.add(fileSize); - } - if (mForceNames || multiReceived || (message.getTrueCounterpart() != null && message.getContact() != null)) { - final String displayName = UIHelper.getMessageDisplayName(message); - if (displayName != null) { - timeInfoBuilder.add(displayName); - } - } - if (bodyLanguage != null) { - timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US)); - } - } else { - if (bodyLanguage != null) { - timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US)); - } - if (fileSize != null) { - timeInfoBuilder.add(fileSize); - } - // for space reasons we display only 'additional status info' (send progress or concrete - // failure reason) or the time - if (additionalStatusInfo != null) { - timeInfoBuilder.add(additionalStatusInfo); - } else { - timeInfoBuilder.add(formattedTime); - } - } - final var timeInfo = timeInfoBuilder.build(); - viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo)); - } - - public static @DrawableRes Integer getMessageStatusAsDrawable( - final Message message, final int status) { - final var transferable = message.getTransferable(); - return switch (status) { - case Message.STATUS_WAITING -> R.drawable.ic_more_horiz_24dp; - case Message.STATUS_UNSEND -> transferable == null ? null : R.drawable.ic_upload_24dp; - case Message.STATUS_SEND -> R.drawable.ic_done_24dp; - case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> R.drawable - .ic_done_all_24dp; - case Message.STATUS_SEND_FAILED -> { - final String errorMessage = message.getErrorMessage(); - if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) { - yield R.drawable.ic_cancel_24dp; - } else { - yield R.drawable.ic_error_24dp; - } - } - case Message.STATUS_OFFERED -> R.drawable.ic_p2p_24dp; - default -> null; - }; - } - - @Nullable - private String getAdditionalStatusInfo(final Message message, final int mergedStatus) { - final String additionalStatusInfo; - if (mergedStatus == Message.STATUS_SEND_FAILED) { - final String errorMessage = Strings.nullToEmpty(message.getErrorMessage()); - final String[] errorParts = errorMessage.split("\\u001f", 2); - if (errorParts.length == 2 && errorParts[0].equals("file-too-large")) { - additionalStatusInfo = getContext().getString(R.string.file_too_large); - } else { - additionalStatusInfo = null; - } - } else if (mergedStatus == Message.STATUS_UNSEND) { - final var transferable = message.getTransferable(); - if (transferable == null) { - return null; - } - return getContext().getString(R.string.sending_file, transferable.getProgress()); - } else { - additionalStatusInfo = null; - } - return additionalStatusInfo; - } - - private void displayInfoMessage( - ViewHolder viewHolder, CharSequence text, final BubbleColor bubbleColor) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - viewHolder.messageBody.setText(text); - viewHolder.messageBody.setTextColor( - bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)); - viewHolder.messageBody.setTextIsSelectable(false); - } - - private void displayEmojiMessage( - final ViewHolder viewHolder, final SpannableStringBuilder body, final BubbleColor bubbleColor) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - setTextColor(viewHolder.messageBody, bubbleColor); - ImageSpan[] imageSpans = body.getSpans(0, body.length(), ImageSpan.class); - float size = imageSpans.length == 1 || Emoticons.isEmoji(body.toString()) ? 3.0f : 2.0f; - body.setSpan( - new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(body); - } - - private void applyQuoteSpan( - final TextView textView, - Editable body, - int start, - int end, - final BubbleColor bubbleColor, - final boolean makeEdits) { - if (makeEdits && start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) { - body.insert(start++, "\n"); - body.setSpan( - new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - end++; - } - if (makeEdits && end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) { - body.insert(end, "\n"); - body.setSpan( - new DividerSpan(false), - end, - end + ("\n".equals(body.subSequence(end + 1, end + 2).toString()) ? 2 : 1), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ); - } - final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); - body.setSpan( - new QuoteSpan(bubbleToOnSurfaceVariant(textView, bubbleColor), metrics), - start, - end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - public boolean handleTextQuotes(final TextView textView, final Editable body) { - return handleTextQuotes(textView, body, true); - } - - public boolean handleTextQuotes(final TextView textView, final Editable body, final boolean deleteMarkers) { - final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles; - final BubbleColor bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE; - return handleTextQuotes(textView, body, bubbleColor, deleteMarkers); - } - - /** - * 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( - final TextView textView, - final Editable body, - final BubbleColor bubbleColor, - final boolean deleteMarkers) { - boolean startsWithQuote = false; - int quoteDepth = 1; - while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) { - char previous = '\n'; - int lineStart = -1; - int lineTextStart = -1; - int quoteStart = -1; - int skipped = 0; - for (int i = 0; i <= body.length(); i++) { - if (!deleteMarkers && QuoteHelper.isRelativeSizeSpanned(body, i)) { - skipped++; - continue; - } - char current = body.length() > i ? body.charAt(i) : '\n'; - if (lineStart == -1) { - if (previous == '\n') { - if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) { - // Line start with quote - lineStart = i; - if (quoteStart == -1) quoteStart = i - skipped; - if (i == 0) startsWithQuote = true; - } else if (quoteStart >= 0) { - // Line start without quote, apply spans there - applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor, deleteMarkers); - quoteStart = -1; - } - } - } else { - // Remove extra spaces between > and first character in the line - // > character will be removed too - if (current != ' ' && lineTextStart == -1) { - lineTextStart = i; - } - if (current == '\n') { - if (deleteMarkers) { - i -= lineTextStart - lineStart; - body.delete(lineStart, lineTextStart); - if (i == lineStart) { - // Avoid empty lines because span over empty line can be hidden - body.insert(i++, " "); - } - } else { - body.setSpan(new RelativeSizeSpan(i - (lineTextStart - lineStart) == lineStart ? 1 : 0), lineStart, lineTextStart, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | StylingHelper.XHTML_REMOVE << Spanned.SPAN_USER_SHIFT); - } - lineStart = -1; - lineTextStart = -1; - } - } - previous = current; - skipped = 0; - } - if (quoteStart >= 0) { - // Apply spans to finishing open quote - applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor, deleteMarkers); - } - quoteDepth++; - } - return startsWithQuote; - } - - private SpannableStringBuilder getSpannableBody(final Message message) { - Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), R.drawable.ic_photo_24dp, null); - return message.getMergedBody((cid) -> { - try { - DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid); - if (f == null || !f.canRead()) { - if (!message.trusted() && !message.getConversation().canInferPresence()) return null; - - try { - new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start(); - } catch (final NoSuchAlgorithmException | URISyntaxException e) { } - return null; - } - - Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true); - if (d == null) { - new ThumbnailTask().execute(f); - } - return d; - } catch (final IOException e) { - return null; - } - }, fallbackImg); - } - - private void displayTextMessage( - final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - setTextColor(viewHolder.messageBody, bubbleColor); - setTextSize(viewHolder.messageBody, this.bubbleDesign.largeFont); - - final ViewGroup.LayoutParams layoutParams = viewHolder.messageBody.getLayoutParams(); - layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; - viewHolder.messageBody.setLayoutParams(layoutParams); - - viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); - - if (message.getBody() != null && !message.getBody().equals("")) { - viewHolder.messageBody.setTextIsSelectable(true); - viewHolder.messageBody.setVisibility(View.VISIBLE); - final String nick = UIHelper.getMessageDisplayName(message); - SpannableStringBuilder body = getSpannableBody(message); - final var processMarkup = body.getSpans(0, body.length(), Message.PlainTextSpan.class).length > 0; - boolean hasMeCommand = message.hasMeCommand(); - if (hasMeCommand) { - body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); - } - if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { - body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); - body.append("\u2026"); - } - Message.MergeSeparator[] mergeSeparators = - body.getSpans(0, body.length(), Message.MergeSeparator.class); - for (Message.MergeSeparator mergeSeparator : mergeSeparators) { - int start = body.getSpanStart(mergeSeparator); - int end = body.getSpanEnd(mergeSeparator); - body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - for (final android.text.style.QuoteSpan quote : body.getSpans(0, body.length(), android.text.style.QuoteSpan.class)) { - int start = body.getSpanStart(quote); - int end = body.getSpanEnd(quote); - body.removeSpan(quote); - applyQuoteSpan(viewHolder.messageBody, body, start, end, bubbleColor, true); - } - boolean startsWithQuote = processMarkup ? handleTextQuotes(viewHolder.messageBody, body, bubbleColor, true) : false; - if (!message.isPrivateMessage()) { - if (hasMeCommand) { - body.setSpan( - new StyleSpan(Typeface.BOLD_ITALIC), - 0, - nick.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } else { - 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())); - } - body.insert(0, privateMarker); - int privateMarkerIndex = privateMarker.length(); - if (startsWithQuote) { - body.insert(privateMarkerIndex, "\n\n"); - body.setSpan( - new DividerSpan(false), - privateMarkerIndex, - privateMarkerIndex + 2, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } else { - body.insert(privateMarkerIndex, " "); - } - body.setSpan( - new ForegroundColorSpan( - bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)), - 0, - privateMarkerIndex, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - body.setSpan( - new StyleSpan(Typeface.BOLD), - 0, - privateMarkerIndex, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - if (hasMeCommand) { - body.setSpan( - new StyleSpan(Typeface.BOLD_ITALIC), - privateMarkerIndex + 1, - privateMarkerIndex + 1 + nick.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - if (message.getConversation().getMode() == Conversation.MODE_MULTI - && message.getStatus() == Message.STATUS_RECEIVED) { - if (message.getConversation() instanceof Conversation conversation) { - Pattern pattern = - NotificationService.generateNickHighlightPattern( - conversation.getMucOptions().getActualNick()); - Matcher matcher = pattern.matcher(body); - while (matcher.find()) { - body.setSpan( - new StyleSpan(Typeface.BOLD), - matcher.start(), - matcher.end(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualName()); - matcher = pattern.matcher(body); - while (matcher.find()) { - body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - } - Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body); - while (matcher.find()) { - if (matcher.start() < matcher.end()) { - body.setSpan( - new RelativeSizeSpan(1.2f), - matcher.start(), - matcher.end(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - - if (processMarkup) StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); - MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid()); - if (highlightedTerm != null) { - StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm); - } - - viewHolder.messageBody.setAutoLinkMask(0); - viewHolder.messageBody.setText(body); - BetterLinkMovementMethod method = new BetterLinkMovementMethod() { - @Override - protected void dispatchUrlLongClick(TextView tv, ClickableSpan span) { - if (span instanceof URLSpan || mOnInlineImageLongClickedListener == null) { - tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); - super.dispatchUrlLongClick(tv, span); - return; - } - - Spannable body = (Spannable) tv.getText(); - ImageSpan[] imageSpans = body.getSpans(body.getSpanStart(span), body.getSpanEnd(span), ImageSpan.class); - if (imageSpans.length > 0) { - Uri uri = Uri.parse(imageSpans[0].getSource()); - Cid cid = BobTransfer.cid(uri); - if (cid == null) return; - if (mOnInlineImageLongClickedListener.onInlineImageLongClicked(cid)) { - tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); - } - } - } - }; - method.setOnLinkLongClickListener((tv, url) -> { - tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); - ShareUtil.copyLinkToClipboard(activity, url); - return true; - }); - viewHolder.messageBody.setMovementMethod(method); - } else { - viewHolder.messageBody.setText(""); - viewHolder.messageBody.setTextIsSelectable(false); - toggleWhisperInfo(viewHolder, message, bubbleColor); - } - } - - private void displayDownloadableMessage( - ViewHolder viewHolder, - final Message message, - String text, - final BubbleColor bubbleColor, final int type) { - displayTextMessage(viewHolder, message, bubbleColor, type); - viewHolder.image.setVisibility(View.GONE); - List thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; - if (thumbs != null && !thumbs.isEmpty()) { - for (Element thumb : thumbs) { - Uri uri = Uri.parse(thumb.getAttribute("uri")); - if (uri.getScheme().equals("data")) { - String[] parts = uri.getSchemeSpecificPart().split(",", 2); - parts = parts[0].split(";"); - if (!parts[0].equals("image/blurhash") && !parts[0].equals("image/thumbhash") && !parts[0].equals("image/jpeg") && !parts[0].equals("image/png") && !parts[0].equals("image/webp") && !parts[0].equals("image/gif")) continue; - } else if (uri.getScheme().equals("cid")) { - Cid cid = BobTransfer.cid(uri); - if (cid == null) continue; - DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid); - if (f == null || !f.canRead()) { - if (!message.trusted() && !message.getConversation().canInferPresence()) continue; - - try { - new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start(); - } catch (final NoSuchAlgorithmException | URISyntaxException e) { } - continue; - } - } else { - continue; - } - - int width = message.getFileParams().width; - if (width < 1 && thumb.getAttribute("width") != null) width = Integer.parseInt(thumb.getAttribute("width")); - if (width < 1) width = 1920; - - int height = message.getFileParams().height; - if (height < 1 && thumb.getAttribute("height") != null) height = Integer.parseInt(thumb.getAttribute("height")); - if (height < 1) height = 1080; - - viewHolder.image.setVisibility(View.VISIBLE); - imagePreviewLayout(width, height, viewHolder.image, true, type, viewHolder); - activity.loadBitmap(message, viewHolder.image); - viewHolder.image.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message)); - - break; - } - } - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(text); - final var attachment = Attachment.of(message); - final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment); - viewHolder.download_button.setIconResource(imageResource); - viewHolder.download_button.setOnClickListener( - v -> ConversationFragment.downloadFile(activity, message)); - } - - private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { - Cid webxdcCid = message.getFileParams().getCids().get(0); - WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService); - displayTextMessage(viewHolder, message, bubbleColor, type); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText("Open " + webxdc.getName()); - viewHolder.download_button.setOnClickListener(v -> { - Conversation conversation = (Conversation) message.getConversation(); - if (!conversation.switchToSession("webxdc\0" + message.getUuid())) { - conversation.startWebxdc(webxdc); - } - }); - - final WebxdcUpdate lastUpdate; - synchronized(lastWebxdcUpdate) { lastUpdate = lastWebxdcUpdate.get(message.getUuid()); } - if (lastUpdate == null) { - new Thread(() -> { - final WebxdcUpdate update = activity.xmppConnectionService.findLastWebxdcUpdate(message); - if (update != null) { - synchronized(lastWebxdcUpdate) { lastWebxdcUpdate.put(message.getUuid(), update); } - activity.xmppConnectionService.updateConversationUi(); - } - }).start(); - } else { - if (lastUpdate != null && (lastUpdate.getSummary() != null || lastUpdate.getDocument() != null)) { - viewHolder.messageBody.setVisibility(View.VISIBLE); - viewHolder.messageBody.setText( - (lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") + - (lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary()) - ); - } - } - - final LruCache cache = activity.xmppConnectionService.getDrawableCache(); - final Drawable d = cache.get("webxdc:icon:" + webxdcCid); - if (d == null) { - new Thread(() -> { - Drawable icon = webxdc.getIcon(); - if (icon != null) { - cache.put("webxdc:icon:" + webxdcCid, icon); - activity.xmppConnectionService.updateConversationUi(); - } - }).start(); - } else { - viewHolder.image.setVisibility(View.VISIBLE); - viewHolder.image.setImageDrawable(d); - } - } - - private void displayOpenableMessage( - ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { - displayTextMessage(viewHolder, message, bubbleColor, type); - viewHolder.image.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))); - final var attachment = Attachment.of(message); - final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment); - viewHolder.download_button.setIconResource(imageResource); - viewHolder.download_button.setOnClickListener(v -> openDownloadable(message)); - } - - private void displayLocationMessage( - ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { - displayTextMessage(viewHolder, message, bubbleColor, type); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(R.string.show_location); - final var attachment = Attachment.of(message); - final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment); - viewHolder.download_button.setIconResource(imageResource); - viewHolder.download_button.setOnClickListener(v -> showLocation(message)); - } - - private void displayAudioMessage( - ViewHolder viewHolder, Message message, final BubbleColor bubbleColor, final int type) { - displayTextMessage(viewHolder, message, bubbleColor, type); - viewHolder.image.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.GONE); - final RelativeLayout audioPlayer = viewHolder.audioPlayer; - audioPlayer.setVisibility(View.VISIBLE); - AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor); - this.audioPlayer.init(audioPlayer, message); - } - - private void displayMediaPreviewMessage( - ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { - displayTextMessage(viewHolder, message, bubbleColor, type); - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.VISIBLE); - final FileParams params = message.getFileParams(); - imagePreviewLayout(params.width, params.height, viewHolder.image, viewHolder.messageBody.getVisibility() != View.GONE, type, viewHolder); - activity.loadBitmap(message, viewHolder.image); - viewHolder.image.setOnClickListener(v -> openDownloadable(message)); - } - - private void imagePreviewLayout(int w, int h, ShapeableImageView image, boolean withOther, int type, ViewHolder viewHolder) { - final float target = activity.getResources().getDimension(R.dimen.image_preview_width); - final int scaledW; - final int scaledH; - if (Math.max(h, w) * metrics.density <= target) { - scaledW = (int) (w * metrics.density); - scaledH = (int) (h * metrics.density); - } else if (Math.max(h, w) <= target) { - scaledW = w; - scaledH = h; - } else if (w <= h) { - scaledW = (int) (w / ((double) h / target)); - scaledH = (int) target; - } else { - scaledW = (int) target; - scaledH = (int) (h / ((double) w / target)); - } - final var small = withOther ? scaledW < target : scaledW < 110 * metrics.density; - final LinearLayout.LayoutParams layoutParams = - new LinearLayout.LayoutParams(scaledW, scaledH); - image.setLayoutParams(layoutParams); - - final var bubbleRadius = activity.getResources().getDimension(R.dimen.bubble_radius); - var shape = new ShapeAppearanceModel.Builder().setTopRightCorner(CornerFamily.ROUNDED, bubbleRadius); - if (type == SENT) { - shape = shape.setTopLeftCorner(CornerFamily.ROUNDED, bubbleRadius); - } - if (small) { - final var imageRadius = activity.getResources().getDimension(R.dimen.image_radius); - shape = shape.setAllCorners(CornerFamily.ROUNDED, imageRadius); - image.setPadding(0, (int)(8 * metrics.density), 0, 0); - } else { - image.setPadding(0, 0, 0, 0); - } - image.setShapeAppearanceModel(shape.build()); - - if (!small) { - final ViewGroup.LayoutParams blayoutParams = viewHolder.messageBody.getLayoutParams(); - blayoutParams.width = (int) (target - (22 * metrics.density)); - viewHolder.messageBody.setLayoutParams(blayoutParams); - } - } - - private void toggleWhisperInfo( - ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) { - 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( - bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)), - 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); - conversation.setHasMessagesLeftOnServer(true); - conversation.setFirstMamReference(null); - long timestamp = conversation.getLastMessageTransmitted().getTimestamp(); - if (timestamp == 0) { - timestamp = System.currentTimeMillis(); - } - conversation.messagesLoaded.set(true); - MessageArchiveService.Query query = - activity.xmppConnectionService - .getMessageArchiveService() - .query(conversation, new MamReference(0), timestamp, false); - if (query != null) { - Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG) - .show(); - } else { - Toast.makeText( - activity, - R.string.not_fetching_history_retention_period, - Toast.LENGTH_SHORT) - .show(); - } - } - - @Override - public View getView(final int position, View view, final @NonNull ViewGroup parent) { - final Message message = getItem(position); - final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; - final boolean isInValidSession = - message.isValidInSession() && (!omemoEncryption || message.isTrusted()); - final Conversational conversation = message.getConversation(); - final Account account = conversation.getAccount(); - final List commands = message.getCommands(); - final int type = getItemViewType(position); - ViewHolder viewHolder; - if (view == null) { - viewHolder = new ViewHolder(); - switch (type) { - case DATE_SEPARATOR: - view = - activity.getLayoutInflater() - .inflate(R.layout.item_message_date_bubble, parent, false); - viewHolder.status_message = view.findViewById(R.id.message_body); - viewHolder.message_box = view.findViewById(R.id.message_box); - break; - case RTP_SESSION: - view = - activity.getLayoutInflater() - .inflate(R.layout.item_message_rtp_session, parent, false); - viewHolder.status_message = view.findViewById(R.id.message_body); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - break; - case SENT: - view = activity.getLayoutInflater().inflate(R.layout.item_message_sent, parent, false); - viewHolder.status_line = view.findViewById(R.id.status_line); - viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.download_button = view.findViewById(R.id.download_button); - viewHolder.indicator = view.findViewById(R.id.security_indicator); - viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); - viewHolder.image = view.findViewById(R.id.message_image); - viewHolder.messageBody = view.findViewById(R.id.message_body); - viewHolder.time = view.findViewById(R.id.message_time); - viewHolder.subject = view.findViewById(R.id.message_subject); - viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - viewHolder.audioPlayer = view.findViewById(R.id.audio_player); - viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon); - break; - case RECEIVED: - view = activity.getLayoutInflater().inflate(R.layout.item_message_received, parent, false); - viewHolder.status_line = view.findViewById(R.id.status_line); - viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.download_button = view.findViewById(R.id.download_button); - viewHolder.indicator = view.findViewById(R.id.security_indicator); - viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); - viewHolder.image = view.findViewById(R.id.message_image); - viewHolder.messageBody = view.findViewById(R.id.message_body); - viewHolder.time = view.findViewById(R.id.message_time); - viewHolder.subject = view.findViewById(R.id.message_subject); - viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - viewHolder.encryption = view.findViewById(R.id.message_encryption); - viewHolder.audioPlayer = view.findViewById(R.id.audio_player); - viewHolder.commands_list = view.findViewById(R.id.commands_list); - viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon); - break; - case STATUS: - view = - activity.getLayoutInflater() - .inflate(R.layout.item_message_status, parent, false); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.status_message = view.findViewById(R.id.status_message); - viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages); - break; - default: - throw new AssertionError("Unknown view type"); - } - view.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) view.getTag(); - if (viewHolder == null) { - return view; - } - } - - if (viewHolder.messageBody != null) { - viewHolder.messageBody.setCustomSelectionActionModeCallback(new MessageTextActionModeCallback(this, viewHolder.messageBody)); - } - - if (viewHolder.thread_identicon != null) { - viewHolder.thread_identicon.setVisibility(View.GONE); - final Element thread = message.getThread(); - if (thread != null) { - final String threadId = thread.getContent(); - if (threadId != null) { - viewHolder.thread_identicon.setVisibility(View.VISIBLE); - viewHolder.thread_identicon.setColor(UIHelper.getColorForName(threadId)); - viewHolder.thread_identicon.setHash(UIHelper.identiconHash(threadId)); - } - } - } - - final var black = MaterialColors.getColor(view, com.google.android.material.R.attr.colorSecondaryContainer) == view.getContext().getColor(android.R.color.black); - final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles; - final BubbleColor bubbleColor; - if (type == RECEIVED) { - if (isInValidSession) { - bubbleColor = colorfulBackground || black ? BubbleColor.SECONDARY : BubbleColor.SURFACE; - } else { - bubbleColor = BubbleColor.WARNING; - } - } else { - if (!colorfulBackground && black) { - bubbleColor = BubbleColor.SECONDARY; - } else { - bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH; - } - } - - if (type == DATE_SEPARATOR) { - if (UIHelper.today(message.getTimeSent())) { - viewHolder.status_message.setText(R.string.today); - } else if (UIHelper.yesterday(message.getTimeSent())) { - viewHolder.status_message.setText(R.string.yesterday); - } else { - viewHolder.status_message.setText( - DateUtils.formatDateTime( - activity, - message.getTimeSent(), - DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)); - } - if (colorfulBackground) { - setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY); - setTextColor(viewHolder.status_message, BubbleColor.PRIMARY); - } else { - setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH); - setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH); - } - return view; - } else if (type == RTP_SESSION) { - final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; - final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody()); - final long duration = rtpSessionStatus.duration; - final String callTime = UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent()); - if (received) { - if (duration > 0) { - viewHolder.status_message.setText( - activity.getString( - R.string.incoming_call_duration_timestamp, - TimeFrameUtils.resolve(activity, duration), - UIHelper.readableTimeDifferenceFull( - activity, message.getTimeSent()))); - } else if (rtpSessionStatus.successful) { - viewHolder.status_message.setText(activity.getString(R.string.incoming_call_timestamp, callTime)); - } else { - viewHolder.status_message.setText( - activity.getString( - R.string.missed_call_timestamp, - UIHelper.readableTimeDifferenceFull( - activity, message.getTimeSent()))); - } - } else { - if (duration > 0) { - viewHolder.status_message.setText( - activity.getString( - R.string.outgoing_call_duration_timestamp, - TimeFrameUtils.resolve(activity, duration), - UIHelper.readableTimeDifferenceFull( - activity, message.getTimeSent()))); - } else { - viewHolder.status_message.setText( - activity.getString( - R.string.outgoing_call_timestamp, - UIHelper.readableTimeDifferenceFull( - activity, message.getTimeSent()))); - } - } - if (colorfulBackground) { - setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY); - setTextColor(viewHolder.status_message, BubbleColor.SECONDARY); - setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY); - } else { - setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH); - setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH); - setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH); - } - viewHolder.indicatorReceived.setImageResource( - RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful)); - return view; - } else if (type == STATUS) { - if ("LOAD_MORE".equals(message.getBody())) { - viewHolder.status_message.setVisibility(View.GONE); - viewHolder.contact_picture.setVisibility(View.GONE); - viewHolder.load_more_messages.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setOnClickListener( - v -> loadMoreMessages((Conversation) message.getConversation())); - } else { - viewHolder.status_message.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setVisibility(View.GONE); - viewHolder.status_message.setText(message.getBody()); - boolean showAvatar; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - showAvatar = true; - AvatarWorkerTask.loadAvatar( - message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); - } else if (message.getCounterpart() != null - || message.getTrueCounterpart() != null - || (message.getCounterparts() != null - && message.getCounterparts().size() > 0)) { - showAvatar = true; - AvatarWorkerTask.loadAvatar( - message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); - } else { - showAvatar = false; - } - if (showAvatar) { - viewHolder.contact_picture.setAlpha(0.5f); - viewHolder.contact_picture.setVisibility(View.VISIBLE); - } else { - viewHolder.contact_picture.setVisibility(View.GONE); - } - } - return view; - } else { - // viewHolder.message_box.setClipToOutline(true); This eats the bubble tails on A14 for some reason - AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar); - } - - resetClickListener(viewHolder.message_box, viewHolder.messageBody); - - viewHolder.message_box.setOnClickListener(v -> { - if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { - MessageAdapter.this.mOnMessageBoxClickedListener - .onContactPictureClicked(message); - } - }); - SwipeDetector swipeDetector = new SwipeDetector((action) -> { - if (action == SwipeDetector.Action.LR && MessageAdapter.this.mOnMessageBoxSwipedListener != null) { - MessageAdapter.this.mOnMessageBoxSwipedListener.onContactPictureClicked(message); - } - }); - viewHolder.message_box.setOnTouchListener(swipeDetector); - viewHolder.image.setOnTouchListener(swipeDetector); - viewHolder.time.setOnTouchListener(swipeDetector); - - // Treat touch-up as click so we don't have to touch twice - // (touch twice is because it's waiting to see if you double-touch for text selection) - viewHolder.messageBody.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_UP) { - if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { - MessageAdapter.this.mOnMessageBoxClickedListener - .onContactPictureClicked(message); - } - } - - swipeDetector.onTouch(v, event); - - return false; - }); - viewHolder.messageBody.setOnClickListener(v -> { - if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { - MessageAdapter.this.mOnMessageBoxClickedListener - .onContactPictureClicked(message); - } - }); - viewHolder.contact_picture.setOnClickListener(v -> { - if (MessageAdapter.this.mOnContactPictureClickedListener != null) { - MessageAdapter.this.mOnContactPictureClickedListener - .onContactPictureClicked(message); - } - - }); - viewHolder.contact_picture.setOnLongClickListener(v -> { - if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { - MessageAdapter.this.mOnContactPictureLongClickedListener - .onContactPictureLongClicked(v, message); - return true; - } else { - return false; - } - }); - viewHolder.messageBody.setAccessibilityDelegate(null); - - boolean footerWrap = false; - - final Transferable transferable = message.getTransferable(); - final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); - - final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && activity.xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), message.getOccupantId(), null, null)); - if (muted) { - // Muted MUC participant - displayInfoMessage(viewHolder, "Muted", bubbleColor); - } else if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) { - if (unInitiatedButKnownSize || (message.isDeleted() && message.getModerated() == null) || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) { - displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type); - } 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)), bubbleColor, type); - } else { - displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity.xmppConnectionService, message).first, bubbleColor); - } - } else if (message.isFileOrImage() - && message.getEncryption() != Message.ENCRYPTION_PGP - && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { - if (message.getFileParams().width > 0 && message.getFileParams().height > 0) { - displayMediaPreviewMessage(viewHolder, message, bubbleColor, type); - if (!black && viewHolder.image.getLayoutParams().width > metrics.density * 110) { - footerWrap = true; - } - } else if (message.getFileParams().runtime > 0) { - displayAudioMessage(viewHolder, message, bubbleColor, type); - } else if ("application/xdc+zip".equals(message.getFileParams().getMediaType()) && message.getConversation() instanceof Conversation && message.getThread() != null && !message.getFileParams().getCids().isEmpty()) { - displayWebxdcMessage(viewHolder, message, bubbleColor, type); - } else { - displayOpenableMessage(viewHolder, message, bubbleColor, type); - } - } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { - if (account.isPgpDecryptionServiceConnected()) { - if (conversation instanceof Conversation - && !account.hasPendingPgpIntent((Conversation) conversation)) { - displayInfoMessage( - viewHolder, - activity.getString(R.string.message_decrypting), - bubbleColor); - } else { - displayInfoMessage( - viewHolder, activity.getString(R.string.pgp_message), bubbleColor); - } - } else { - displayInfoMessage( - viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor); - viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall); - viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall); - } - } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { - displayInfoMessage( - viewHolder, activity.getString(R.string.decryption_failed), bubbleColor); - } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) { - displayInfoMessage( - viewHolder, - activity.getString(R.string.not_encrypted_for_this_device), - bubbleColor); - } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { - displayInfoMessage( - viewHolder, activity.getString(R.string.omemo_decryption_failed), bubbleColor); - } else { - if (message.isGeoUri()) { - displayLocationMessage(viewHolder, message, bubbleColor, type); - } else if (message.treatAsDownloadable()) { - try { - final URI uri = message.getOob(); - displayDownloadableMessage(viewHolder, - message, - activity.getString( - R.string.check_x_filesize_on_host, - UIHelper.getFileDescriptionString(activity, message), - uri.getHost()), - bubbleColor, type); - } catch (Exception e) { - displayDownloadableMessage( - viewHolder, - message, - activity.getString( - R.string.check_x_filesize, - UIHelper.getFileDescriptionString(activity, message)), - bubbleColor, type); - } - } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) { - displayEmojiMessage(viewHolder, getSpannableBody(message), bubbleColor); - } else { - displayTextMessage(viewHolder, message, bubbleColor, message.getType()); - } - } - - viewHolder.message_box_inner.setMinimumWidth(footerWrap ? (int) (110 * metrics.density) : 0); - LinearLayout.LayoutParams statusParams = (LinearLayout.LayoutParams) viewHolder.status_line.getLayoutParams(); - statusParams.width = footerWrap ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT; - viewHolder.status_line.setLayoutParams(statusParams); - - setBackgroundTint(viewHolder.message_box, bubbleColor); - setTextColor(viewHolder.messageBody, bubbleColor); - viewHolder.messageBody.setLinkTextColor(bubbleToOnSurfaceColor(viewHolder.messageBody, bubbleColor)); - - if (type == RECEIVED) { - if (!muted && commands != null && conversation instanceof Conversation) { - CommandButtonAdapter adapter = new CommandButtonAdapter(activity); - adapter.addAll(commands); - viewHolder.commands_list.setAdapter(adapter); - viewHolder.commands_list.setVisibility(View.VISIBLE); - viewHolder.commands_list.setOnItemClickListener((p, v, pos, id) -> { - final Element command = adapter.getItem(pos); - activity.startCommand(conversation.getAccount(), command.getAttributeAsJid("jid"), command.getAttribute("node")); - }); - } else { - // It's unclear if we can set this to null... - ListAdapter adapter = viewHolder.commands_list.getAdapter(); - if (adapter instanceof ArrayAdapter) { - ((ArrayAdapter) adapter).clear(); - } - viewHolder.commands_list.setVisibility(View.GONE); - viewHolder.commands_list.setOnItemClickListener(null); - } - - setTextColor(viewHolder.encryption, bubbleColor); - - if (isInValidSession) { - viewHolder.encryption.setVisibility(View.GONE); - } else { - viewHolder.encryption.setVisibility(View.VISIBLE); - if (omemoEncryption && !message.isTrusted()) { - viewHolder.encryption.setText(R.string.not_trusted); - } else { - viewHolder.encryption.setText( - CryptoHelper.encryptionTypeToText(message.getEncryption())); - } - } - } - - if (type == RECEIVED || type == SENT) { - String subject = message.getSubject(); - if (subject == null && message.getThread() != null) { - final var thread = ((Conversation) message.getConversation()).getThread(message.getThread().getContent()); - if (thread != null) subject = thread.getSubject(); - } - if (muted || subject == null) { - viewHolder.subject.setVisibility(View.GONE); - } else { - viewHolder.subject.setVisibility(View.VISIBLE); - viewHolder.subject.setText(subject); - } - } - - displayStatus(viewHolder, message, type, bubbleColor); - - viewHolder.messageBody.setAccessibilityDelegate(new View.AccessibilityDelegate() { - @Override - public void sendAccessibilityEvent(View host, int eventType) { - super.sendAccessibilityEvent(host, eventType); - if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) { - if (viewHolder.messageBody.hasSelection()) { - selectionUuid = message.getUuid(); - } else if (message.getUuid() != null && message.getUuid().equals(selectionUuid)) { - selectionUuid = null; - } - } - } - }); - - return view; - } - - private void promptOpenKeychainInstall(View view) { - activity.showInstallPgpDialog(); - } - - public FileBackend getFileBackend() { - return activity.xmppConnectionService.getFileBackend(); - } - - public void stopAudioPlayer() { - audioPlayer.stop(); - } - - public void unregisterListenerInAudioPlayer() { - audioPlayer.unregisterListener(); - } - - public void startStopPending() { - audioPlayer.startStopPending(); - } - - public void openDownloadable(Message message) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU - && ContextCompat.checkSelfPermission( - activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - ConversationFragment.registerPendingMessage(activity, message); - ActivityCompat.requestPermissions( - activity, - new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, - ConversationsActivity.REQUEST_OPEN_MESSAGE); - return; - } - final DownloadableFile file = - activity.xmppConnectionService.getFileBackend().getFile(message); - ViewUtil.view(activity, file); - } - - private void showLocation(Message message) { - for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) { - if (intent.resolveActivity(getContext().getPackageManager()) != null) { - getContext().startActivity(intent); - return; - } - } - Toast.makeText( - activity, - R.string.no_application_found_to_display_location, - Toast.LENGTH_SHORT) - .show(); - } - - public void updatePreferences() { - final AppSettings appSettings = new AppSettings(activity); - this.bubbleDesign = - new BubbleDesign(appSettings.isColorfulChatBubbles(), appSettings.isLargeFont()); - } - - public void setHighlightedTerm(List terms) { - this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); - } - - public interface OnContactPictureClicked { - void onContactPictureClicked(Message message); - } - - public interface OnContactPictureLongClicked { - void onContactPictureLongClicked(View v, Message message); - } - - public interface OnInlineImageLongClicked { - boolean onInlineImageLongClicked(Cid cid); - } - - private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) { - view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor)); - } - - private static ColorStateList bubbleToColorStateList( - final View view, final BubbleColor bubbleColor) { - final @AttrRes int colorAttributeResId = - switch (bubbleColor) { - case SURFACE -> Activities.isNightMode(view.getContext()) - ? com.google.android.material.R.attr.colorSurfaceContainerHigh - : com.google.android.material.R.attr.colorSurfaceContainerLow; - case SURFACE_HIGH -> Activities.isNightMode(view.getContext()) - ? com.google.android.material.R.attr.colorSurfaceContainerHighest - : com.google.android.material.R.attr.colorSurfaceContainerHigh; - case PRIMARY -> com.google.android.material.R.attr.colorPrimaryContainer; - case SECONDARY -> com.google.android.material.R.attr.colorSecondaryContainer; - case TERTIARY -> com.google.android.material.R.attr.colorTertiaryContainer; - case WARNING -> com.google.android.material.R.attr.colorErrorContainer; - }; - return ColorStateList.valueOf(MaterialColors.getColor(view, colorAttributeResId)); - } - - public static void setImageTint(final ImageView imageView, final BubbleColor bubbleColor) { - ImageViewCompat.setImageTintList( - imageView, bubbleToOnSurfaceColorStateList(imageView, bubbleColor)); - } - - public static void setImageTintError(final ImageView imageView) { - ImageViewCompat.setImageTintList( - imageView, - ColorStateList.valueOf( - MaterialColors.getColor( - imageView, com.google.android.material.R.attr.colorError))); - } - - public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) { - final var color = bubbleToOnSurfaceColor(textView, bubbleColor); - textView.setTextColor(color); - if (BubbleColor.SURFACES.contains(bubbleColor)) { - textView.setLinkTextColor( - MaterialColors.getColor( - textView, com.google.android.material.R.attr.colorPrimary)); - } else { - textView.setLinkTextColor(color); - } - } - - private static void setTextSize(final TextView textView, final boolean largeFont) { - if (largeFont) { - textView.setTextAppearance( - com.google.android.material.R.style.TextAppearance_Material3_TitleLarge); - } else { - textView.setTextAppearance( - com.google.android.material.R.style.TextAppearance_Material3_BodyMedium); - } - } - - private static @ColorInt int bubbleToOnSurfaceVariant( - final View view, final BubbleColor bubbleColor) { - final @AttrRes int colorAttributeResId; - if (BubbleColor.SURFACES.contains(bubbleColor)) { - colorAttributeResId = com.google.android.material.R.attr.colorOnSurfaceVariant; - } else { - colorAttributeResId = bubbleToOnSurface(bubbleColor); - } - return MaterialColors.getColor(view, colorAttributeResId); - } - - private static @ColorInt int bubbleToOnSurfaceColor( - final View view, final BubbleColor bubbleColor) { - return MaterialColors.getColor(view, bubbleToOnSurface(bubbleColor)); - } - - public static ColorStateList bubbleToOnSurfaceColorStateList( - final View view, final BubbleColor bubbleColor) { - return ColorStateList.valueOf(bubbleToOnSurfaceColor(view, bubbleColor)); - } - - private static @AttrRes int bubbleToOnSurface(final BubbleColor bubbleColor) { - return switch (bubbleColor) { - case SURFACE, SURFACE_HIGH -> com.google.android.material.R.attr.colorOnSurface; - case PRIMARY -> com.google.android.material.R.attr.colorOnPrimaryContainer; - case SECONDARY -> com.google.android.material.R.attr.colorOnSecondaryContainer; - case TERTIARY -> com.google.android.material.R.attr.colorOnTertiaryContainer; - case WARNING -> com.google.android.material.R.attr.colorOnErrorContainer; - }; - } - - public enum BubbleColor { - SURFACE, - SURFACE_HIGH, - PRIMARY, - SECONDARY, - TERTIARY, - WARNING; - - private static final Collection SURFACES = - Arrays.asList(BubbleColor.SURFACE, BubbleColor.SURFACE_HIGH); - } - - private static class BubbleDesign { - public final boolean colorfulChatBubbles; - public final boolean largeFont; - - private BubbleDesign(final boolean colorfulChatBubbles, final boolean largeFont) { - this.colorfulChatBubbles = colorfulChatBubbles; - this.largeFont = largeFont; - } - } - - private static class ViewHolder { - - public MaterialButton load_more_messages; - public ImageView edit_indicator; - public RelativeLayout audioPlayer; - protected View status_line; - protected LinearLayout message_box; - protected View message_box_inner; - protected MaterialButton download_button; - protected ShapeableImageView image; - protected ImageView indicator; - protected ImageView indicatorReceived; - protected TextView time; - protected TextView subject; - protected TextView messageBody; - protected ImageView contact_picture; - protected TextView status_message; - protected TextView encryption; - protected ListView commands_list; - protected GithubIdenticonView thread_identicon; - } - - class ThumbnailTask extends AsyncTask { - @Override - protected Drawable[] doInBackground(DownloadableFile... params) { - if (isCancelled()) return null; - - Drawable[] d = new Drawable[params.length]; - for (int i = 0; i < params.length; i++) { - try { - d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false); - } catch (final IOException e) { - d[i] = null; - } - } - - return d; - } - - @Override - protected void onPostExecute(final Drawable[] d) { - if (isCancelled()) return; - activity.xmppConnectionService.updateConversationUi(); - } - } -}