That fuck shit the fascists are using
at master 950 lines 39 kB view raw
1package org.tm.archive.conversation; 2 3import android.animation.Animator; 4import android.animation.AnimatorSet; 5import android.animation.ObjectAnimator; 6import android.animation.ValueAnimator; 7import android.app.Activity; 8import android.content.Context; 9import android.content.res.Configuration; 10import android.graphics.Bitmap; 11import android.graphics.PointF; 12import android.graphics.Rect; 13import android.graphics.drawable.BitmapDrawable; 14import android.os.Build; 15import android.util.AttributeSet; 16import android.view.HapticFeedbackConstants; 17import android.view.MotionEvent; 18import android.view.View; 19import android.view.Window; 20import android.view.animation.DecelerateInterpolator; 21import android.view.animation.Interpolator; 22import android.widget.FrameLayout; 23 24import androidx.annotation.NonNull; 25import androidx.annotation.Nullable; 26import androidx.constraintlayout.widget.ConstraintLayout; 27import androidx.constraintlayout.widget.ConstraintSet; 28import androidx.core.content.ContextCompat; 29import androidx.core.view.ViewKt; 30import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat; 31 32import com.annimon.stream.Stream; 33 34import org.signal.core.util.DimensionUnit; 35import org.tm.archive.R; 36import org.tm.archive.animation.AnimationCompleteListener; 37import org.tm.archive.components.emoji.EmojiImageView; 38import org.tm.archive.components.emoji.EmojiUtil; 39import org.tm.archive.components.menu.ActionItem; 40import org.tm.archive.database.model.MessageRecord; 41import org.tm.archive.database.model.ReactionRecord; 42import org.tm.archive.keyvalue.SignalStore; 43import org.tm.archive.recipients.Recipient; 44import org.tm.archive.util.ThemeUtil; 45import org.tm.archive.util.Util; 46import org.tm.archive.util.ViewUtil; 47import org.tm.archive.util.WindowUtil; 48 49import java.util.ArrayList; 50import java.util.List; 51 52import kotlin.Unit; 53 54public final class ConversationReactionOverlay extends FrameLayout { 55 56 private static final Interpolator INTERPOLATOR = new DecelerateInterpolator(); 57 58 private final Rect emojiViewGlobalRect = new Rect(); 59 private final Rect emojiStripViewBounds = new Rect(); 60 private float segmentSize; 61 62 private final Boundary horizontalEmojiBoundary = new Boundary(); 63 private final Boundary verticalScrubBoundary = new Boundary(); 64 private final PointF deadzoneTouchPoint = new PointF(); 65 66 private Activity activity; 67 private Recipient conversationRecipient; 68 private MessageRecord messageRecord; 69 private SelectedConversationModel selectedConversationModel; 70 private OverlayState overlayState = OverlayState.HIDDEN; 71 private boolean isNonAdminInAnnouncementGroup; 72 73 private boolean downIsOurs; 74 private int selected = -1; 75 private int customEmojiIndex; 76 private int originalStatusBarColor; 77 private int originalNavigationBarColor; 78 79 private View dropdownAnchor; 80 private View toolbarShade; 81 private View inputShade; 82 private View conversationItem; 83 private View backgroundView; 84 private ConstraintLayout foregroundView; 85 private View selectedView; 86 private EmojiImageView[] emojiViews; 87 88 private ConversationContextMenu contextMenu; 89 90 private float touchDownDeadZoneSize; 91 private float distanceFromTouchDownPointToBottomOfScrubberDeadZone; 92 private int scrubberWidth; 93 private int selectedVerticalTranslation; 94 private int scrubberHorizontalMargin; 95 private int animationEmojiStartDelayFactor; 96 private int statusBarHeight; 97 private int bottomNavigationBarHeight; 98 99 private OnReactionSelectedListener onReactionSelectedListener; 100 private OnActionSelectedListener onActionSelectedListener; 101 private OnHideListener onHideListener; 102 103 private AnimatorSet revealAnimatorSet = new AnimatorSet(); 104 private AnimatorSet hideAnimatorSet = new AnimatorSet(); 105 106 public ConversationReactionOverlay(@NonNull Context context) { 107 super(context); 108 } 109 110 public ConversationReactionOverlay(@NonNull Context context, @Nullable AttributeSet attrs) { 111 super(context, attrs); 112 } 113 114 @Override 115 protected void onFinishInflate() { 116 super.onFinishInflate(); 117 118 dropdownAnchor = findViewById(R.id.dropdown_anchor); 119 toolbarShade = findViewById(R.id.toolbar_shade); 120 inputShade = findViewById(R.id.input_shade); 121 conversationItem = findViewById(R.id.conversation_item); 122 backgroundView = findViewById(R.id.conversation_reaction_scrubber_background); 123 foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground); 124 selectedView = findViewById(R.id.conversation_reaction_current_selection_indicator); 125 126 emojiViews = new EmojiImageView[] { findViewById(R.id.reaction_1), 127 findViewById(R.id.reaction_2), 128 findViewById(R.id.reaction_3), 129 findViewById(R.id.reaction_4), 130 findViewById(R.id.reaction_5), 131 findViewById(R.id.reaction_6), 132 findViewById(R.id.reaction_7) }; 133 134 customEmojiIndex = emojiViews.length - 1; 135 136 distanceFromTouchDownPointToBottomOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom); 137 138 touchDownDeadZoneSize = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_touch_deadzone_size); 139 scrubberWidth = getResources().getDimensionPixelOffset(R.dimen.reaction_scrubber_width); 140 selectedVerticalTranslation = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation); 141 scrubberHorizontalMargin = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin); 142 143 animationEmojiStartDelayFactor = getResources().getInteger(R.integer.reaction_scrubber_emoji_reveal_duration_start_delay_factor); 144 145 initAnimators(); 146 } 147 148 public void show(@NonNull Activity activity, 149 @NonNull Recipient conversationRecipient, 150 @NonNull ConversationMessage conversationMessage, 151 @NonNull PointF lastSeenDownPoint, 152 boolean isNonAdminInAnnouncementGroup, 153 @NonNull SelectedConversationModel selectedConversationModel) 154 { 155 if (overlayState != OverlayState.HIDDEN) { 156 return; 157 } 158 159 this.messageRecord = conversationMessage.getMessageRecord(); 160 this.conversationRecipient = conversationRecipient; 161 this.selectedConversationModel = selectedConversationModel; 162 this.isNonAdminInAnnouncementGroup = isNonAdminInAnnouncementGroup; 163 overlayState = OverlayState.UNINITAILIZED; 164 selected = -1; 165 166 setupSelectedEmoji(); 167 168 View statusBarBackground = activity.findViewById(android.R.id.statusBarBackground); 169 statusBarHeight = statusBarBackground == null ? 0 : statusBarBackground.getHeight(); 170 171 View navigationBarBackground = activity.findViewById(android.R.id.navigationBarBackground); 172 bottomNavigationBarHeight = navigationBarBackground == null ? 0 : navigationBarBackground.getHeight(); 173 174 if (zeroNavigationBarHeightForConfiguration()) { 175 bottomNavigationBarHeight = 0; 176 } 177 178 Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap(); 179 180 conversationItem.setLayoutParams(new LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight())); 181 conversationItem.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot)); 182 183 boolean isMessageOnLeft = selectedConversationModel.isOutgoing() ^ ViewUtil.isLtr(this); 184 185 conversationItem.setScaleX(ConversationItem.LONG_PRESS_SCALE_FACTOR); 186 conversationItem.setScaleY(ConversationItem.LONG_PRESS_SCALE_FACTOR); 187 188 setVisibility(View.INVISIBLE); 189 190 this.activity = activity; 191 updateSystemUiOnShow(activity); 192 193 ViewKt.doOnLayout(this, v -> { 194 showAfterLayout(activity, conversationMessage, lastSeenDownPoint, isMessageOnLeft); 195 return Unit.INSTANCE; 196 }); 197 } 198 199 private void showAfterLayout(@NonNull Activity activity, 200 @NonNull ConversationMessage conversationMessage, 201 @NonNull PointF lastSeenDownPoint, 202 boolean isMessageOnLeft) { 203 updateToolbarShade(); 204 updateInputShade(); 205 206 contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(conversationMessage)); 207 208 conversationItem.setX(selectedConversationModel.getSnapshotMetrics().getSnapshotOffset()); 209 conversationItem.setY(selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() - statusBarHeight); 210 211 Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap(); 212 boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth(); 213 214 int overlayHeight = getHeight() - bottomNavigationBarHeight; 215 int bubbleWidth = selectedConversationModel.getBubbleWidth(); 216 217 float endX = selectedConversationModel.getSnapshotMetrics().getSnapshotOffset(); 218 float endY = conversationItem.getY(); 219 float endApparentTop = endY; 220 float endScale = 1f; 221 222 float menuPadding = DimensionUnit.DP.toPixels(12f); 223 float reactionBarTopPadding = DimensionUnit.DP.toPixels(32f); 224 int reactionBarHeight = backgroundView.getHeight(); 225 226 float reactionBarBackgroundY; 227 228 if (isWideLayout) { 229 boolean everythingFitsVertically = reactionBarHeight + menuPadding + reactionBarTopPadding + conversationItemSnapshot.getHeight() < overlayHeight; 230 if (everythingFitsVertically) { 231 boolean reactionBarFitsAboveItem = conversationItem.getY() > reactionBarHeight + menuPadding + reactionBarTopPadding; 232 233 if (reactionBarFitsAboveItem) { 234 reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight; 235 } else { 236 endY = reactionBarHeight + menuPadding + reactionBarTopPadding; 237 reactionBarBackgroundY = reactionBarTopPadding; 238 } 239 } else { 240 float spaceAvailableForItem = overlayHeight - reactionBarHeight - menuPadding - reactionBarTopPadding; 241 242 endScale = spaceAvailableForItem / conversationItem.getHeight(); 243 endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1); 244 endY = reactionBarHeight + menuPadding + reactionBarTopPadding - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale); 245 reactionBarBackgroundY = reactionBarTopPadding; 246 } 247 } else { 248 float reactionBarOffset = DimensionUnit.DP.toPixels(48); 249 float spaceForReactionBar = Math.max(reactionBarHeight + reactionBarOffset - conversationItemSnapshot.getHeight(), 0); 250 boolean everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.getHeight() + menuPadding + spaceForReactionBar < overlayHeight; 251 252 if (everythingFitsVertically) { 253 float bubbleBottom = selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight(); 254 boolean menuFitsBelowItem = bubbleBottom + menuPadding + contextMenu.getMaxHeight() <= overlayHeight + statusBarHeight; 255 256 if (menuFitsBelowItem) { 257 if (conversationItem.getY() < 0) { 258 endY = 0; 259 } 260 float contextMenuTop = endY + conversationItemSnapshot.getHeight(); 261 reactionBarBackgroundY = getReactionBarOffsetForTouch(lastSeenDownPoint, contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY); 262 263 if (reactionBarBackgroundY <= reactionBarTopPadding) { 264 endY = backgroundView.getHeight() + menuPadding + reactionBarTopPadding; 265 } 266 } else { 267 endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight(); 268 269 float contextMenuTop = endY + conversationItemSnapshot.getHeight(); 270 reactionBarBackgroundY = getReactionBarOffsetForTouch(lastSeenDownPoint, contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY); 271 } 272 273 endApparentTop = endY; 274 } else if (reactionBarOffset + reactionBarHeight + contextMenu.getMaxHeight() + menuPadding < overlayHeight) { 275 float spaceAvailableForItem = (float) overlayHeight - contextMenu.getMaxHeight() - menuPadding - spaceForReactionBar; 276 277 endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight(); 278 endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1); 279 endY = spaceForReactionBar - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale); 280 281 float contextMenuTop = endY + (conversationItemSnapshot.getHeight() * endScale); 282 reactionBarBackgroundY = getReactionBarOffsetForTouch(lastSeenDownPoint, contextMenuTop + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale), menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY); 283 endApparentTop = endY + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale); 284 } else { 285 contextMenu.setHeight(contextMenu.getMaxHeight() / 2); 286 287 int menuHeight = contextMenu.getHeight(); 288 boolean fitsVertically = menuHeight + conversationItem.getHeight() + menuPadding * 2 + reactionBarHeight + reactionBarTopPadding < overlayHeight; 289 290 if (fitsVertically) { 291 float bubbleBottom = selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight(); 292 boolean menuFitsBelowItem = bubbleBottom + menuPadding + menuHeight <= overlayHeight + statusBarHeight; 293 294 if (menuFitsBelowItem) { 295 reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight; 296 297 if (reactionBarBackgroundY < reactionBarTopPadding) { 298 endY = reactionBarTopPadding + reactionBarHeight + menuPadding; 299 reactionBarBackgroundY = reactionBarTopPadding; 300 } 301 } else { 302 endY = overlayHeight - menuHeight - menuPadding - conversationItemSnapshot.getHeight(); 303 reactionBarBackgroundY = endY - reactionBarHeight - menuPadding; 304 } 305 endApparentTop = endY; 306 } else { 307 float spaceAvailableForItem = (float) overlayHeight - menuHeight - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding; 308 309 endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight(); 310 endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1); 311 endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale) + menuPadding + reactionBarTopPadding; 312 reactionBarBackgroundY = reactionBarTopPadding; 313 endApparentTop = reactionBarHeight + menuPadding + reactionBarTopPadding; 314 } 315 } 316 } 317 318 reactionBarBackgroundY = Math.max(reactionBarBackgroundY, -statusBarHeight); 319 320 hideAnimatorSet.end(); 321 setVisibility(View.VISIBLE); 322 323 float scrubberX; 324 if (isMessageOnLeft) { 325 scrubberX = scrubberHorizontalMargin; 326 } else { 327 scrubberX = getWidth() - scrubberWidth - scrubberHorizontalMargin; 328 } 329 330 foregroundView.setX(scrubberX); 331 foregroundView.setY(reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.getHeight() / 2f); 332 333 backgroundView.setX(scrubberX); 334 backgroundView.setY(reactionBarBackgroundY); 335 336 verticalScrubBoundary.update(reactionBarBackgroundY, 337 lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone); 338 339 updateBoundsOnLayoutChanged(); 340 341 revealAnimatorSet.start(); 342 343 if (isWideLayout) { 344 float scrubberRight = scrubberX + scrubberWidth; 345 float offsetX = isMessageOnLeft ? scrubberRight + menuPadding : scrubberX - contextMenu.getMaxWidth() - menuPadding; 346 contextMenu.show((int) offsetX, (int) Math.min(backgroundView.getY(), overlayHeight - contextMenu.getMaxHeight())); 347 } else { 348 float contentX = selectedConversationModel.getSnapshotMetrics().getContextMenuPadding(); 349 float offsetX = isMessageOnLeft ? contentX : -contextMenu.getMaxWidth() + contentX + bubbleWidth; 350 351 float menuTop = endApparentTop + (conversationItemSnapshot.getHeight() * endScale); 352 contextMenu.show((int) offsetX, (int) (menuTop + menuPadding)); 353 } 354 355 int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration); 356 357 conversationItem.animate() 358 .x(endX) 359 .y(endY) 360 .scaleX(endScale) 361 .scaleY(endScale) 362 .setDuration(revealDuration); 363 } 364 365 private float getReactionBarOffsetForTouch(@NonNull PointF touchPoint, 366 float contextMenuTop, 367 float contextMenuPadding, 368 float reactionBarOffset, 369 int reactionBarHeight, 370 float spaceNeededBetweenTopOfScreenAndTopOfReactionBar, 371 float messageTop) 372 { 373 float adjustedTouchY = touchPoint.y - statusBarHeight; 374 float reactionStartingPoint = Math.min(adjustedTouchY, contextMenuTop); 375 376 float spaceBetweenTopOfMessageAndTopOfContextMenu = Math.abs(messageTop - contextMenuTop); 377 378 if (spaceBetweenTopOfMessageAndTopOfContextMenu < DimensionUnit.DP.toPixels(150)) { 379 float offsetToMakeReactionBarOffsetMatchMenuPadding = reactionBarOffset - contextMenuPadding; 380 reactionStartingPoint = messageTop + offsetToMakeReactionBarOffsetMatchMenuPadding; 381 } 382 383 return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar); 384 } 385 386 private void updateToolbarShade() { 387 LayoutParams layoutParams = (LayoutParams) toolbarShade.getLayoutParams(); 388 layoutParams.height = 0; 389 toolbarShade.setLayoutParams(layoutParams); 390 } 391 392 private void updateInputShade() { 393 LayoutParams layoutParams = (LayoutParams) inputShade.getLayoutParams(); 394 layoutParams.height = 0; 395 inputShade.setLayoutParams(layoutParams); 396 } 397 398 /** 399 * Returns true when the device is in a configuration where the navigation bar doesn't take up 400 * space at the bottom of the screen. 401 */ 402 private boolean zeroNavigationBarHeightForConfiguration() { 403 boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; 404 405 if (Build.VERSION.SDK_INT >= 29) { 406 return getRootWindowInsets().getSystemGestureInsets().bottom == 0 && isLandscape; 407 } else { 408 return isLandscape; 409 } 410 } 411 412 private void updateSystemUiOnShow(@NonNull Activity activity) { 413 Window window = activity.getWindow(); 414 int barColor = ContextCompat.getColor(getContext(), R.color.conversation_item_selected_system_ui); 415 416 originalStatusBarColor = window.getStatusBarColor(); 417 WindowUtil.setStatusBarColor(window, barColor); 418 419 originalNavigationBarColor = window.getNavigationBarColor(); 420 WindowUtil.setNavigationBarColor(activity, barColor); 421 422 if (!ThemeUtil.isDarkTheme(getContext())) { 423 WindowUtil.clearLightStatusBar(window); 424 WindowUtil.clearLightNavigationBar(window); 425 } 426 } 427 428 public void hide() { 429 hideInternal(onHideListener); 430 } 431 432 public void hideForReactWithAny() { 433 hideInternal(onHideListener); 434 } 435 436 private void hideInternal(@Nullable OnHideListener onHideListener) { 437 overlayState = OverlayState.HIDDEN; 438 439 AnimatorSet animatorSet = newHideAnimatorSet(); 440 hideAnimatorSet = animatorSet; 441 442 revealAnimatorSet.end(); 443 animatorSet.start(); 444 445 if (onHideListener != null) { 446 onHideListener.startHide(selectedConversationModel.getFocusedView()); 447 } 448 449 animatorSet.addListener(new AnimationCompleteListener() { 450 @Override public void onAnimationEnd(Animator animation) { 451 animatorSet.removeListener(this); 452 453 toolbarShade.setVisibility(INVISIBLE); 454 inputShade.setVisibility(INVISIBLE); 455 456 if (onHideListener != null) { 457 onHideListener.onHide(); 458 } 459 } 460 }); 461 462 if (contextMenu != null) { 463 contextMenu.dismiss(); 464 } 465 } 466 467 public boolean isShowing() { 468 return overlayState != OverlayState.HIDDEN; 469 } 470 471 public @NonNull MessageRecord getMessageRecord() { 472 return messageRecord; 473 } 474 475 @Override 476 protected void onLayout(boolean changed, int l, int t, int r, int b) { 477 super.onLayout(changed, l, t, r, b); 478 479 updateBoundsOnLayoutChanged(); 480 } 481 482 private void updateBoundsOnLayoutChanged() { 483 backgroundView.getGlobalVisibleRect(emojiStripViewBounds); 484 emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect); 485 emojiStripViewBounds.left = getStart(emojiViewGlobalRect); 486 emojiViews[emojiViews.length - 1].getGlobalVisibleRect(emojiViewGlobalRect); 487 emojiStripViewBounds.right = getEnd(emojiViewGlobalRect); 488 489 segmentSize = emojiStripViewBounds.width() / (float) emojiViews.length; 490 } 491 492 private int getStart(@NonNull Rect rect) { 493 if (ViewUtil.isLtr(this)) { 494 return rect.left; 495 } else { 496 return rect.right; 497 } 498 } 499 500 private int getEnd(@NonNull Rect rect) { 501 if (ViewUtil.isLtr(this)) { 502 return rect.right; 503 } else { 504 return rect.left; 505 } 506 } 507 508 public boolean applyTouchEvent(@NonNull MotionEvent motionEvent) { 509 if (!isShowing()) { 510 throw new IllegalStateException("Touch events should only be propagated to this method if we are displaying the scrubber."); 511 } 512 513 if ((motionEvent.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) != 0) { 514 return true; 515 } 516 517 if (overlayState == OverlayState.UNINITAILIZED) { 518 downIsOurs = false; 519 520 deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY()); 521 522 overlayState = OverlayState.DEADZONE; 523 } 524 525 if (overlayState == OverlayState.DEADZONE) { 526 float deltaX = Math.abs(deadzoneTouchPoint.x - motionEvent.getX()); 527 float deltaY = Math.abs(deadzoneTouchPoint.y - motionEvent.getY()); 528 529 if (deltaX > touchDownDeadZoneSize || deltaY > touchDownDeadZoneSize) { 530 overlayState = OverlayState.SCRUB; 531 } else { 532 if (motionEvent.getAction() == MotionEvent.ACTION_UP) { 533 overlayState = OverlayState.TAP; 534 535 if (downIsOurs) { 536 handleUpEvent(); 537 return true; 538 } 539 } 540 541 return MotionEvent.ACTION_MOVE == motionEvent.getAction(); 542 } 543 } 544 545 switch (motionEvent.getAction()) { 546 case MotionEvent.ACTION_DOWN: 547 selected = getSelectedIndexViaDownEvent(motionEvent); 548 549 deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY()); 550 overlayState = OverlayState.DEADZONE; 551 downIsOurs = true; 552 return true; 553 case MotionEvent.ACTION_MOVE: 554 selected = getSelectedIndexViaMoveEvent(motionEvent); 555 return true; 556 case MotionEvent.ACTION_UP: 557 handleUpEvent(); 558 return downIsOurs; 559 case MotionEvent.ACTION_CANCEL: 560 hide(); 561 return downIsOurs; 562 default: 563 return false; 564 } 565 } 566 567 private void setupSelectedEmoji() { 568 final List<String> emojis = SignalStore.emojiValues().getReactions(); 569 final String oldEmoji = getOldEmoji(messageRecord); 570 571 if (oldEmoji == null) { 572 selectedView.setVisibility(View.GONE); 573 } 574 575 boolean foundSelected = false; 576 577 for (int i = 0; i < emojiViews.length; i++) { 578 final EmojiImageView view = emojiViews[i]; 579 580 view.setScaleX(1.0f); 581 view.setScaleY(1.0f); 582 view.setTranslationY(0); 583 584 boolean isAtCustomIndex = i == customEmojiIndex; 585 boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && oldEmoji != null && EmojiUtil.isCanonicallyEqual(emojis.get(i), oldEmoji); 586 boolean isAtCustomIndexAndOldEmojiExists = isAtCustomIndex && oldEmoji != null; 587 588 if (!foundSelected && 589 (isNotAtCustomIndexAndOldEmojiMatches || isAtCustomIndexAndOldEmojiExists)) 590 { 591 foundSelected = true; 592 selectedView.setVisibility(View.VISIBLE); 593 594 ConstraintSet constraintSet = new ConstraintSet(); 595 constraintSet.clone(foregroundView); 596 constraintSet.clear(selectedView.getId(), ConstraintSet.LEFT); 597 constraintSet.clear(selectedView.getId(), ConstraintSet.RIGHT); 598 constraintSet.connect(selectedView.getId(), ConstraintSet.LEFT, view.getId(), ConstraintSet.LEFT); 599 constraintSet.connect(selectedView.getId(), ConstraintSet.RIGHT, view.getId(), ConstraintSet.RIGHT); 600 constraintSet.applyTo(foregroundView); 601 602 if (isAtCustomIndex) { 603 view.setImageEmoji(oldEmoji); 604 view.setTag(oldEmoji); 605 } else { 606 view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(emojis.get(i))); 607 } 608 } else if (isAtCustomIndex) { 609 view.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_any_emoji_32)); 610 view.setTag(null); 611 } else { 612 view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(emojis.get(i))); 613 } 614 } 615 } 616 617 private int getSelectedIndexViaDownEvent(@NonNull MotionEvent motionEvent) { 618 return getSelectedIndexViaMotionEvent(motionEvent, new Boundary(emojiStripViewBounds.top, emojiStripViewBounds.bottom)); 619 } 620 621 private int getSelectedIndexViaMoveEvent(@NonNull MotionEvent motionEvent) { 622 return getSelectedIndexViaMotionEvent(motionEvent, verticalScrubBoundary); 623 } 624 625 private int getSelectedIndexViaMotionEvent(@NonNull MotionEvent motionEvent, @NonNull Boundary boundary) { 626 int selected = -1; 627 628 if (backgroundView.getVisibility() != View.VISIBLE) { 629 return selected; 630 } 631 632 for (int i = 0; i < emojiViews.length; i++) { 633 final float emojiLeft = (segmentSize * i) + emojiStripViewBounds.left; 634 horizontalEmojiBoundary.update(emojiLeft, emojiLeft + segmentSize); 635 636 if (horizontalEmojiBoundary.contains(motionEvent.getX()) && boundary.contains(motionEvent.getY())) { 637 selected = i; 638 } 639 } 640 641 if (this.selected != -1 && this.selected != selected) { 642 shrinkView(emojiViews[this.selected]); 643 } 644 645 if (this.selected != selected && selected != -1) { 646 growView(emojiViews[selected]); 647 } 648 649 return selected; 650 } 651 652 private void growView(@NonNull View view) { 653 view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP); 654 view.animate() 655 .scaleY(1.5f) 656 .scaleX(1.5f) 657 .translationY(-selectedVerticalTranslation) 658 .setDuration(200) 659 .setInterpolator(INTERPOLATOR) 660 .start(); 661 } 662 663 private void shrinkView(@NonNull View view) { 664 view.animate() 665 .scaleX(1.0f) 666 .scaleY(1.0f) 667 .translationY(0) 668 .setDuration(200) 669 .setInterpolator(INTERPOLATOR) 670 .start(); 671 } 672 673 private void handleUpEvent() { 674 if (selected != -1 && onReactionSelectedListener != null && backgroundView.getVisibility() == View.VISIBLE) { 675 if (selected == customEmojiIndex) { 676 onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null); 677 } else { 678 onReactionSelectedListener.onReactionSelected(messageRecord, SignalStore.emojiValues().getPreferredVariation(SignalStore.emojiValues().getReactions().get(selected))); 679 } 680 } else { 681 hide(); 682 } 683 } 684 685 public void setOnReactionSelectedListener(@Nullable OnReactionSelectedListener onReactionSelectedListener) { 686 this.onReactionSelectedListener = onReactionSelectedListener; 687 } 688 689 public void setOnActionSelectedListener(@Nullable OnActionSelectedListener onActionSelectedListener) { 690 this.onActionSelectedListener = onActionSelectedListener; 691 } 692 693 public void setOnHideListener(@Nullable OnHideListener onHideListener) { 694 this.onHideListener = onHideListener; 695 } 696 697 private static @Nullable String getOldEmoji(@NonNull MessageRecord messageRecord) { 698 return Stream.of(messageRecord.getReactions()) 699 .filter(record -> record.getAuthor() 700 .serialize() 701 .equals(Recipient.self() 702 .getId() 703 .serialize())) 704 .findFirst() 705 .map(ReactionRecord::getEmoji) 706 .orElse(null); 707 } 708 709 private @NonNull List<ActionItem> getMenuActionItems(@NonNull ConversationMessage conversationMessage) { 710 MenuState menuState = MenuState.getMenuState(conversationRecipient, conversationMessage.getMultiselectCollection().toSet(), false, isNonAdminInAnnouncementGroup); 711 712 List<ActionItem> items = new ArrayList<>(); 713 714 if (menuState.shouldShowReplyAction()) { 715 items.add(new ActionItem(R.drawable.symbol_reply_24, getResources().getString(R.string.conversation_selection__menu_reply), () -> handleActionItemClicked(Action.REPLY))); 716 } 717 718 if (menuState.shouldShowEditAction()) { 719 items.add(new ActionItem(R.drawable.symbol_edit_24, getResources().getString(R.string.conversation_selection__menu_edit), () -> handleActionItemClicked(Action.EDIT))); 720 } 721 722 if (menuState.shouldShowForwardAction()) { 723 items.add(new ActionItem(R.drawable.symbol_forward_24, getResources().getString(R.string.conversation_selection__menu_forward), () -> handleActionItemClicked(Action.FORWARD))); 724 } 725 726 if (menuState.shouldShowResendAction()) { 727 items.add(new ActionItem(R.drawable.symbol_refresh_24, getResources().getString(R.string.conversation_selection__menu_resend_message), () -> handleActionItemClicked(Action.RESEND))); 728 } 729 730 if (menuState.shouldShowSaveAttachmentAction()) { 731 items.add(new ActionItem(R.drawable.symbol_save_android_24, getResources().getString(R.string.conversation_selection__menu_save), () -> handleActionItemClicked(Action.DOWNLOAD))); 732 } 733 734 if (menuState.shouldShowCopyAction()) { 735 items.add(new ActionItem(R.drawable.symbol_copy_android_24, getResources().getString(R.string.conversation_selection__menu_copy), () -> handleActionItemClicked(Action.COPY))); 736 } 737 738 if (menuState.shouldShowPaymentDetails()) { 739 items.add(new ActionItem(R.drawable.symbol_payment_24, getResources().getString(R.string.conversation_selection__menu_payment_details), () -> handleActionItemClicked(Action.PAYMENT_DETAILS))); 740 } 741 742 items.add(new ActionItem(R.drawable.symbol_check_circle_24, getResources().getString(R.string.conversation_selection__menu_multi_select), () -> handleActionItemClicked(Action.MULTISELECT))); 743 744 if (menuState.shouldShowDetailsAction()) { 745 items.add(new ActionItem(R.drawable.symbol_info_24, getResources().getString(R.string.conversation_selection__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO))); 746 } 747 748 backgroundView.setVisibility(menuState.shouldShowReactions() ? View.VISIBLE : View.INVISIBLE); 749 foregroundView.setVisibility(menuState.shouldShowReactions() ? View.VISIBLE : View.INVISIBLE); 750 751 items.add(new ActionItem(R.drawable.symbol_trash_24, getResources().getString(R.string.conversation_selection__menu_delete), () -> handleActionItemClicked(Action.DELETE))); 752 753 return items; 754 } 755 756 private void handleActionItemClicked(@NonNull Action action) { 757 hideInternal(new OnHideListener() { 758 @Override 759 public void startHide(@Nullable View focusedView) { 760 if (onHideListener != null) { 761 onHideListener.startHide(focusedView); 762 } 763 } 764 765 @Override 766 public void onHide() { 767 if (onHideListener != null) { 768 onHideListener.onHide(); 769 } 770 771 if (onActionSelectedListener != null) { 772 onActionSelectedListener.onActionSelected(action); 773 } 774 } 775 }); 776 } 777 778 private void initAnimators() { 779 780 int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration); 781 int revealOffset = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_offset); 782 783 List<Animator> reveals = Stream.of(emojiViews) 784 .mapIndexed((idx, v) -> { 785 Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_reveal); 786 anim.setTarget(v); 787 anim.setStartDelay(idx * animationEmojiStartDelayFactor); 788 return anim; 789 }) 790 .toList(); 791 792 Animator backgroundRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in); 793 backgroundRevealAnim.setTarget(backgroundView); 794 backgroundRevealAnim.setDuration(revealDuration); 795 backgroundRevealAnim.setStartDelay(revealOffset); 796 reveals.add(backgroundRevealAnim); 797 798 Animator selectedRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in); 799 selectedRevealAnim.setTarget(selectedView); 800 backgroundRevealAnim.setDuration(revealDuration); 801 backgroundRevealAnim.setStartDelay(revealOffset); 802 reveals.add(selectedRevealAnim); 803 804 revealAnimatorSet.setInterpolator(INTERPOLATOR); 805 revealAnimatorSet.playTogether(reveals); 806 } 807 808 private @NonNull AnimatorSet newHideAnimatorSet() { 809 AnimatorSet set = new AnimatorSet(); 810 811 set.addListener(new AnimationCompleteListener() { 812 @Override 813 public void onAnimationEnd(Animator animation) { 814 setVisibility(View.GONE); 815 } 816 }); 817 set.setInterpolator(INTERPOLATOR); 818 819 set.playTogether(newHideAnimators()); 820 821 return set; 822 } 823 824 private @NonNull List<Animator> newHideAnimators() { 825 int duration = getContext().getResources().getInteger(R.integer.reaction_scrubber_hide_duration); 826 827 List<Animator> animators = new ArrayList<>(Stream.of(emojiViews) 828 .mapIndexed((idx, v) -> { 829 Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide); 830 anim.setTarget(v); 831 return anim; 832 }) 833 .toList()); 834 835 Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out); 836 backgroundHideAnim.setTarget(backgroundView); 837 backgroundHideAnim.setDuration(duration); 838 animators.add(backgroundHideAnim); 839 840 Animator selectedHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out); 841 selectedHideAnim.setTarget(selectedView); 842 selectedHideAnim.setDuration(duration); 843 animators.add(selectedHideAnim); 844 845 ObjectAnimator itemScaleXAnim = new ObjectAnimator(); 846 itemScaleXAnim.setProperty(View.SCALE_X); 847 itemScaleXAnim.setFloatValues(1f); 848 itemScaleXAnim.setTarget(conversationItem); 849 itemScaleXAnim.setDuration(duration); 850 animators.add(itemScaleXAnim); 851 852 ObjectAnimator itemScaleYAnim = new ObjectAnimator(); 853 itemScaleYAnim.setProperty(View.SCALE_Y); 854 itemScaleYAnim.setFloatValues(1f); 855 itemScaleYAnim.setTarget(conversationItem); 856 itemScaleYAnim.setDuration(duration); 857 animators.add(itemScaleYAnim); 858 859 ObjectAnimator itemXAnim = new ObjectAnimator(); 860 itemXAnim.setProperty(View.X); 861 itemXAnim.setFloatValues(selectedConversationModel.getSnapshotMetrics().getSnapshotOffset()); 862 itemXAnim.setTarget(conversationItem); 863 itemXAnim.setDuration(duration); 864 animators.add(itemXAnim); 865 866 ObjectAnimator itemYAnim = new ObjectAnimator(); 867 itemYAnim.setProperty(View.Y); 868 itemYAnim.setFloatValues(selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() - statusBarHeight); 869 itemYAnim.setTarget(conversationItem); 870 itemYAnim.setDuration(duration); 871 animators.add(itemYAnim); 872 873 if (activity != null) { 874 ValueAnimator statusBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalStatusBarColor); 875 statusBarAnim.setDuration(duration); 876 statusBarAnim.addUpdateListener(animation -> { 877 WindowUtil.setStatusBarColor(activity.getWindow(), (int) animation.getAnimatedValue()); 878 }); 879 animators.add(statusBarAnim); 880 881 ValueAnimator navigationBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalNavigationBarColor); 882 navigationBarAnim.setDuration(duration); 883 navigationBarAnim.addUpdateListener(animation -> { 884 WindowUtil.setNavigationBarColor(activity, (int) animation.getAnimatedValue()); 885 }); 886 animators.add(navigationBarAnim); 887 } 888 889 return animators; 890 } 891 892 public interface OnHideListener { 893 void startHide(@Nullable View focusedView); 894 void onHide(); 895 } 896 897 public interface OnReactionSelectedListener { 898 void onReactionSelected(@NonNull MessageRecord messageRecord, String emoji); 899 void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji); 900 } 901 902 public interface OnActionSelectedListener { 903 void onActionSelected(@NonNull Action action); 904 } 905 906 private static class Boundary { 907 private float min; 908 private float max; 909 910 Boundary() {} 911 912 Boundary(float min, float max) { 913 update(min, max); 914 } 915 916 private void update(float min, float max) { 917 this.min = min; 918 this.max = max; 919 } 920 921 public boolean contains(float value) { 922 if (min < max) { 923 return this.min < value && this.max > value; 924 } else { 925 return this.min > value && this.max < value; 926 } 927 } 928 } 929 930 private enum OverlayState { 931 HIDDEN, 932 UNINITAILIZED, 933 DEADZONE, 934 SCRUB, 935 TAP 936 } 937 938 public enum Action { 939 REPLY, 940 EDIT, 941 FORWARD, 942 RESEND, 943 DOWNLOAD, 944 COPY, 945 MULTISELECT, 946 PAYMENT_DETAILS, 947 VIEW_INFO, 948 DELETE, 949 } 950}