That fuck shit the fascists are using
1package org.tm.archive.components;
2
3
4import android.content.Context;
5import android.content.res.TypedArray;
6import android.graphics.Canvas;
7import android.text.TextUtils;
8import android.util.AttributeSet;
9import android.view.View;
10import android.view.ViewGroup;
11import android.widget.ImageView;
12import android.widget.TextView;
13
14import androidx.annotation.NonNull;
15import androidx.annotation.Nullable;
16import androidx.constraintlayout.widget.ConstraintLayout;
17
18import com.bumptech.glide.RequestManager;
19import com.bumptech.glide.load.engine.DiskCacheStrategy;
20import com.google.android.material.imageview.ShapeableImageView;
21import com.google.android.material.shape.CornerFamily;
22import com.google.android.material.shape.ShapeAppearanceModel;
23
24import org.signal.core.util.DimensionUnit;
25import org.signal.core.util.logging.Log;
26import org.tm.archive.R;
27import org.tm.archive.attachments.Attachment;
28import org.tm.archive.components.emoji.EmojiImageView;
29import org.tm.archive.components.emoji.EmojiTextView;
30import org.tm.archive.components.mention.MentionAnnotation;
31import org.tm.archive.components.quotes.QuoteViewColorTheme;
32import org.tm.archive.conversation.MessageStyler;
33import org.tm.archive.database.model.Mention;
34import org.tm.archive.database.model.databaseprotos.BodyRangeList;
35import org.tm.archive.mms.DecryptableStreamUriLoader.DecryptableUri;
36import org.tm.archive.mms.QuoteModel;
37import org.tm.archive.mms.Slide;
38import org.tm.archive.mms.SlideDeck;
39import org.tm.archive.recipients.LiveRecipient;
40import org.tm.archive.recipients.Recipient;
41import org.tm.archive.recipients.RecipientForeverObserver;
42import org.tm.archive.stories.StoryTextPostModel;
43import org.tm.archive.util.MediaUtil;
44import org.tm.archive.util.Projection;
45import org.tm.archive.util.Util;
46import org.tm.archive.util.views.Stub;
47
48import java.io.IOException;
49import java.util.List;
50
51public class QuoteView extends ConstraintLayout implements RecipientForeverObserver {
52
53 private static final String TAG = Log.tag(QuoteView.class);
54
55 public enum MessageType {
56 // These codes must match the values for the QuoteView_message_type XML attribute.
57 PREVIEW(0),
58 OUTGOING(1),
59 INCOMING(2),
60 STORY_REPLY_OUTGOING(3),
61 STORY_REPLY_INCOMING(4),
62 STORY_REPLY_PREVIEW(5);
63
64 private final int code;
65
66 MessageType(int code) {
67 this.code = code;
68 }
69
70 private static @NonNull MessageType fromCode(int code) {
71 for (MessageType value : values()) {
72 if (value.code == code) {
73 return value;
74 }
75 }
76
77 throw new IllegalArgumentException("Unsupported code " + code);
78 }
79 }
80
81 private TextView authorView;
82 private EmojiTextView bodyView;
83 private View quoteBarView;
84 private ShapeableImageView thumbnailView;
85 private Stub<View> attachmentVideoOVerlayStub;
86 private Stub<TextView> attachmentNameViewStub;
87 private Stub<ImageView> dismissStub;
88 private EmojiImageView missingStoryReaction;
89 private EmojiImageView storyReactionEmoji;
90
91 private long id;
92 private LiveRecipient author;
93 private CharSequence body;
94 private TextView mediaDescriptionText;
95 private Stub<TextView> missingLinkTextStub;
96 private SlideDeck attachments;
97 private MessageType messageType;
98 private int largeCornerRadius;
99 private int smallCornerRadius;
100 private CornerMask cornerMask;
101 private QuoteModel.Type quoteType;
102 private boolean isWallpaperEnabled;
103
104 private int thumbHeight;
105 private int thumbWidth;
106
107 public QuoteView(Context context) {
108 super(context);
109 initialize(null);
110 }
111
112 public QuoteView(Context context, AttributeSet attrs) {
113 super(context, attrs);
114 initialize(attrs);
115 }
116
117 public QuoteView(Context context, AttributeSet attrs, int defStyleAttr) {
118 super(context, attrs, defStyleAttr);
119 initialize(attrs);
120 }
121
122 public QuoteView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
123 super(context, attrs, defStyleAttr, defStyleRes);
124 initialize(attrs);
125 }
126
127 private void initialize(@Nullable AttributeSet attrs) {
128 inflate(getContext(), R.layout.v2_quote_view, this);
129
130 this.authorView = findViewById(R.id.quote_author);
131 this.bodyView = findViewById(R.id.quote_text);
132 this.quoteBarView = findViewById(R.id.quote_bar);
133 this.thumbnailView = findViewById(R.id.quote_thumbnail);
134 this.attachmentVideoOVerlayStub = new Stub<>(findViewById(R.id.quote_video_overlay_stub));
135 this.attachmentNameViewStub = new Stub<>(findViewById(R.id.quote_attachment_name_stub));
136 this.dismissStub = new Stub<>(findViewById(R.id.quote_dismiss_stub));
137 this.mediaDescriptionText = findViewById(R.id.media_type);
138 this.missingLinkTextStub = new Stub<>(findViewById(R.id.quote_missing_text_stub));
139 this.missingStoryReaction = findViewById(R.id.quote_missing_story_reaction_emoji);
140 this.storyReactionEmoji = findViewById(R.id.quote_story_reaction_emoji);
141 this.largeCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_large);
142 this.smallCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_bottom);
143
144 cornerMask = new CornerMask(this);
145
146 if (attrs != null) {
147 TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0);
148
149 messageType = MessageType.fromCode(typedArray.getInt(R.styleable.QuoteView_message_type, 0));
150 typedArray.recycle();
151
152 dismissStub.setVisibility(messageType == MessageType.PREVIEW ? VISIBLE : GONE);
153 }
154
155 setMessageType(messageType);
156 }
157
158 @Override
159 protected void dispatchDraw(Canvas canvas) {
160 super.dispatchDraw(canvas);
161 cornerMask.mask(canvas);
162 }
163
164 @Override
165 protected void onDetachedFromWindow() {
166 super.onDetachedFromWindow();
167 if (author != null) author.removeForeverObserver(this);
168 }
169
170 public void setMessageType(@NonNull MessageType messageType) {
171 this.messageType = messageType;
172
173 cornerMask.setRadii(largeCornerRadius, largeCornerRadius, smallCornerRadius, smallCornerRadius);
174 thumbWidth = thumbHeight = getResources().getDimensionPixelSize(R.dimen.quote_thumb_size);
175
176 if (messageType == MessageType.PREVIEW) {
177 int radius = getResources().getDimensionPixelOffset(R.dimen.quote_corner_radius_preview);
178 cornerMask.setTopLeftRadius(radius);
179 cornerMask.setTopRightRadius(radius);
180 } else if (isStoryReply()) {
181 thumbWidth = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_width);
182 thumbHeight = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_height);
183 }
184
185 ViewGroup.LayoutParams params = thumbnailView.getLayoutParams();
186 params.height = thumbHeight;
187 params.width = thumbWidth;
188
189 thumbnailView.setLayoutParams(params);
190 dismissStub.setVisibility(messageType == MessageType.PREVIEW ? View.VISIBLE : View.GONE);
191 if (dismissStub.resolved()) {
192 dismissStub.get().setOnClickListener(view -> setVisibility(GONE));
193 }
194 }
195
196 public void setQuote(RequestManager requestManager,
197 long id,
198 @NonNull Recipient author,
199 @Nullable CharSequence body,
200 boolean originalMissing,
201 @NonNull SlideDeck attachments,
202 @Nullable String storyReaction,
203 @NonNull QuoteModel.Type quoteType)
204 {
205 if (this.author != null) this.author.removeForeverObserver(this);
206
207 this.id = id;
208 this.author = author.live();
209 this.body = body;
210 this.attachments = attachments;
211 this.quoteType = quoteType;
212
213 this.author.observeForever(this);
214 setQuoteAuthor(author);
215 setQuoteText(resolveBody(body, quoteType), attachments, originalMissing, storyReaction);
216 setQuoteAttachment(requestManager, body, attachments, originalMissing);
217 setQuoteMissingFooter(originalMissing);
218 applyColorTheme();
219 }
220
221 private @Nullable CharSequence resolveBody(@Nullable CharSequence body, @NonNull QuoteModel.Type quoteType) {
222 return quoteType == QuoteModel.Type.GIFT_BADGE ? getContext().getString(R.string.QuoteView__donation_for_a_friend) : body;
223 }
224
225 public void setTopCornerSizes(boolean topLeftLarge, boolean topRightLarge) {
226 cornerMask.setTopLeftRadius(topLeftLarge ? largeCornerRadius : smallCornerRadius);
227 cornerMask.setTopRightRadius(topRightLarge ? largeCornerRadius : smallCornerRadius);
228 }
229
230 public void dismiss() {
231 if (this.author != null) this.author.removeForeverObserver(this);
232
233 this.id = 0;
234 this.author = null;
235 this.body = null;
236
237 setVisibility(GONE);
238 }
239
240 public void setWallpaperEnabled(boolean isWallpaperEnabled) {
241 this.isWallpaperEnabled = isWallpaperEnabled;
242 applyColorTheme();
243 }
244
245 @Override
246 public void onRecipientChanged(@NonNull Recipient recipient) {
247 setQuoteAuthor(recipient);
248 }
249 public @NonNull Projection.Corners getCorners() {
250 return new Projection.Corners(cornerMask.getRadii());
251 }
252
253 private void setQuoteAuthor(@NonNull Recipient author) {
254 if (isStoryReply()) {
255 authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_your_story)
256 : getContext().getString(R.string.QuoteView_s_story, author.getDisplayName(getContext())));
257 } else {
258 authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
259 : author.getDisplayName(getContext()));
260 }
261 }
262
263 private boolean isStoryReply() {
264 return messageType == MessageType.STORY_REPLY_OUTGOING ||
265 messageType == MessageType.STORY_REPLY_INCOMING ||
266 messageType == MessageType.STORY_REPLY_PREVIEW;
267 }
268
269 private void setQuoteText(@Nullable CharSequence body,
270 @NonNull SlideDeck attachments,
271 boolean originalMissing,
272 @Nullable String storyReaction)
273 {
274 if (originalMissing && isStoryReply()) {
275 bodyView.setVisibility(GONE);
276 storyReactionEmoji.setVisibility(View.GONE);
277 mediaDescriptionText.setVisibility(VISIBLE);
278
279 mediaDescriptionText.setText(R.string.QuoteView_no_longer_available);
280 if (storyReaction != null) {
281 missingStoryReaction.setVisibility(View.VISIBLE);
282 missingStoryReaction.setImageEmoji(storyReaction);
283 } else {
284 missingStoryReaction.setVisibility(View.GONE);
285 }
286 return;
287 }
288
289 if (storyReaction != null) {
290 storyReactionEmoji.setImageEmoji(storyReaction);
291 storyReactionEmoji.setVisibility(View.VISIBLE);
292 missingStoryReaction.setVisibility(View.INVISIBLE);
293 } else {
294 storyReactionEmoji.setVisibility(View.GONE);
295 missingStoryReaction.setVisibility(View.GONE);
296 }
297
298 StoryTextPostModel textPostModel = isStoryReply() ? getStoryTextPost(body) : null;
299 if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
300 if (textPostModel != null) {
301 try {
302 bodyView.setText(textPostModel.getText());
303 } catch (Exception e) {
304 Log.w(TAG, "Could not parse body of text post.", e);
305 bodyView.setText("");
306 }
307 } else {
308 bodyView.setText(body == null ? "" : body);
309 }
310
311 bodyView.setVisibility(VISIBLE);
312 mediaDescriptionText.setVisibility(GONE);
313 return;
314 }
315
316 bodyView.setVisibility(GONE);
317 mediaDescriptionText.setVisibility(VISIBLE);
318
319 Slide audioSlide = attachments.getSlides().stream().filter(Slide::hasAudio).findFirst().orElse(null);
320 Slide documentSlide = attachments.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
321 Slide imageSlide = attachments.getSlides().stream().filter(Slide::hasImage).findFirst().orElse(null);
322 Slide videoSlide = attachments.getSlides().stream().filter(Slide::hasVideo).findFirst().orElse(null);
323 Slide stickerSlide = attachments.getSlides().stream().filter(Slide::hasSticker).findFirst().orElse(null);
324 Slide viewOnceSlide = attachments.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
325
326 // Given that most types have images, we specifically check images last
327 if (viewOnceSlide != null) {
328 mediaDescriptionText.setText(R.string.QuoteView_view_once_media);
329 } else if (audioSlide != null) {
330 mediaDescriptionText.setText(R.string.QuoteView_audio);
331 } else if (documentSlide != null) {
332 mediaDescriptionText.setVisibility(GONE);
333 } else if (videoSlide != null) {
334 if (videoSlide.isVideoGif()) {
335 mediaDescriptionText.setText(R.string.QuoteView_gif);
336 } else {
337 mediaDescriptionText.setText(R.string.QuoteView_video);
338 }
339 } else if (stickerSlide != null) {
340 mediaDescriptionText.setText(R.string.QuoteView_sticker);
341 } else if (imageSlide != null) {
342 if (MediaUtil.isGif(imageSlide.getContentType())) {
343 mediaDescriptionText.setText(R.string.QuoteView_gif);
344 } else {
345 mediaDescriptionText.setText(R.string.QuoteView_photo);
346 }
347 }
348 }
349
350 private void setQuoteAttachment(@NonNull RequestManager requestManager, @NonNull CharSequence body, @NonNull SlideDeck slideDeck, boolean originalMissing) {
351 boolean outgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING;
352 boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
353
354 if (isStoryReply() && originalMissing) {
355 thumbnailView.setVisibility(GONE);
356 attachmentVideoOVerlayStub.setVisibility(GONE);
357 attachmentNameViewStub.setVisibility(GONE);
358 return;
359 }
360
361 // TODO [alex] -- do we need this? mainView.setMinimumHeight(isStoryReply() && originalMissing ? 0 : thumbHeight);
362 thumbnailView.setPadding(0, 0, 0, 0);
363
364 StoryTextPostModel model = isStoryReply() ? getStoryTextPost(body) : null;
365 if (model != null) {
366 attachmentVideoOVerlayStub.setVisibility(GONE);
367 attachmentNameViewStub.setVisibility(GONE);
368 thumbnailView.setVisibility(VISIBLE);
369 requestManager.load(model)
370 .centerCrop()
371 .override(thumbWidth, thumbHeight)
372 .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
373 .into(thumbnailView);
374 return;
375 }
376
377 if (quoteType == QuoteModel.Type.GIFT_BADGE) {
378 if (outgoing && !preview) {
379 int oneDp = (int) DimensionUnit.DP.toPixels(1);
380 thumbnailView.setPadding(oneDp, oneDp, oneDp, oneDp);
381 thumbnailView.setShapeAppearanceModel(buildShapeAppearanceForLayoutDirection());
382 }
383
384 attachmentVideoOVerlayStub.setVisibility(GONE);
385 attachmentNameViewStub.setVisibility(GONE);
386 thumbnailView.setVisibility(VISIBLE);
387 requestManager.load(R.drawable.ic_gift_thumbnail)
388 .centerCrop()
389 .override(thumbWidth, thumbHeight)
390 .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
391 .into(thumbnailView);
392 return;
393 }
394
395 Slide imageVideoSlide = slideDeck.getSlides().stream().filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).findFirst().orElse(null);
396 Slide documentSlide = slideDeck.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
397 Slide viewOnceSlide = slideDeck.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
398
399 attachmentVideoOVerlayStub.setVisibility(GONE);
400
401 if (viewOnceSlide != null) {
402 thumbnailView.setVisibility(GONE);
403 attachmentNameViewStub.setVisibility(GONE);
404 } else if (imageVideoSlide != null && imageVideoSlide.getUri() != null) {
405 thumbnailView.setVisibility(VISIBLE);
406 attachmentNameViewStub.setVisibility(GONE);
407
408 if (dismissStub.resolved()) {
409 dismissStub.get().setBackgroundResource(R.drawable.dismiss_background);
410 }
411 if (imageVideoSlide.hasVideo() && !imageVideoSlide.isVideoGif()) {
412 attachmentVideoOVerlayStub.setVisibility(VISIBLE);
413 }
414 requestManager.load(new DecryptableUri(imageVideoSlide.getUri()))
415 .centerCrop()
416 .override(thumbWidth, thumbHeight)
417 .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
418 .into(thumbnailView);
419 } else if (documentSlide != null){
420 thumbnailView.setVisibility(GONE);
421 attachmentNameViewStub.setVisibility(VISIBLE);
422 attachmentNameViewStub.get().setText(documentSlide.getFileName().orElse(""));
423 } else {
424 thumbnailView.setVisibility(GONE);
425 attachmentNameViewStub.setVisibility(GONE);
426
427 if (dismissStub.resolved()) {
428 dismissStub.get().setBackground(null);
429 }
430 }
431 }
432
433 private void setQuoteMissingFooter(boolean missing) {
434 missingLinkTextStub.setVisibility(missing && !isStoryReply() ? VISIBLE : GONE);
435 }
436
437 private @Nullable StoryTextPostModel getStoryTextPost(@Nullable CharSequence body) {
438 if (Util.isEmpty(body)) {
439 return null;
440 }
441
442 try {
443 return StoryTextPostModel.parseFrom(body.toString(), id, author.getId(), MessageStyler.getStyling(body));
444 } catch (IOException ioException) {
445 return null;
446 }
447 }
448
449 public void setTextSize(int unit, float size) {
450 bodyView.setTextSize(unit, size);
451 }
452
453 public long getQuoteId() {
454 return id;
455 }
456
457 public Recipient getAuthor() {
458 return author.get();
459 }
460
461 public CharSequence getBody() {
462 return body;
463 }
464
465 public List<Attachment> getAttachments() {
466 return attachments.asAttachments();
467 }
468
469 public @NonNull QuoteModel.Type getQuoteType() {
470 return quoteType;
471 }
472
473 public @NonNull List<Mention> getMentions() {
474 return MentionAnnotation.getMentionsFromAnnotations(body);
475 }
476
477 public @Nullable BodyRangeList getBodyRanges() {
478 return MessageStyler.getStyling(body);
479 }
480
481 private @NonNull ShapeAppearanceModel buildShapeAppearanceForLayoutDirection() {
482 int fourDp = (int) DimensionUnit.DP.toPixels(4);
483 if (getLayoutDirection() == LAYOUT_DIRECTION_LTR) {
484 return ShapeAppearanceModel.builder()
485 .setTopRightCorner(CornerFamily.ROUNDED, fourDp)
486 .setBottomRightCorner(CornerFamily.ROUNDED, fourDp)
487 .build();
488 } else {
489 return ShapeAppearanceModel.builder()
490 .setTopLeftCorner(CornerFamily.ROUNDED, fourDp)
491 .setBottomLeftCorner(CornerFamily.ROUNDED, fourDp)
492 .build();
493 }
494 }
495
496 private void applyColorTheme() {
497 boolean isOutgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING;
498 boolean isPreview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
499
500 QuoteViewColorTheme quoteViewColorTheme = QuoteViewColorTheme.resolveTheme(isOutgoing, isPreview, isWallpaperEnabled);
501
502 quoteBarView.setBackgroundColor(quoteViewColorTheme.getBarColor(getContext()));
503 setBackgroundColor(quoteViewColorTheme.getBackgroundColor(getContext()));
504 authorView.setTextColor(quoteViewColorTheme.getForegroundColor(getContext()));
505 bodyView.setTextColor(quoteViewColorTheme.getForegroundColor(getContext()));
506
507 if (attachmentNameViewStub.resolved()) {
508 attachmentNameViewStub.get().setTextColor(quoteViewColorTheme.getForegroundColor(getContext()));
509 }
510 mediaDescriptionText.setTextColor(quoteViewColorTheme.getForegroundColor(getContext()));
511
512 if (missingLinkTextStub.resolved()) {
513 missingLinkTextStub.get().setTextColor(quoteViewColorTheme.getForegroundColor(getContext()));
514 missingLinkTextStub.get().setBackgroundColor(quoteViewColorTheme.getBackgroundColor(getContext()));
515 }
516 }
517}