MessageAdapter.java

   1package eu.siacs.conversations.ui.adapter;
   2
   3import android.Manifest;
   4import android.app.Activity;
   5import android.content.Intent;
   6import android.content.pm.PackageManager;
   7import android.content.res.ColorStateList;
   8import android.graphics.Typeface;
   9import android.os.Build;
  10import android.text.Spannable;
  11import android.text.SpannableString;
  12import android.text.SpannableStringBuilder;
  13import android.text.format.DateUtils;
  14import android.text.style.ForegroundColorSpan;
  15import android.text.style.RelativeSizeSpan;
  16import android.text.style.StyleSpan;
  17import android.util.DisplayMetrics;
  18import android.util.Log;
  19import android.view.View;
  20import android.view.ViewGroup;
  21import android.view.WindowManager;
  22import android.widget.ArrayAdapter;
  23import android.widget.Button;
  24import android.widget.ImageView;
  25import android.widget.LinearLayout;
  26import android.widget.RelativeLayout;
  27import android.widget.TextView;
  28import android.widget.Toast;
  29
  30import androidx.annotation.AttrRes;
  31import androidx.annotation.ColorInt;
  32import androidx.annotation.DrawableRes;
  33import androidx.annotation.NonNull;
  34import androidx.annotation.Nullable;
  35import androidx.core.app.ActivityCompat;
  36import androidx.core.content.ContextCompat;
  37import androidx.core.widget.ImageViewCompat;
  38import androidx.databinding.DataBindingUtil;
  39import androidx.emoji2.emojipicker.EmojiViewItem;
  40import androidx.emoji2.emojipicker.RecentEmojiProvider;
  41
  42import com.google.android.material.button.MaterialButton;
  43import com.google.android.material.chip.ChipGroup;
  44import com.google.android.material.color.MaterialColors;
  45import com.google.android.material.dialog.MaterialAlertDialogBuilder;
  46import com.google.common.base.Joiner;
  47import com.google.common.base.Strings;
  48import com.google.common.collect.ImmutableList;
  49import com.google.common.collect.ImmutableSet;
  50
  51import eu.siacs.conversations.AppSettings;
  52import eu.siacs.conversations.Config;
  53import eu.siacs.conversations.R;
  54import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  55import eu.siacs.conversations.databinding.DialogAddReactionBinding;
  56import eu.siacs.conversations.entities.Account;
  57import eu.siacs.conversations.entities.Conversation;
  58import eu.siacs.conversations.entities.Conversational;
  59import eu.siacs.conversations.entities.DownloadableFile;
  60import eu.siacs.conversations.entities.Message;
  61import eu.siacs.conversations.entities.Message.FileParams;
  62import eu.siacs.conversations.entities.RtpSessionStatus;
  63import eu.siacs.conversations.entities.Transferable;
  64import eu.siacs.conversations.persistance.FileBackend;
  65import eu.siacs.conversations.services.MessageArchiveService;
  66import eu.siacs.conversations.services.NotificationService;
  67import eu.siacs.conversations.ui.Activities;
  68import eu.siacs.conversations.ui.BindingAdapters;
  69import eu.siacs.conversations.ui.ConversationFragment;
  70import eu.siacs.conversations.ui.ConversationsActivity;
  71import eu.siacs.conversations.ui.XmppActivity;
  72import eu.siacs.conversations.ui.service.AudioPlayer;
  73import eu.siacs.conversations.ui.text.DividerSpan;
  74import eu.siacs.conversations.ui.text.QuoteSpan;
  75import eu.siacs.conversations.ui.util.Attachment;
  76import eu.siacs.conversations.ui.util.AvatarWorkerTask;
  77import eu.siacs.conversations.ui.util.MyLinkify;
  78import eu.siacs.conversations.ui.util.QuoteHelper;
  79import eu.siacs.conversations.ui.util.ViewUtil;
  80import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
  81import eu.siacs.conversations.utils.CryptoHelper;
  82import eu.siacs.conversations.utils.Emoticons;
  83import eu.siacs.conversations.utils.GeoHelper;
  84import eu.siacs.conversations.utils.MessageUtils;
  85import eu.siacs.conversations.utils.StylingHelper;
  86import eu.siacs.conversations.utils.TimeFrameUtils;
  87import eu.siacs.conversations.utils.UIHelper;
  88import eu.siacs.conversations.xmpp.Jid;
  89import eu.siacs.conversations.xmpp.mam.MamReference;
  90import kotlin.coroutines.Continuation;
  91
  92import java.net.URI;
  93import java.util.Arrays;
  94import java.util.Collection;
  95import java.util.List;
  96import java.util.Locale;
  97import java.util.function.Consumer;
  98import java.util.regex.Matcher;
  99import java.util.regex.Pattern;
 100
 101public class MessageAdapter extends ArrayAdapter<Message> {
 102
 103    public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
 104    private static final int SENT = 0;
 105    private static final int RECEIVED = 1;
 106    private static final int STATUS = 2;
 107    private static final int DATE_SEPARATOR = 3;
 108    private static final int RTP_SESSION = 4;
 109    private final XmppActivity activity;
 110    private final AudioPlayer audioPlayer;
 111    private List<String> highlightedTerm = null;
 112    private final DisplayMetrics metrics;
 113    private OnContactPictureClicked mOnContactPictureClickedListener;
 114    private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
 115    private BubbleDesign bubbleDesign = new BubbleDesign(false, false);
 116    private final boolean mForceNames;
 117
 118    public MessageAdapter(
 119            final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
 120        super(activity, 0, messages);
 121        this.audioPlayer = new AudioPlayer(this);
 122        this.activity = activity;
 123        metrics = getContext().getResources().getDisplayMetrics();
 124        updatePreferences();
 125        this.mForceNames = forceNames;
 126    }
 127
 128    public MessageAdapter(final XmppActivity activity, final List<Message> messages) {
 129        this(activity, messages, false);
 130    }
 131
 132    private static void resetClickListener(View... views) {
 133        for (View view : views) {
 134            view.setOnClickListener(null);
 135        }
 136    }
 137
 138    public void flagScreenOn() {
 139        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 140    }
 141
 142    public void flagScreenOff() {
 143        activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 144    }
 145
 146    public void setVolumeControl(final int stream) {
 147        activity.setVolumeControlStream(stream);
 148    }
 149
 150    public void setOnContactPictureClicked(OnContactPictureClicked listener) {
 151        this.mOnContactPictureClickedListener = listener;
 152    }
 153
 154    public Activity getActivity() {
 155        return activity;
 156    }
 157
 158    public void setOnContactPictureLongClicked(OnContactPictureLongClicked listener) {
 159        this.mOnContactPictureLongClickedListener = listener;
 160    }
 161
 162    @Override
 163    public int getViewTypeCount() {
 164        return 5;
 165    }
 166
 167    private int getItemViewType(Message message) {
 168        if (message.getType() == Message.TYPE_STATUS) {
 169            if (DATE_SEPARATOR_BODY.equals(message.getBody())) {
 170                return DATE_SEPARATOR;
 171            } else {
 172                return STATUS;
 173            }
 174        } else if (message.getType() == Message.TYPE_RTP_SESSION) {
 175            return RTP_SESSION;
 176        } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
 177            return RECEIVED;
 178        } else {
 179            return SENT;
 180        }
 181    }
 182
 183    @Override
 184    public int getItemViewType(int position) {
 185        return this.getItemViewType(getItem(position));
 186    }
 187
 188    private void displayStatus(
 189            final ViewHolder viewHolder,
 190            final Message message,
 191            final int type,
 192            final BubbleColor bubbleColor) {
 193        final int mergedStatus = message.getMergedStatus();
 194        final boolean error;
 195        if (viewHolder.indicatorReceived != null) {
 196            viewHolder.indicatorReceived.setVisibility(View.GONE);
 197        }
 198        final Transferable transferable = message.getTransferable();
 199        final boolean multiReceived =
 200                message.getConversation().getMode() == Conversation.MODE_MULTI
 201                        && mergedStatus <= Message.STATUS_RECEIVED;
 202        final String fileSize;
 203        if (message.isFileOrImage()
 204                || transferable != null
 205                || MessageUtils.unInitiatedButKnownSize(message)) {
 206            final FileParams params = message.getFileParams();
 207            fileSize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
 208            if (message.getStatus() == Message.STATUS_SEND_FAILED
 209                    || (transferable != null
 210                            && (transferable.getStatus() == Transferable.STATUS_FAILED
 211                                    || transferable.getStatus()
 212                                            == Transferable.STATUS_CANCELLED))) {
 213                error = true;
 214            } else {
 215                error = message.getStatus() == Message.STATUS_SEND_FAILED;
 216            }
 217        } else {
 218            fileSize = null;
 219            error = message.getStatus() == Message.STATUS_SEND_FAILED;
 220        }
 221        if (type == SENT) {
 222            final @DrawableRes Integer receivedIndicator =
 223                    getMessageStatusAsDrawable(message, mergedStatus);
 224            if (receivedIndicator == null) {
 225                viewHolder.indicatorReceived.setVisibility(View.INVISIBLE);
 226            } else {
 227                viewHolder.indicatorReceived.setImageResource(receivedIndicator);
 228                if (mergedStatus == Message.STATUS_SEND_FAILED) {
 229                    setImageTintError(viewHolder.indicatorReceived);
 230                } else {
 231                    setImageTint(viewHolder.indicatorReceived, bubbleColor);
 232                }
 233                viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
 234            }
 235        }
 236        final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus);
 237
 238        if (error && type == SENT) {
 239            viewHolder.time.setTextColor(
 240                    MaterialColors.getColor(
 241                            viewHolder.time, com.google.android.material.R.attr.colorError));
 242        } else {
 243            setTextColor(viewHolder.time, bubbleColor);
 244        }
 245        if (message.getEncryption() == Message.ENCRYPTION_NONE) {
 246            viewHolder.indicator.setVisibility(View.GONE);
 247        } else {
 248            boolean verified = false;
 249            if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 250                final FingerprintStatus status =
 251                        message.getConversation()
 252                                .getAccount()
 253                                .getAxolotlService()
 254                                .getFingerprintTrust(message.getFingerprint());
 255                if (status != null && status.isVerified()) {
 256                    verified = true;
 257                }
 258            }
 259            if (verified) {
 260                viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp);
 261            } else {
 262                viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp);
 263            }
 264            if (error && type == SENT) {
 265                setImageTintError(viewHolder.indicator);
 266            } else {
 267                setImageTint(viewHolder.indicator, bubbleColor);
 268            }
 269            viewHolder.indicator.setVisibility(View.VISIBLE);
 270        }
 271
 272        if (viewHolder.edit_indicator != null) {
 273            if (message.edited()) {
 274                viewHolder.edit_indicator.setVisibility(View.VISIBLE);
 275                if (error && type == SENT) {
 276                    setImageTintError(viewHolder.edit_indicator);
 277                } else {
 278                    setImageTint(viewHolder.edit_indicator, bubbleColor);
 279                }
 280            } else {
 281                viewHolder.edit_indicator.setVisibility(View.GONE);
 282            }
 283        }
 284
 285        final String formattedTime =
 286                UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
 287        final String bodyLanguage = message.getBodyLanguage();
 288        final ImmutableList.Builder<String> timeInfoBuilder = new ImmutableList.Builder<>();
 289        if (message.getStatus() <= Message.STATUS_RECEIVED) {
 290            timeInfoBuilder.add(formattedTime);
 291            if (fileSize != null) {
 292                timeInfoBuilder.add(fileSize);
 293            }
 294            if (mForceNames || multiReceived) {
 295                final String displayName = UIHelper.getMessageDisplayName(message);
 296                if (displayName != null) {
 297                    timeInfoBuilder.add(displayName);
 298                }
 299            }
 300            if (bodyLanguage != null) {
 301                timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
 302            }
 303        } else {
 304            if (bodyLanguage != null) {
 305                timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
 306            }
 307            if (fileSize != null) {
 308                timeInfoBuilder.add(fileSize);
 309            }
 310            // for space reasons we display only 'additional status info' (send progress or concrete
 311            // failure reason) or the time
 312            if (additionalStatusInfo != null) {
 313                timeInfoBuilder.add(additionalStatusInfo);
 314            } else {
 315                timeInfoBuilder.add(formattedTime);
 316            }
 317        }
 318        final var timeInfo = timeInfoBuilder.build();
 319        viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo));
 320    }
 321
 322    public static @DrawableRes Integer getMessageStatusAsDrawable(
 323            final Message message, final int status) {
 324        final var transferable = message.getTransferable();
 325        return switch (status) {
 326            case Message.STATUS_WAITING -> R.drawable.ic_more_horiz_24dp;
 327            case Message.STATUS_UNSEND -> transferable == null ? null : R.drawable.ic_upload_24dp;
 328            case Message.STATUS_SEND -> R.drawable.ic_done_24dp;
 329            case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> R.drawable
 330                    .ic_done_all_24dp;
 331            case Message.STATUS_SEND_FAILED -> {
 332                final String errorMessage = message.getErrorMessage();
 333                if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) {
 334                    yield R.drawable.ic_cancel_24dp;
 335                } else {
 336                    yield R.drawable.ic_error_24dp;
 337                }
 338            }
 339            case Message.STATUS_OFFERED -> R.drawable.ic_p2p_24dp;
 340            default -> null;
 341        };
 342    }
 343
 344    @Nullable
 345    private String getAdditionalStatusInfo(final Message message, final int mergedStatus) {
 346        final String additionalStatusInfo;
 347        if (mergedStatus == Message.STATUS_SEND_FAILED) {
 348            final String errorMessage = Strings.nullToEmpty(message.getErrorMessage());
 349            final String[] errorParts = errorMessage.split("\\u001f", 2);
 350            if (errorParts.length == 2 && errorParts[0].equals("file-too-large")) {
 351                additionalStatusInfo = getContext().getString(R.string.file_too_large);
 352            } else {
 353                additionalStatusInfo = null;
 354            }
 355        } else if (mergedStatus == Message.STATUS_UNSEND) {
 356            final var transferable = message.getTransferable();
 357            if (transferable == null) {
 358                return null;
 359            }
 360            return getContext().getString(R.string.sending_file, transferable.getProgress());
 361        } else {
 362            additionalStatusInfo = null;
 363        }
 364        return additionalStatusInfo;
 365    }
 366
 367    private void displayInfoMessage(
 368            ViewHolder viewHolder, CharSequence text, final BubbleColor bubbleColor) {
 369        viewHolder.download_button.setVisibility(View.GONE);
 370        viewHolder.audioPlayer.setVisibility(View.GONE);
 371        viewHolder.image.setVisibility(View.GONE);
 372        viewHolder.messageBody.setVisibility(View.VISIBLE);
 373        viewHolder.messageBody.setText(text);
 374        viewHolder.messageBody.setTextColor(
 375                bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor));
 376        viewHolder.messageBody.setTextIsSelectable(false);
 377    }
 378
 379    private void displayEmojiMessage(
 380            final ViewHolder viewHolder, final String body, final BubbleColor bubbleColor) {
 381        viewHolder.download_button.setVisibility(View.GONE);
 382        viewHolder.audioPlayer.setVisibility(View.GONE);
 383        viewHolder.image.setVisibility(View.GONE);
 384        viewHolder.messageBody.setVisibility(View.VISIBLE);
 385        setTextColor(viewHolder.messageBody, bubbleColor);
 386        final Spannable span = new SpannableString(body);
 387        float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f;
 388        span.setSpan(
 389                new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 390        viewHolder.messageBody.setText(span);
 391    }
 392
 393    private void applyQuoteSpan(
 394            final TextView textView,
 395            SpannableStringBuilder body,
 396            int start,
 397            int end,
 398            final BubbleColor bubbleColor) {
 399        if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
 400            body.insert(start++, "\n");
 401            body.setSpan(
 402                    new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 403            end++;
 404        }
 405        if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
 406            body.insert(end, "\n");
 407            body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 408        }
 409        final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
 410        body.setSpan(
 411                new QuoteSpan(bubbleToOnSurfaceVariant(textView, bubbleColor), metrics),
 412                start,
 413                end,
 414                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 415    }
 416
 417    /**
 418     * Applies QuoteSpan to group of lines which starts with > or » characters. Appends likebreaks
 419     * and applies DividerSpan to them to show a padding between quote and text.
 420     */
 421    private boolean handleTextQuotes(
 422            final TextView textView,
 423            final SpannableStringBuilder body,
 424            final BubbleColor bubbleColor) {
 425        boolean startsWithQuote = false;
 426        int quoteDepth = 1;
 427        while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
 428            char previous = '\n';
 429            int lineStart = -1;
 430            int lineTextStart = -1;
 431            int quoteStart = -1;
 432            for (int i = 0; i <= body.length(); i++) {
 433                char current = body.length() > i ? body.charAt(i) : '\n';
 434                if (lineStart == -1) {
 435                    if (previous == '\n') {
 436                        if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
 437                            // Line start with quote
 438                            lineStart = i;
 439                            if (quoteStart == -1) quoteStart = i;
 440                            if (i == 0) startsWithQuote = true;
 441                        } else if (quoteStart >= 0) {
 442                            // Line start without quote, apply spans there
 443                            applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor);
 444                            quoteStart = -1;
 445                        }
 446                    }
 447                } else {
 448                    // Remove extra spaces between > and first character in the line
 449                    // > character will be removed too
 450                    if (current != ' ' && lineTextStart == -1) {
 451                        lineTextStart = i;
 452                    }
 453                    if (current == '\n') {
 454                        body.delete(lineStart, lineTextStart);
 455                        i -= lineTextStart - lineStart;
 456                        if (i == lineStart) {
 457                            // Avoid empty lines because span over empty line can be hidden
 458                            body.insert(i++, " ");
 459                        }
 460                        lineStart = -1;
 461                        lineTextStart = -1;
 462                    }
 463                }
 464                previous = current;
 465            }
 466            if (quoteStart >= 0) {
 467                // Apply spans to finishing open quote
 468                applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor);
 469            }
 470            quoteDepth++;
 471        }
 472        return startsWithQuote;
 473    }
 474
 475    private void displayTextMessage(
 476            final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
 477        viewHolder.download_button.setVisibility(View.GONE);
 478        viewHolder.image.setVisibility(View.GONE);
 479        viewHolder.audioPlayer.setVisibility(View.GONE);
 480        viewHolder.messageBody.setVisibility(View.VISIBLE);
 481        setTextColor(viewHolder.messageBody, bubbleColor);
 482        setTextSize(viewHolder.messageBody, this.bubbleDesign.largeFont);
 483        viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
 484
 485        if (message.getBody() != null) {
 486            final String nick = UIHelper.getMessageDisplayName(message);
 487            SpannableStringBuilder body = message.getMergedBody();
 488            boolean hasMeCommand = message.hasMeCommand();
 489            if (hasMeCommand) {
 490                body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
 491            }
 492            if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
 493                body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
 494                body.append("\u2026");
 495            }
 496            Message.MergeSeparator[] mergeSeparators =
 497                    body.getSpans(0, body.length(), Message.MergeSeparator.class);
 498            for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
 499                int start = body.getSpanStart(mergeSeparator);
 500                int end = body.getSpanEnd(mergeSeparator);
 501                body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 502            }
 503            boolean startsWithQuote = handleTextQuotes(viewHolder.messageBody, body, bubbleColor);
 504            if (!message.isPrivateMessage()) {
 505                if (hasMeCommand) {
 506                    body.setSpan(
 507                            new StyleSpan(Typeface.BOLD_ITALIC),
 508                            0,
 509                            nick.length(),
 510                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 511                }
 512            } else {
 513                String privateMarker;
 514                if (message.getStatus() <= Message.STATUS_RECEIVED) {
 515                    privateMarker = activity.getString(R.string.private_message);
 516                } else {
 517                    Jid cp = message.getCounterpart();
 518                    privateMarker =
 519                            activity.getString(
 520                                    R.string.private_message_to,
 521                                    Strings.nullToEmpty(cp == null ? null : cp.getResource()));
 522                }
 523                body.insert(0, privateMarker);
 524                int privateMarkerIndex = privateMarker.length();
 525                if (startsWithQuote) {
 526                    body.insert(privateMarkerIndex, "\n\n");
 527                    body.setSpan(
 528                            new DividerSpan(false),
 529                            privateMarkerIndex,
 530                            privateMarkerIndex + 2,
 531                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 532                } else {
 533                    body.insert(privateMarkerIndex, " ");
 534                }
 535                body.setSpan(
 536                        new ForegroundColorSpan(
 537                                bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
 538                        0,
 539                        privateMarkerIndex,
 540                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 541                body.setSpan(
 542                        new StyleSpan(Typeface.BOLD),
 543                        0,
 544                        privateMarkerIndex,
 545                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 546                if (hasMeCommand) {
 547                    body.setSpan(
 548                            new StyleSpan(Typeface.BOLD_ITALIC),
 549                            privateMarkerIndex + 1,
 550                            privateMarkerIndex + 1 + nick.length(),
 551                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 552                }
 553            }
 554            if (message.getConversation().getMode() == Conversation.MODE_MULTI
 555                    && message.getStatus() == Message.STATUS_RECEIVED) {
 556                if (message.getConversation() instanceof Conversation conversation) {
 557                    Pattern pattern =
 558                            NotificationService.generateNickHighlightPattern(
 559                                    conversation.getMucOptions().getActualNick());
 560                    Matcher matcher = pattern.matcher(body);
 561                    while (matcher.find()) {
 562                        body.setSpan(
 563                                new StyleSpan(Typeface.BOLD),
 564                                matcher.start(),
 565                                matcher.end(),
 566                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 567                    }
 568                }
 569            }
 570            Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body);
 571            while (matcher.find()) {
 572                if (matcher.start() < matcher.end()) {
 573                    body.setSpan(
 574                            new RelativeSizeSpan(1.2f),
 575                            matcher.start(),
 576                            matcher.end(),
 577                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 578                }
 579            }
 580
 581            StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
 582            MyLinkify.addLinks(body, true);
 583            if (highlightedTerm != null) {
 584                StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm);
 585            }
 586            viewHolder.messageBody.setAutoLinkMask(0);
 587            viewHolder.messageBody.setText(body);
 588            viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
 589        } else {
 590            viewHolder.messageBody.setText("");
 591            viewHolder.messageBody.setTextIsSelectable(false);
 592        }
 593    }
 594
 595    private void displayDownloadableMessage(
 596            ViewHolder viewHolder,
 597            final Message message,
 598            String text,
 599            final BubbleColor bubbleColor) {
 600        toggleWhisperInfo(viewHolder, message, bubbleColor);
 601        viewHolder.image.setVisibility(View.GONE);
 602        viewHolder.audioPlayer.setVisibility(View.GONE);
 603        viewHolder.download_button.setVisibility(View.VISIBLE);
 604        viewHolder.download_button.setText(text);
 605        final var attachment = Attachment.of(message);
 606        final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
 607        viewHolder.download_button.setIconResource(imageResource);
 608        viewHolder.download_button.setOnClickListener(
 609                v -> ConversationFragment.downloadFile(activity, message));
 610    }
 611
 612    private void displayOpenableMessage(
 613            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
 614        toggleWhisperInfo(viewHolder, message, bubbleColor);
 615        viewHolder.image.setVisibility(View.GONE);
 616        viewHolder.audioPlayer.setVisibility(View.GONE);
 617        viewHolder.download_button.setVisibility(View.VISIBLE);
 618        viewHolder.download_button.setText(
 619                activity.getString(
 620                        R.string.open_x_file,
 621                        UIHelper.getFileDescriptionString(activity, message)));
 622        final var attachment = Attachment.of(message);
 623        final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
 624        viewHolder.download_button.setIconResource(imageResource);
 625        viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
 626    }
 627
 628    private void displayLocationMessage(
 629            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
 630        toggleWhisperInfo(viewHolder, message, bubbleColor);
 631        viewHolder.image.setVisibility(View.GONE);
 632        viewHolder.audioPlayer.setVisibility(View.GONE);
 633        viewHolder.download_button.setVisibility(View.VISIBLE);
 634        viewHolder.download_button.setText(R.string.show_location);
 635        final var attachment = Attachment.of(message);
 636        final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
 637        viewHolder.download_button.setIconResource(imageResource);
 638        viewHolder.download_button.setOnClickListener(v -> showLocation(message));
 639    }
 640
 641    private void displayAudioMessage(
 642            ViewHolder viewHolder, Message message, final BubbleColor bubbleColor) {
 643        toggleWhisperInfo(viewHolder, message, bubbleColor);
 644        viewHolder.image.setVisibility(View.GONE);
 645        viewHolder.download_button.setVisibility(View.GONE);
 646        final RelativeLayout audioPlayer = viewHolder.audioPlayer;
 647        audioPlayer.setVisibility(View.VISIBLE);
 648        AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor);
 649        this.audioPlayer.init(audioPlayer, message);
 650    }
 651
 652    private void displayMediaPreviewMessage(
 653            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
 654        toggleWhisperInfo(viewHolder, message, bubbleColor);
 655        viewHolder.download_button.setVisibility(View.GONE);
 656        viewHolder.audioPlayer.setVisibility(View.GONE);
 657        viewHolder.image.setVisibility(View.VISIBLE);
 658        final FileParams params = message.getFileParams();
 659        final float target = activity.getResources().getDimension(R.dimen.image_preview_width);
 660        final int scaledW;
 661        final int scaledH;
 662        if (Math.max(params.height, params.width) * metrics.density <= target) {
 663            scaledW = (int) (params.width * metrics.density);
 664            scaledH = (int) (params.height * metrics.density);
 665        } else if (Math.max(params.height, params.width) <= target) {
 666            scaledW = params.width;
 667            scaledH = params.height;
 668        } else if (params.width <= params.height) {
 669            scaledW = (int) (params.width / ((double) params.height / target));
 670            scaledH = (int) target;
 671        } else {
 672            scaledW = (int) target;
 673            scaledH = (int) (params.height / ((double) params.width / target));
 674        }
 675        final LinearLayout.LayoutParams layoutParams =
 676                new LinearLayout.LayoutParams(scaledW, scaledH);
 677        viewHolder.image.setLayoutParams(layoutParams);
 678        activity.loadBitmap(message, viewHolder.image);
 679        viewHolder.image.setOnClickListener(v -> openDownloadable(message));
 680    }
 681
 682    private void toggleWhisperInfo(
 683            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
 684        if (message.isPrivateMessage()) {
 685            final String privateMarker;
 686            if (message.getStatus() <= Message.STATUS_RECEIVED) {
 687                privateMarker = activity.getString(R.string.private_message);
 688            } else {
 689                Jid cp = message.getCounterpart();
 690                privateMarker =
 691                        activity.getString(
 692                                R.string.private_message_to,
 693                                Strings.nullToEmpty(cp == null ? null : cp.getResource()));
 694            }
 695            final SpannableString body = new SpannableString(privateMarker);
 696            body.setSpan(
 697                    new ForegroundColorSpan(
 698                            bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
 699                    0,
 700                    privateMarker.length(),
 701                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 702            body.setSpan(
 703                    new StyleSpan(Typeface.BOLD),
 704                    0,
 705                    privateMarker.length(),
 706                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 707            viewHolder.messageBody.setText(body);
 708            viewHolder.messageBody.setVisibility(View.VISIBLE);
 709        } else {
 710            viewHolder.messageBody.setVisibility(View.GONE);
 711        }
 712    }
 713
 714    private void loadMoreMessages(Conversation conversation) {
 715        conversation.setLastClearHistory(0, null);
 716        activity.xmppConnectionService.updateConversation(conversation);
 717        conversation.setHasMessagesLeftOnServer(true);
 718        conversation.setFirstMamReference(null);
 719        long timestamp = conversation.getLastMessageTransmitted().getTimestamp();
 720        if (timestamp == 0) {
 721            timestamp = System.currentTimeMillis();
 722        }
 723        conversation.messagesLoaded.set(true);
 724        MessageArchiveService.Query query =
 725                activity.xmppConnectionService
 726                        .getMessageArchiveService()
 727                        .query(conversation, new MamReference(0), timestamp, false);
 728        if (query != null) {
 729            Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG)
 730                    .show();
 731        } else {
 732            Toast.makeText(
 733                            activity,
 734                            R.string.not_fetching_history_retention_period,
 735                            Toast.LENGTH_SHORT)
 736                    .show();
 737        }
 738    }
 739
 740    @Override
 741    public View getView(final int position, View view, final @NonNull ViewGroup parent) {
 742        final Message message = getItem(position);
 743        final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
 744        final boolean isInValidSession =
 745                message.isValidInSession() && (!omemoEncryption || message.isTrusted());
 746        final Conversational conversation = message.getConversation();
 747        final Account account = conversation.getAccount();
 748        final int type = getItemViewType(position);
 749        ViewHolder viewHolder;
 750        if (view == null) {
 751            viewHolder = new ViewHolder();
 752            switch (type) {
 753                case DATE_SEPARATOR:
 754                    view =
 755                            activity.getLayoutInflater()
 756                                    .inflate(R.layout.item_message_date_bubble, parent, false);
 757                    viewHolder.status_message = view.findViewById(R.id.message_body);
 758                    viewHolder.message_box = view.findViewById(R.id.message_box);
 759                    break;
 760                case RTP_SESSION:
 761                    view =
 762                            activity.getLayoutInflater()
 763                                    .inflate(R.layout.item_message_rtp_session, parent, false);
 764                    viewHolder.status_message = view.findViewById(R.id.message_body);
 765                    viewHolder.message_box = view.findViewById(R.id.message_box);
 766                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
 767                    break;
 768                case SENT:
 769                    view =
 770                            activity.getLayoutInflater()
 771                                    .inflate(R.layout.item_message_sent, parent, false);
 772                    viewHolder.message_box = view.findViewById(R.id.message_box);
 773                    viewHolder.contact_picture = view.findViewById(R.id.message_photo);
 774                    viewHolder.download_button = view.findViewById(R.id.download_button);
 775                    viewHolder.indicator = view.findViewById(R.id.security_indicator);
 776                    viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
 777                    viewHolder.image = view.findViewById(R.id.message_image);
 778                    viewHolder.messageBody = view.findViewById(R.id.message_body);
 779                    viewHolder.time = view.findViewById(R.id.message_time);
 780                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
 781                    viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
 782                    viewHolder.reactions = view.findViewById(R.id.reactions);
 783                    break;
 784                case RECEIVED:
 785                    view =
 786                            activity.getLayoutInflater()
 787                                    .inflate(R.layout.item_message_received, parent, false);
 788                    viewHolder.message_box = view.findViewById(R.id.message_box);
 789                    viewHolder.contact_picture = view.findViewById(R.id.message_photo);
 790                    viewHolder.download_button = view.findViewById(R.id.download_button);
 791                    viewHolder.indicator = view.findViewById(R.id.security_indicator);
 792                    viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
 793                    viewHolder.image = view.findViewById(R.id.message_image);
 794                    viewHolder.messageBody = view.findViewById(R.id.message_body);
 795                    viewHolder.time = view.findViewById(R.id.message_time);
 796                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
 797                    viewHolder.encryption = view.findViewById(R.id.message_encryption);
 798                    viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
 799                    viewHolder.reactions = view.findViewById(R.id.reactions);
 800                    break;
 801                case STATUS:
 802                    view =
 803                            activity.getLayoutInflater()
 804                                    .inflate(R.layout.item_message_status, parent, false);
 805                    viewHolder.contact_picture = view.findViewById(R.id.message_photo);
 806                    viewHolder.status_message = view.findViewById(R.id.status_message);
 807                    viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages);
 808                    break;
 809                default:
 810                    throw new AssertionError("Unknown view type");
 811            }
 812            view.setTag(viewHolder);
 813        } else {
 814            viewHolder = (ViewHolder) view.getTag();
 815            if (viewHolder == null) {
 816                return view;
 817            }
 818        }
 819
 820        final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
 821        final BubbleColor bubbleColor;
 822        if (type == RECEIVED) {
 823            if (isInValidSession) {
 824                bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
 825            } else {
 826                bubbleColor = BubbleColor.WARNING;
 827            }
 828        } else {
 829            bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH;
 830        }
 831
 832        if (type == DATE_SEPARATOR) {
 833            if (UIHelper.today(message.getTimeSent())) {
 834                viewHolder.status_message.setText(R.string.today);
 835            } else if (UIHelper.yesterday(message.getTimeSent())) {
 836                viewHolder.status_message.setText(R.string.yesterday);
 837            } else {
 838                viewHolder.status_message.setText(
 839                        DateUtils.formatDateTime(
 840                                activity,
 841                                message.getTimeSent(),
 842                                DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
 843            }
 844            if (colorfulBackground) {
 845                setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY);
 846                setTextColor(viewHolder.status_message, BubbleColor.PRIMARY);
 847            } else {
 848                setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
 849                setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
 850            }
 851            return view;
 852        } else if (type == RTP_SESSION) {
 853            final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
 854            final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
 855            final long duration = rtpSessionStatus.duration;
 856            if (received) {
 857                if (duration > 0) {
 858                    viewHolder.status_message.setText(
 859                            activity.getString(
 860                                    R.string.incoming_call_duration_timestamp,
 861                                    TimeFrameUtils.resolve(activity, duration),
 862                                    UIHelper.readableTimeDifferenceFull(
 863                                            activity, message.getTimeSent())));
 864                } else if (rtpSessionStatus.successful) {
 865                    viewHolder.status_message.setText(R.string.incoming_call);
 866                } else {
 867                    viewHolder.status_message.setText(
 868                            activity.getString(
 869                                    R.string.missed_call_timestamp,
 870                                    UIHelper.readableTimeDifferenceFull(
 871                                            activity, message.getTimeSent())));
 872                }
 873            } else {
 874                if (duration > 0) {
 875                    viewHolder.status_message.setText(
 876                            activity.getString(
 877                                    R.string.outgoing_call_duration_timestamp,
 878                                    TimeFrameUtils.resolve(activity, duration),
 879                                    UIHelper.readableTimeDifferenceFull(
 880                                            activity, message.getTimeSent())));
 881                } else {
 882                    viewHolder.status_message.setText(
 883                            activity.getString(
 884                                    R.string.outgoing_call_timestamp,
 885                                    UIHelper.readableTimeDifferenceFull(
 886                                            activity, message.getTimeSent())));
 887                }
 888            }
 889            if (colorfulBackground) {
 890                setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY);
 891                setTextColor(viewHolder.status_message, BubbleColor.SECONDARY);
 892                setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY);
 893            } else {
 894                setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
 895                setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
 896                setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH);
 897            }
 898            viewHolder.indicatorReceived.setImageResource(
 899                    RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful));
 900            return view;
 901        } else if (type == STATUS) {
 902            if ("LOAD_MORE".equals(message.getBody())) {
 903                viewHolder.status_message.setVisibility(View.GONE);
 904                viewHolder.contact_picture.setVisibility(View.GONE);
 905                viewHolder.load_more_messages.setVisibility(View.VISIBLE);
 906                viewHolder.load_more_messages.setOnClickListener(
 907                        v -> loadMoreMessages((Conversation) message.getConversation()));
 908            } else {
 909                viewHolder.status_message.setVisibility(View.VISIBLE);
 910                viewHolder.load_more_messages.setVisibility(View.GONE);
 911                viewHolder.status_message.setText(message.getBody());
 912                boolean showAvatar;
 913                if (conversation.getMode() == Conversation.MODE_SINGLE) {
 914                    showAvatar = true;
 915                    AvatarWorkerTask.loadAvatar(
 916                            message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
 917                } else if (message.getCounterpart() != null
 918                        || message.getTrueCounterpart() != null
 919                        || (message.getCounterparts() != null
 920                                && message.getCounterparts().size() > 0)) {
 921                    showAvatar = true;
 922                    AvatarWorkerTask.loadAvatar(
 923                            message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
 924                } else {
 925                    showAvatar = false;
 926                }
 927                if (showAvatar) {
 928                    viewHolder.contact_picture.setAlpha(0.5f);
 929                    viewHolder.contact_picture.setVisibility(View.VISIBLE);
 930                } else {
 931                    viewHolder.contact_picture.setVisibility(View.GONE);
 932                }
 933            }
 934            return view;
 935        } else {
 936            viewHolder.message_box.setClipToOutline(true);
 937            AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar);
 938        }
 939
 940        resetClickListener(viewHolder.message_box, viewHolder.messageBody);
 941
 942        viewHolder.contact_picture.setOnClickListener(
 943                v -> {
 944                    if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
 945                        MessageAdapter.this.mOnContactPictureClickedListener
 946                                .onContactPictureClicked(message);
 947                    }
 948                });
 949        viewHolder.contact_picture.setOnLongClickListener(
 950                v -> {
 951                    if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
 952                        MessageAdapter.this.mOnContactPictureLongClickedListener
 953                                .onContactPictureLongClicked(v, message);
 954                        return true;
 955                    } else {
 956                        return false;
 957                    }
 958                });
 959
 960        final Transferable transferable = message.getTransferable();
 961        final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
 962        if (unInitiatedButKnownSize
 963                || message.isDeleted()
 964                || (transferable != null
 965                        && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
 966            if (unInitiatedButKnownSize
 967                    || transferable != null
 968                            && transferable.getStatus() == Transferable.STATUS_OFFER) {
 969                displayDownloadableMessage(
 970                        viewHolder,
 971                        message,
 972                        activity.getString(
 973                                R.string.download_x_file,
 974                                UIHelper.getFileDescriptionString(activity, message)),
 975                        bubbleColor);
 976            } else if (transferable != null
 977                    && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
 978                displayDownloadableMessage(
 979                        viewHolder,
 980                        message,
 981                        activity.getString(
 982                                R.string.check_x_filesize,
 983                                UIHelper.getFileDescriptionString(activity, message)),
 984                        bubbleColor);
 985            } else {
 986                displayInfoMessage(
 987                        viewHolder,
 988                        UIHelper.getMessagePreview(activity, message).first,
 989                        bubbleColor);
 990            }
 991        } else if (message.isFileOrImage()
 992                && message.getEncryption() != Message.ENCRYPTION_PGP
 993                && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
 994            if (message.getFileParams().width > 0 && message.getFileParams().height > 0) {
 995                displayMediaPreviewMessage(viewHolder, message, bubbleColor);
 996            } else if (message.getFileParams().runtime > 0) {
 997                displayAudioMessage(viewHolder, message, bubbleColor);
 998            } else {
 999                displayOpenableMessage(viewHolder, message, bubbleColor);
1000            }
1001        } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1002            if (account.isPgpDecryptionServiceConnected()) {
1003                if (conversation instanceof Conversation
1004                        && !account.hasPendingPgpIntent((Conversation) conversation)) {
1005                    displayInfoMessage(
1006                            viewHolder,
1007                            activity.getString(R.string.message_decrypting),
1008                            bubbleColor);
1009                } else {
1010                    displayInfoMessage(
1011                            viewHolder, activity.getString(R.string.pgp_message), bubbleColor);
1012                }
1013            } else {
1014                displayInfoMessage(
1015                        viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor);
1016                viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall);
1017                viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall);
1018            }
1019        } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
1020            displayInfoMessage(
1021                    viewHolder, activity.getString(R.string.decryption_failed), bubbleColor);
1022        } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
1023            displayInfoMessage(
1024                    viewHolder,
1025                    activity.getString(R.string.not_encrypted_for_this_device),
1026                    bubbleColor);
1027        } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1028            displayInfoMessage(
1029                    viewHolder, activity.getString(R.string.omemo_decryption_failed), bubbleColor);
1030        } else {
1031            if (message.isGeoUri()) {
1032                displayLocationMessage(viewHolder, message, bubbleColor);
1033            } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) {
1034                displayEmojiMessage(viewHolder, message.getBody().trim(), bubbleColor);
1035            } else if (message.treatAsDownloadable()) {
1036                try {
1037                    final URI uri = new URI(message.getBody());
1038                    displayDownloadableMessage(
1039                            viewHolder,
1040                            message,
1041                            activity.getString(
1042                                    R.string.check_x_filesize_on_host,
1043                                    UIHelper.getFileDescriptionString(activity, message),
1044                                    uri.getHost()),
1045                            bubbleColor);
1046                } catch (Exception e) {
1047                    displayDownloadableMessage(
1048                            viewHolder,
1049                            message,
1050                            activity.getString(
1051                                    R.string.check_x_filesize,
1052                                    UIHelper.getFileDescriptionString(activity, message)),
1053                            bubbleColor);
1054                }
1055            } else {
1056                displayTextMessage(viewHolder, message, bubbleColor);
1057            }
1058        }
1059
1060        setBackgroundTint(viewHolder.message_box, bubbleColor);
1061        setTextColor(viewHolder.messageBody, bubbleColor);
1062
1063        if (type == RECEIVED) {
1064            setTextColor(viewHolder.encryption, bubbleColor);
1065            if (isInValidSession) {
1066                viewHolder.encryption.setVisibility(View.GONE);
1067            } else {
1068                viewHolder.encryption.setVisibility(View.VISIBLE);
1069                if (omemoEncryption && !message.isTrusted()) {
1070                    viewHolder.encryption.setText(R.string.not_trusted);
1071                } else {
1072                    viewHolder.encryption.setText(
1073                            CryptoHelper.encryptionTypeToText(message.getEncryption()));
1074                }
1075            }
1076            BindingAdapters.setReactionsOnReceived(
1077                    viewHolder.reactions,
1078                    message.getAggregatedReactions(),
1079                    reactions -> sendReactions(message, reactions),
1080                    () -> addReaction(message));
1081        } else if (type == SENT) {
1082            BindingAdapters.setReactionsOnSent(
1083                    viewHolder.reactions,
1084                    message.getAggregatedReactions(),
1085                    reactions -> sendReactions(message, reactions));
1086        }
1087
1088        displayStatus(viewHolder, message, type, bubbleColor);
1089        return view;
1090    }
1091
1092    private void sendReactions(final Message message, final Collection<String> reactions) {
1093        if (activity.xmppConnectionService.sendReactions(message, reactions)) {
1094            return;
1095        }
1096        Toast.makeText(activity, R.string.could_not_add_reaction, Toast.LENGTH_LONG).show();
1097    }
1098
1099    private void addReaction(final Message message) {
1100        activity.addReaction(message, reactions -> activity.xmppConnectionService.sendReactions(message,reactions));
1101    }
1102
1103    private void promptOpenKeychainInstall(View view) {
1104        activity.showInstallPgpDialog();
1105    }
1106
1107    public FileBackend getFileBackend() {
1108        return activity.xmppConnectionService.getFileBackend();
1109    }
1110
1111    public void stopAudioPlayer() {
1112        audioPlayer.stop();
1113    }
1114
1115    public void unregisterListenerInAudioPlayer() {
1116        audioPlayer.unregisterListener();
1117    }
1118
1119    public void startStopPending() {
1120        audioPlayer.startStopPending();
1121    }
1122
1123    public void openDownloadable(Message message) {
1124        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
1125                && ContextCompat.checkSelfPermission(
1126                                activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
1127                        != PackageManager.PERMISSION_GRANTED) {
1128            ConversationFragment.registerPendingMessage(activity, message);
1129            ActivityCompat.requestPermissions(
1130                    activity,
1131                    new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
1132                    ConversationsActivity.REQUEST_OPEN_MESSAGE);
1133            return;
1134        }
1135        final DownloadableFile file =
1136                activity.xmppConnectionService.getFileBackend().getFile(message);
1137        ViewUtil.view(activity, file);
1138    }
1139
1140    private void showLocation(Message message) {
1141        for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) {
1142            if (intent.resolveActivity(getContext().getPackageManager()) != null) {
1143                getContext().startActivity(intent);
1144                return;
1145            }
1146        }
1147        Toast.makeText(
1148                        activity,
1149                        R.string.no_application_found_to_display_location,
1150                        Toast.LENGTH_SHORT)
1151                .show();
1152    }
1153
1154    public void updatePreferences() {
1155        final AppSettings appSettings = new AppSettings(activity);
1156        this.bubbleDesign =
1157                new BubbleDesign(appSettings.isColorfulChatBubbles(), appSettings.isLargeFont());
1158    }
1159
1160    public void setHighlightedTerm(List<String> terms) {
1161        this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
1162    }
1163
1164    public interface OnContactPictureClicked {
1165        void onContactPictureClicked(Message message);
1166    }
1167
1168    public interface OnContactPictureLongClicked {
1169        void onContactPictureLongClicked(View v, Message message);
1170    }
1171
1172    private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) {
1173        view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor));
1174    }
1175
1176    private static ColorStateList bubbleToColorStateList(
1177            final View view, final BubbleColor bubbleColor) {
1178        final @AttrRes int colorAttributeResId =
1179                switch (bubbleColor) {
1180                    case SURFACE -> Activities.isNightMode(view.getContext())
1181                            ? com.google.android.material.R.attr.colorSurfaceContainerHigh
1182                            : com.google.android.material.R.attr.colorSurfaceContainerLow;
1183                    case SURFACE_HIGH -> Activities.isNightMode(view.getContext())
1184                            ? com.google.android.material.R.attr.colorSurfaceContainerHighest
1185                            : com.google.android.material.R.attr.colorSurfaceContainerHigh;
1186                    case PRIMARY -> com.google.android.material.R.attr.colorPrimaryContainer;
1187                    case SECONDARY -> com.google.android.material.R.attr.colorSecondaryContainer;
1188                    case TERTIARY -> com.google.android.material.R.attr.colorTertiaryContainer;
1189                    case WARNING -> com.google.android.material.R.attr.colorErrorContainer;
1190                };
1191        return ColorStateList.valueOf(MaterialColors.getColor(view, colorAttributeResId));
1192    }
1193
1194    public static void setImageTint(final ImageView imageView, final BubbleColor bubbleColor) {
1195        ImageViewCompat.setImageTintList(
1196                imageView, bubbleToOnSurfaceColorStateList(imageView, bubbleColor));
1197    }
1198
1199    public static void setImageTintError(final ImageView imageView) {
1200        ImageViewCompat.setImageTintList(
1201                imageView,
1202                ColorStateList.valueOf(
1203                        MaterialColors.getColor(
1204                                imageView, com.google.android.material.R.attr.colorError)));
1205    }
1206
1207    public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) {
1208        final var color = bubbleToOnSurfaceColor(textView, bubbleColor);
1209        textView.setTextColor(color);
1210        if (BubbleColor.SURFACES.contains(bubbleColor)) {
1211            textView.setLinkTextColor(
1212                    MaterialColors.getColor(
1213                            textView, com.google.android.material.R.attr.colorPrimary));
1214        } else {
1215            textView.setLinkTextColor(color);
1216        }
1217    }
1218
1219    private static void setTextSize(final TextView textView, final boolean largeFont) {
1220        if (largeFont) {
1221            textView.setTextAppearance(
1222                    com.google.android.material.R.style.TextAppearance_Material3_BodyLarge);
1223        } else {
1224            textView.setTextAppearance(
1225                    com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
1226        }
1227    }
1228
1229    private static @ColorInt int bubbleToOnSurfaceVariant(
1230            final View view, final BubbleColor bubbleColor) {
1231        final @AttrRes int colorAttributeResId;
1232        if (BubbleColor.SURFACES.contains(bubbleColor)) {
1233            colorAttributeResId = com.google.android.material.R.attr.colorOnSurfaceVariant;
1234        } else {
1235            colorAttributeResId = bubbleToOnSurface(bubbleColor);
1236        }
1237        return MaterialColors.getColor(view, colorAttributeResId);
1238    }
1239
1240    private static @ColorInt int bubbleToOnSurfaceColor(
1241            final View view, final BubbleColor bubbleColor) {
1242        return MaterialColors.getColor(view, bubbleToOnSurface(bubbleColor));
1243    }
1244
1245    public static ColorStateList bubbleToOnSurfaceColorStateList(
1246            final View view, final BubbleColor bubbleColor) {
1247        return ColorStateList.valueOf(bubbleToOnSurfaceColor(view, bubbleColor));
1248    }
1249
1250    private static @AttrRes int bubbleToOnSurface(final BubbleColor bubbleColor) {
1251        return switch (bubbleColor) {
1252            case SURFACE, SURFACE_HIGH -> com.google.android.material.R.attr.colorOnSurface;
1253            case PRIMARY -> com.google.android.material.R.attr.colorOnPrimaryContainer;
1254            case SECONDARY -> com.google.android.material.R.attr.colorOnSecondaryContainer;
1255            case TERTIARY -> com.google.android.material.R.attr.colorOnTertiaryContainer;
1256            case WARNING -> com.google.android.material.R.attr.colorOnErrorContainer;
1257        };
1258    }
1259
1260    public enum BubbleColor {
1261        SURFACE,
1262        SURFACE_HIGH,
1263        PRIMARY,
1264        SECONDARY,
1265        TERTIARY,
1266        WARNING;
1267
1268        private static final Collection<BubbleColor> SURFACES =
1269                Arrays.asList(BubbleColor.SURFACE, BubbleColor.SURFACE_HIGH);
1270    }
1271
1272    private static class BubbleDesign {
1273        public final boolean colorfulChatBubbles;
1274        public final boolean largeFont;
1275
1276        private BubbleDesign(final boolean colorfulChatBubbles, final boolean largeFont) {
1277            this.colorfulChatBubbles = colorfulChatBubbles;
1278            this.largeFont = largeFont;
1279        }
1280    }
1281
1282    private static class ViewHolder {
1283
1284        public MaterialButton load_more_messages;
1285        public ImageView edit_indicator;
1286        public RelativeLayout audioPlayer;
1287        protected LinearLayout message_box;
1288        protected MaterialButton download_button;
1289        protected ImageView image;
1290        protected ImageView indicator;
1291        protected ImageView indicatorReceived;
1292        protected TextView time;
1293        protected TextView messageBody;
1294        protected ImageView contact_picture;
1295        protected TextView status_message;
1296        protected TextView encryption;
1297        protected ChipGroup reactions;
1298    }
1299}