/* * Copyright (C) 2011 Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.tm.archive.conversation; import android.annotation.SuppressLint; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.text.Annotation; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextPaint; import android.text.style.BackgroundColorSpan; import android.text.style.CharacterStyle; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.URLSpan; import android.util.AttributeSet; import android.util.TypedValue; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.TouchDelegate; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.ColorInt; import androidx.annotation.DimenRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.lifecycle.LifecycleOwner; import androidx.media3.common.MediaItem; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.RequestManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.collect.Sets; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.signal.core.util.DimensionUnit; import org.signal.core.util.StringUtil; import org.signal.core.util.logging.Log; import org.signal.ringrtc.CallLinkRootKey; import org.tm.archive.BindableConversationItem; import org.tm.archive.R; import org.tm.archive.attachments.Attachment; import org.tm.archive.attachments.AttachmentId; import org.tm.archive.attachments.DatabaseAttachment; import org.tm.archive.badges.BadgeImageView; import org.tm.archive.badges.gifts.GiftMessageView; import org.tm.archive.badges.gifts.OpenableGift; import org.tm.archive.calls.links.CallLinkJoinButton; import org.tm.archive.calls.links.CallLinks; import org.tm.archive.components.AlertView; import org.tm.archive.components.AudioView; import org.tm.archive.components.AvatarImageView; import org.tm.archive.components.BorderlessImageView; import org.tm.archive.components.ConversationItemFooter; import org.tm.archive.components.ConversationItemThumbnail; import org.tm.archive.components.DocumentView; import org.tm.archive.components.LinkPreviewView; import org.tm.archive.components.Outliner; import org.tm.archive.components.PlaybackSpeedToggleTextView; import org.tm.archive.components.QuoteView; import org.tm.archive.components.SharedContactView; import org.tm.archive.components.ThumbnailView; import org.tm.archive.components.emoji.EmojiTextView; import org.tm.archive.components.mention.MentionAnnotation; import org.tm.archive.contactshare.Contact; import org.tm.archive.conversation.clicklisteners.AttachmentCancelClickListener; import org.tm.archive.conversation.clicklisteners.ResendClickListener; import org.tm.archive.conversation.colors.Colorizer; import org.tm.archive.conversation.mutiselect.MultiselectCollection; import org.tm.archive.conversation.mutiselect.MultiselectPart; import org.tm.archive.conversation.ui.payment.PaymentMessageView; import org.tm.archive.conversation.v2.items.InteractiveConversationElement; import org.tm.archive.conversation.v2.items.V2ConversationItemUtils; import org.tm.archive.database.AttachmentTable; import org.tm.archive.database.MediaTable; import org.tm.archive.database.model.MessageRecord; import org.tm.archive.database.model.MmsMessageRecord; import org.tm.archive.database.model.Quote; import org.tm.archive.dependencies.ApplicationDependencies; import org.tm.archive.events.PartProgressEvent; import org.tm.archive.giph.mp4.GiphyMp4PlaybackPolicy; import org.tm.archive.giph.mp4.GiphyMp4PlaybackPolicyEnforcer; import org.tm.archive.jobmanager.JobManager; import org.tm.archive.jobs.AttachmentDownloadJob; import org.tm.archive.keyvalue.SignalStore; import org.tm.archive.linkpreview.LinkPreview; import org.tm.archive.mediapreview.MediaIntentFactory; import org.tm.archive.mediapreview.MediaPreviewCache; import org.tm.archive.mediapreview.MediaPreviewV2Fragment; import org.tm.archive.mms.ImageSlide; import org.tm.archive.mms.PartAuthority; import org.tm.archive.mms.Slide; import org.tm.archive.mms.SlideClickListener; import org.tm.archive.mms.SlideDeck; import org.tm.archive.mms.SlidesClickedListener; import org.tm.archive.mms.TextSlide; import org.tm.archive.mms.VideoSlide; import org.tm.archive.reactions.ReactionsConversationView; import org.tm.archive.recipients.LiveRecipient; import org.tm.archive.recipients.Recipient; import org.tm.archive.recipients.RecipientForeverObserver; import org.tm.archive.recipients.RecipientId; import org.tm.archive.revealable.ViewOnceMessageView; import org.tm.archive.util.DateUtils; import org.tm.archive.util.FeatureFlags; import org.tm.archive.util.InterceptableLongClickCopyLinkSpan; import org.tm.archive.util.LongClickMovementMethod; import org.tm.archive.util.MediaUtil; import org.tm.archive.util.MessageRecordUtil; import org.tm.archive.util.PlaceholderURLSpan; import org.tm.archive.util.Projection; import org.tm.archive.util.ProjectionList; import org.tm.archive.util.SearchUtil; import org.tm.archive.util.ThemeUtil; import org.tm.archive.util.UrlClickHandler; import org.tm.archive.util.Util; import org.tm.archive.util.VibrateUtil; import org.tm.archive.util.ViewUtil; import org.tm.archive.util.views.NullableStub; import org.tm.archive.util.views.Stub; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import kotlin.Unit; import kotlin.jvm.functions.Function1; /** * A view that displays an individual conversation item within a conversation * thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter. * * @author Moxie Marlinspike */ public final class ConversationItem extends RelativeLayout implements BindableConversationItem, RecipientForeverObserver, OpenableGift, InteractiveConversationElement { private static final String TAG = Log.tag(ConversationItem.class); private static final int MAX_MEASURE_CALLS = 3; private static final Rect SWIPE_RECT = new Rect(); public static final float LONG_PRESS_SCALE_FACTOR = 0.95f; private static final int SHRINK_BUBBLE_DELAY_MILLIS = 100; private static final long MAX_CLUSTERING_TIME_DIFF = TimeUnit.MINUTES.toMillis(3); private static final int CONDENSED_MODE_MAX_LINES = 3; private static final SearchUtil.StyleFactory STYLE_FACTORY = () -> new CharacterStyle[] { new BackgroundColorSpan(Color.YELLOW), new ForegroundColorSpan(Color.BLACK) }; private ConversationMessage conversationMessage; private MessageRecord messageRecord; private Optional nextMessageRecord; private Locale locale; private boolean groupThread; private LiveRecipient author; private RequestManager requestManager; private Optional previousMessage; private ConversationItemDisplayMode displayMode; private ConversationItemBodyBubble bodyBubble; private View reply; private View replyIcon; @Nullable private ViewGroup contactPhotoHolder; @Nullable private QuoteView quoteView; private EmojiTextView bodyText; private ConversationItemFooter footer; @Nullable private ConversationItemFooter stickerFooter; @Nullable private TextView groupSender; @Nullable private View groupSenderHolder; private AvatarImageView contactPhoto; private AlertView alertView; private ReactionsConversationView reactionsView; private BadgeImageView badgeImageView; private View storyReactionLabelWrapper; private TextView storyReactionLabel; private View quotedIndicator; private View scheduledIndicator; private @NonNull Set batchSelected = new HashSet<>(); private final @NonNull Outliner outliner = new Outliner(); private final @NonNull Outliner pulseOutliner = new Outliner(); private final @NonNull List outliners = new ArrayList<>(2); private LiveRecipient conversationRecipient; private NullableStub mediaThumbnailStub; private Stub audioViewStub; private Stub documentViewStub; private Stub sharedContactStub; private Stub linkPreviewStub; private Stub stickerStub; private Stub revealableStub; private Stub joinCallLinkStub; private Stub