That fuck shit the fascists are using
at master 2778 lines 126 kB view raw
1/* 2 * Copyright (C) 2011 Whisper Systems 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17package org.tm.archive.conversation; 18 19import android.annotation.SuppressLint; 20import android.content.ActivityNotFoundException; 21import android.content.Context; 22import android.content.Intent; 23import android.graphics.Color; 24import android.graphics.PorterDuff; 25import android.graphics.Rect; 26import android.graphics.Typeface; 27import android.net.Uri; 28import android.os.Build; 29import android.text.Annotation; 30import android.text.Spannable; 31import android.text.SpannableString; 32import android.text.SpannableStringBuilder; 33import android.text.Spanned; 34import android.text.TextPaint; 35import android.text.style.BackgroundColorSpan; 36import android.text.style.CharacterStyle; 37import android.text.style.ClickableSpan; 38import android.text.style.ForegroundColorSpan; 39import android.text.style.StyleSpan; 40import android.text.style.URLSpan; 41import android.util.AttributeSet; 42import android.util.TypedValue; 43import android.view.HapticFeedbackConstants; 44import android.view.MotionEvent; 45import android.view.TouchDelegate; 46import android.view.View; 47import android.view.ViewGroup; 48import android.widget.Button; 49import android.widget.RelativeLayout; 50import android.widget.TextView; 51import android.widget.Toast; 52 53import androidx.annotation.ColorInt; 54import androidx.annotation.DimenRes; 55import androidx.annotation.NonNull; 56import androidx.annotation.Nullable; 57import androidx.core.content.ContextCompat; 58import androidx.lifecycle.LifecycleOwner; 59import androidx.media3.common.MediaItem; 60import androidx.recyclerview.widget.RecyclerView; 61 62import com.bumptech.glide.RequestManager; 63import com.google.android.material.dialog.MaterialAlertDialogBuilder; 64import com.google.common.collect.Sets; 65 66import org.greenrobot.eventbus.EventBus; 67import org.greenrobot.eventbus.Subscribe; 68import org.greenrobot.eventbus.ThreadMode; 69import org.signal.core.util.DimensionUnit; 70import org.signal.core.util.StringUtil; 71import org.signal.core.util.logging.Log; 72import org.signal.ringrtc.CallLinkRootKey; 73import org.tm.archive.BindableConversationItem; 74import org.tm.archive.R; 75import org.tm.archive.attachments.Attachment; 76import org.tm.archive.attachments.AttachmentId; 77import org.tm.archive.attachments.DatabaseAttachment; 78import org.tm.archive.badges.BadgeImageView; 79import org.tm.archive.badges.gifts.GiftMessageView; 80import org.tm.archive.badges.gifts.OpenableGift; 81import org.tm.archive.calls.links.CallLinkJoinButton; 82import org.tm.archive.calls.links.CallLinks; 83import org.tm.archive.components.AlertView; 84import org.tm.archive.components.AudioView; 85import org.tm.archive.components.AvatarImageView; 86import org.tm.archive.components.BorderlessImageView; 87import org.tm.archive.components.ConversationItemFooter; 88import org.tm.archive.components.ConversationItemThumbnail; 89import org.tm.archive.components.DocumentView; 90import org.tm.archive.components.LinkPreviewView; 91import org.tm.archive.components.Outliner; 92import org.tm.archive.components.PlaybackSpeedToggleTextView; 93import org.tm.archive.components.QuoteView; 94import org.tm.archive.components.SharedContactView; 95import org.tm.archive.components.ThumbnailView; 96import org.tm.archive.components.emoji.EmojiTextView; 97import org.tm.archive.components.mention.MentionAnnotation; 98import org.tm.archive.contactshare.Contact; 99import org.tm.archive.conversation.clicklisteners.AttachmentCancelClickListener; 100import org.tm.archive.conversation.clicklisteners.ResendClickListener; 101import org.tm.archive.conversation.colors.Colorizer; 102import org.tm.archive.conversation.mutiselect.MultiselectCollection; 103import org.tm.archive.conversation.mutiselect.MultiselectPart; 104import org.tm.archive.conversation.ui.payment.PaymentMessageView; 105import org.tm.archive.conversation.v2.items.InteractiveConversationElement; 106import org.tm.archive.conversation.v2.items.V2ConversationItemUtils; 107import org.tm.archive.database.AttachmentTable; 108import org.tm.archive.database.MediaTable; 109import org.tm.archive.database.model.MessageRecord; 110import org.tm.archive.database.model.MmsMessageRecord; 111import org.tm.archive.database.model.Quote; 112import org.tm.archive.dependencies.ApplicationDependencies; 113import org.tm.archive.events.PartProgressEvent; 114import org.tm.archive.giph.mp4.GiphyMp4PlaybackPolicy; 115import org.tm.archive.giph.mp4.GiphyMp4PlaybackPolicyEnforcer; 116import org.tm.archive.jobmanager.JobManager; 117import org.tm.archive.jobs.AttachmentDownloadJob; 118import org.tm.archive.keyvalue.SignalStore; 119import org.tm.archive.linkpreview.LinkPreview; 120import org.tm.archive.mediapreview.MediaIntentFactory; 121import org.tm.archive.mediapreview.MediaPreviewCache; 122import org.tm.archive.mediapreview.MediaPreviewV2Fragment; 123import org.tm.archive.mms.ImageSlide; 124import org.tm.archive.mms.PartAuthority; 125import org.tm.archive.mms.Slide; 126import org.tm.archive.mms.SlideClickListener; 127import org.tm.archive.mms.SlideDeck; 128import org.tm.archive.mms.SlidesClickedListener; 129import org.tm.archive.mms.TextSlide; 130import org.tm.archive.mms.VideoSlide; 131import org.tm.archive.reactions.ReactionsConversationView; 132import org.tm.archive.recipients.LiveRecipient; 133import org.tm.archive.recipients.Recipient; 134import org.tm.archive.recipients.RecipientForeverObserver; 135import org.tm.archive.recipients.RecipientId; 136import org.tm.archive.revealable.ViewOnceMessageView; 137import org.tm.archive.util.DateUtils; 138import org.tm.archive.util.FeatureFlags; 139import org.tm.archive.util.InterceptableLongClickCopyLinkSpan; 140import org.tm.archive.util.LongClickMovementMethod; 141import org.tm.archive.util.MediaUtil; 142import org.tm.archive.util.MessageRecordUtil; 143import org.tm.archive.util.PlaceholderURLSpan; 144import org.tm.archive.util.Projection; 145import org.tm.archive.util.ProjectionList; 146import org.tm.archive.util.SearchUtil; 147import org.tm.archive.util.ThemeUtil; 148import org.tm.archive.util.UrlClickHandler; 149import org.tm.archive.util.Util; 150import org.tm.archive.util.VibrateUtil; 151import org.tm.archive.util.ViewUtil; 152import org.tm.archive.util.views.NullableStub; 153import org.tm.archive.util.views.Stub; 154 155import java.util.ArrayList; 156import java.util.Collections; 157import java.util.HashSet; 158import java.util.List; 159import java.util.Locale; 160import java.util.Objects; 161import java.util.Optional; 162import java.util.Set; 163import java.util.concurrent.TimeUnit; 164 165import kotlin.Unit; 166import kotlin.jvm.functions.Function1; 167 168/** 169 * A view that displays an individual conversation item within a conversation 170 * thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter. 171 * 172 * @author Moxie Marlinspike 173 */ 174 175public final class ConversationItem extends RelativeLayout implements BindableConversationItem, 176 RecipientForeverObserver, 177 OpenableGift, 178 InteractiveConversationElement 179{ 180 private static final String TAG = Log.tag(ConversationItem.class); 181 182 private static final int MAX_MEASURE_CALLS = 3; 183 184 private static final Rect SWIPE_RECT = new Rect(); 185 186 public static final float LONG_PRESS_SCALE_FACTOR = 0.95f; 187 private static final int SHRINK_BUBBLE_DELAY_MILLIS = 100; 188 private static final long MAX_CLUSTERING_TIME_DIFF = TimeUnit.MINUTES.toMillis(3); 189 private static final int CONDENSED_MODE_MAX_LINES = 3; 190 191 private static final SearchUtil.StyleFactory STYLE_FACTORY = () -> new CharacterStyle[] { new BackgroundColorSpan(Color.YELLOW), new ForegroundColorSpan(Color.BLACK) }; 192 193 private ConversationMessage conversationMessage; 194 private MessageRecord messageRecord; 195 private Optional<MessageRecord> nextMessageRecord; 196 private Locale locale; 197 private boolean groupThread; 198 private LiveRecipient author; 199 private RequestManager requestManager; 200 private Optional<MessageRecord> previousMessage; 201 private ConversationItemDisplayMode displayMode; 202 203 private ConversationItemBodyBubble bodyBubble; 204 private View reply; 205 private View replyIcon; 206 @Nullable private ViewGroup contactPhotoHolder; 207 @Nullable private QuoteView quoteView; 208 private EmojiTextView bodyText; 209 private ConversationItemFooter footer; 210 @Nullable private ConversationItemFooter stickerFooter; 211 @Nullable private TextView groupSender; 212 @Nullable private View groupSenderHolder; 213 private AvatarImageView contactPhoto; 214 private AlertView alertView; 215 private ReactionsConversationView reactionsView; 216 private BadgeImageView badgeImageView; 217 private View storyReactionLabelWrapper; 218 private TextView storyReactionLabel; 219 private View quotedIndicator; 220 private View scheduledIndicator; 221 222 private @NonNull Set<MultiselectPart> batchSelected = new HashSet<>(); 223 private final @NonNull Outliner outliner = new Outliner(); 224 private final @NonNull Outliner pulseOutliner = new Outliner(); 225 private final @NonNull List<Outliner> outliners = new ArrayList<>(2); 226 private LiveRecipient conversationRecipient; 227 private NullableStub<ConversationItemThumbnail> mediaThumbnailStub; 228 private Stub<AudioView> audioViewStub; 229 private Stub<DocumentView> documentViewStub; 230 private Stub<SharedContactView> sharedContactStub; 231 private Stub<LinkPreviewView> linkPreviewStub; 232 private Stub<BorderlessImageView> stickerStub; 233 private Stub<ViewOnceMessageView> revealableStub; 234 private Stub<CallLinkJoinButton> joinCallLinkStub; 235 private Stub<Button> callToActionStub; 236 private Stub<GiftMessageView> giftViewStub; 237 private Stub<PaymentMessageView> paymentViewStub; 238 private @Nullable EventListener eventListener; 239 240 private int defaultBubbleColor; 241 private int defaultBubbleColorForWallpaper; 242 private int measureCalls; 243 private boolean updatingFooter; 244 245 private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); 246 private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener(); 247 private final PlayVideoClickListener playVideoClickListener = new PlayVideoClickListener(); 248 private final AttachmentCancelClickListener attachmentCancelClickListener = new AttachmentCancelClickListener(); 249 private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener); 250 private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener(); 251 private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener(); 252 private final LinkPreviewClickListener linkPreviewClickListener = new LinkPreviewClickListener(); 253 private final ViewOnceMessageClickListener revealableClickListener = new ViewOnceMessageClickListener(); 254 private final QuotedIndicatorClickListener quotedIndicatorClickListener = new QuotedIndicatorClickListener(); 255 private final ScheduledIndicatorClickListener scheduledIndicatorClickListener = new ScheduledIndicatorClickListener(); 256 private final UrlClickListener urlClickListener = new UrlClickListener(); 257 private final Rect thumbnailMaskingRect = new Rect(); 258 private final TouchDelegateChangedListener touchDelegateChangedListener = new TouchDelegateChangedListener(); 259 private final GiftMessageViewCallback giftMessageViewCallback = new GiftMessageViewCallback(); 260 261 private final Context context; 262 263 private MediaItem mediaItem; 264 private boolean canPlayContent; 265 private Projection.Corners bodyBubbleCorners; 266 private Colorizer colorizer; 267 private boolean hasWallpaper; 268 private float lastYDownRelativeToThis; 269 private final ProjectionList colorizerProjections = new ProjectionList(3); 270 private boolean isBound = false; 271 272 private final Runnable shrinkBubble = new Runnable() { 273 @Override 274 public void run() { 275 bodyBubble.animate() 276 .scaleX(LONG_PRESS_SCALE_FACTOR) 277 .scaleY(LONG_PRESS_SCALE_FACTOR) 278 .setUpdateListener(animation -> { 279 View parent = (View) getParent(); 280 if (parent != null) { 281 parent.invalidate(); 282 } 283 }); 284 285 reactionsView.animate() 286 .scaleX(LONG_PRESS_SCALE_FACTOR) 287 .scaleY(LONG_PRESS_SCALE_FACTOR); 288 289 if (quotedIndicator != null) { 290 quotedIndicator.animate() 291 .scaleX(LONG_PRESS_SCALE_FACTOR) 292 .scaleY(LONG_PRESS_SCALE_FACTOR); 293 } 294 if (scheduledIndicator != null) { 295 scheduledIndicator.animate() 296 .scaleX(LONG_PRESS_SCALE_FACTOR) 297 .scaleY(LONG_PRESS_SCALE_FACTOR); 298 } 299 } 300 }; 301 302 public ConversationItem(Context context) { 303 this(context, null); 304 } 305 306 public ConversationItem(Context context, AttributeSet attrs) { 307 super(context, attrs); 308 this.context = context; 309 } 310 311 @Override 312 public void setOnClickListener(OnClickListener l) { 313 super.setOnClickListener(new ClickListener(l)); 314 } 315 316 @Override 317 protected void onFinishInflate() { 318 super.onFinishInflate(); 319 320 initializeAttributes(); 321 322 this.bodyText = findViewById(R.id.conversation_item_body); 323 this.footer = findViewById(R.id.conversation_item_footer); 324 this.stickerFooter = findViewById(R.id.conversation_item_sticker_footer); 325 this.groupSender = findViewById(R.id.group_message_sender); 326 this.alertView = findViewById(R.id.indicators_parent); 327 this.contactPhoto = findViewById(R.id.contact_photo); 328 this.contactPhotoHolder = findViewById(R.id.contact_photo_container); 329 this.bodyBubble = findViewById(R.id.body_bubble); 330 this.mediaThumbnailStub = new NullableStub<>(findViewById(R.id.image_view_stub)); 331 this.audioViewStub = new Stub<>(findViewById(R.id.audio_view_stub)); 332 this.documentViewStub = new Stub<>(findViewById(R.id.document_view_stub)); 333 this.sharedContactStub = new Stub<>(findViewById(R.id.shared_contact_view_stub)); 334 this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview_stub)); 335 this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub)); 336 this.revealableStub = new Stub<>(findViewById(R.id.revealable_view_stub)); 337 this.joinCallLinkStub = ViewUtil.findStubById(this, R.id.conversation_item_join_button); 338 this.callToActionStub = ViewUtil.findStubById(this, R.id.conversation_item_call_to_action_stub); 339 this.groupSenderHolder = findViewById(R.id.group_sender_holder); 340 this.quoteView = findViewById(R.id.quote_view); 341 this.reply = findViewById(R.id.reply_icon_wrapper); 342 this.replyIcon = findViewById(R.id.reply_icon); 343 this.reactionsView = findViewById(R.id.reactions_view); 344 this.badgeImageView = findViewById(R.id.badge); 345 this.storyReactionLabelWrapper = findViewById(R.id.story_reacted_label_holder); 346 this.storyReactionLabel = findViewById(R.id.story_reacted_label); 347 this.giftViewStub = new Stub<>(findViewById(R.id.gift_view_stub)); 348 this.quotedIndicator = findViewById(R.id.quoted_indicator); 349 this.paymentViewStub = new Stub<>(findViewById(R.id.payment_view_stub)); 350 this.scheduledIndicator = findViewById(R.id.scheduled_indicator); 351 352 setOnClickListener(new ClickListener(null)); 353 354 bodyText.setOnLongClickListener(passthroughClickListener); 355 bodyText.setOnClickListener(passthroughClickListener); 356 footer.setOnTouchDelegateChangedListener(touchDelegateChangedListener); 357 } 358 359 @Override 360 public void bind(@NonNull LifecycleOwner lifecycleOwner, 361 @NonNull ConversationMessage conversationMessage, 362 @NonNull Optional<MessageRecord> previousMessageRecord, 363 @NonNull Optional<MessageRecord> nextMessageRecord, 364 @NonNull RequestManager requestManager, 365 @NonNull Locale locale, 366 @NonNull Set<MultiselectPart> batchSelected, 367 @NonNull Recipient conversationRecipient, 368 @Nullable String searchQuery, 369 boolean pulse, 370 boolean hasWallpaper, 371 boolean isMessageRequestAccepted, 372 boolean allowedToPlayInline, 373 @NonNull Colorizer colorizer, 374 @NonNull ConversationItemDisplayMode displayMode) 375 { 376 unbind(); 377 378 lastYDownRelativeToThis = 0; 379 380 conversationRecipient = conversationRecipient.resolve(); 381 382 this.conversationMessage = conversationMessage; 383 this.messageRecord = conversationMessage.getMessageRecord(); 384 this.nextMessageRecord = nextMessageRecord; 385 this.locale = locale; 386 this.requestManager = requestManager; 387 this.batchSelected = batchSelected; 388 this.conversationRecipient = conversationRecipient.live(); 389 this.groupThread = conversationRecipient.isGroup(); 390 this.author = messageRecord.getFromRecipient().live(); 391 this.canPlayContent = false; 392 this.mediaItem = null; 393 this.colorizer = colorizer; 394 this.displayMode = displayMode; 395 this.previousMessage = previousMessageRecord; 396 397 setGutterSizes(messageRecord, groupThread); 398 setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread); 399 setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted, allowedToPlayInline); 400 setBodyText(messageRecord, searchQuery, isMessageRequestAccepted); 401 setBubbleState(messageRecord, messageRecord.getFromRecipient(), hasWallpaper, colorizer); 402 setInteractionState(conversationMessage, pulse); 403 setStatusIcons(messageRecord, hasWallpaper); 404 setContactPhoto(author.get()); 405 setGroupMessageStatus(messageRecord, author.get()); 406 setGroupAuthorColor(messageRecord, hasWallpaper, colorizer); 407 setAuthor(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper); 408 setQuote(messageRecord, previousMessageRecord, nextMessageRecord, groupThread); 409 setMessageSpacing(context, messageRecord, previousMessageRecord, nextMessageRecord, groupThread); 410 setReactions(messageRecord); 411 setFooter(messageRecord, nextMessageRecord, locale, groupThread, hasWallpaper); 412 setStoryReactionLabel(messageRecord); 413 setHasBeenQuoted(conversationMessage); 414 setHasBeenScheduled(conversationMessage); 415 416 if (audioViewStub.resolved()) { 417 audioViewStub.get().setOnLongClickListener(passthroughClickListener); 418 } 419 420 isBound = true; 421 this.author.observeForever(this); 422 this.conversationRecipient.observeForever(this); 423 } 424 425 @Override 426 public void setParentScrolling(boolean isParentScrolling) { 427 bodyBubble.setParentScrolling(isParentScrolling); 428 } 429 430 @Override 431 public void updateSelectedState() { 432 setHasBeenQuoted(conversationMessage); 433 } 434 435 @Override 436 public void updateTimestamps() { 437 getActiveFooter(messageRecord).setMessageRecord(messageRecord, locale, displayMode); 438 } 439 440 @Override 441 public void updateContactNameColor() { 442 setGroupAuthorColor(messageRecord, hasWallpaper, colorizer); 443 } 444 445 @Override 446 public boolean dispatchTouchEvent(MotionEvent ev) { 447 if (isCondensedMode()) return super.dispatchTouchEvent(ev); 448 449 switch (ev.getAction()) { 450 case MotionEvent.ACTION_DOWN: 451 getHandler().postDelayed(shrinkBubble, SHRINK_BUBBLE_DELAY_MILLIS); 452 break; 453 case MotionEvent.ACTION_UP: 454 case MotionEvent.ACTION_CANCEL: 455 getHandler().removeCallbacks(shrinkBubble); 456 bodyBubble.animate() 457 .scaleX(1.0f) 458 .scaleY(1.0f); 459 reactionsView.animate() 460 .scaleX(1.0f) 461 .scaleY(1.0f); 462 463 if (quotedIndicator != null) { 464 quotedIndicator.animate() 465 .scaleX(1.0f) 466 .scaleY(1.0f); 467 } 468 break; 469 } 470 471 return super.dispatchTouchEvent(ev); 472 } 473 474 @Override 475 public boolean onInterceptTouchEvent(MotionEvent ev) { 476 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 477 lastYDownRelativeToThis = ev.getY(); 478 } 479 480 if (batchSelected.isEmpty()) { 481 return super.onInterceptTouchEvent(ev); 482 } else { 483 return true; 484 } 485 } 486 487 @Override 488 protected void onDetachedFromWindow() { 489 ConversationSwipeAnimationHelper.update(this, 0f, 1f); 490 unbind(); 491 super.onDetachedFromWindow(); 492 } 493 494 @Override 495 public void setEventListener(@Nullable EventListener eventListener) { 496 this.eventListener = eventListener; 497 } 498 499 public boolean disallowSwipe(float downX, float downY) { 500 if (!hasAudio(messageRecord)) return false; 501 502 audioViewStub.get().getSeekBarGlobalVisibleRect(SWIPE_RECT); 503 return SWIPE_RECT.contains((int) downX, (int) downY); 504 } 505 506 public @Nullable ConversationItemBodyBubble getBodyBubble() { 507 return bodyBubble; 508 } 509 510 public @Nullable View getQuotedIndicator() { 511 return quotedIndicator; 512 } 513 514 public @Nullable ReactionsConversationView getReactionsView() { 515 return reactionsView; 516 } 517 518 @Override 519 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 520 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 521 522 if (isInEditMode()) { 523 return; 524 } 525 526 boolean needsMeasure = false; 527 528 if (hasQuote(messageRecord)) { 529 if (quoteView == null) { 530 throw new AssertionError(); 531 } 532 int quoteWidth = quoteView.getMeasuredWidth(); 533 int availableWidth = getAvailableMessageBubbleWidth(quoteView); 534 535 if (quoteWidth != availableWidth) { 536 quoteView.getLayoutParams().width = availableWidth; 537 needsMeasure = true; 538 } 539 } 540 541 int defaultTopMargin = readDimen(R.dimen.message_bubble_default_footer_bottom_margin); 542 int defaultBottomMargin = readDimen(R.dimen.message_bubble_bottom_padding); 543 int collapsedBottomMargin = readDimen(R.dimen.message_bubble_collapsed_bottom_padding); 544 545 if (!updatingFooter && 546 getActiveFooter(messageRecord) == footer && 547 !hasAudio(messageRecord) && 548 !isStoryReaction(messageRecord) && 549 isFooterVisible(messageRecord, nextMessageRecord, groupThread) && 550 !bodyText.isJumbomoji() && 551 conversationMessage.getBottomButton() == null && 552 !StringUtil.hasMixedTextDirection(bodyText.getText()) && 553 !messageRecord.isRemoteDelete() && 554 bodyText.getLastLineWidth() > 0) 555 { 556 View dateView = footer.getDateView(); 557 int footerWidth = footer.getMeasuredWidth(); 558 int availableWidth = getAvailableMessageBubbleWidth(bodyText); 559 int collapsedTopMargin = -1 * (dateView.getMeasuredHeight() + ViewUtil.dpToPx(4)); 560 561 if (bodyText.isSingleLine() && !messageRecord.isFailed()) { 562 int maxBubbleWidth = hasBigImageLinkPreview(messageRecord) || hasThumbnail(messageRecord) ? readDimen(R.dimen.media_bubble_max_width) : getMaxBubbleWidth(); 563 int bodyMargins = ViewUtil.getLeftMargin(bodyText) + ViewUtil.getRightMargin(bodyText); 564 int sizeWithMargins = bodyText.getMeasuredWidth() + ViewUtil.dpToPx(6) + footerWidth + bodyMargins; 565 int minSize = Math.min(maxBubbleWidth, Math.max(bodyText.getMeasuredWidth() + ViewUtil.dpToPx(6) + footerWidth + bodyMargins, bodyBubble.getMeasuredWidth())); 566 567 if (hasQuote(messageRecord) && sizeWithMargins < availableWidth) { 568 ViewUtil.setTopMargin(footer, collapsedTopMargin, false); 569 ViewUtil.setBottomMargin(footer, collapsedBottomMargin, false); 570 needsMeasure = true; 571 updatingFooter = true; 572 } else if (sizeWithMargins != bodyText.getMeasuredWidth() && sizeWithMargins <= minSize) { 573 bodyBubble.getLayoutParams().width = minSize; 574 ViewUtil.setTopMargin(footer, collapsedTopMargin, false); 575 ViewUtil.setBottomMargin(footer, collapsedBottomMargin, false); 576 needsMeasure = true; 577 updatingFooter = true; 578 } 579 } 580 581 if (!updatingFooter && !messageRecord.isFailed() && bodyText.getLastLineWidth() + ViewUtil.dpToPx(6) + footerWidth <= bodyText.getMeasuredWidth()) { 582 ViewUtil.setTopMargin(footer, collapsedTopMargin, false); 583 ViewUtil.setBottomMargin(footer, collapsedBottomMargin, false); 584 updatingFooter = true; 585 needsMeasure = true; 586 } 587 } 588 589 int defaultTopMarginForRecord = getDefaultTopMarginForRecord(messageRecord, defaultTopMargin, defaultBottomMargin); 590 if (!updatingFooter && ViewUtil.getTopMargin(footer) != defaultTopMarginForRecord) { 591 ViewUtil.setTopMargin(footer, defaultTopMarginForRecord, false); 592 ViewUtil.setBottomMargin(footer, defaultBottomMargin, false); 593 needsMeasure = true; 594 } 595 596 if (hasSharedContact(messageRecord)) { 597 int contactWidth = sharedContactStub.get().getMeasuredWidth(); 598 int availableWidth = getAvailableMessageBubbleWidth(sharedContactStub.get()); 599 600 if (contactWidth != availableWidth) { 601 sharedContactStub.get().getLayoutParams().width = availableWidth; 602 needsMeasure = true; 603 } 604 } 605 606 if (hasAudio(messageRecord)) { 607 ConversationItemFooter activeFooter = getActiveFooter(messageRecord); 608 int availableWidth = getAvailableMessageBubbleWidth(footer); 609 610 if (activeFooter.getVisibility() != GONE && activeFooter.getMeasuredWidth() != availableWidth) { 611 activeFooter.getLayoutParams().width = availableWidth; 612 needsMeasure = true; 613 } 614 615 int desiredWidth = audioViewStub.get().getMeasuredWidth() + ViewUtil.getLeftMargin(audioViewStub.get()) + ViewUtil.getRightMargin(audioViewStub.get()); 616 if (bodyBubble.getMeasuredWidth() != desiredWidth) { 617 bodyBubble.getLayoutParams().width = desiredWidth; 618 needsMeasure = true; 619 } 620 } 621 622 if (needsMeasure) { 623 if (measureCalls < MAX_MEASURE_CALLS) { 624 measureCalls++; 625 measure(widthMeasureSpec, heightMeasureSpec); 626 } else { 627 Log.w(TAG, "Hit measure() cap of " + MAX_MEASURE_CALLS); 628 } 629 } else { 630 measureCalls = 0; 631 updatingFooter = false; 632 } 633 } 634 635 private int getDefaultTopMarginForRecord(@NonNull MessageRecord messageRecord, int defaultTopMargin, int defaultBottomMargin) { 636 if (isStoryReaction(messageRecord) && !messageRecord.isRemoteDelete()) { 637 return defaultBottomMargin; 638 } else { 639 return defaultTopMargin; 640 } 641 } 642 643 @Override 644 public void onRecipientChanged(@NonNull Recipient modified) { 645 if (!isBound) { 646 return; 647 } 648 649 if (conversationRecipient.getId().equals(modified.getId())) { 650 setBubbleState(messageRecord, modified, modified.hasWallpaper(), colorizer); 651 652 if (quoteView != null) { 653 quoteView.setWallpaperEnabled(modified.hasWallpaper()); 654 } 655 656 if (audioViewStub.resolved()) { 657 setAudioViewTint(messageRecord); 658 } 659 } 660 661 if (author.getId().equals(modified.getId())) { 662 setContactPhoto(modified); 663 setGroupMessageStatus(messageRecord, modified); 664 } 665 } 666 667 private int getAvailableMessageBubbleWidth(@NonNull View forView) { 668 int availableWidth; 669 if (hasAudio(messageRecord)) { 670 availableWidth = audioViewStub.get().getMeasuredWidth() + ViewUtil.getLeftMargin(audioViewStub.get()) + ViewUtil.getRightMargin(audioViewStub.get()); 671 } else if (!isViewOnceMessage(messageRecord) && (hasThumbnail(messageRecord) || hasBigImageLinkPreview(messageRecord))) { 672 availableWidth = mediaThumbnailStub.require().getMeasuredWidth(); 673 } else { 674 availableWidth = bodyBubble.getMeasuredWidth() - bodyBubble.getPaddingLeft() - bodyBubble.getPaddingRight(); 675 } 676 677 availableWidth = Math.min(availableWidth, getMaxBubbleWidth()); 678 679 availableWidth -= ViewUtil.getLeftMargin(forView) + ViewUtil.getRightMargin(forView); 680 681 return availableWidth; 682 } 683 684 private int getMaxBubbleWidth() { 685 int paddings = getPaddingLeft() + getPaddingRight() + ViewUtil.getLeftMargin(bodyBubble) + ViewUtil.getRightMargin(bodyBubble); 686 if (groupThread && !messageRecord.isOutgoing() && !messageRecord.isRemoteDelete()) { 687 paddings += contactPhoto.getLayoutParams().width + ViewUtil.getLeftMargin(contactPhoto) + ViewUtil.getRightMargin(contactPhoto); 688 } 689 return getMeasuredWidth() - paddings; 690 } 691 692 private void initializeAttributes() { 693 defaultBubbleColor = ContextCompat.getColor(context, R.color.conversation_item_recv_bubble_color_normal); 694 defaultBubbleColorForWallpaper = ContextCompat.getColor(context, R.color.conversation_item_recv_bubble_color_wallpaper); 695 } 696 697 private @ColorInt int getDefaultBubbleColor(boolean hasWallpaper) { 698 return hasWallpaper ? defaultBubbleColorForWallpaper : defaultBubbleColor; 699 } 700 701 @Override 702 public void unbind() { 703 isBound = false; 704 705 if (author != null) { 706 author.removeForeverObserver(this); 707 } 708 709 if (conversationRecipient != null) { 710 conversationRecipient.removeForeverObserver(this); 711 } 712 713 bodyBubble.setVideoPlayerProjection(null); 714 bodyBubble.setQuoteViewProjection(null); 715 716 requestManager = null; 717 } 718 719 @Override 720 public @NonNull MultiselectPart getMultiselectPartForLatestTouch() { 721 MultiselectCollection parts = conversationMessage.getMultiselectCollection(); 722 723 if (parts.isSingle()) { 724 return parts.asSingle().getSinglePart(); 725 } 726 727 MultiselectPart top = parts.asDouble().getTopPart(); 728 MultiselectPart bottom = parts.asDouble().getBottomPart(); 729 730 if (hasThumbnail(messageRecord)) { 731 return isTouchBelowBoundary(mediaThumbnailStub.require()) ? bottom : top; 732 } else if (hasDocument(messageRecord)) { 733 return isTouchBelowBoundary(documentViewStub.get()) ? bottom : top; 734 } else if (hasAudio(messageRecord)) { 735 return isTouchBelowBoundary(audioViewStub.get()) ? bottom : top; 736 } { 737 throw new IllegalStateException("Found a situation where we have something other than a thumbnail or a document."); 738 } 739 } 740 741 private boolean isTouchBelowBoundary(@NonNull View child) { 742 Projection childProjection = Projection.relativeToParent(this, child, null); 743 float childBoundary = childProjection.getY() + childProjection.getHeight(); 744 745 return lastYDownRelativeToThis > childBoundary; 746 } 747 748 @Override 749 public int getTopBoundaryOfMultiselectPart(@NonNull MultiselectPart multiselectPart) { 750 751 boolean isTextPart = multiselectPart instanceof MultiselectPart.Text; 752 boolean isAttachmentPart = multiselectPart instanceof MultiselectPart.Attachments; 753 754 if (hasThumbnail(messageRecord) && isAttachmentPart) { 755 return getProjectionTop(mediaThumbnailStub.require()); 756 } else if (hasThumbnail(messageRecord) && isTextPart) { 757 return getProjectionBottom(mediaThumbnailStub.require()); 758 } else if (hasDocument(messageRecord) && isAttachmentPart) { 759 return getProjectionTop(documentViewStub.get()); 760 } else if (hasDocument(messageRecord) && isTextPart) { 761 return getProjectionBottom(documentViewStub.get()); 762 } else if (hasAudio(messageRecord) && isAttachmentPart) { 763 return getProjectionTop(audioViewStub.get()); 764 } else if (hasAudio(messageRecord) && isTextPart) { 765 return getProjectionBottom(audioViewStub.get()); 766 } else if (hasNoBubble(messageRecord)) { 767 return getTop(); 768 } else { 769 return getProjectionTop(bodyBubble); 770 } 771 } 772 773 private static int getProjectionTop(@NonNull View child) { 774 Projection projection = Projection.relativeToViewRoot(child, null); 775 int y = (int) projection.getY(); 776 projection.release(); 777 return y; 778 } 779 780 private static int getProjectionBottom(@NonNull View child) { 781 Projection projection = Projection.relativeToViewRoot(child, null); 782 int bottom = (int) projection.getY() + projection.getHeight(); 783 projection.release(); 784 return bottom; 785 } 786 787 @Override 788 public int getBottomBoundaryOfMultiselectPart(@NonNull MultiselectPart multiselectPart) { 789 if (multiselectPart instanceof MultiselectPart.Attachments && hasThumbnail(messageRecord)) { 790 return getProjectionBottom(mediaThumbnailStub.require()); 791 } else if (multiselectPart instanceof MultiselectPart.Attachments && hasDocument(messageRecord)) { 792 return getProjectionBottom(documentViewStub.get()); 793 } else if (multiselectPart instanceof MultiselectPart.Attachments && hasAudio(messageRecord)) { 794 return getProjectionBottom(audioViewStub.get()); 795 } else if (hasNoBubble(messageRecord)) { 796 return getBottom(); 797 } else { 798 return getProjectionBottom(bodyBubble); 799 } 800 } 801 802 @Override 803 public boolean hasNonSelectableMedia() { 804 return hasQuote(messageRecord) || hasLinkPreview(messageRecord); 805 } 806 807 @Override 808 public @NonNull ConversationMessage getConversationMessage() { 809 return conversationMessage; 810 } 811 812 public boolean isOutgoing() { 813 return conversationMessage.getMessageRecord().isOutgoing(); 814 } 815 816 /// MessageRecord Attribute Parsers 817 818 private void setBubbleState(MessageRecord messageRecord, @NonNull Recipient recipient, boolean hasWallpaper, @NonNull Colorizer colorizer) { 819 this.hasWallpaper = hasWallpaper; 820 821 ViewUtil.updateLayoutParams(bodyBubble, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 822 bodyText.setTextColor(colorizer.getIncomingBodyTextColor(context, hasWallpaper)); 823 bodyText.setLinkTextColor(colorizer.getIncomingBodyTextColor(context, hasWallpaper)); 824 825 if (messageRecord.isOutgoing() && !messageRecord.isRemoteDelete()) { 826 bodyBubble.getBackground().setColorFilter(recipient.getChatColors().getChatBubbleColorFilter()); 827 bodyText.setTextColor(colorizer.getOutgoingBodyTextColor(context)); 828 bodyText.setLinkTextColor(colorizer.getOutgoingBodyTextColor(context)); 829 footer.setTextColor(colorizer.getOutgoingFooterTextColor(context)); 830 footer.setIconColor(colorizer.getOutgoingFooterIconColor(context)); 831 footer.setRevealDotColor(colorizer.getOutgoingFooterIconColor(context)); 832 footer.setOnlyShowSendingStatus(false, messageRecord); 833 } else if (messageRecord.isRemoteDelete()) { 834 if (hasWallpaper) { 835 bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.wallpaper_bubble_color), PorterDuff.Mode.SRC_IN); 836 } else { 837 bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.signal_background_primary), PorterDuff.Mode.MULTIPLY); 838 footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary)); 839 footer.setRevealDotColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary)); 840 } 841 footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary)); 842 footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord); 843 } else { 844 bodyBubble.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.SRC_IN); 845 footer.setTextColor(colorizer.getIncomingFooterTextColor(context, hasWallpaper)); 846 footer.setIconColor(colorizer.getIncomingFooterIconColor(context, hasWallpaper)); 847 footer.setRevealDotColor(colorizer.getIncomingFooterIconColor(context, hasWallpaper)); 848 footer.setOnlyShowSendingStatus(false, messageRecord); 849 } 850 851 outliner.setColor(ContextCompat.getColor(context, R.color.signal_text_secondary)); 852 853 pulseOutliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent)); 854 pulseOutliner.setStrokeWidth(ViewUtil.dpToPx(4)); 855 856 outliners.clear(); 857 if (shouldDrawBodyBubbleOutline(messageRecord, hasWallpaper)) { 858 outliners.add(outliner); 859 } 860 outliners.add(pulseOutliner); 861 862 bodyBubble.setOutliners(outliners); 863 864 if (audioViewStub.resolved()) { 865 setAudioViewTint(messageRecord); 866 } 867 868 if (hasWallpaper) { 869 replyIcon.setBackgroundResource(R.drawable.wallpaper_message_decoration_background); 870 } else { 871 replyIcon.setBackground(null); 872 } 873 } 874 875 private void setAudioViewTint(MessageRecord messageRecord) { 876 if (hasAudio(messageRecord)) { 877 if (!messageRecord.isOutgoing()) { 878 if (hasWallpaper) { 879 audioViewStub.get().setTint(getContext().getResources().getColor(R.color.conversation_item_incoming_audio_foreground_tint_wallpaper)); 880 audioViewStub.get().setProgressAndPlayBackgroundTint(getContext().getResources().getColor(R.color.conversation_item_incoming_audio_play_pause_background_tint_wallpaper)); 881 } else { 882 audioViewStub.get().setTint(getContext().getResources().getColor(R.color.conversation_item_incoming_audio_foreground_tint_normal)); 883 audioViewStub.get().setProgressAndPlayBackgroundTint(getContext().getResources().getColor(R.color.conversation_item_incoming_audio_play_pause_background_tint_normal)); 884 } 885 } else { 886 audioViewStub.get().setTint(getContext().getResources().getColor(R.color.conversation_item_outgoing_audio_foreground_tint)); 887 audioViewStub.get().setProgressAndPlayBackgroundTint(getContext().getResources().getColor(R.color.signal_colorTransparent2)); 888 } 889 } 890 } 891 892 private void setInteractionState(ConversationMessage conversationMessage, boolean pulseMention) { 893 Set<MultiselectPart> multiselectParts = conversationMessage.getMultiselectCollection().toSet(); 894 boolean isMessageSelected = Util.hasItems(Sets.intersection(multiselectParts, batchSelected)); 895 896 if (isMessageSelected) { 897 setSelected(true); 898 } else if (pulseMention) { 899 setSelected(false); 900 } else { 901 setSelected(false); 902 } 903 904 if (mediaThumbnailStub.resolved()) { 905 mediaThumbnailStub.require().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty()); 906 mediaThumbnailStub.require().setClickable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty()); 907 mediaThumbnailStub.require().setLongClickable(batchSelected.isEmpty()); 908 } 909 910 if (audioViewStub.resolved()) { 911 audioViewStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty()); 912 audioViewStub.get().setClickable(batchSelected.isEmpty()); 913 audioViewStub.get().setEnabled(batchSelected.isEmpty()); 914 } 915 916 if (documentViewStub.resolved()) { 917 documentViewStub.get().setFocusable(!shouldInterceptClicks(conversationMessage.getMessageRecord()) && batchSelected.isEmpty()); 918 documentViewStub.get().setClickable(batchSelected.isEmpty()); 919 } 920 } 921 922 private boolean shouldDrawBodyBubbleOutline(MessageRecord messageRecord, boolean hasWallpaper) { 923 if (hasWallpaper) { 924 return false; 925 } else { 926 return messageRecord.isRemoteDelete(); 927 } 928 } 929 930 /** 931 * Whether or not we're rendering this item in a constrained space. 932 * Today this is only {@link org.tm.archive.conversation.quotes.MessageQuotesBottomSheet}. 933 */ 934 private boolean isCondensedMode() { 935 return displayMode instanceof ConversationItemDisplayMode.Condensed; 936 } 937 938 /** 939 * Whether or not we want to condense the actual content of the bubble. e.g. shorten image height, text content, etc. 940 * Today, we only want to do this for the first message when we're in condensed mode. 941 */ 942 private boolean isContentCondensed() { 943 return isCondensedMode() && !previousMessage.isPresent(); 944 } 945 946 private boolean isStoryReaction(MessageRecord messageRecord) { 947 return MessageRecordUtil.isStoryReaction(messageRecord); 948 } 949 950 private boolean isCaptionlessMms(MessageRecord messageRecord) { 951 return MessageRecordUtil.isCaptionlessMms(messageRecord, context); 952 } 953 954 private boolean hasAudio(MessageRecord messageRecord) { 955 return MessageRecordUtil.hasAudio(messageRecord); 956 } 957 958 private boolean hasThumbnail(MessageRecord messageRecord) { 959 return MessageRecordUtil.hasThumbnail(messageRecord); 960 } 961 962 private boolean hasSticker(MessageRecord messageRecord) { 963 return MessageRecordUtil.hasSticker(messageRecord); 964 } 965 966 private boolean isBorderless(MessageRecord messageRecord) { 967 return MessageRecordUtil.isBorderless(messageRecord, context); 968 } 969 970 private boolean hasNoBubble(MessageRecord messageRecord) { 971 return MessageRecordUtil.hasNoBubble(messageRecord, context); 972 } 973 974 private boolean hasOnlyThumbnail(MessageRecord messageRecord) { 975 return MessageRecordUtil.hasOnlyThumbnail(messageRecord, context); 976 } 977 978 private boolean hasDocument(MessageRecord messageRecord) { 979 return MessageRecordUtil.hasDocument(messageRecord); 980 } 981 982 private boolean hasExtraText(MessageRecord messageRecord) { 983 return MessageRecordUtil.hasExtraText(messageRecord) || (!messageRecord.isDisplayBodyEmpty(context) && isContentCondensed()); 984 } 985 986 private boolean hasQuote(MessageRecord messageRecord) { 987 return MessageRecordUtil.hasQuote(messageRecord); 988 } 989 990 private boolean hasSharedContact(MessageRecord messageRecord) { 991 return MessageRecordUtil.hasSharedContact(messageRecord); 992 } 993 994 private boolean hasLinkPreview(MessageRecord messageRecord) { 995 return MessageRecordUtil.hasLinkPreview(messageRecord); 996 } 997 998 private boolean hasBigImageLinkPreview(MessageRecord messageRecord) { 999 return MessageRecordUtil.hasBigImageLinkPreview(messageRecord, context) && !isContentCondensed(); 1000 } 1001 1002 private boolean isViewOnceMessage(MessageRecord messageRecord) { 1003 return MessageRecordUtil.isViewOnceMessage(messageRecord); 1004 } 1005 1006 private boolean isGiftMessage(MessageRecord messageRecord) { 1007 return MessageRecordUtil.hasGiftBadge(messageRecord); 1008 } 1009 1010 private void setBodyText(@NonNull MessageRecord messageRecord, 1011 @Nullable String searchQuery, 1012 boolean messageRequestAccepted) 1013 { 1014 bodyText.setClickable(false); 1015 bodyText.setFocusable(false); 1016 bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().getMessageFontSize()); 1017 bodyText.setMovementMethod(LongClickMovementMethod.getInstance(getContext())); 1018 1019 if (messageRecord.isRemoteDelete()) { 1020 String deletedMessage = context.getString(messageRecord.isOutgoing() ? R.string.ConversationItem_you_deleted_this_message : R.string.ConversationItem_this_message_was_deleted); 1021 SpannableString italics = new SpannableString(deletedMessage); 1022 italics.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1023 italics.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, R.color.signal_text_primary)), 1024 0, 1025 deletedMessage.length(), 1026 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1027 1028 bodyText.setText(italics); 1029 bodyText.setVisibility(View.VISIBLE); 1030 bodyText.setOverflowText(null); 1031 } else if (isCaptionlessMms(messageRecord) || isStoryReaction(messageRecord) || isGiftMessage(messageRecord) || messageRecord.isPaymentNotification()) { 1032 bodyText.setText(null); 1033 bodyText.setOverflowText(null); 1034 bodyText.setVisibility(View.GONE); 1035 } else { 1036 Spannable styledText = conversationMessage.getDisplayBody(getContext()); 1037 if (messageRequestAccepted) { 1038 linkifyMessageBody(styledText, batchSelected.isEmpty()); 1039 } 1040 styledText = SearchUtil.getHighlightedSpan(locale, STYLE_FACTORY, styledText, searchQuery, SearchUtil.STRICT); 1041 1042 if (hasExtraText(messageRecord)) { 1043 bodyText.setOverflowText(getLongMessageSpan(messageRecord)); 1044 } else { 1045 bodyText.setOverflowText(null); 1046 } 1047 1048 if (messageRecord.isOutgoing()) { 1049 bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25)); 1050 } else { 1051 bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, ThemeUtil.isDarkTheme(context) ? R.color.core_grey_60 : R.color.core_grey_20)); 1052 } 1053 1054 if (isContentCondensed()) { 1055 bodyText.setMaxLines(CONDENSED_MODE_MAX_LINES); 1056 } else { 1057 bodyText.setMaxLines(Integer.MAX_VALUE); 1058 } 1059 1060 bodyText.setText(StringUtil.trim(styledText)); 1061 bodyText.setVisibility(View.VISIBLE); 1062 1063 if (conversationMessage.getBottomButton() != null) { 1064 callToActionStub.get().setVisibility(View.VISIBLE); 1065 callToActionStub.get().setText(conversationMessage.getBottomButton().label); 1066 callToActionStub.get().setOnClickListener(v -> { 1067 if (eventListener != null) { 1068 eventListener.onCallToAction(conversationMessage.getBottomButton().action); 1069 } 1070 }); 1071 } else if (callToActionStub.resolved()) { 1072 callToActionStub.get().setVisibility(View.GONE); 1073 } 1074 } 1075 } 1076 1077 private void setMediaAttributes(@NonNull MessageRecord messageRecord, 1078 @NonNull Optional<MessageRecord> previousRecord, 1079 @NonNull Optional<MessageRecord> nextRecord, 1080 boolean isGroupThread, 1081 boolean hasWallpaper, 1082 boolean messageRequestAccepted, 1083 boolean allowedToPlayInline) 1084 { 1085 boolean showControls = !MessageRecordUtil.isScheduled(messageRecord) && (messageRecord.isMediaPending() || !messageRecord.isFailed()); 1086 1087 ViewUtil.setTopMargin(bodyText, readDimen(R.dimen.message_bubble_top_padding)); 1088 1089 bodyBubble.setQuoteViewProjection(null); 1090 bodyBubble.setVideoPlayerProjection(null); 1091 1092 if (eventListener != null && audioViewStub.resolved()) { 1093 Log.d(TAG, "setMediaAttributes: unregistering voice note callbacks for audio slide " + audioViewStub.get().getAudioSlideUri()); 1094 eventListener.onUnregisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver()); 1095 } 1096 1097 footer.setPlaybackSpeedListener(null); 1098 1099 if (isViewOnceMessage(messageRecord) && !messageRecord.isRemoteDelete()) { 1100 revealableStub.get().setVisibility(VISIBLE); 1101 if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(View.GONE); 1102 if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); 1103 if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); 1104 if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE); 1105 if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); 1106 if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); 1107 if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE); 1108 if (callToActionStub.resolved()) callToActionStub.get().setVisibility(View.GONE); 1109 if (joinCallLinkStub.resolved()) joinCallLinkStub.get().setVisibility(View.GONE); 1110 paymentViewStub.setVisibility(View.GONE); 1111 1112 revealableStub.get().setMessage((MmsMessageRecord) messageRecord, hasWallpaper); 1113 revealableStub.get().setOnClickListener(revealableClickListener); 1114 revealableStub.get().setOnLongClickListener(passthroughClickListener); 1115 1116 updateRevealableMargins(messageRecord, previousRecord, nextRecord, isGroupThread); 1117 1118 footer.setVisibility(VISIBLE); 1119 } else if (hasSharedContact(messageRecord)) { 1120 sharedContactStub.get().setVisibility(VISIBLE); 1121 if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); 1122 if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(View.GONE); 1123 if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); 1124 if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); 1125 if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); 1126 if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE); 1127 if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE); 1128 if (joinCallLinkStub.resolved()) joinCallLinkStub.get().setVisibility(View.GONE); 1129 paymentViewStub.setVisibility(View.GONE); 1130 1131 sharedContactStub.get().setContact(((MmsMessageRecord) messageRecord).getSharedContacts().get(0), requestManager, locale); 1132 sharedContactStub.get().setEventListener(sharedContactEventListener); 1133 sharedContactStub.get().setOnClickListener(sharedContactClickListener); 1134 sharedContactStub.get().setOnLongClickListener(passthroughClickListener); 1135 1136 setSharedContactCorners(messageRecord, previousRecord, nextRecord, isGroupThread); 1137 1138 ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1139 ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1140 footer.setVisibility(GONE); 1141 } else if (hasLinkPreview(messageRecord) && messageRequestAccepted) { 1142 linkPreviewStub.get().setVisibility(View.VISIBLE); 1143 if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); 1144 if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(View.GONE); 1145 if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); 1146 if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE); 1147 if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); 1148 if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE); 1149 if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE); 1150 if (joinCallLinkStub.resolved()) joinCallLinkStub.get().setVisibility(View.GONE); 1151 paymentViewStub.setVisibility(View.GONE); 1152 1153 //noinspection ConstantConditions 1154 LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0); 1155 1156 if (FeatureFlags.adHocCalling()) { 1157 CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl()); 1158 if (callLinkRootKey != null) { 1159 joinCallLinkStub.setVisibility(View.VISIBLE); 1160 joinCallLinkStub.get().setTextColor(messageRecord.isOutgoing() ? R.color.signal_colorOnCustom : R.color.signal_colorPrimary); 1161 joinCallLinkStub.get().setStrokeColor(messageRecord.isOutgoing() ? R.color.signal_colorOnCustom : R.color.signal_colorOutline); 1162 joinCallLinkStub.get().setJoinClickListener(v -> { 1163 if (eventListener != null) { 1164 eventListener.onJoinCallLink(callLinkRootKey); 1165 } 1166 }); 1167 } 1168 } 1169 1170 if (hasBigImageLinkPreview(messageRecord)) { 1171 mediaThumbnailStub.require().setVisibility(VISIBLE); 1172 mediaThumbnailStub.require().setMinimumThumbnailWidth(readDimen(R.dimen.media_bubble_min_width_with_content)); 1173 mediaThumbnailStub.require().setMaximumThumbnailHeight(readDimen(R.dimen.media_bubble_max_height)); 1174 mediaThumbnailStub.require().setImageResource(requestManager, Collections.singletonList(new ImageSlide(linkPreview.getThumbnail().get())), showControls, false); 1175 mediaThumbnailStub.require().setThumbnailClickListener(new LinkPreviewThumbnailClickListener()); 1176 mediaThumbnailStub.require().setStartTransferClickListener(downloadClickListener); 1177 mediaThumbnailStub.require().setCancelTransferClickListener(attachmentCancelClickListener); 1178 mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener); 1179 mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener); 1180 1181 linkPreviewStub.get().setLinkPreview(requestManager, linkPreview, false); 1182 1183 setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread); 1184 setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, true); 1185 1186 ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1187 ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1188 ViewUtil.setTopMargin(linkPreviewStub.get(), 0); 1189 } else { 1190 linkPreviewStub.get().setLinkPreview(requestManager, linkPreview, true, !isContentCondensed(), displayMode.getScheduleMessageMode()); 1191 linkPreviewStub.get().setDownloadClickedListener(downloadClickListener); 1192 setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false); 1193 ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1194 ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1195 1196 //noinspection ConstantConditions 1197 int topMargin = isGroupThread && isStartOfMessageCluster(messageRecord, previousRecord, isGroupThread) && !messageRecord.isOutgoing() ? readDimen(R.dimen.message_bubble_top_padding) : 0; 1198 ViewUtil.setTopMargin(linkPreviewStub.get(), topMargin); 1199 } 1200 1201 linkPreviewStub.get().setOnClickListener(linkPreviewClickListener); 1202 linkPreviewStub.get().setOnLongClickListener(passthroughClickListener); 1203 linkPreviewStub.get().setBackgroundColor(getDefaultBubbleColor(hasWallpaper)); 1204 1205 footer.setVisibility(VISIBLE); 1206 } else if (hasAudio(messageRecord)) { 1207 audioViewStub.get().setVisibility(View.VISIBLE); 1208 if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(View.GONE); 1209 if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); 1210 if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE); 1211 if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); 1212 if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); 1213 if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE); 1214 if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE); 1215 if (joinCallLinkStub.resolved()) joinCallLinkStub.get().setVisibility(View.GONE); 1216 paymentViewStub.setVisibility(View.GONE); 1217 1218 audioViewStub.get().setAudio(Objects.requireNonNull(((MmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide()), new AudioViewCallbacks(), showControls, true); 1219 audioViewStub.get().setDownloadClickListener(singleDownloadClickListener); 1220 audioViewStub.get().setOnLongClickListener(passthroughClickListener); 1221 1222 if (eventListener != null) { 1223 Log.d(TAG, "setMediaAttributes: registered listener for audio slide " + audioViewStub.get().getAudioSlideUri()); 1224 eventListener.onRegisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver()); 1225 } else { 1226 Log.w(TAG, "setMediaAttributes: could not register listener for audio slide " + audioViewStub.get().getAudioSlideUri()); 1227 } 1228 1229 ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1230 ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1231 1232 footer.setPlaybackSpeedListener(new AudioPlaybackSpeedToggleListener()); 1233 footer.setVisibility(VISIBLE); 1234 } else if (hasDocument(messageRecord)) { 1235 documentViewStub.get().setVisibility(View.VISIBLE); 1236 if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(View.GONE); 1237 if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); 1238 if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE); 1239 if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); 1240 if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); 1241 if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE); 1242 if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE); 1243 if (joinCallLinkStub.resolved()) joinCallLinkStub.get().setVisibility(View.GONE); 1244 paymentViewStub.setVisibility(View.GONE); 1245 1246 //noinspection ConstantConditions 1247 documentViewStub.get().setDocument( 1248 ((MmsMessageRecord) messageRecord).getSlideDeck().getDocumentSlide(), 1249 showControls, 1250 displayMode != ConversationItemDisplayMode.Detailed.INSTANCE 1251 ); 1252 documentViewStub.get().setDocumentClickListener(new ThumbnailClickListener()); 1253 documentViewStub.get().setDownloadClickListener(singleDownloadClickListener); 1254 documentViewStub.get().setOnLongClickListener(passthroughClickListener); 1255 1256 ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1257 ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1258 ViewUtil.setTopMargin(bodyText, 0); 1259 1260 footer.setVisibility(VISIBLE); 1261 } else if ((hasSticker(messageRecord) && isCaptionlessMms(messageRecord)) || isBorderless(messageRecord)) { 1262 bodyBubble.setBackgroundColor(Color.TRANSPARENT); 1263 1264 stickerStub.get().setVisibility(View.VISIBLE); 1265 if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(View.GONE); 1266 if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); 1267 if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); 1268 if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE); 1269 if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); 1270 if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE); 1271 if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE); 1272 if (joinCallLinkStub.resolved()) joinCallLinkStub.get().setVisibility(View.GONE); 1273 paymentViewStub.setVisibility(View.GONE); 1274 1275 if (hasSticker(messageRecord)) { 1276 //noinspection ConstantConditions 1277 stickerStub.get().setSlide(requestManager, ((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide()); 1278 stickerStub.get().setThumbnailClickListener(new StickerClickListener()); 1279 } else { 1280 //noinspection ConstantConditions 1281 stickerStub.get().setSlide(requestManager, ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlide()); 1282 stickerStub.get().setThumbnailClickListener((v, slide) -> performClick()); 1283 } 1284 1285 stickerStub.get().setDownloadClickListener(downloadClickListener); 1286 stickerStub.get().setOnLongClickListener(passthroughClickListener); 1287 stickerStub.get().setOnClickListener(passthroughClickListener); 1288 1289 ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1290 ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1291 1292 footer.setVisibility(VISIBLE); 1293 } else if (hasNoBubble(messageRecord)) { 1294 bodyBubble.setBackgroundColor(Color.TRANSPARENT); 1295 } else if (hasThumbnail(messageRecord)) { 1296 mediaThumbnailStub.require().setVisibility(View.VISIBLE); 1297 if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); 1298 if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); 1299 if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE); 1300 if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); 1301 if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); 1302 if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE); 1303 if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE); 1304 if (joinCallLinkStub.resolved()) joinCallLinkStub.get().setVisibility(View.GONE); 1305 paymentViewStub.setVisibility(View.GONE); 1306 1307 final SlideDeck slideDeck = ((MmsMessageRecord) messageRecord).getSlideDeck(); 1308 List<Slide> thumbnailSlides = slideDeck.getThumbnailSlides(); 1309 mediaThumbnailStub.require().setMinimumThumbnailWidth(readDimen(isCaptionlessMms(messageRecord) ? R.dimen.media_bubble_min_width_solo 1310 : R.dimen.media_bubble_min_width_with_content)); 1311 mediaThumbnailStub.require().setMaximumThumbnailHeight(readDimen(isContentCondensed() ? R.dimen.media_bubble_max_height_condensed 1312 : R.dimen.media_bubble_max_height)); 1313 1314 mediaThumbnailStub.require().setThumbnailClickListener(new ThumbnailClickListener()); 1315 mediaThumbnailStub.require().setCancelTransferClickListener(attachmentCancelClickListener); 1316 mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener); 1317 mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener); 1318 mediaThumbnailStub.require().setOnClickListener(passthroughClickListener); 1319 mediaThumbnailStub.require().showShade(messageRecord.isDisplayBodyEmpty(getContext()) && !hasExtraText(messageRecord)); 1320 mediaThumbnailStub.require().setImageResource(requestManager, 1321 thumbnailSlides, 1322 showControls, 1323 false); 1324 if (!messageRecord.isOutgoing()) { 1325 mediaThumbnailStub.require().setConversationColor(getDefaultBubbleColor(hasWallpaper)); 1326 mediaThumbnailStub.require().setStartTransferClickListener(downloadClickListener); 1327 } else { 1328 mediaThumbnailStub.require().setConversationColor(Color.TRANSPARENT); 1329 if (doAnySlidesLackData(slideDeck)) { 1330 mediaThumbnailStub.require().setStartTransferClickListener(downloadClickListener); 1331 } else { 1332 mediaThumbnailStub.require().setStartTransferClickListener(new ResendClickListener(messageRecord)); 1333 } 1334 } 1335 1336 mediaThumbnailStub.require().setBorderless(false); 1337 1338 setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread); 1339 1340 ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1341 ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1342 1343 footer.setVisibility(VISIBLE); 1344 1345 if (thumbnailSlides.size() == 1 && 1346 thumbnailSlides.get(0).isVideoGif() && 1347 thumbnailSlides.get(0) instanceof VideoSlide) 1348 { 1349 Uri uri = thumbnailSlides.get(0).getUri(); 1350 if (uri != null) { 1351 mediaItem = MediaItem.fromUri(uri); 1352 } else { 1353 mediaItem = null; 1354 } 1355 1356 canPlayContent = (GiphyMp4PlaybackPolicy.autoplay() || allowedToPlayInline) && mediaItem != null; 1357 } 1358 1359 } else if (isGiftMessage(messageRecord)) { 1360 if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(GONE); 1361 if (audioViewStub.resolved()) audioViewStub.get().setVisibility(GONE); 1362 if (documentViewStub.resolved()) documentViewStub.get().setVisibility(GONE); 1363 if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE); 1364 if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); 1365 if (stickerStub.resolved()) stickerStub.get().setVisibility(GONE); 1366 if (revealableStub.resolved()) revealableStub.get().setVisibility(GONE); 1367 if (joinCallLinkStub.resolved()) joinCallLinkStub.get().setVisibility(View.GONE); 1368 paymentViewStub.setVisibility(View.GONE); 1369 1370 MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord; 1371 giftViewStub.get().setGiftBadge(requestManager, Objects.requireNonNull(mmsMessageRecord.getGiftBadge()), messageRecord.isOutgoing(), giftMessageViewCallback, messageRecord.getFromRecipient(), messageRecord.getToRecipient()); 1372 giftViewStub.get().setVisibility(VISIBLE); 1373 1374 footer.setVisibility(VISIBLE); 1375 } else if (messageRecord.isPaymentNotification()) { 1376 if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(GONE); 1377 if (audioViewStub.resolved()) audioViewStub.get().setVisibility(GONE); 1378 if (documentViewStub.resolved()) documentViewStub.get().setVisibility(GONE); 1379 if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE); 1380 if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); 1381 if (stickerStub.resolved()) stickerStub.get().setVisibility(GONE); 1382 if (revealableStub.resolved()) revealableStub.get().setVisibility(GONE); 1383 if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE); 1384 if (joinCallLinkStub.resolved()) joinCallLinkStub.get().setVisibility(View.GONE); 1385 1386 MmsMessageRecord mediaMmsMessageRecord = (MmsMessageRecord) messageRecord; 1387 1388 paymentViewStub.setVisibility(View.VISIBLE); 1389 paymentViewStub.get().bindPayment(conversationRecipient.get(), Objects.requireNonNull(mediaMmsMessageRecord.getPayment()), colorizer); 1390 1391 footer.setVisibility(VISIBLE); 1392 } else { 1393 if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(View.GONE); 1394 if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE); 1395 if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); 1396 if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE); 1397 if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE); 1398 if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE); 1399 if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE); 1400 if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE); 1401 if (joinCallLinkStub.resolved()) joinCallLinkStub.get().setVisibility(View.GONE); 1402 paymentViewStub.setVisibility(View.GONE); 1403 1404 ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1405 ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 1406 1407 footer.setVisibility(VISIBLE); 1408 1409 //noinspection ConstantConditions 1410 int topMargin = !messageRecord.isOutgoing() && isGroupThread && isStartOfMessageCluster(messageRecord, previousRecord, isGroupThread) 1411 ? readDimen(R.dimen.message_bubble_text_only_top_margin) 1412 : readDimen(R.dimen.message_bubble_top_padding); 1413 ViewUtil.setTopMargin(bodyText, topMargin); 1414 } 1415 } 1416 1417 private void updateRevealableMargins(MessageRecord messageRecord, Optional<MessageRecord> previous, Optional<MessageRecord> next, boolean isGroupThread) { 1418 int bigMargin = readDimen(R.dimen.message_bubble_revealable_padding); 1419 int smallMargin = readDimen(R.dimen.message_bubble_top_padding); 1420 1421 //noinspection ConstantConditions 1422 if (messageRecord.isOutgoing() || !isStartOfMessageCluster(messageRecord, previous, isGroupThread)) { 1423 ViewUtil.setTopMargin(revealableStub.get(), bigMargin); 1424 } else { 1425 ViewUtil.setTopMargin(revealableStub.get(), smallMargin); 1426 } 1427 1428 if (isFooterVisible(messageRecord, next, isGroupThread)) { 1429 ViewUtil.setBottomMargin(revealableStub.get(), smallMargin); 1430 } else { 1431 ViewUtil.setBottomMargin(revealableStub.get(), bigMargin); 1432 } 1433 } 1434 1435 private void setThumbnailCorners(@NonNull MessageRecord current, 1436 @NonNull Optional<MessageRecord> previous, 1437 @NonNull Optional<MessageRecord> next, 1438 boolean isGroupThread) 1439 { 1440 int defaultRadius = readDimen(R.dimen.message_corner_radius); 1441 int collapseRadius = readDimen(R.dimen.message_corner_collapse_radius); 1442 1443 int topStart = defaultRadius; 1444 int topEnd = defaultRadius; 1445 int bottomStart = defaultRadius; 1446 int bottomEnd = defaultRadius; 1447 1448 if (isSingularMessage(current, previous, next, isGroupThread)) { 1449 topStart = defaultRadius; 1450 topEnd = defaultRadius; 1451 bottomStart = defaultRadius; 1452 bottomEnd = defaultRadius; 1453 } else if (isStartOfMessageCluster(current, previous, isGroupThread)) { 1454 if (current.isOutgoing()) { 1455 bottomEnd = collapseRadius; 1456 } else { 1457 bottomStart = collapseRadius; 1458 } 1459 } else if (isEndOfMessageCluster(current, next, isGroupThread)) { 1460 if (current.isOutgoing()) { 1461 topEnd = collapseRadius; 1462 } else { 1463 topStart = collapseRadius; 1464 } 1465 } else { 1466 if (current.isOutgoing()) { 1467 topEnd = collapseRadius; 1468 bottomEnd = collapseRadius; 1469 } else { 1470 topStart = collapseRadius; 1471 bottomStart = collapseRadius; 1472 } 1473 } 1474 1475 if (!current.isDisplayBodyEmpty(getContext())) { 1476 bottomStart = 0; 1477 bottomEnd = 0; 1478 } 1479 1480 if (isStartOfMessageCluster(current, previous, isGroupThread) && !current.isOutgoing() && isGroupThread) { 1481 topStart = 0; 1482 topEnd = 0; 1483 } 1484 1485 if (hasQuote(messageRecord)) { 1486 topStart = 0; 1487 topEnd = 0; 1488 } 1489 1490 if (hasLinkPreview(messageRecord) || hasExtraText(messageRecord)) { 1491 bottomStart = 0; 1492 bottomEnd = 0; 1493 } 1494 1495 if (ViewUtil.isRtl(this)) { 1496 mediaThumbnailStub.require().setCorners(topEnd, topStart, bottomStart, bottomEnd); 1497 } else { 1498 mediaThumbnailStub.require().setCorners(topStart, topEnd, bottomEnd, bottomStart); 1499 } 1500 } 1501 1502 private void setSharedContactCorners(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) { 1503 if (messageRecord.isDisplayBodyEmpty(getContext())) { 1504 if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) { 1505 sharedContactStub.get().setSingularStyle(); 1506 } else if (current.isOutgoing()) { 1507 sharedContactStub.get().setClusteredOutgoingStyle(); 1508 } else { 1509 sharedContactStub.get().setClusteredIncomingStyle(); 1510 } 1511 } 1512 } 1513 1514 private void setLinkPreviewCorners(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread, boolean bigImage) { 1515 int defaultRadius = readDimen(R.dimen.message_corner_radius); 1516 int collapseRadius = readDimen(R.dimen.message_corner_collapse_radius); 1517 1518 if (bigImage || hasQuote(current)) { 1519 linkPreviewStub.get().setCorners(0, 0); 1520 } else if (isStartOfMessageCluster(current, previous, isGroupThread) && !current.isOutgoing() && isGroupThread) { 1521 linkPreviewStub.get().setCorners(0, 0); 1522 } else if (isSingularMessage(current, previous, next, isGroupThread) || isStartOfMessageCluster(current, previous, isGroupThread)) { 1523 linkPreviewStub.get().setCorners(defaultRadius, defaultRadius); 1524 } else if (current.isOutgoing()) { 1525 linkPreviewStub.get().setCorners(defaultRadius, collapseRadius); 1526 } else { 1527 linkPreviewStub.get().setCorners(collapseRadius, defaultRadius); 1528 } 1529 } 1530 1531 private void setContactPhoto(@NonNull Recipient recipient) { 1532 if (contactPhoto == null) return; 1533 1534 final RecipientId recipientId = recipient.getId(); 1535 1536 contactPhoto.setOnClickListener(v -> { 1537 if (eventListener != null) { 1538 eventListener.onGroupMemberClicked(recipientId, conversationRecipient.get().requireGroupId()); 1539 } 1540 }); 1541 1542 contactPhoto.setAvatar(requestManager, recipient, false); 1543 badgeImageView.setBadgeFromRecipient(recipient, requestManager); 1544 badgeImageView.setClickable(false); 1545 } 1546 1547 private void linkifyMessageBody(@NonNull Spannable messageBody, 1548 boolean shouldLinkifyAllLinks) 1549 { 1550 V2ConversationItemUtils.linkifyUrlLinks(messageBody, shouldLinkifyAllLinks, urlClickListener); 1551 1552 if (conversationMessage.hasStyleLinks()) { 1553 for (PlaceholderURLSpan placeholder : messageBody.getSpans(0, messageBody.length(), PlaceholderURLSpan.class)) { 1554 int start = messageBody.getSpanStart(placeholder); 1555 int end = messageBody.getSpanEnd(placeholder); 1556 URLSpan span = new InterceptableLongClickCopyLinkSpan(placeholder.getValue(), 1557 urlClickListener, 1558 ContextCompat.getColor(getContext(), R.color.signal_accent_primary), 1559 false); 1560 1561 messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1562 } 1563 } 1564 1565 List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(messageBody); 1566 for (Annotation annotation : mentionAnnotations) { 1567 messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1568 } 1569 } 1570 1571 private void setStatusIcons(MessageRecord messageRecord, boolean hasWallpaper) { 1572 bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.symbol_key_24 : 0, 0); 1573 1574 if (!messageRecord.isMediaPending() && messageRecord.isFailed()) { 1575 alertView.setFailed(); 1576 } else if (messageRecord.isPendingInsecureSmsFallback()) { 1577 alertView.setPendingApproval(); 1578 } else if (messageRecord.isRateLimited()) { 1579 alertView.setRateLimited(); 1580 } else { 1581 alertView.setNone(); 1582 } 1583 1584 if (hasWallpaper) { 1585 alertView.setBackgroundResource(R.drawable.wallpaper_message_decoration_background); 1586 } else { 1587 alertView.setBackground(null); 1588 } 1589 } 1590 1591 private void setQuote(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) { 1592 boolean startOfCluster = isStartOfMessageCluster(current, previous, isGroupThread); 1593 if (hasQuote(messageRecord)) { 1594 if (quoteView == null) { 1595 throw new AssertionError(); 1596 } 1597 Quote quote = ((MmsMessageRecord) current).getQuote(); 1598 1599 if (((MmsMessageRecord) current).getParentStoryId() != null) { 1600 quoteView.setMessageType(current.isOutgoing() ? QuoteView.MessageType.STORY_REPLY_OUTGOING : QuoteView.MessageType.STORY_REPLY_INCOMING); 1601 } else { 1602 quoteView.setMessageType(current.isOutgoing() ? QuoteView.MessageType.OUTGOING : QuoteView.MessageType.INCOMING); 1603 } 1604 1605 //noinspection ConstantConditions 1606 quoteView.setQuote(requestManager, 1607 quote.getId(), 1608 Recipient.live(quote.getAuthor()).get(), 1609 quote.getDisplayText(), 1610 quote.isOriginalMissing(), 1611 quote.getAttachment(), 1612 isStoryReaction(current) ? current.getBody() : null, 1613 quote.getQuoteType()); 1614 1615 quoteView.setWallpaperEnabled(hasWallpaper); 1616 quoteView.setVisibility(View.VISIBLE); 1617 quoteView.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().getMessageQuoteFontSize(context)); 1618 quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; 1619 1620 quoteView.setOnClickListener(view -> { 1621 if (eventListener != null && batchSelected.isEmpty()) { 1622 eventListener.onQuoteClicked((MmsMessageRecord) current); 1623 } else { 1624 passthroughClickListener.onClick(view); 1625 } 1626 }); 1627 1628 quoteView.setOnLongClickListener(passthroughClickListener); 1629 1630 if (startOfCluster) { 1631 if (current.isOutgoing()) { 1632 quoteView.setTopCornerSizes(true, true); 1633 } else if (isGroupThread) { 1634 quoteView.setTopCornerSizes(false, false); 1635 } else { 1636 quoteView.setTopCornerSizes(true, true); 1637 } 1638 } else if (!isSingularMessage(current, previous, next, isGroupThread)) { 1639 if (current.isOutgoing()) { 1640 quoteView.setTopCornerSizes(true, false); 1641 } else { 1642 quoteView.setTopCornerSizes(false, true); 1643 } 1644 } 1645 1646 if (!isFooterVisible(current, next, isGroupThread) && isStoryReaction(current)) { 1647 ViewUtil.setBottomMargin(quoteView, (int) DimensionUnit.DP.toPixels(8), false); 1648 } else { 1649 ViewUtil.setBottomMargin(quoteView, 0, false); 1650 } 1651 1652 if (mediaThumbnailStub.resolved()) { 1653 ViewUtil.setTopMargin(mediaThumbnailStub.require(), readDimen(R.dimen.message_bubble_top_padding), false); 1654 } 1655 1656 if (linkPreviewStub.resolved() && !hasBigImageLinkPreview(current)) { 1657 ViewUtil.setTopMargin(linkPreviewStub.get(), readDimen(R.dimen.message_bubble_top_padding), false); 1658 } 1659 } else { 1660 if (quoteView != null) { 1661 quoteView.dismiss(); 1662 } 1663 1664 int topMargin = (current.isOutgoing() || !startOfCluster || !groupThread) ? 0 : readDimen(R.dimen.message_bubble_top_image_margin); 1665 if (mediaThumbnailStub.resolved()) { 1666 ViewUtil.setTopMargin(mediaThumbnailStub.require(), topMargin, false); 1667 } 1668 } 1669 } 1670 1671 private void setGutterSizes(@NonNull MessageRecord current, boolean isGroupThread) { 1672 if (isGroupThread && current.isOutgoing()) { 1673 ViewUtil.setPaddingStart(this, readDimen(R.dimen.conversation_group_left_gutter)); 1674 ViewUtil.setPaddingEnd(this, readDimen(R.dimen.conversation_individual_right_gutter)); 1675 } else if (current.isOutgoing()) { 1676 ViewUtil.setPaddingStart(this, readDimen(R.dimen.conversation_individual_left_gutter)); 1677 ViewUtil.setPaddingEnd(this, readDimen(R.dimen.conversation_individual_right_gutter)); 1678 } else { 1679 ViewUtil.setPaddingStart(this, readDimen(R.dimen.conversation_individual_received_left_gutter)); 1680 ViewUtil.setPaddingEnd(this, readDimen(R.dimen.conversation_individual_right_gutter)); 1681 } 1682 } 1683 1684 private void setReactions(@NonNull MessageRecord current) { 1685 bodyBubble.setOnSizeChangedListener(null); 1686 1687 if (current.getReactions().isEmpty()) { 1688 reactionsView.clear(); 1689 return; 1690 } 1691 1692 setReactionsWithWidth(current, bodyBubble.getWidth()); 1693 bodyBubble.setOnSizeChangedListener((width, height) -> setReactionsWithWidth(current, width)); 1694 } 1695 1696 private void setReactionsWithWidth(@NonNull MessageRecord current, int width) { 1697 reactionsView.setReactions(current.getReactions()); 1698 reactionsView.setBubbleWidth(width); 1699 reactionsView.setOnClickListener(v -> { 1700 if (eventListener == null) return; 1701 1702 eventListener.onReactionClicked(new MultiselectPart.Message(conversationMessage), current.getId(), current.isMms()); 1703 }); 1704 } 1705 1706 private void setFooter(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, @NonNull Locale locale, boolean isGroupThread, boolean hasWallpaper) { 1707 ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 1708 ViewUtil.setTopMargin(footer, readDimen(R.dimen.message_bubble_default_footer_bottom_margin)); 1709 1710 footer.setVisibility(GONE); 1711 ViewUtil.setVisibilityIfNonNull(stickerFooter, GONE); 1712 if (sharedContactStub.resolved()) sharedContactStub.get().getFooter().setVisibility(GONE); 1713 if (mediaThumbnailStub.resolved() && mediaThumbnailStub.require().getFooter().resolved()) { 1714 mediaThumbnailStub.require().getFooter().setVisibility(GONE); 1715 } 1716 1717 if (isFooterVisible(current, next, isGroupThread)) { 1718 ConversationItemFooter activeFooter = getActiveFooter(current); 1719 activeFooter.setVisibility(VISIBLE); 1720 activeFooter.setMessageRecord(current, locale, displayMode); 1721 1722 if (MessageRecordUtil.isEditMessage(current)) { 1723 activeFooter.getDateView().setOnClickListener(v -> { 1724 if (eventListener != null) { 1725 eventListener.onEditedIndicatorClicked(current); 1726 } 1727 }); 1728 } else { 1729 activeFooter.getDateView().setOnClickListener(null); 1730 activeFooter.getDateView().setClickable(false); 1731 } 1732 1733 if (hasWallpaper && hasNoBubble((messageRecord))) { 1734 if (messageRecord.isOutgoing()) { 1735 activeFooter.disableBubbleBackground(); 1736 activeFooter.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_sent_text_secondary_color)); 1737 activeFooter.setIconColor(ContextCompat.getColor(context, R.color.conversation_item_sent_text_secondary_color)); 1738 activeFooter.setRevealDotColor(ContextCompat.getColor(context, R.color.conversation_item_sent_text_secondary_color)); 1739 } else { 1740 activeFooter.enableBubbleBackground(R.drawable.wallpaper_bubble_background_tintable_11, getDefaultBubbleColor(hasWallpaper)); 1741 } 1742 } else if (hasNoBubble(messageRecord)) { 1743 activeFooter.disableBubbleBackground(); 1744 activeFooter.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary)); 1745 activeFooter.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary)); 1746 activeFooter.setRevealDotColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary)); 1747 } else { 1748 activeFooter.disableBubbleBackground(); 1749 } 1750 } 1751 } 1752 1753 private void setStoryReactionLabel(@NonNull MessageRecord record) { 1754 if (isStoryReaction(record) && !record.isRemoteDelete()) { 1755 storyReactionLabelWrapper.setVisibility(View.VISIBLE); 1756 storyReactionLabel.setTextColor(record.isOutgoing() ? colorizer.getOutgoingBodyTextColor(context) : ContextCompat.getColor(context, R.color.signal_text_primary)); 1757 storyReactionLabel.setText(getStoryReactionLabelText(messageRecord)); 1758 } else if (storyReactionLabelWrapper != null) { 1759 storyReactionLabelWrapper.setVisibility(View.GONE); 1760 } 1761 } 1762 1763 private @NonNull String getStoryReactionLabelText(@NonNull MessageRecord messageRecord) { 1764 if (hasQuote(messageRecord)) { 1765 MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord; 1766 RecipientId author = mmsMessageRecord.getQuote().getAuthor(); 1767 1768 if (author.equals(Recipient.self().getId())) { 1769 return context.getString(R.string.ConversationItem__reacted_to_your_story); 1770 } else { 1771 return context.getString(R.string.ConversationItem__you_reacted_to_s_story, Recipient.resolved(author).getShortDisplayName(context)); 1772 } 1773 } else { 1774 return context.getString(R.string.ConversationItem__reacted_to_a_story); 1775 } 1776 } 1777 1778 private void setHasBeenQuoted(@NonNull ConversationMessage message) { 1779 if (message.hasBeenQuoted() && !isCondensedMode() && quotedIndicator != null && batchSelected.isEmpty() && displayMode != ConversationItemDisplayMode.EditHistory.INSTANCE) { 1780 quotedIndicator.setVisibility(VISIBLE); 1781 quotedIndicator.setOnClickListener(quotedIndicatorClickListener); 1782 } else if (quotedIndicator != null) { 1783 quotedIndicator.setVisibility(GONE); 1784 quotedIndicator.setOnClickListener(null); 1785 } 1786 } 1787 1788 private void setHasBeenScheduled(@NonNull ConversationMessage message) { 1789 if (scheduledIndicator == null) { 1790 return; 1791 } 1792 if (message.hasBeenScheduled()) { 1793 scheduledIndicator.setVisibility(View.VISIBLE); 1794 scheduledIndicator.setOnClickListener(scheduledIndicatorClickListener); 1795 } else { 1796 scheduledIndicator.setVisibility(View.GONE); 1797 scheduledIndicator.setOnClickListener(null); 1798 } 1799 } 1800 1801 private boolean forceFooter(@NonNull MessageRecord messageRecord) { 1802 return hasAudio(messageRecord) || MessageRecordUtil.isEditMessage(messageRecord) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE; 1803 } 1804 1805 private boolean forceGroupHeader(@NonNull MessageRecord messageRecord) { 1806 return displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE; 1807 } 1808 1809 private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) { 1810 if (hasNoBubble(messageRecord) && stickerFooter != null) { 1811 return stickerFooter; 1812 } else if (hasSharedContact(messageRecord) && messageRecord.isDisplayBodyEmpty(getContext())) { 1813 return sharedContactStub.get().getFooter(); 1814 } else if (hasOnlyThumbnail(messageRecord) && messageRecord.isDisplayBodyEmpty(getContext())) { 1815 return mediaThumbnailStub.require().getFooter().get(); 1816 } else { 1817 return footer; 1818 } 1819 } 1820 1821 private int readDimen(@DimenRes int dimenId) { 1822 return context.getResources().getDimensionPixelOffset(dimenId); 1823 } 1824 1825 private boolean shouldInterceptClicks(MessageRecord messageRecord) { 1826 return batchSelected.isEmpty() && 1827 ((messageRecord.isFailed() && !messageRecord.isMmsNotification()) || 1828 (messageRecord.isRateLimited() && SignalStore.rateLimit().needsRecaptcha()) || 1829 messageRecord.isPendingInsecureSmsFallback() || 1830 messageRecord.isBundleKeyExchange()); 1831 } 1832 1833 @SuppressLint("SetTextI18n") 1834 private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) { 1835 if (groupThread && !messageRecord.isOutgoing() && groupSender != null) { 1836 groupSender.setText(recipient.getDisplayName(getContext())); 1837 } 1838 } 1839 1840 private void setGroupAuthorColor(@NonNull MessageRecord messageRecord, boolean hasWallpaper, @NonNull Colorizer colorizer) { 1841 if (groupSender != null) { 1842 groupSender.setTextColor(colorizer.getIncomingGroupSenderColor(getContext(), messageRecord.getFromRecipient())); 1843 } 1844 } 1845 1846 @SuppressWarnings("ConstantConditions") 1847 private void setAuthor(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread, boolean hasWallpaper) { 1848 if (isGroupThread && !current.isOutgoing()) { 1849 contactPhotoHolder.setVisibility(VISIBLE); 1850 1851 if (!previous.isPresent() || previous.get().isUpdate() || !current.getFromRecipient().equals(previous.get().getFromRecipient()) || 1852 !DateUtils.isSameDay(previous.get().getTimestamp(), current.getTimestamp()) || !isWithinClusteringTime(current, previous.get()) || forceGroupHeader(current)) 1853 { 1854 groupSenderHolder.setVisibility(VISIBLE); 1855 1856 if (hasWallpaper && hasNoBubble(current)) { 1857 groupSenderHolder.setBackgroundResource(R.drawable.wallpaper_bubble_background_tintable_11); 1858 groupSenderHolder.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.MULTIPLY); 1859 } else { 1860 groupSenderHolder.setBackground(null); 1861 } 1862 } else { 1863 groupSenderHolder.setVisibility(GONE); 1864 } 1865 1866 if (!next.isPresent() || next.get().isUpdate() || !current.getFromRecipient().equals(next.get().getFromRecipient()) || !isWithinClusteringTime(current, next.get()) || forceGroupHeader(current)) { 1867 contactPhoto.setVisibility(VISIBLE); 1868 badgeImageView.setVisibility(VISIBLE); 1869 } else { 1870 contactPhoto.setVisibility(GONE); 1871 badgeImageView.setVisibility(GONE); 1872 } 1873 } else { 1874 if (groupSenderHolder != null) { 1875 groupSenderHolder.setVisibility(GONE); 1876 } 1877 1878 if (contactPhotoHolder != null) { 1879 contactPhotoHolder.setVisibility(GONE); 1880 } 1881 1882 if (badgeImageView != null) { 1883 badgeImageView.setVisibility(GONE); 1884 } 1885 } 1886 } 1887 1888 private void setOutlinerRadii(Outliner outliner, int topStart, int topEnd, int bottomEnd, int bottomStart) { 1889 if (ViewUtil.isRtl(this)) { 1890 outliner.setRadii(topEnd, topStart, bottomStart, bottomEnd); 1891 } else { 1892 outliner.setRadii(topStart, topEnd, bottomEnd, bottomStart); 1893 } 1894 } 1895 1896 private @NonNull Projection.Corners getBodyBubbleCorners(int topStart, int topEnd, int bottomEnd, int bottomStart) { 1897 if (ViewUtil.isRtl(this)) { 1898 return new Projection.Corners(topEnd, topStart, bottomStart, bottomEnd); 1899 } else { 1900 return new Projection.Corners(topStart, topEnd, bottomEnd, bottomStart); 1901 } 1902 } 1903 1904 private void setMessageShape(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) { 1905 int bigRadius = readDimen(R.dimen.message_corner_radius); 1906 int smallRadius = readDimen(R.dimen.message_corner_collapse_radius); 1907 1908 int background; 1909 1910 if (isSingularMessage(current, previous, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) { 1911 if (current.isOutgoing()) { 1912 background = R.drawable.message_bubble_background_sent_alone; 1913 outliner.setRadius(bigRadius); 1914 pulseOutliner.setRadius(bigRadius); 1915 bodyBubbleCorners = new Projection.Corners(bigRadius); 1916 } else { 1917 background = R.drawable.message_bubble_background_received_alone; 1918 outliner.setRadius(bigRadius); 1919 pulseOutliner.setRadius(bigRadius); 1920 bodyBubbleCorners = new Projection.Corners(bigRadius); 1921 } 1922 } else if (isStartOfMessageCluster(current, previous, isGroupThread)) { 1923 if (current.isOutgoing()) { 1924 background = R.drawable.message_bubble_background_sent_start; 1925 setOutlinerRadii(outliner, bigRadius, bigRadius, smallRadius, bigRadius); 1926 setOutlinerRadii(pulseOutliner, bigRadius, bigRadius, smallRadius, bigRadius); 1927 bodyBubbleCorners = getBodyBubbleCorners(bigRadius, bigRadius, smallRadius, bigRadius); 1928 } else { 1929 background = R.drawable.message_bubble_background_received_start; 1930 setOutlinerRadii(outliner, bigRadius, bigRadius, bigRadius, smallRadius); 1931 setOutlinerRadii(pulseOutliner, bigRadius, bigRadius, bigRadius, smallRadius); 1932 bodyBubbleCorners = getBodyBubbleCorners(bigRadius, bigRadius, bigRadius, smallRadius); 1933 } 1934 } else if (isEndOfMessageCluster(current, next, isGroupThread)) { 1935 if (current.isOutgoing()) { 1936 background = R.drawable.message_bubble_background_sent_end; 1937 setOutlinerRadii(outliner, bigRadius, smallRadius, bigRadius, bigRadius); 1938 setOutlinerRadii(pulseOutliner, bigRadius, smallRadius, bigRadius, bigRadius); 1939 bodyBubbleCorners = getBodyBubbleCorners(bigRadius, smallRadius, bigRadius, bigRadius); 1940 } else { 1941 background = R.drawable.message_bubble_background_received_end; 1942 setOutlinerRadii(outliner, smallRadius, bigRadius, bigRadius, bigRadius); 1943 setOutlinerRadii(pulseOutliner, smallRadius, bigRadius, bigRadius, bigRadius); 1944 bodyBubbleCorners = getBodyBubbleCorners(smallRadius, bigRadius, bigRadius, bigRadius); 1945 } 1946 } else { 1947 if (current.isOutgoing()) { 1948 background = R.drawable.message_bubble_background_sent_middle; 1949 setOutlinerRadii(outliner, bigRadius, smallRadius, smallRadius, bigRadius); 1950 setOutlinerRadii(pulseOutliner, bigRadius, smallRadius, smallRadius, bigRadius); 1951 bodyBubbleCorners = getBodyBubbleCorners(bigRadius, smallRadius, smallRadius, bigRadius); 1952 } else { 1953 background = R.drawable.message_bubble_background_received_middle; 1954 setOutlinerRadii(outliner, smallRadius, bigRadius, bigRadius, smallRadius); 1955 setOutlinerRadii(pulseOutliner, smallRadius, bigRadius, bigRadius, smallRadius); 1956 bodyBubbleCorners = getBodyBubbleCorners(smallRadius, bigRadius, bigRadius, smallRadius); 1957 } 1958 } 1959 1960 bodyBubble.setBackgroundResource(background); 1961 } 1962 1963 private boolean isStartOfMessageCluster(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, boolean isGroupThread) { 1964 if (isGroupThread) { 1965 return !previous.isPresent() || previous.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), previous.get().getTimestamp()) || 1966 !current.getFromRecipient().equals(previous.get().getFromRecipient()) || !isWithinClusteringTime(current, previous.get()) || MessageRecordUtil.isScheduled(current); 1967 } else { 1968 return !previous.isPresent() || previous.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), previous.get().getTimestamp()) || 1969 current.isOutgoing() != previous.get().isOutgoing() || previous.get().isSecure() != current.isSecure() || !isWithinClusteringTime(current, previous.get()) || 1970 MessageRecordUtil.isScheduled(current); 1971 } 1972 } 1973 1974 private boolean isEndOfMessageCluster(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, boolean isGroupThread) { 1975 if (isGroupThread) { 1976 return !next.isPresent() || next.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), next.get().getTimestamp()) || 1977 !current.getFromRecipient().equals(next.get().getFromRecipient()) || !current.getReactions().isEmpty() || !isWithinClusteringTime(current, next.get()) || 1978 MessageRecordUtil.isScheduled(current); 1979 } else { 1980 return !next.isPresent() || next.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), next.get().getTimestamp()) || 1981 current.isOutgoing() != next.get().isOutgoing() || !current.getReactions().isEmpty() || next.get().isSecure() != current.isSecure() || 1982 !isWithinClusteringTime(current, next.get()) || MessageRecordUtil.isScheduled(current); 1983 } 1984 } 1985 1986 private boolean isSingularMessage(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) { 1987 return isStartOfMessageCluster(current, previous, isGroupThread) && isEndOfMessageCluster(current, next, isGroupThread); 1988 } 1989 1990 private boolean isFooterVisible(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, boolean isGroupThread) { 1991 boolean differentTimestamps = next.isPresent() && !DateUtils.isSameExtendedRelativeTimestamp(next.get().getTimestamp(), current.getTimestamp()); 1992 1993 return forceFooter(messageRecord) || current.getExpiresIn() > 0 || !current.isSecure() || current.isPending() || current.isPendingInsecureSmsFallback() || 1994 current.isFailed() || current.isRateLimited() || differentTimestamps || isEndOfMessageCluster(current, next, isGroupThread); 1995 } 1996 1997 private static boolean isWithinClusteringTime(@NonNull MessageRecord lhs, @NonNull MessageRecord rhs) { 1998 long timeDiff = Math.abs(lhs.getDateSent() - rhs.getDateSent()); 1999 return timeDiff <= MAX_CLUSTERING_TIME_DIFF; 2000 } 2001 2002 private void setMessageSpacing(@NonNull Context context, @NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) { 2003 int spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_collapse); 2004 int spacingBottom = spacingTop; 2005 2006 if (isStartOfMessageCluster(current, previous, isGroupThread) && (displayMode != ConversationItemDisplayMode.EditHistory.INSTANCE || next.isEmpty())) { 2007 spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_default); 2008 } 2009 2010 if (isEndOfMessageCluster(current, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) { 2011 spacingBottom = readDimen(context, R.dimen.conversation_vertical_message_spacing_default); 2012 } 2013 2014 ViewUtil.setPaddingTop(this, spacingTop); 2015 ViewUtil.setPaddingBottom(this, spacingBottom); 2016 } 2017 2018 private int readDimen(@NonNull Context context, @DimenRes int dimenId) { 2019 return context.getResources().getDimensionPixelOffset(dimenId); 2020 } 2021 2022 private boolean doAnySlidesLackData(SlideDeck deck) { 2023 for (Attachment attachment : deck.asAttachments()) { 2024 if (attachment instanceof DatabaseAttachment && !((DatabaseAttachment) attachment).hasData) { 2025 return true; 2026 } 2027 } 2028 return false; 2029 } 2030 2031 /// Event handlers 2032 2033 private Spannable getLongMessageSpan(@NonNull MessageRecord messageRecord) { 2034 String message; 2035 Runnable action; 2036 2037 if (messageRecord.isMms()) { 2038 TextSlide slide = ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide(); 2039 2040 if (slide != null && (slide.asAttachment().transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || MessageRecordUtil.isScheduled(messageRecord))) { 2041 message = getResources().getString(R.string.ConversationItem_read_more); 2042 action = () -> eventListener.onMoreTextClicked(conversationRecipient.getId(), messageRecord.getId(), messageRecord.isMms()); 2043 } else if (slide != null && slide.asAttachment().transferState == AttachmentTable.TRANSFER_PROGRESS_STARTED) { 2044 message = getResources().getString(R.string.ConversationItem_pending); 2045 action = () -> {}; 2046 } else if (slide != null) { 2047 message = getResources().getString(R.string.ConversationItem_download_more); 2048 action = () -> singleDownloadClickListener.onClick(bodyText, slide); 2049 } else { 2050 message = getResources().getString(R.string.ConversationItem_read_more); 2051 action = () -> eventListener.onMoreTextClicked(conversationRecipient.getId(), messageRecord.getId(), messageRecord.isMms()); 2052 } 2053 } else { 2054 message = getResources().getString(R.string.ConversationItem_read_more); 2055 action = () -> eventListener.onMoreTextClicked(conversationRecipient.getId(), messageRecord.getId(), messageRecord.isMms()); 2056 } 2057 2058 SpannableStringBuilder span = new SpannableStringBuilder(message); 2059 CharacterStyle style = new ClickableSpan() { 2060 @Override 2061 public void onClick(@NonNull View widget) { 2062 if (eventListener != null && batchSelected.isEmpty()) { 2063 action.run(); 2064 } 2065 } 2066 2067 @Override 2068 public void updateDrawState(@NonNull TextPaint ds) { 2069 ds.setTypeface(Typeface.DEFAULT_BOLD); 2070 } 2071 }; 2072 span.setSpan(style, 0, span.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 2073 return span; 2074 } 2075 2076 @Override 2077 public void showProjectionArea() { 2078 if (mediaThumbnailStub != null && mediaThumbnailStub.resolved()) { 2079 mediaThumbnailStub.require().showThumbnailView(); 2080 bodyBubble.setVideoPlayerProjection(null); 2081 } 2082 } 2083 2084 @Override 2085 public void hideProjectionArea() { 2086 if (mediaThumbnailStub != null && mediaThumbnailStub.resolved()) { 2087 mediaThumbnailStub.require().hideThumbnailView(); 2088 mediaThumbnailStub.require().getDrawingRect(thumbnailMaskingRect); 2089 bodyBubble.setVideoPlayerProjection(Projection.relativeToViewWithCommonRoot(mediaThumbnailStub.require(), bodyBubble, null)); 2090 } 2091 } 2092 2093 @Override 2094 public @Nullable MediaItem getMediaItem() { 2095 return mediaItem; 2096 } 2097 2098 @Override 2099 public @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() { 2100 if (GiphyMp4PlaybackPolicy.autoplay()) { 2101 return null; 2102 } else { 2103 return new GiphyMp4PlaybackPolicyEnforcer(() -> { 2104 if (eventListener != null) { 2105 eventListener.onPlayInlineContent(null); 2106 } 2107 }); 2108 } 2109 } 2110 2111 @Override 2112 public int getAdapterPosition() { 2113 throw new UnsupportedOperationException("Do not delegate to this method"); 2114 } 2115 2116 @Override 2117 public @NonNull Projection getGiphyMp4PlayableProjection(@NonNull ViewGroup recyclerView) { 2118 if (mediaThumbnailStub != null && mediaThumbnailStub.isResolvable()) { 2119 ConversationItemThumbnail thumbnail = mediaThumbnailStub.require(); 2120 return Projection.relativeToParent(recyclerView, thumbnail, thumbnail.getCorners()) 2121 .scale(bodyBubble.getScaleX()) 2122 .translateX(Util.halfOffsetFromScale(thumbnail.getWidth(), bodyBubble.getScaleX())) 2123 .translateY(Util.halfOffsetFromScale(thumbnail.getHeight(), bodyBubble.getScaleY())) 2124 .translateY(getTranslationY()) 2125 .translateX(bodyBubble.getTranslationX()) 2126 .translateX(getTranslationX()); 2127 } else { 2128 return Projection.relativeToParent(recyclerView, bodyBubble, bodyBubbleCorners) 2129 .translateY(getTranslationY()) 2130 .translateX(bodyBubble.getTranslationX()) 2131 .translateX(getTranslationX()); 2132 } 2133 } 2134 2135 @Override 2136 public boolean canPlayContent() { 2137 return mediaThumbnailStub != null && mediaThumbnailStub.isResolvable() && canPlayContent; 2138 } 2139 2140 @Override 2141 public boolean shouldProjectContent() { 2142 return canPlayContent() && bodyBubble.getVisibility() == VISIBLE; 2143 } 2144 2145 @Override 2146 public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) { 2147 return getSnapshotProjections(coordinateRoot, true, true); 2148 } 2149 2150 @Override 2151 public @NonNull ProjectionList getSnapshotProjections(@NonNull ViewGroup coordinateRoot, boolean clipOutMedia) { 2152 return getSnapshotProjections(coordinateRoot, clipOutMedia, true); 2153 } 2154 2155 @Override 2156 public @NonNull ProjectionList getSnapshotProjections(@NonNull ViewGroup coordinateRoot, boolean clipOutMedia, boolean outgoingOnly) { 2157 colorizerProjections.clear(); 2158 2159 if ((messageRecord.isOutgoing() || !outgoingOnly) && 2160 !hasNoBubble(messageRecord) && 2161 !messageRecord.isRemoteDelete() && 2162 bodyBubbleCorners != null && 2163 bodyBubble.getVisibility() == VISIBLE) 2164 { 2165 Projection bodyBubbleToRoot = Projection.relativeToParent(coordinateRoot, bodyBubble, bodyBubbleCorners).translateX(bodyBubble.getTranslationX()); 2166 Projection videoToBubble = bodyBubble.getVideoPlayerProjection(); 2167 Projection mediaThumb = clipOutMedia && mediaThumbnailStub.resolved() ? Projection.relativeToParent(coordinateRoot, mediaThumbnailStub.require(), null) : null; 2168 2169 float translationX = Util.halfOffsetFromScale(bodyBubble.getWidth(), bodyBubble.getScaleX()); 2170 float translationY = Util.halfOffsetFromScale(bodyBubble.getHeight(), bodyBubble.getScaleY()); 2171 2172 if (videoToBubble != null) { 2173 Projection videoToRoot = Projection.translateFromDescendantToParentCoords(videoToBubble, bodyBubble, coordinateRoot); 2174 2175 List<Projection> projections = Projection.getCapAndTail(bodyBubbleToRoot, videoToRoot); 2176 if (!projections.isEmpty()) { 2177 projections.get(0) 2178 .scale(bodyBubble.getScaleX()) 2179 .translateX(translationX) 2180 .translateY(translationY); 2181 projections.get(1) 2182 .scale(bodyBubble.getScaleX()) 2183 .translateX(translationX) 2184 .translateY(-translationY); 2185 } 2186 2187 colorizerProjections.addAll(projections); 2188 } else if (hasThumbnail(messageRecord) && mediaThumb != null) { 2189 if (hasQuote(messageRecord) && quoteView != null) { 2190 Projection quote = Projection.relativeToParent(coordinateRoot, bodyBubble, bodyBubbleCorners).translateX(bodyBubble.getTranslationX()); 2191 int quoteViewTop = (int) quote.getY(); 2192 int mediaTop = (int) mediaThumb.getY(); 2193 2194 colorizerProjections.add( 2195 quote.insetBottom(quote.getHeight() - (mediaTop - quoteViewTop)) 2196 .scale(bodyBubble.getScaleX()) 2197 .translateX(translationX) 2198 .translateY(translationY) 2199 ); 2200 } 2201 2202 colorizerProjections.add( 2203 bodyBubbleToRoot.scale(bodyBubble.getScaleX()) 2204 .insetTop((int) (mediaThumb.getHeight() * bodyBubble.getScaleX())) 2205 .translateX(translationX) 2206 .translateY(translationY) 2207 ); 2208 } else { 2209 colorizerProjections.add( 2210 bodyBubbleToRoot.scale(bodyBubble.getScaleX()) 2211 .translateX(translationX) 2212 .translateY(translationY) 2213 ); 2214 } 2215 2216 if (mediaThumb != null) { 2217 mediaThumb.release(); 2218 } 2219 } 2220 2221 if ((messageRecord.isOutgoing() || !outgoingOnly) && 2222 hasNoBubble(messageRecord) && 2223 hasWallpaper && 2224 bodyBubble.getVisibility() == VISIBLE) 2225 { 2226 ConversationItemFooter footer = getActiveFooter(messageRecord); 2227 Projection footerProjection = footer.getProjection(coordinateRoot); 2228 if (footerProjection != null) { 2229 colorizerProjections.add( 2230 footerProjection.translateX(bodyBubble.getTranslationX()) 2231 .scale(bodyBubble.getScaleX()) 2232 .translateX(Util.halfOffsetFromScale(footer.getWidth(), bodyBubble.getScaleX())) 2233 .translateY(-Util.halfOffsetFromScale(footer.getHeight(), bodyBubble.getScaleY())) 2234 ); 2235 } 2236 } 2237 2238 for (int i = 0; i < colorizerProjections.size(); i++) { 2239 colorizerProjections.get(i).translateY(getTranslationY()); 2240 } 2241 2242 return colorizerProjections; 2243 } 2244 2245 @Override 2246 public @Nullable View getHorizontalTranslationTarget() { 2247 if (messageRecord.isOutgoing()) { 2248 return null; 2249 } else if (groupThread) { 2250 return contactPhotoHolder; 2251 } else { 2252 return bodyBubble; 2253 } 2254 } 2255 2256 @Override 2257 public @Nullable Projection getOpenableGiftProjection(boolean isAnimating) { 2258 if (!isGiftMessage(messageRecord) || messageRecord.isRemoteDelete() || (messageRecord.isViewed() && !isAnimating)) { 2259 return null; 2260 } 2261 2262 return Projection.relativeToViewRoot(bodyBubble, bodyBubbleCorners) 2263 .translateX(bodyBubble.getTranslationX()) 2264 .translateX(getTranslationX()) 2265 .scale(bodyBubble.getScaleX()); 2266 } 2267 2268 @Override 2269 public long getGiftId() { 2270 return messageRecord.getId(); 2271 } 2272 2273 @Override 2274 public void setOpenGiftCallback(@NonNull Function1<? super OpenableGift, Unit> openGift) { 2275 if (giftViewStub.resolved()) { 2276 bodyBubble.setOnClickListener(unused -> { 2277 openGift.invoke(this); 2278 eventListener.onGiftBadgeRevealed(messageRecord); 2279 bodyBubble.performHapticFeedback(Build.VERSION.SDK_INT >= 30 ? HapticFeedbackConstants.CONFIRM 2280 : HapticFeedbackConstants.KEYBOARD_TAP); 2281 }); 2282 giftViewStub.get().onGiftNotOpened(); 2283 } 2284 } 2285 2286 @Override 2287 public void clearOpenGiftCallback() { 2288 if (giftViewStub.resolved()) { 2289 bodyBubble.setOnClickListener(null); 2290 bodyBubble.setClickable(false); 2291 giftViewStub.get().onGiftOpened(); 2292 } 2293 } 2294 2295 @Override 2296 public @NonNull AnimationSign getAnimationSign() { 2297 return AnimationSign.get(ViewUtil.isLtr(this), messageRecord.isOutgoing()); 2298 } 2299 2300 @Override 2301 public @Nullable View getQuotedIndicatorView() { 2302 return quotedIndicator; 2303 } 2304 2305 @Override 2306 public @NonNull View getReplyView() { 2307 return reply; 2308 } 2309 2310 @Override 2311 public @Nullable View getContactPhotoHolderView() { 2312 return contactPhotoHolder; 2313 } 2314 2315 @Override 2316 public @Nullable View getBadgeImageView() { 2317 return badgeImageView; 2318 } 2319 2320 @NonNull @Override public List<View> getBubbleViews() { 2321 return Collections.singletonList(bodyBubble); 2322 } 2323 2324 @Override 2325 public int getAdapterPosition(@NonNull RecyclerView recyclerView) { 2326 return recyclerView.getChildViewHolder(this).getBindingAdapterPosition(); 2327 } 2328 2329 @Override 2330 public @NonNull ViewGroup getRoot() { 2331 return this; 2332 } 2333 2334 @Override 2335 public @NonNull View getBubbleView() { 2336 return bodyBubble; 2337 } 2338 2339 @Override 2340 public void invalidateChatColorsDrawable(@NonNull ViewGroup coordinateRoot) { 2341 // Intentionally left blank. 2342 } 2343 2344 @Override public @Nullable SnapshotStrategy getSnapshotStrategy() { 2345 return null; 2346 } 2347 2348 private class SharedContactEventListener implements SharedContactView.EventListener { 2349 @Override 2350 public void onAddToContactsClicked(@NonNull Contact contact) { 2351 if (eventListener != null && batchSelected.isEmpty()) { 2352 eventListener.onAddToContactsClicked(contact); 2353 } else { 2354 passthroughClickListener.onClick(sharedContactStub.get()); 2355 } 2356 } 2357 2358 @Override 2359 public void onInviteClicked(@NonNull List<Recipient> choices) { 2360 if (eventListener != null && batchSelected.isEmpty()) { 2361 eventListener.onInviteSharedContactClicked(choices); 2362 } else { 2363 passthroughClickListener.onClick(sharedContactStub.get()); 2364 } 2365 } 2366 2367 @Override 2368 public void onMessageClicked(@NonNull List<Recipient> choices) { 2369 if (eventListener != null && batchSelected.isEmpty()) { 2370 eventListener.onMessageSharedContactClicked(choices); 2371 } else { 2372 passthroughClickListener.onClick(sharedContactStub.get()); 2373 } 2374 } 2375 } 2376 2377 private class SharedContactClickListener implements View.OnClickListener { 2378 @Override 2379 public void onClick(View view) { 2380 if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getSharedContacts().isEmpty()) { 2381 eventListener.onSharedContactDetailsClicked(((MmsMessageRecord) messageRecord).getSharedContacts().get(0), (View) sharedContactStub.get().getAvatarView().getParent()); 2382 } else { 2383 passthroughClickListener.onClick(view); 2384 } 2385 } 2386 } 2387 2388 private class LinkPreviewClickListener implements View.OnClickListener { 2389 @Override 2390 public void onClick(View view) { 2391 if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) { 2392 eventListener.onLinkPreviewClicked(((MmsMessageRecord) messageRecord).getLinkPreviews().get(0)); 2393 } else { 2394 passthroughClickListener.onClick(view); 2395 } 2396 } 2397 } 2398 2399 private class ViewOnceMessageClickListener implements View.OnClickListener { 2400 @Override 2401 public void onClick(View view) { 2402 ViewOnceMessageView revealView = (ViewOnceMessageView) view; 2403 2404 if (batchSelected.isEmpty() && messageRecord.isMms() && revealView.requiresTapToDownload((MmsMessageRecord) messageRecord)) { 2405 singleDownloadClickListener.onClick(view, ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlide()); 2406 } else if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms()) { 2407 eventListener.onViewOnceMessageClicked((MmsMessageRecord) messageRecord); 2408 } else { 2409 passthroughClickListener.onClick(view); 2410 } 2411 } 2412 } 2413 2414 private class LinkPreviewThumbnailClickListener implements SlideClickListener { 2415 public void onClick(final View v, final Slide slide) { 2416 if (eventListener != null && batchSelected.isEmpty() && messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) { 2417 eventListener.onLinkPreviewClicked(((MmsMessageRecord) messageRecord).getLinkPreviews().get(0)); 2418 } else { 2419 performClick(); 2420 } 2421 } 2422 } 2423 2424 private class QuotedIndicatorClickListener implements View.OnClickListener { 2425 public void onClick(final View view) { 2426 if (eventListener != null && batchSelected.isEmpty() && conversationMessage.hasBeenQuoted()) { 2427 eventListener.onQuotedIndicatorClicked((messageRecord)); 2428 } else { 2429 passthroughClickListener.onClick(view); 2430 } 2431 } 2432 } 2433 2434 private class ScheduledIndicatorClickListener implements View.OnClickListener { 2435 public void onClick(final View view) { 2436 if (eventListener != null && batchSelected.isEmpty()) { 2437 eventListener.onScheduledIndicatorClicked(view, (conversationMessage)); 2438 } else { 2439 passthroughClickListener.onClick(view); 2440 } 2441 } 2442 } 2443 2444 private class AttachmentDownloadClickListener implements SlidesClickedListener { 2445 @Override 2446 public void onClick(View v, final List<Slide> slides) { 2447 Log.i(TAG, "onClick() for attachment download"); 2448 if (messageRecord.isMmsNotification()) { 2449 Log.w(TAG, "Ignoring MMS download."); 2450 } else { 2451 Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items"); 2452 2453 for (Slide slide : slides) { 2454 ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(messageRecord.getId(), 2455 ((DatabaseAttachment) slide.asAttachment()).attachmentId, 2456 true)); 2457 } 2458 } 2459 } 2460 } 2461 2462 private class PlayVideoClickListener implements SlideClickListener { 2463 private static final float MINIMUM_DOWNLOADED_THRESHOLD = 0.05f; 2464 private View parentView; 2465 private Slide activeSlide; 2466 2467 @Override 2468 public void onClick(View v, Slide slide) { 2469 if (messageRecord.isOutgoing()) { 2470 Log.d(TAG, "Video player button for outgoing slide clicked."); 2471 return; 2472 } 2473 if (MediaUtil.isInstantVideoSupported(slide)) { 2474 final DatabaseAttachment databaseAttachment = (DatabaseAttachment) slide.asAttachment(); 2475 if (databaseAttachment.transferState != AttachmentTable.TRANSFER_PROGRESS_STARTED) { 2476 final AttachmentId attachmentId = databaseAttachment.attachmentId; 2477 final JobManager jobManager = ApplicationDependencies.getJobManager(); 2478 final String queue = AttachmentDownloadJob.constructQueueString(attachmentId); 2479 setup(v, slide); 2480 jobManager.add(new AttachmentDownloadJob(messageRecord.getId(), 2481 attachmentId, 2482 true)); 2483 jobManager.addListener(queue, (job, jobState) -> { 2484 if (jobState.isComplete()) { 2485 cleanup(); 2486 } 2487 }); 2488 } else { 2489 launchMediaPreview(v, slide); 2490 cleanup(); 2491 } 2492 } else { 2493 Log.d(TAG, "Non-eligible slide clicked."); 2494 } 2495 } 2496 2497 private void setup(View v, Slide slide) { 2498 parentView = v; 2499 activeSlide = slide; 2500 if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this); 2501 } 2502 2503 private void cleanup() { 2504 parentView = null; 2505 activeSlide = null; 2506 if (EventBus.getDefault().isRegistered(this)) { 2507 EventBus.getDefault().unregister(this); 2508 } 2509 } 2510 2511 @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) 2512 public void onEventAsync(PartProgressEvent event) { 2513 final Slide currentActiveSlide = activeSlide; 2514 if (currentActiveSlide == null || !event.attachment.equals(currentActiveSlide.asAttachment())) { 2515 return; 2516 } 2517 2518 final View currentParentView = parentView; 2519 float progressPercent = ((float) event.progress) / event.total; 2520 if (progressPercent >= MINIMUM_DOWNLOADED_THRESHOLD && currentParentView != null) { 2521 cleanup(); 2522 launchMediaPreview(currentParentView, currentActiveSlide); 2523 } 2524 } 2525 } 2526 2527 private class SlideClickPassthroughListener implements SlideClickListener { 2528 2529 private final SlidesClickedListener original; 2530 2531 private SlideClickPassthroughListener(@NonNull SlidesClickedListener original) { 2532 this.original = original; 2533 } 2534 2535 @Override 2536 public void onClick(View v, Slide slide) { 2537 original.onClick(v, Collections.singletonList(slide)); 2538 } 2539 } 2540 2541 private class StickerClickListener implements SlideClickListener { 2542 @Override 2543 public void onClick(View v, Slide slide) { 2544 if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) { 2545 performClick(); 2546 } else if (eventListener != null && hasSticker(messageRecord)) { 2547 //noinspection ConstantConditions 2548 eventListener.onStickerClicked(((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide().asAttachment().stickerLocator); 2549 } 2550 } 2551 } 2552 2553 private class ThumbnailClickListener implements SlideClickListener { 2554 public void onClick(final View v, final Slide slide) { 2555 if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty() || isCondensedMode()) { 2556 performClick(); 2557 } else if (!canPlayContent && mediaItem != null && eventListener != null) { 2558 eventListener.onPlayInlineContent(conversationMessage); 2559 } else if (MediaPreviewV2Fragment.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { 2560 launchMediaPreview(v, slide); 2561 } else if (slide.getUri() != null) { 2562 Log.i(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType()); 2563 Uri publicUri = PartAuthority.getAttachmentPublicUri(slide.getUri()); 2564 Log.i(TAG, "Public URI: " + publicUri); 2565 Intent intent = new Intent(Intent.ACTION_VIEW); 2566 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 2567 intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), Intent.normalizeMimeType(slide.getContentType())); 2568 try { 2569 context.startActivity(intent); 2570 } catch (ActivityNotFoundException anfe) { 2571 Log.w(TAG, "No activity existed to view the media."); 2572 Toast.makeText(context, R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show(); 2573 } 2574 } else if (slide.asAttachment().isPermanentlyFailed()) { 2575 String failedMessage; 2576 2577 if (slide instanceof ImageSlide) { 2578 failedMessage = messageRecord.isOutgoing() ? context.getString(R.string.ConversationItem_cant_download_image_you_will_need_to_send_it_again) 2579 : context.getString(R.string.ConversationItem_cant_download_image_s_will_need_to_send_it_again, messageRecord.getFromRecipient().getShortDisplayName(context)); 2580 } else if (slide instanceof VideoSlide) { 2581 failedMessage = messageRecord.isOutgoing() ? context.getString(R.string.ConversationItem_cant_download_video_you_will_need_to_send_it_again) 2582 : context.getString(R.string.ConversationItem_cant_download_video_s_will_need_to_send_it_again, messageRecord.getFromRecipient().getShortDisplayName(context)); 2583 } else { 2584 failedMessage = messageRecord.isOutgoing() ? context.getString(R.string.ConversationItem_cant_download_message_you_will_need_to_send_it_again) 2585 : context.getString(R.string.ConversationItem_cant_download_message_s_will_need_to_send_it_again, messageRecord.getFromRecipient().getShortDisplayName(context)); 2586 } 2587 2588 new MaterialAlertDialogBuilder(getContext()) 2589 .setMessage(failedMessage) 2590 .setPositiveButton(android.R.string.ok, null) 2591 .setNegativeButton(android.R.string.cancel, null) 2592 .show(); 2593 } 2594 } 2595 } 2596 2597 private void launchMediaPreview(View v, Slide slide) { 2598 if (eventListener == null) { 2599 Log.w(TAG, "Could not launch media preview for item: eventListener was null"); 2600 return; 2601 } 2602 2603 Uri mediaUri = slide.getUri(); 2604 2605 if (mediaUri == null) { 2606 Log.w(TAG, "Could not launch media preview for item: uri was null"); 2607 return; 2608 } 2609 2610 MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs( 2611 messageRecord.getThreadId(), 2612 messageRecord.getTimestamp(), 2613 mediaUri, 2614 slide.getContentType(), 2615 slide.asAttachment().size, 2616 slide.getCaption().orElse(null), 2617 false, 2618 false, 2619 false, 2620 false, 2621 MediaTable.Sorting.Newest, 2622 slide.isVideoGif(), 2623 new MediaIntentFactory.SharedElementArgs( 2624 slide.asAttachment().width, 2625 slide.asAttachment().height, 2626 mediaThumbnailStub.require().getCorners().getTopLeft(), 2627 mediaThumbnailStub.require().getCorners().getTopRight(), 2628 mediaThumbnailStub.require().getCorners().getBottomRight(), 2629 mediaThumbnailStub.require().getCorners().getBottomLeft() 2630 ), 2631 false); 2632 if (v instanceof ThumbnailView) { 2633 MediaPreviewCache.INSTANCE.setDrawable(((ThumbnailView) v).getImageDrawable()); 2634 } 2635 eventListener.goToMediaPreview(ConversationItem.this, v, args); 2636 } 2637 2638 private class PassthroughClickListener implements View.OnLongClickListener, View.OnClickListener { 2639 2640 @Override 2641 public boolean onLongClick(View v) { 2642 if (bodyText.hasSelection()) { 2643 return false; 2644 } 2645 performLongClick(); 2646 return true; 2647 } 2648 2649 @Override 2650 public void onClick(View v) { 2651 performClick(); 2652 } 2653 } 2654 2655 private class GiftMessageViewCallback implements GiftMessageView.Callback { 2656 2657 @Override 2658 public void onViewGiftBadgeClicked() { 2659 eventListener.onViewGiftBadgeClicked(messageRecord); 2660 } 2661 } 2662 2663 private class ClickListener implements View.OnClickListener { 2664 private final OnClickListener parent; 2665 2666 ClickListener(@Nullable OnClickListener parent) { 2667 this.parent = parent; 2668 } 2669 2670 public void onClick(View v) { 2671 if (!shouldInterceptClicks(messageRecord) && parent != null) { 2672 parent.onClick(v); 2673 } else if (messageRecord.isFailed()) { 2674 if (eventListener != null) { 2675 eventListener.onMessageWithErrorClicked(messageRecord); 2676 } 2677 } else if (messageRecord.isRateLimited() && SignalStore.rateLimit().needsRecaptcha()) { 2678 if (eventListener != null) { 2679 eventListener.onMessageWithRecaptchaNeededClicked(messageRecord); 2680 } 2681 } else if (!messageRecord.isOutgoing() && messageRecord.isIdentityMismatchFailure()) { 2682 if (eventListener != null) { 2683 eventListener.onIncomingIdentityMismatchClicked(messageRecord.getFromRecipient().getId()); 2684 } 2685 } 2686 } 2687 } 2688 2689 private final class TouchDelegateChangedListener implements ConversationItemFooter.OnTouchDelegateChangedListener { 2690 @Override 2691 public void onTouchDelegateChanged(@NonNull Rect delegateRect, @NonNull View delegateView) { 2692 offsetDescendantRectToMyCoords(footer, delegateRect); 2693 setTouchDelegate(new TouchDelegate(delegateRect, delegateView)); 2694 } 2695 } 2696 2697 private final class UrlClickListener implements UrlClickHandler { 2698 2699 @Override 2700 public boolean handleOnClick(@NonNull String url) { 2701 return eventListener != null && eventListener.onUrlClicked(url); 2702 } 2703 } 2704 2705 private class MentionClickableSpan extends ClickableSpan { 2706 private final RecipientId mentionedRecipientId; 2707 2708 MentionClickableSpan(RecipientId mentionedRecipientId) { 2709 this.mentionedRecipientId = mentionedRecipientId; 2710 } 2711 2712 @Override 2713 public void onClick(@NonNull View widget) { 2714 if (eventListener != null && batchSelected.isEmpty()) { 2715 VibrateUtil.vibrateTick(context); 2716 eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId()); 2717 } 2718 } 2719 2720 @Override 2721 public void updateDrawState(@NonNull TextPaint ds) {} 2722 } 2723 2724 private final class AudioPlaybackSpeedToggleListener implements PlaybackSpeedToggleTextView.PlaybackSpeedListener { 2725 @Override 2726 public void onPlaybackSpeedChanged(float speed) { 2727 if (eventListener == null || !audioViewStub.resolved()) { 2728 return; 2729 } 2730 2731 Uri uri = audioViewStub.get().getAudioSlideUri(); 2732 if (uri == null) { 2733 return; 2734 } 2735 2736 eventListener.onVoiceNotePlaybackSpeedChanged(uri, speed); 2737 } 2738 } 2739 2740 private final class AudioViewCallbacks implements AudioView.Callbacks { 2741 2742 @Override 2743 public void onPlay(@NonNull Uri audioUri, double progress) { 2744 if (eventListener == null) return; 2745 2746 eventListener.onVoiceNotePlay(audioUri, messageRecord.getId(), progress); 2747 } 2748 2749 @Override 2750 public void onPause(@NonNull Uri audioUri) { 2751 if (eventListener == null) return; 2752 2753 eventListener.onVoiceNotePause(audioUri); 2754 } 2755 2756 @Override 2757 public void onSeekTo(@NonNull Uri audioUri, double progress) { 2758 if (eventListener == null) return; 2759 2760 eventListener.onVoiceNoteSeekTo(audioUri, progress); 2761 } 2762 2763 @Override 2764 public void onStopAndReset(@NonNull Uri audioUri) { 2765 throw new UnsupportedOperationException(); 2766 } 2767 2768 @Override 2769 public void onSpeedChanged(float speed, boolean isPlaying) { 2770 footer.setAudioPlaybackSpeed(speed, isPlaying); 2771 } 2772 2773 @Override 2774 public void onProgressUpdated(long durationMillis, long playheadMillis) { 2775 footer.setAudioDuration(durationMillis, playheadMillis); 2776 } 2777 } 2778}