That fuck shit the fascists are using
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}