mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 16:57:50 +00:00
Convert ConversationReactionOverlay to Kotlin
This commit is contained in:
parent
3fa4122f77
commit
ac37b2b9de
@ -1,902 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.ThemeUtil;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import kotlin.Unit;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public final class ConversationReactionOverlay extends FrameLayout {
|
||||
|
||||
public static final float LONG_PRESS_SCALE_FACTOR = 0.95f;
|
||||
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
|
||||
|
||||
private final Rect emojiViewGlobalRect = new Rect();
|
||||
private final Rect emojiStripViewBounds = new Rect();
|
||||
private float segmentSize;
|
||||
|
||||
private final Boundary horizontalEmojiBoundary = new Boundary();
|
||||
private final Boundary verticalScrubBoundary = new Boundary();
|
||||
private final PointF deadzoneTouchPoint = new PointF();
|
||||
|
||||
private Activity activity;
|
||||
private MessageRecord messageRecord;
|
||||
private SelectedConversationModel selectedConversationModel;
|
||||
private String blindedPublicKey;
|
||||
private OverlayState overlayState = OverlayState.HIDDEN;
|
||||
private RecentEmojiPageModel recentEmojiPageModel;
|
||||
|
||||
private boolean downIsOurs;
|
||||
private int selected = -1;
|
||||
private int customEmojiIndex;
|
||||
private int originalStatusBarColor;
|
||||
private int originalNavigationBarColor;
|
||||
|
||||
private View dropdownAnchor;
|
||||
private LinearLayout conversationItem;
|
||||
private View conversationBubble;
|
||||
private TextView conversationTimestamp;
|
||||
private View backgroundView;
|
||||
private ConstraintLayout foregroundView;
|
||||
private EmojiImageView[] emojiViews;
|
||||
|
||||
private ConversationContextMenu contextMenu;
|
||||
|
||||
private float touchDownDeadZoneSize;
|
||||
private float distanceFromTouchDownPointToBottomOfScrubberDeadZone;
|
||||
private int scrubberWidth;
|
||||
private int selectedVerticalTranslation;
|
||||
private int scrubberHorizontalMargin;
|
||||
private int animationEmojiStartDelayFactor;
|
||||
private int statusBarHeight;
|
||||
|
||||
private OnReactionSelectedListener onReactionSelectedListener;
|
||||
private OnActionSelectedListener onActionSelectedListener;
|
||||
private OnHideListener onHideListener;
|
||||
|
||||
private AnimatorSet revealAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAnimatorSet = new AnimatorSet();
|
||||
|
||||
public ConversationReactionOverlay(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ConversationReactionOverlay(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
dropdownAnchor = findViewById(R.id.dropdown_anchor);
|
||||
conversationItem = findViewById(R.id.conversation_item);
|
||||
conversationBubble = conversationItem.findViewById(R.id.conversation_item_bubble);
|
||||
conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp);
|
||||
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
|
||||
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
|
||||
|
||||
emojiViews = new EmojiImageView[] { findViewById(R.id.reaction_1),
|
||||
findViewById(R.id.reaction_2),
|
||||
findViewById(R.id.reaction_3),
|
||||
findViewById(R.id.reaction_4),
|
||||
findViewById(R.id.reaction_5),
|
||||
findViewById(R.id.reaction_6),
|
||||
findViewById(R.id.reaction_7) };
|
||||
|
||||
customEmojiIndex = emojiViews.length - 1;
|
||||
|
||||
distanceFromTouchDownPointToBottomOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom);
|
||||
|
||||
touchDownDeadZoneSize = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_touch_deadzone_size);
|
||||
scrubberWidth = getResources().getDimensionPixelOffset(R.dimen.reaction_scrubber_width);
|
||||
selectedVerticalTranslation = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation);
|
||||
scrubberHorizontalMargin = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin);
|
||||
|
||||
animationEmojiStartDelayFactor = getResources().getInteger(R.integer.reaction_scrubber_emoji_reveal_duration_start_delay_factor);
|
||||
|
||||
initAnimators();
|
||||
}
|
||||
|
||||
public void show(@NonNull Activity activity,
|
||||
@NonNull MessageRecord messageRecord,
|
||||
@NonNull PointF lastSeenDownPoint,
|
||||
@NonNull SelectedConversationModel selectedConversationModel,
|
||||
@Nullable String blindedPublicKey)
|
||||
{
|
||||
if (overlayState != OverlayState.HIDDEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageRecord = messageRecord;
|
||||
this.selectedConversationModel = selectedConversationModel;
|
||||
this.blindedPublicKey = blindedPublicKey;
|
||||
overlayState = OverlayState.UNINITAILIZED;
|
||||
selected = -1;
|
||||
recentEmojiPageModel = new RecentEmojiPageModel(activity);
|
||||
|
||||
setupSelectedEmoji();
|
||||
|
||||
View statusBarBackground = activity.findViewById(android.R.id.statusBarBackground);
|
||||
statusBarHeight = statusBarBackground == null ? 0 : statusBarBackground.getHeight();
|
||||
|
||||
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
|
||||
|
||||
conversationBubble.setLayoutParams(new LinearLayout.LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
|
||||
conversationBubble.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot));
|
||||
conversationTimestamp.setText(DateUtils.getDisplayFormattedTimeSpanString(getContext(), Locale.getDefault(), messageRecord.getTimestamp()));
|
||||
|
||||
updateConversationTimestamp(messageRecord);
|
||||
|
||||
boolean isMessageOnLeft = selectedConversationModel.isOutgoing() ^ ViewUtil.isLtr(this);
|
||||
|
||||
conversationItem.setScaleX(LONG_PRESS_SCALE_FACTOR);
|
||||
conversationItem.setScaleY(LONG_PRESS_SCALE_FACTOR);
|
||||
|
||||
setVisibility(View.INVISIBLE);
|
||||
|
||||
this.activity = activity;
|
||||
updateSystemUiOnShow(activity);
|
||||
|
||||
ViewKt.doOnLayout(this, v -> {
|
||||
showAfterLayout(messageRecord, lastSeenDownPoint, isMessageOnLeft);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private void updateConversationTimestamp(MessageRecord message) {
|
||||
if (message.isOutgoing()) conversationBubble.bringToFront();
|
||||
else conversationTimestamp.bringToFront();
|
||||
}
|
||||
|
||||
private void showAfterLayout(@NonNull MessageRecord messageRecord,
|
||||
@NonNull PointF lastSeenDownPoint,
|
||||
boolean isMessageOnLeft) {
|
||||
contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(messageRecord));
|
||||
|
||||
float endX = isMessageOnLeft ? scrubberHorizontalMargin :
|
||||
selectedConversationModel.getBubbleX() - conversationItem.getWidth() + selectedConversationModel.getBubbleWidth();
|
||||
float endY = selectedConversationModel.getBubbleY() - statusBarHeight;
|
||||
conversationItem.setX(endX);
|
||||
conversationItem.setY(endY);
|
||||
|
||||
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
|
||||
boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth();
|
||||
|
||||
int overlayHeight = getHeight();
|
||||
int bubbleWidth = selectedConversationModel.getBubbleWidth();
|
||||
|
||||
float endApparentTop = endY;
|
||||
float endScale = 1f;
|
||||
|
||||
float menuPadding = DimensionUnit.DP.toPixels(12f);
|
||||
float reactionBarTopPadding = DimensionUnit.DP.toPixels(32f);
|
||||
int reactionBarHeight = backgroundView.getHeight();
|
||||
|
||||
float reactionBarBackgroundY;
|
||||
|
||||
if (isWideLayout) {
|
||||
boolean everythingFitsVertically = reactionBarHeight + menuPadding + reactionBarTopPadding + conversationItemSnapshot.getHeight() < overlayHeight;
|
||||
if (everythingFitsVertically) {
|
||||
boolean reactionBarFitsAboveItem = conversationItem.getY() > reactionBarHeight + menuPadding + reactionBarTopPadding;
|
||||
|
||||
if (reactionBarFitsAboveItem) {
|
||||
reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
|
||||
} else {
|
||||
endY = reactionBarHeight + menuPadding + reactionBarTopPadding;
|
||||
reactionBarBackgroundY = reactionBarTopPadding;
|
||||
}
|
||||
} else {
|
||||
float spaceAvailableForItem = overlayHeight - reactionBarHeight - menuPadding - reactionBarTopPadding;
|
||||
|
||||
endScale = spaceAvailableForItem / conversationItem.getHeight();
|
||||
endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
|
||||
endY = reactionBarHeight + menuPadding + reactionBarTopPadding - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
|
||||
reactionBarBackgroundY = reactionBarTopPadding;
|
||||
}
|
||||
} else {
|
||||
float reactionBarOffset = DimensionUnit.DP.toPixels(48);
|
||||
float spaceForReactionBar = Math.max(reactionBarHeight + reactionBarOffset, 0);
|
||||
boolean everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.getHeight() + menuPadding + spaceForReactionBar < overlayHeight;
|
||||
|
||||
if (everythingFitsVertically) {
|
||||
float bubbleBottom = selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight();
|
||||
boolean menuFitsBelowItem = bubbleBottom + menuPadding + contextMenu.getMaxHeight() <= overlayHeight + statusBarHeight;
|
||||
|
||||
if (menuFitsBelowItem) {
|
||||
if (conversationItem.getY() < 0) {
|
||||
endY = 0;
|
||||
}
|
||||
float contextMenuTop = endY + conversationItemSnapshot.getHeight();
|
||||
reactionBarBackgroundY = getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
|
||||
|
||||
if (reactionBarBackgroundY <= reactionBarTopPadding) {
|
||||
endY = backgroundView.getHeight() + menuPadding + reactionBarTopPadding;
|
||||
}
|
||||
} else {
|
||||
endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight();
|
||||
reactionBarBackgroundY = endY - reactionBarHeight - menuPadding;
|
||||
}
|
||||
|
||||
endApparentTop = endY;
|
||||
} else if (reactionBarOffset + reactionBarHeight + contextMenu.getMaxHeight() + menuPadding < overlayHeight) {
|
||||
float spaceAvailableForItem = (float) overlayHeight - contextMenu.getMaxHeight() - menuPadding - spaceForReactionBar;
|
||||
|
||||
endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
|
||||
endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
|
||||
endY = spaceForReactionBar - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
|
||||
|
||||
float contextMenuTop = endY + (conversationItemSnapshot.getHeight() * endScale);
|
||||
reactionBarBackgroundY = reactionBarTopPadding;//getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale), menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
|
||||
endApparentTop = endY + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
|
||||
} else {
|
||||
contextMenu.setHeight(contextMenu.getMaxHeight() / 2);
|
||||
|
||||
int menuHeight = contextMenu.getHeight();
|
||||
boolean fitsVertically = menuHeight + conversationItem.getHeight() + menuPadding * 2 + reactionBarHeight + reactionBarTopPadding < overlayHeight;
|
||||
|
||||
if (fitsVertically) {
|
||||
float bubbleBottom = selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight();
|
||||
boolean menuFitsBelowItem = bubbleBottom + menuPadding + menuHeight <= overlayHeight + statusBarHeight;
|
||||
|
||||
if (menuFitsBelowItem) {
|
||||
reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
|
||||
|
||||
if (reactionBarBackgroundY < reactionBarTopPadding) {
|
||||
endY = reactionBarTopPadding + reactionBarHeight + menuPadding;
|
||||
reactionBarBackgroundY = reactionBarTopPadding;
|
||||
}
|
||||
} else {
|
||||
endY = overlayHeight - menuHeight - menuPadding - conversationItemSnapshot.getHeight();
|
||||
reactionBarBackgroundY = endY - reactionBarHeight - menuPadding;
|
||||
}
|
||||
endApparentTop = endY;
|
||||
} else {
|
||||
float spaceAvailableForItem = (float) overlayHeight - menuHeight - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding;
|
||||
|
||||
endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
|
||||
endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
|
||||
endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale) + menuPadding + reactionBarTopPadding;
|
||||
reactionBarBackgroundY = reactionBarTopPadding;
|
||||
endApparentTop = reactionBarHeight + menuPadding + reactionBarTopPadding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reactionBarBackgroundY = Math.max(reactionBarBackgroundY, -statusBarHeight);
|
||||
|
||||
hideAnimatorSet.end();
|
||||
setVisibility(View.VISIBLE);
|
||||
|
||||
float scrubberX;
|
||||
if (isMessageOnLeft) {
|
||||
scrubberX = scrubberHorizontalMargin;
|
||||
} else {
|
||||
scrubberX = getWidth() - scrubberWidth - scrubberHorizontalMargin;
|
||||
}
|
||||
|
||||
foregroundView.setX(scrubberX);
|
||||
foregroundView.setY(reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.getHeight() / 2f);
|
||||
|
||||
backgroundView.setX(scrubberX);
|
||||
backgroundView.setY(reactionBarBackgroundY);
|
||||
|
||||
verticalScrubBoundary.update(reactionBarBackgroundY,
|
||||
lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone);
|
||||
|
||||
updateBoundsOnLayoutChanged();
|
||||
|
||||
revealAnimatorSet.start();
|
||||
|
||||
if (isWideLayout) {
|
||||
float scrubberRight = scrubberX + scrubberWidth;
|
||||
float offsetX = isMessageOnLeft ? scrubberRight + menuPadding : scrubberX - contextMenu.getMaxWidth() - menuPadding;
|
||||
contextMenu.show((int) offsetX, (int) Math.min(backgroundView.getY(), overlayHeight - contextMenu.getMaxHeight()));
|
||||
} else {
|
||||
float contentX = isMessageOnLeft ? scrubberHorizontalMargin : selectedConversationModel.getBubbleX();
|
||||
float offsetX = isMessageOnLeft ? contentX : -contextMenu.getMaxWidth() + contentX + bubbleWidth;
|
||||
|
||||
float menuTop = endApparentTop + (conversationItemSnapshot.getHeight() * endScale);
|
||||
contextMenu.show((int) offsetX, (int) (menuTop + menuPadding));
|
||||
}
|
||||
|
||||
int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
|
||||
|
||||
conversationBubble.animate()
|
||||
.scaleX(endScale)
|
||||
.scaleY(endScale)
|
||||
.setDuration(revealDuration);
|
||||
|
||||
conversationItem.animate()
|
||||
.x(endX)
|
||||
.y(endY)
|
||||
.setDuration(revealDuration);
|
||||
}
|
||||
|
||||
private float getReactionBarOffsetForTouch(float itemY,
|
||||
float contextMenuTop,
|
||||
float contextMenuPadding,
|
||||
float reactionBarOffset,
|
||||
int reactionBarHeight,
|
||||
float spaceNeededBetweenTopOfScreenAndTopOfReactionBar,
|
||||
float messageTop)
|
||||
{
|
||||
float adjustedTouchY = itemY - statusBarHeight;
|
||||
float reactionStartingPoint = Math.min(adjustedTouchY, contextMenuTop);
|
||||
|
||||
float spaceBetweenTopOfMessageAndTopOfContextMenu = Math.abs(messageTop - contextMenuTop);
|
||||
|
||||
if (spaceBetweenTopOfMessageAndTopOfContextMenu < DimensionUnit.DP.toPixels(150)) {
|
||||
float offsetToMakeReactionBarOffsetMatchMenuPadding = reactionBarOffset - contextMenuPadding;
|
||||
reactionStartingPoint = messageTop + offsetToMakeReactionBarOffsetMatchMenuPadding;
|
||||
}
|
||||
|
||||
return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar);
|
||||
}
|
||||
|
||||
private void updateSystemUiOnShow(@NonNull Activity activity) {
|
||||
Window window = activity.getWindow();
|
||||
int barColor = ContextCompat.getColor(getContext(), R.color.reactions_screen_dark_shade_color);
|
||||
|
||||
originalStatusBarColor = window.getStatusBarColor();
|
||||
WindowUtil.setStatusBarColor(window, barColor);
|
||||
|
||||
originalNavigationBarColor = window.getNavigationBarColor();
|
||||
WindowUtil.setNavigationBarColor(window, barColor);
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext())) {
|
||||
WindowUtil.clearLightStatusBar(window);
|
||||
WindowUtil.clearLightNavigationBar(window);
|
||||
}
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
hideInternal(onHideListener);
|
||||
}
|
||||
|
||||
public void hideForReactWithAny() {
|
||||
hideInternal(onHideListener);
|
||||
}
|
||||
|
||||
private void hideInternal(@Nullable OnHideListener onHideListener) {
|
||||
overlayState = OverlayState.HIDDEN;
|
||||
|
||||
AnimatorSet animatorSet = newHideAnimatorSet();
|
||||
hideAnimatorSet = animatorSet;
|
||||
|
||||
revealAnimatorSet.end();
|
||||
animatorSet.start();
|
||||
|
||||
if (onHideListener != null) {
|
||||
onHideListener.startHide();
|
||||
}
|
||||
|
||||
if (selectedConversationModel.getFocusedView() != null) {
|
||||
ViewUtil.focusAndShowKeyboard(selectedConversationModel.getFocusedView());
|
||||
}
|
||||
|
||||
animatorSet.addListener(new AnimationCompleteListener() {
|
||||
@Override public void onAnimationEnd(Animator animation) {
|
||||
animatorSet.removeListener(this);
|
||||
|
||||
if (onHideListener != null) {
|
||||
onHideListener.onHide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (contextMenu != null) {
|
||||
contextMenu.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return overlayState != OverlayState.HIDDEN;
|
||||
}
|
||||
|
||||
public @NonNull MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
|
||||
updateBoundsOnLayoutChanged();
|
||||
}
|
||||
|
||||
private void updateBoundsOnLayoutChanged() {
|
||||
backgroundView.getGlobalVisibleRect(emojiStripViewBounds);
|
||||
emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect);
|
||||
emojiStripViewBounds.left = getStart(emojiViewGlobalRect);
|
||||
emojiViews[emojiViews.length - 1].getGlobalVisibleRect(emojiViewGlobalRect);
|
||||
emojiStripViewBounds.right = getEnd(emojiViewGlobalRect);
|
||||
|
||||
segmentSize = emojiStripViewBounds.width() / (float) emojiViews.length;
|
||||
}
|
||||
|
||||
private int getStart(@NonNull Rect rect) {
|
||||
if (ViewUtil.isLtr(this)) {
|
||||
return rect.left;
|
||||
} else {
|
||||
return rect.right;
|
||||
}
|
||||
}
|
||||
|
||||
private int getEnd(@NonNull Rect rect) {
|
||||
if (ViewUtil.isLtr(this)) {
|
||||
return rect.right;
|
||||
} else {
|
||||
return rect.left;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean applyTouchEvent(@NonNull MotionEvent motionEvent) {
|
||||
if (!isShowing()) {
|
||||
throw new IllegalStateException("Touch events should only be propagated to this method if we are displaying the scrubber.");
|
||||
}
|
||||
|
||||
if ((motionEvent.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) != 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (overlayState == OverlayState.UNINITAILIZED) {
|
||||
downIsOurs = false;
|
||||
|
||||
deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY());
|
||||
|
||||
overlayState = OverlayState.DEADZONE;
|
||||
}
|
||||
|
||||
if (overlayState == OverlayState.DEADZONE) {
|
||||
float deltaX = Math.abs(deadzoneTouchPoint.x - motionEvent.getX());
|
||||
float deltaY = Math.abs(deadzoneTouchPoint.y - motionEvent.getY());
|
||||
|
||||
if (deltaX > touchDownDeadZoneSize || deltaY > touchDownDeadZoneSize) {
|
||||
overlayState = OverlayState.SCRUB;
|
||||
} else {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
|
||||
overlayState = OverlayState.TAP;
|
||||
|
||||
if (downIsOurs) {
|
||||
handleUpEvent();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return MotionEvent.ACTION_MOVE == motionEvent.getAction();
|
||||
}
|
||||
}
|
||||
|
||||
switch (motionEvent.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
selected = getSelectedIndexViaDownEvent(motionEvent);
|
||||
|
||||
deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY());
|
||||
overlayState = OverlayState.DEADZONE;
|
||||
downIsOurs = true;
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
selected = getSelectedIndexViaMoveEvent(motionEvent);
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
handleUpEvent();
|
||||
return downIsOurs;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
hide();
|
||||
return downIsOurs;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupSelectedEmoji() {
|
||||
final List<String> emojis = recentEmojiPageModel.getEmoji();
|
||||
|
||||
for (int i = 0; i < emojiViews.length; i++) {
|
||||
final EmojiImageView view = emojiViews[i];
|
||||
|
||||
view.setScaleX(1.0f);
|
||||
view.setScaleY(1.0f);
|
||||
view.setTranslationY(0);
|
||||
|
||||
boolean isAtCustomIndex = i == customEmojiIndex;
|
||||
|
||||
if (isAtCustomIndex) {
|
||||
view.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_baseline_add_24));
|
||||
view.setTag(null);
|
||||
} else {
|
||||
view.setImageEmoji(emojis.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getSelectedIndexViaDownEvent(@NonNull MotionEvent motionEvent) {
|
||||
return getSelectedIndexViaMotionEvent(motionEvent, new Boundary(emojiStripViewBounds.top, emojiStripViewBounds.bottom));
|
||||
}
|
||||
|
||||
private int getSelectedIndexViaMoveEvent(@NonNull MotionEvent motionEvent) {
|
||||
return getSelectedIndexViaMotionEvent(motionEvent, verticalScrubBoundary);
|
||||
}
|
||||
|
||||
private int getSelectedIndexViaMotionEvent(@NonNull MotionEvent motionEvent, @NonNull Boundary boundary) {
|
||||
int selected = -1;
|
||||
|
||||
if (backgroundView.getVisibility() != View.VISIBLE) {
|
||||
return selected;
|
||||
}
|
||||
|
||||
for (int i = 0; i < emojiViews.length; i++) {
|
||||
final float emojiLeft = (segmentSize * i) + emojiStripViewBounds.left;
|
||||
horizontalEmojiBoundary.update(emojiLeft, emojiLeft + segmentSize);
|
||||
|
||||
if (horizontalEmojiBoundary.contains(motionEvent.getX()) && boundary.contains(motionEvent.getY())) {
|
||||
selected = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selected != -1 && this.selected != selected) {
|
||||
shrinkView(emojiViews[this.selected]);
|
||||
}
|
||||
|
||||
if (this.selected != selected && selected != -1) {
|
||||
growView(emojiViews[selected]);
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private void growView(@NonNull View view) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
view.animate()
|
||||
.scaleY(1.5f)
|
||||
.scaleX(1.5f)
|
||||
.translationY(-selectedVerticalTranslation)
|
||||
.setDuration(200)
|
||||
.setInterpolator(INTERPOLATOR)
|
||||
.start();
|
||||
}
|
||||
|
||||
private void shrinkView(@NonNull View view) {
|
||||
view.animate()
|
||||
.scaleX(1.0f)
|
||||
.scaleY(1.0f)
|
||||
.translationY(0)
|
||||
.setDuration(200)
|
||||
.setInterpolator(INTERPOLATOR)
|
||||
.start();
|
||||
}
|
||||
|
||||
private void handleUpEvent() {
|
||||
if (selected != -1 && onReactionSelectedListener != null && backgroundView.getVisibility() == View.VISIBLE) {
|
||||
if (selected == customEmojiIndex) {
|
||||
onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null);
|
||||
} else {
|
||||
onReactionSelectedListener.onReactionSelected(messageRecord, recentEmojiPageModel.getEmoji().get(selected));
|
||||
}
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnReactionSelectedListener(@Nullable OnReactionSelectedListener onReactionSelectedListener) {
|
||||
this.onReactionSelectedListener = onReactionSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnActionSelectedListener(@Nullable OnActionSelectedListener onActionSelectedListener) {
|
||||
this.onActionSelectedListener = onActionSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnHideListener(@Nullable OnHideListener onHideListener) {
|
||||
this.onHideListener = onHideListener;
|
||||
}
|
||||
|
||||
private @Nullable String getOldEmoji(@NonNull MessageRecord messageRecord) {
|
||||
return Stream.of(messageRecord.getReactions())
|
||||
.filter(record -> record.getAuthor().equals(TextSecurePreferences.getLocalNumber(getContext())))
|
||||
.findFirst()
|
||||
.map(ReactionRecord::getEmoji)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private @NonNull List<ActionItem> getMenuActionItems(@NonNull MessageRecord message) {
|
||||
List<ActionItem> items = new ArrayList<>();
|
||||
|
||||
// Prepare
|
||||
boolean containsControlMessage = message.isUpdate();
|
||||
boolean hasText = !message.getBody().isEmpty();
|
||||
OpenGroup openGroup = DatabaseComponent.get(getContext()).lokiThreadDatabase().getOpenGroupChat(message.getThreadId());
|
||||
Recipient recipient = DatabaseComponent.get(getContext()).threadDatabase().getRecipientForThreadId(message.getThreadId());
|
||||
if (recipient == null) return Collections.emptyList();
|
||||
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
// Select message
|
||||
items.add(new ActionItem(R.attr.menu_select_icon, getContext().getResources().getString(R.string.conversation_context__menu_select), () -> handleActionItemClicked(Action.SELECT),
|
||||
getContext().getResources().getString(R.string.AccessibilityId_select)));
|
||||
// Reply
|
||||
boolean canWrite = openGroup == null || openGroup.getCanWrite();
|
||||
if (canWrite && !message.isPending() && !message.isFailed()) {
|
||||
items.add(
|
||||
new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_reply), () -> handleActionItemClicked(Action.REPLY),
|
||||
getContext().getResources().getString(R.string.AccessibilityId_reply_message))
|
||||
);
|
||||
}
|
||||
// Copy message text
|
||||
if (!containsControlMessage && hasText) {
|
||||
items.add(new ActionItem(R.attr.menu_copy_icon, getContext().getResources().getString(R.string.copy), () -> handleActionItemClicked(Action.COPY_MESSAGE)));
|
||||
}
|
||||
// Copy Session ID
|
||||
if (recipient.isGroupRecipient() && !recipient.isOpenGroupRecipient() && !message.getRecipient().getAddress().toString().equals(userPublicKey)) {
|
||||
items.add(new ActionItem(
|
||||
R.attr.menu_copy_icon, getContext().getResources().getString(R.string.activity_conversation_menu_copy_session_id), () -> handleActionItemClicked(Action.COPY_SESSION_ID))
|
||||
);
|
||||
}
|
||||
// Delete message
|
||||
if (ConversationMenuItemHelper.userCanDeleteSelectedItems(getContext(), message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.delete),
|
||||
() -> handleActionItemClicked(Action.DELETE),
|
||||
getContext().getResources().getString(R.string.AccessibilityId_delete_message)
|
||||
)
|
||||
);
|
||||
}
|
||||
// Ban user
|
||||
if (ConversationMenuItemHelper.userCanBanSelectedUsers(getContext(), message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
items.add(new ActionItem(R.attr.menu_block_icon, getContext().getResources().getString(R.string.conversation_context__menu_ban_user), () -> handleActionItemClicked(Action.BAN_USER)));
|
||||
}
|
||||
// Ban and delete all
|
||||
if (ConversationMenuItemHelper.userCanBanSelectedUsers(getContext(), message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.conversation_context__menu_ban_and_delete_all), () -> handleActionItemClicked(Action.BAN_AND_DELETE_ALL)));
|
||||
}
|
||||
// Message detail
|
||||
items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
|
||||
// Resend
|
||||
if (message.isFailed()) {
|
||||
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
|
||||
}
|
||||
// Resync
|
||||
if (message.isSyncFailed()) {
|
||||
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resync_message), () -> handleActionItemClicked(Action.RESYNC)));
|
||||
}
|
||||
// Save media
|
||||
if (message.isMms() && ((MediaMmsMessageRecord)message).containsMediaSlide()) {
|
||||
items.add(new ActionItem(R.attr.menu_save_icon, getContext().getResources().getString(R.string.conversation_context_image__save_attachment), () -> handleActionItemClicked(Action.DOWNLOAD),
|
||||
getContext().getResources().getString(R.string.AccessibilityId_save_attachment))
|
||||
);
|
||||
}
|
||||
|
||||
backgroundView.setVisibility(View.VISIBLE);
|
||||
foregroundView.setVisibility(View.VISIBLE);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private void handleActionItemClicked(@NonNull Action action) {
|
||||
hideInternal(new OnHideListener() {
|
||||
@Override public void startHide() {
|
||||
if (onHideListener != null) {
|
||||
onHideListener.startHide();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onHide() {
|
||||
if (onHideListener != null) {
|
||||
onHideListener.onHide();
|
||||
}
|
||||
|
||||
if (onActionSelectedListener != null) {
|
||||
onActionSelectedListener.onActionSelected(action);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initAnimators() {
|
||||
|
||||
int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
|
||||
int revealOffset = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_offset);
|
||||
|
||||
List<Animator> reveals = Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_reveal);
|
||||
anim.setTarget(v);
|
||||
anim.setStartDelay(idx * animationEmojiStartDelayFactor);
|
||||
return anim;
|
||||
})
|
||||
.toList();
|
||||
|
||||
Animator backgroundRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
|
||||
backgroundRevealAnim.setTarget(backgroundView);
|
||||
backgroundRevealAnim.setDuration(revealDuration);
|
||||
backgroundRevealAnim.setStartDelay(revealOffset);
|
||||
reveals.add(backgroundRevealAnim);
|
||||
|
||||
revealAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealAnimatorSet.playTogether(reveals);
|
||||
}
|
||||
|
||||
private @NonNull AnimatorSet newHideAnimatorSet() {
|
||||
AnimatorSet set = new AnimatorSet();
|
||||
|
||||
set.addListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
set.setInterpolator(INTERPOLATOR);
|
||||
|
||||
set.playTogether(newHideAnimators());
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private @NonNull List<Animator> newHideAnimators() {
|
||||
int duration = getContext().getResources().getInteger(R.integer.reaction_scrubber_hide_duration);
|
||||
|
||||
List<Animator> animators = new ArrayList<>(Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
|
||||
anim.setTarget(v);
|
||||
return anim;
|
||||
})
|
||||
.toList());
|
||||
|
||||
Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
backgroundHideAnim.setTarget(backgroundView);
|
||||
backgroundHideAnim.setDuration(duration);
|
||||
animators.add(backgroundHideAnim);
|
||||
|
||||
ObjectAnimator itemScaleXAnim = new ObjectAnimator();
|
||||
itemScaleXAnim.setProperty(View.SCALE_X);
|
||||
itemScaleXAnim.setFloatValues(1f);
|
||||
itemScaleXAnim.setTarget(conversationItem);
|
||||
itemScaleXAnim.setDuration(duration);
|
||||
animators.add(itemScaleXAnim);
|
||||
|
||||
ObjectAnimator itemScaleYAnim = new ObjectAnimator();
|
||||
itemScaleYAnim.setProperty(View.SCALE_Y);
|
||||
itemScaleYAnim.setFloatValues(1f);
|
||||
itemScaleYAnim.setTarget(conversationItem);
|
||||
itemScaleYAnim.setDuration(duration);
|
||||
animators.add(itemScaleYAnim);
|
||||
|
||||
ObjectAnimator itemXAnim = new ObjectAnimator();
|
||||
itemXAnim.setProperty(View.X);
|
||||
itemXAnim.setFloatValues(selectedConversationModel.getBubbleX());
|
||||
itemXAnim.setTarget(conversationItem);
|
||||
itemXAnim.setDuration(duration);
|
||||
animators.add(itemXAnim);
|
||||
|
||||
ObjectAnimator itemYAnim = new ObjectAnimator();
|
||||
itemYAnim.setProperty(View.Y);
|
||||
itemYAnim.setFloatValues(selectedConversationModel.getBubbleY() - statusBarHeight);
|
||||
itemYAnim.setTarget(conversationItem);
|
||||
itemYAnim.setDuration(duration);
|
||||
animators.add(itemYAnim);
|
||||
|
||||
if (activity != null) {
|
||||
ValueAnimator statusBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalStatusBarColor);
|
||||
statusBarAnim.setDuration(duration);
|
||||
statusBarAnim.addUpdateListener(animation -> {
|
||||
WindowUtil.setStatusBarColor(activity.getWindow(), (int) animation.getAnimatedValue());
|
||||
});
|
||||
animators.add(statusBarAnim);
|
||||
|
||||
ValueAnimator navigationBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalNavigationBarColor);
|
||||
navigationBarAnim.setDuration(duration);
|
||||
navigationBarAnim.addUpdateListener(animation -> {
|
||||
WindowUtil.setNavigationBarColor(activity.getWindow(), (int) animation.getAnimatedValue());
|
||||
});
|
||||
animators.add(navigationBarAnim);
|
||||
}
|
||||
|
||||
return animators;
|
||||
}
|
||||
|
||||
public interface OnHideListener {
|
||||
void startHide();
|
||||
void onHide();
|
||||
}
|
||||
|
||||
public interface OnReactionSelectedListener {
|
||||
void onReactionSelected(@NonNull MessageRecord messageRecord, String emoji);
|
||||
void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji);
|
||||
}
|
||||
|
||||
public interface OnActionSelectedListener {
|
||||
void onActionSelected(@NonNull Action action);
|
||||
}
|
||||
|
||||
private static class Boundary {
|
||||
private float min;
|
||||
private float max;
|
||||
|
||||
Boundary() {}
|
||||
|
||||
Boundary(float min, float max) {
|
||||
update(min, max);
|
||||
}
|
||||
|
||||
private void update(float min, float max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
public boolean contains(float value) {
|
||||
if (min < max) {
|
||||
return this.min < value && this.max > value;
|
||||
} else {
|
||||
return this.min > value && this.max < value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum OverlayState {
|
||||
HIDDEN,
|
||||
UNINITAILIZED,
|
||||
DEADZONE,
|
||||
SCRUB,
|
||||
TAP
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
REPLY,
|
||||
RESEND,
|
||||
RESYNC,
|
||||
DOWNLOAD,
|
||||
COPY_MESSAGE,
|
||||
COPY_SESSION_ID,
|
||||
VIEW_INFO,
|
||||
SELECT,
|
||||
DELETE,
|
||||
BAN_USER,
|
||||
BAN_AND_DELETE_ALL,
|
||||
}
|
||||
}
|
@ -0,0 +1,689 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
||||
import org.session.libsession.utilities.ThemeUtil
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanBanSelectedUsers
|
||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanDeleteSelectedItems
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
|
||||
import org.thoughtcrime.securesms.util.AnimationCompleteListener
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import java.util.Locale
|
||||
|
||||
class ConversationReactionOverlay : FrameLayout {
|
||||
private val emojiViewGlobalRect = Rect()
|
||||
private val emojiStripViewBounds = Rect()
|
||||
private var segmentSize = 0f
|
||||
private val horizontalEmojiBoundary = Boundary()
|
||||
private val verticalScrubBoundary = Boundary()
|
||||
private val deadzoneTouchPoint = PointF()
|
||||
private lateinit var activity: Activity
|
||||
lateinit var messageRecord: MessageRecord
|
||||
private lateinit var selectedConversationModel: SelectedConversationModel
|
||||
private var blindedPublicKey: String? = null
|
||||
private var overlayState = OverlayState.HIDDEN
|
||||
private lateinit var recentEmojiPageModel: RecentEmojiPageModel
|
||||
private var downIsOurs = false
|
||||
private var selected = -1
|
||||
private var customEmojiIndex = 0
|
||||
private var originalStatusBarColor = 0
|
||||
private var originalNavigationBarColor = 0
|
||||
private lateinit var dropdownAnchor: View
|
||||
private lateinit var conversationItem: LinearLayout
|
||||
private lateinit var conversationBubble: View
|
||||
private lateinit var conversationTimestamp: TextView
|
||||
private lateinit var backgroundView: View
|
||||
private lateinit var foregroundView: ConstraintLayout
|
||||
private lateinit var emojiViews: List<EmojiImageView>
|
||||
private var contextMenu: ConversationContextMenu? = null
|
||||
private var touchDownDeadZoneSize = 0f
|
||||
private var distanceFromTouchDownPointToBottomOfScrubberDeadZone = 0f
|
||||
private var scrubberWidth = 0
|
||||
private var selectedVerticalTranslation = 0
|
||||
private var scrubberHorizontalMargin = 0
|
||||
private var animationEmojiStartDelayFactor = 0
|
||||
private var statusBarHeight = 0
|
||||
private var onReactionSelectedListener: OnReactionSelectedListener? = null
|
||||
private var onActionSelectedListener: OnActionSelectedListener? = null
|
||||
private var onHideListener: OnHideListener? = null
|
||||
private val revealAnimatorSet = AnimatorSet()
|
||||
private var hideAnimatorSet = AnimatorSet()
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
dropdownAnchor = findViewById(R.id.dropdown_anchor)
|
||||
conversationItem = findViewById(R.id.conversation_item)
|
||||
conversationBubble = conversationItem.findViewById(R.id.conversation_item_bubble)
|
||||
conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp)
|
||||
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background)
|
||||
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground)
|
||||
emojiViews = listOf(R.id.reaction_1, R.id.reaction_2, R.id.reaction_3, R.id.reaction_4, R.id.reaction_5, R.id.reaction_6, R.id.reaction_7).map { findViewById(it) }
|
||||
customEmojiIndex = emojiViews.size - 1
|
||||
distanceFromTouchDownPointToBottomOfScrubberDeadZone = resources.getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom).toFloat()
|
||||
touchDownDeadZoneSize = resources.getDimensionPixelSize(R.dimen.conversation_reaction_touch_deadzone_size).toFloat()
|
||||
scrubberWidth = resources.getDimensionPixelOffset(R.dimen.reaction_scrubber_width)
|
||||
selectedVerticalTranslation = resources.getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation)
|
||||
scrubberHorizontalMargin = resources.getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin)
|
||||
animationEmojiStartDelayFactor = resources.getInteger(R.integer.reaction_scrubber_emoji_reveal_duration_start_delay_factor)
|
||||
initAnimators()
|
||||
}
|
||||
|
||||
fun show(activity: Activity,
|
||||
messageRecord: MessageRecord,
|
||||
lastSeenDownPoint: PointF,
|
||||
selectedConversationModel: SelectedConversationModel,
|
||||
blindedPublicKey: String?) {
|
||||
if (overlayState != OverlayState.HIDDEN) {
|
||||
return
|
||||
}
|
||||
this.messageRecord = messageRecord
|
||||
this.selectedConversationModel = selectedConversationModel
|
||||
this.blindedPublicKey = blindedPublicKey
|
||||
overlayState = OverlayState.UNINITAILIZED
|
||||
selected = -1
|
||||
recentEmojiPageModel = RecentEmojiPageModel(activity)
|
||||
setupSelectedEmoji()
|
||||
val statusBarBackground = activity.findViewById<View>(android.R.id.statusBarBackground)
|
||||
statusBarHeight = statusBarBackground?.height ?: 0
|
||||
val conversationItemSnapshot = selectedConversationModel.bitmap
|
||||
conversationBubble.layoutParams = LinearLayout.LayoutParams(conversationItemSnapshot.width, conversationItemSnapshot.height)
|
||||
conversationBubble.background = BitmapDrawable(resources, conversationItemSnapshot)
|
||||
conversationTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), messageRecord.timestamp)
|
||||
updateConversationTimestamp(messageRecord)
|
||||
val isMessageOnLeft = selectedConversationModel.isOutgoing xor ViewUtil.isLtr(this)
|
||||
conversationItem.scaleX = LONG_PRESS_SCALE_FACTOR
|
||||
conversationItem.scaleY = LONG_PRESS_SCALE_FACTOR
|
||||
visibility = INVISIBLE
|
||||
this.activity = activity
|
||||
updateSystemUiOnShow(activity)
|
||||
doOnLayout { showAfterLayout(messageRecord, lastSeenDownPoint, isMessageOnLeft) }
|
||||
}
|
||||
|
||||
private fun updateConversationTimestamp(message: MessageRecord) {
|
||||
if (message.isOutgoing) conversationBubble.bringToFront() else conversationTimestamp.bringToFront()
|
||||
}
|
||||
|
||||
private fun showAfterLayout(messageRecord: MessageRecord,
|
||||
lastSeenDownPoint: PointF,
|
||||
isMessageOnLeft: Boolean) {
|
||||
val contextMenu = ConversationContextMenu(dropdownAnchor, getMenuActionItems(messageRecord))
|
||||
this.contextMenu = contextMenu
|
||||
var endX = if (isMessageOnLeft) scrubberHorizontalMargin.toFloat() else selectedConversationModel.bubbleX - conversationItem.width + selectedConversationModel.bubbleWidth
|
||||
var endY = selectedConversationModel.bubbleY - statusBarHeight
|
||||
conversationItem.x = endX
|
||||
conversationItem.y = endY
|
||||
val conversationItemSnapshot = selectedConversationModel.bitmap
|
||||
val isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < width
|
||||
val overlayHeight = height
|
||||
val bubbleWidth = selectedConversationModel.bubbleWidth
|
||||
var endApparentTop = endY
|
||||
var endScale = 1f
|
||||
val menuPadding = DimensionUnit.DP.toPixels(12f)
|
||||
val reactionBarTopPadding = DimensionUnit.DP.toPixels(32f)
|
||||
val reactionBarHeight = backgroundView.height
|
||||
var reactionBarBackgroundY: Float
|
||||
if (isWideLayout) {
|
||||
val everythingFitsVertically = reactionBarHeight + menuPadding + reactionBarTopPadding + conversationItemSnapshot.height < overlayHeight
|
||||
if (everythingFitsVertically) {
|
||||
val reactionBarFitsAboveItem = conversationItem.y > reactionBarHeight + menuPadding + reactionBarTopPadding
|
||||
if (reactionBarFitsAboveItem) {
|
||||
reactionBarBackgroundY = conversationItem.y - menuPadding - reactionBarHeight
|
||||
} else {
|
||||
endY = reactionBarHeight + menuPadding + reactionBarTopPadding
|
||||
reactionBarBackgroundY = reactionBarTopPadding
|
||||
}
|
||||
} else {
|
||||
val spaceAvailableForItem = overlayHeight - reactionBarHeight - menuPadding - reactionBarTopPadding
|
||||
endScale = spaceAvailableForItem / conversationItem.height
|
||||
endX += Util.halfOffsetFromScale(conversationItemSnapshot.width, endScale) * if (isMessageOnLeft) -1 else 1
|
||||
endY = reactionBarHeight + menuPadding + reactionBarTopPadding - Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale)
|
||||
reactionBarBackgroundY = reactionBarTopPadding
|
||||
}
|
||||
} else {
|
||||
val reactionBarOffset = DimensionUnit.DP.toPixels(48f)
|
||||
val spaceForReactionBar = Math.max(reactionBarHeight + reactionBarOffset, 0f)
|
||||
val everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.height + menuPadding + spaceForReactionBar < overlayHeight
|
||||
if (everythingFitsVertically) {
|
||||
val bubbleBottom = selectedConversationModel.bubbleY + conversationItemSnapshot.height
|
||||
val menuFitsBelowItem = bubbleBottom + menuPadding + contextMenu.getMaxHeight() <= overlayHeight + statusBarHeight
|
||||
if (menuFitsBelowItem) {
|
||||
if (conversationItem.y < 0) {
|
||||
endY = 0f
|
||||
}
|
||||
val contextMenuTop = endY + conversationItemSnapshot.height
|
||||
reactionBarBackgroundY = getReactionBarOffsetForTouch(selectedConversationModel.bubbleY, contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY)
|
||||
if (reactionBarBackgroundY <= reactionBarTopPadding) {
|
||||
endY = backgroundView.height + menuPadding + reactionBarTopPadding
|
||||
}
|
||||
} else {
|
||||
endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.height
|
||||
reactionBarBackgroundY = endY - reactionBarHeight - menuPadding
|
||||
}
|
||||
endApparentTop = endY
|
||||
} else if (reactionBarOffset + reactionBarHeight + contextMenu.getMaxHeight() + menuPadding < overlayHeight) {
|
||||
val spaceAvailableForItem = overlayHeight.toFloat() - contextMenu.getMaxHeight() - menuPadding - spaceForReactionBar
|
||||
endScale = spaceAvailableForItem / conversationItemSnapshot.height
|
||||
endX += Util.halfOffsetFromScale(conversationItemSnapshot.width, endScale) * if (isMessageOnLeft) -1 else 1
|
||||
endY = spaceForReactionBar - Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale)
|
||||
val contextMenuTop = endY + conversationItemSnapshot.height * endScale
|
||||
reactionBarBackgroundY = reactionBarTopPadding //getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale), menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
|
||||
endApparentTop = endY + Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale)
|
||||
} else {
|
||||
contextMenu.height = contextMenu.getMaxHeight() / 2
|
||||
val menuHeight = contextMenu.height
|
||||
val fitsVertically = menuHeight + conversationItem.height + menuPadding * 2 + reactionBarHeight + reactionBarTopPadding < overlayHeight
|
||||
if (fitsVertically) {
|
||||
val bubbleBottom = selectedConversationModel.bubbleY + conversationItemSnapshot.height
|
||||
val menuFitsBelowItem = bubbleBottom + menuPadding + menuHeight <= overlayHeight + statusBarHeight
|
||||
if (menuFitsBelowItem) {
|
||||
reactionBarBackgroundY = conversationItem.y - menuPadding - reactionBarHeight
|
||||
if (reactionBarBackgroundY < reactionBarTopPadding) {
|
||||
endY = reactionBarTopPadding + reactionBarHeight + menuPadding
|
||||
reactionBarBackgroundY = reactionBarTopPadding
|
||||
}
|
||||
} else {
|
||||
endY = overlayHeight - menuHeight - menuPadding - conversationItemSnapshot.height
|
||||
reactionBarBackgroundY = endY - reactionBarHeight - menuPadding
|
||||
}
|
||||
endApparentTop = endY
|
||||
} else {
|
||||
val spaceAvailableForItem = overlayHeight.toFloat() - menuHeight - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding
|
||||
endScale = spaceAvailableForItem / conversationItemSnapshot.height
|
||||
endX += Util.halfOffsetFromScale(conversationItemSnapshot.width, endScale) * if (isMessageOnLeft) -1 else 1
|
||||
endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale) + menuPadding + reactionBarTopPadding
|
||||
reactionBarBackgroundY = reactionBarTopPadding
|
||||
endApparentTop = reactionBarHeight + menuPadding + reactionBarTopPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
reactionBarBackgroundY = Math.max(reactionBarBackgroundY, -statusBarHeight.toFloat())
|
||||
hideAnimatorSet.end()
|
||||
visibility = VISIBLE
|
||||
val scrubberX = if (isMessageOnLeft) {
|
||||
scrubberHorizontalMargin.toFloat()
|
||||
} else {
|
||||
(width - scrubberWidth - scrubberHorizontalMargin).toFloat()
|
||||
}
|
||||
foregroundView.x = scrubberX
|
||||
foregroundView.y = reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.height / 2f
|
||||
backgroundView.x = scrubberX
|
||||
backgroundView.y = reactionBarBackgroundY
|
||||
verticalScrubBoundary.update(reactionBarBackgroundY,
|
||||
lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone)
|
||||
updateBoundsOnLayoutChanged()
|
||||
revealAnimatorSet.start()
|
||||
if (isWideLayout) {
|
||||
val scrubberRight = scrubberX + scrubberWidth
|
||||
val offsetX = if (isMessageOnLeft) scrubberRight + menuPadding else scrubberX - contextMenu.getMaxWidth() - menuPadding
|
||||
contextMenu.show(offsetX.toInt(), Math.min(backgroundView.y, (overlayHeight - contextMenu.getMaxHeight()).toFloat()).toInt())
|
||||
} else {
|
||||
val contentX = if (isMessageOnLeft) scrubberHorizontalMargin.toFloat() else selectedConversationModel.bubbleX
|
||||
val offsetX = if (isMessageOnLeft) contentX else -contextMenu.getMaxWidth() + contentX + bubbleWidth
|
||||
val menuTop = endApparentTop + conversationItemSnapshot.height * endScale
|
||||
contextMenu.show(offsetX.toInt(), (menuTop + menuPadding).toInt())
|
||||
}
|
||||
val revealDuration = context.resources.getInteger(R.integer.reaction_scrubber_reveal_duration)
|
||||
conversationBubble.animate()
|
||||
.scaleX(endScale)
|
||||
.scaleY(endScale)
|
||||
.setDuration(revealDuration.toLong())
|
||||
conversationItem.animate()
|
||||
.x(endX)
|
||||
.y(endY)
|
||||
.setDuration(revealDuration.toLong())
|
||||
}
|
||||
|
||||
private fun getReactionBarOffsetForTouch(itemY: Float,
|
||||
contextMenuTop: Float,
|
||||
contextMenuPadding: Float,
|
||||
reactionBarOffset: Float,
|
||||
reactionBarHeight: Int,
|
||||
spaceNeededBetweenTopOfScreenAndTopOfReactionBar: Float,
|
||||
messageTop: Float): Float {
|
||||
val adjustedTouchY = itemY - statusBarHeight
|
||||
var reactionStartingPoint = Math.min(adjustedTouchY, contextMenuTop)
|
||||
val spaceBetweenTopOfMessageAndTopOfContextMenu = Math.abs(messageTop - contextMenuTop)
|
||||
if (spaceBetweenTopOfMessageAndTopOfContextMenu < DimensionUnit.DP.toPixels(150f)) {
|
||||
val offsetToMakeReactionBarOffsetMatchMenuPadding = reactionBarOffset - contextMenuPadding
|
||||
reactionStartingPoint = messageTop + offsetToMakeReactionBarOffsetMatchMenuPadding
|
||||
}
|
||||
return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar)
|
||||
}
|
||||
|
||||
private fun updateSystemUiOnShow(activity: Activity) {
|
||||
val window = activity.window
|
||||
val barColor = ContextCompat.getColor(context, R.color.reactions_screen_dark_shade_color)
|
||||
originalStatusBarColor = window.statusBarColor
|
||||
WindowUtil.setStatusBarColor(window, barColor)
|
||||
originalNavigationBarColor = window.navigationBarColor
|
||||
WindowUtil.setNavigationBarColor(window, barColor)
|
||||
if (!ThemeUtil.isDarkTheme(context)) {
|
||||
WindowUtil.clearLightStatusBar(window)
|
||||
WindowUtil.clearLightNavigationBar(window)
|
||||
}
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
hideInternal(onHideListener)
|
||||
}
|
||||
|
||||
fun hideForReactWithAny() {
|
||||
hideInternal(onHideListener)
|
||||
}
|
||||
|
||||
private fun hideInternal(onHideListener: OnHideListener?) {
|
||||
overlayState = OverlayState.HIDDEN
|
||||
val animatorSet = newHideAnimatorSet()
|
||||
hideAnimatorSet = animatorSet
|
||||
revealAnimatorSet.end()
|
||||
animatorSet.start()
|
||||
onHideListener?.startHide()
|
||||
selectedConversationModel.focusedView?.let(ViewUtil::focusAndShowKeyboard)
|
||||
animatorSet.addListener(object : AnimationCompleteListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
animatorSet.removeListener(this)
|
||||
onHideListener?.onHide()
|
||||
}
|
||||
})
|
||||
contextMenu?.dismiss()
|
||||
}
|
||||
|
||||
val isShowing: Boolean
|
||||
get() = overlayState != OverlayState.HIDDEN
|
||||
|
||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||
super.onLayout(changed, l, t, r, b)
|
||||
updateBoundsOnLayoutChanged()
|
||||
}
|
||||
|
||||
private fun updateBoundsOnLayoutChanged() {
|
||||
backgroundView.getGlobalVisibleRect(emojiStripViewBounds)
|
||||
emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect)
|
||||
emojiStripViewBounds.left = getStart(emojiViewGlobalRect)
|
||||
emojiViews[emojiViews.size - 1].getGlobalVisibleRect(emojiViewGlobalRect)
|
||||
emojiStripViewBounds.right = getEnd(emojiViewGlobalRect)
|
||||
segmentSize = emojiStripViewBounds.width() / emojiViews.size.toFloat()
|
||||
}
|
||||
|
||||
private fun getStart(rect: Rect): Int = if (ViewUtil.isLtr(this)) rect.left else rect.right
|
||||
|
||||
private fun getEnd(rect: Rect): Int = if (ViewUtil.isLtr(this)) rect.right else rect.left
|
||||
|
||||
fun applyTouchEvent(motionEvent: MotionEvent): Boolean {
|
||||
check(isShowing) { "Touch events should only be propagated to this method if we are displaying the scrubber." }
|
||||
if (motionEvent.action and MotionEvent.ACTION_POINTER_INDEX_MASK != 0) {
|
||||
return true
|
||||
}
|
||||
if (overlayState == OverlayState.UNINITAILIZED) {
|
||||
downIsOurs = false
|
||||
deadzoneTouchPoint[motionEvent.x] = motionEvent.y
|
||||
overlayState = OverlayState.DEADZONE
|
||||
}
|
||||
if (overlayState == OverlayState.DEADZONE) {
|
||||
val deltaX = Math.abs(deadzoneTouchPoint.x - motionEvent.x)
|
||||
val deltaY = Math.abs(deadzoneTouchPoint.y - motionEvent.y)
|
||||
if (deltaX > touchDownDeadZoneSize || deltaY > touchDownDeadZoneSize) {
|
||||
overlayState = OverlayState.SCRUB
|
||||
} else {
|
||||
if (motionEvent.action == MotionEvent.ACTION_UP) {
|
||||
overlayState = OverlayState.TAP
|
||||
if (downIsOurs) {
|
||||
handleUpEvent()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return MotionEvent.ACTION_MOVE == motionEvent.action
|
||||
}
|
||||
}
|
||||
return when (motionEvent.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
selected = getSelectedIndexViaDownEvent(motionEvent)
|
||||
deadzoneTouchPoint[motionEvent.x] = motionEvent.y
|
||||
overlayState = OverlayState.DEADZONE
|
||||
downIsOurs = true
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
selected = getSelectedIndexViaMoveEvent(motionEvent)
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
handleUpEvent()
|
||||
downIsOurs
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
hide()
|
||||
downIsOurs
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSelectedEmoji() {
|
||||
val emojis = recentEmojiPageModel.emoji
|
||||
emojiViews.forEachIndexed { i, view ->
|
||||
view.scaleX = 1.0f
|
||||
view.scaleY = 1.0f
|
||||
view.translationY = 0f
|
||||
val isAtCustomIndex = i == customEmojiIndex
|
||||
if (isAtCustomIndex) {
|
||||
view.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_baseline_add_24))
|
||||
view.tag = null
|
||||
} else {
|
||||
view.setImageEmoji(emojis[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSelectedIndexViaDownEvent(motionEvent: MotionEvent): Int =
|
||||
getSelectedIndexViaMotionEvent(motionEvent, Boundary(emojiStripViewBounds.top.toFloat(), emojiStripViewBounds.bottom.toFloat()))
|
||||
|
||||
private fun getSelectedIndexViaMoveEvent(motionEvent: MotionEvent): Int =
|
||||
getSelectedIndexViaMotionEvent(motionEvent, verticalScrubBoundary)
|
||||
|
||||
private fun getSelectedIndexViaMotionEvent(motionEvent: MotionEvent, boundary: Boundary): Int {
|
||||
var selected = -1
|
||||
if (backgroundView.visibility != VISIBLE) {
|
||||
return selected
|
||||
}
|
||||
for (i in emojiViews.indices) {
|
||||
val emojiLeft = segmentSize * i + emojiStripViewBounds.left
|
||||
horizontalEmojiBoundary.update(emojiLeft, emojiLeft + segmentSize)
|
||||
if (horizontalEmojiBoundary.contains(motionEvent.x) && boundary.contains(motionEvent.y)) {
|
||||
selected = i
|
||||
}
|
||||
}
|
||||
if (this.selected != -1 && this.selected != selected) {
|
||||
shrinkView(emojiViews[this.selected])
|
||||
}
|
||||
if (this.selected != selected && selected != -1) {
|
||||
growView(emojiViews[selected])
|
||||
}
|
||||
return selected
|
||||
}
|
||||
|
||||
private fun growView(view: View) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
view.animate()
|
||||
.scaleY(1.5f)
|
||||
.scaleX(1.5f)
|
||||
.translationY(-selectedVerticalTranslation.toFloat())
|
||||
.setDuration(200)
|
||||
.setInterpolator(INTERPOLATOR)
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun shrinkView(view: View) {
|
||||
view.animate()
|
||||
.scaleX(1.0f)
|
||||
.scaleY(1.0f)
|
||||
.translationY(0f)
|
||||
.setDuration(200)
|
||||
.setInterpolator(INTERPOLATOR)
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun handleUpEvent() {
|
||||
val onReactionSelectedListener = onReactionSelectedListener
|
||||
if (selected != -1 && onReactionSelectedListener != null && backgroundView.visibility == VISIBLE) {
|
||||
if (selected == customEmojiIndex) {
|
||||
onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].tag != null)
|
||||
} else {
|
||||
onReactionSelectedListener.onReactionSelected(messageRecord, recentEmojiPageModel.emoji[selected])
|
||||
}
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnReactionSelectedListener(onReactionSelectedListener: OnReactionSelectedListener?) {
|
||||
this.onReactionSelectedListener = onReactionSelectedListener
|
||||
}
|
||||
|
||||
fun setOnActionSelectedListener(onActionSelectedListener: OnActionSelectedListener?) {
|
||||
this.onActionSelectedListener = onActionSelectedListener
|
||||
}
|
||||
|
||||
fun setOnHideListener(onHideListener: OnHideListener?) {
|
||||
this.onHideListener = onHideListener
|
||||
}
|
||||
|
||||
private fun getOldEmoji(messageRecord: MessageRecord): String? =
|
||||
messageRecord.reactions
|
||||
.filter { it.author == getLocalNumber(context) }
|
||||
.firstOrNull()
|
||||
?.let(ReactionRecord::emoji)
|
||||
|
||||
private fun getMenuActionItems(message: MessageRecord): List<ActionItem> {
|
||||
val items: MutableList<ActionItem> = ArrayList()
|
||||
|
||||
// Prepare
|
||||
val containsControlMessage = message.isUpdate
|
||||
val hasText = !message.body.isEmpty()
|
||||
val openGroup = get(context).lokiThreadDatabase().getOpenGroupChat(message.threadId)
|
||||
val recipient = get(context).threadDatabase().getRecipientForThreadId(message.threadId)
|
||||
?: return emptyList()
|
||||
val userPublicKey = getLocalNumber(context)!!
|
||||
// Select message
|
||||
items += ActionItem(R.attr.menu_select_icon, context.resources.getString(R.string.conversation_context__menu_select), { handleActionItemClicked(Action.SELECT) },
|
||||
context.resources.getString(R.string.AccessibilityId_select))
|
||||
// Reply
|
||||
val canWrite = openGroup == null || openGroup.canWrite
|
||||
if (canWrite && !message.isPending && !message.isFailed) {
|
||||
items += ActionItem(R.attr.menu_reply_icon, context.resources.getString(R.string.conversation_context__menu_reply), { handleActionItemClicked(Action.REPLY) },
|
||||
context.resources.getString(R.string.AccessibilityId_reply_message))
|
||||
}
|
||||
// Copy message text
|
||||
if (!containsControlMessage && hasText) {
|
||||
items += ActionItem(R.attr.menu_copy_icon, context.resources.getString(R.string.copy), { handleActionItemClicked(Action.COPY_MESSAGE) })
|
||||
}
|
||||
// Copy Session ID
|
||||
if (recipient.isGroupRecipient && !recipient.isOpenGroupRecipient && message.recipient.address.toString() != userPublicKey) {
|
||||
items += ActionItem(R.attr.menu_copy_icon, context.resources.getString(R.string.activity_conversation_menu_copy_session_id), { handleActionItemClicked(Action.COPY_SESSION_ID) })
|
||||
}
|
||||
// Delete message
|
||||
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
items += ActionItem(R.attr.menu_trash_icon, context.resources.getString(R.string.delete), { handleActionItemClicked(Action.DELETE) }, context.resources.getString(R.string.AccessibilityId_delete_message))
|
||||
}
|
||||
// Ban user
|
||||
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
items += ActionItem(R.attr.menu_block_icon, context.resources.getString(R.string.conversation_context__menu_ban_user), { handleActionItemClicked(Action.BAN_USER) })
|
||||
}
|
||||
// Ban and delete all
|
||||
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
items += ActionItem(R.attr.menu_trash_icon, context.resources.getString(R.string.conversation_context__menu_ban_and_delete_all), { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
|
||||
}
|
||||
// Message detail
|
||||
items += ActionItem(R.attr.menu_info_icon, context.resources.getString(R.string.conversation_context__menu_message_details), { handleActionItemClicked(Action.VIEW_INFO) })
|
||||
// Resend
|
||||
if (message.isFailed) {
|
||||
items += ActionItem(R.attr.menu_reply_icon, context.resources.getString(R.string.conversation_context__menu_resend_message), { handleActionItemClicked(Action.RESEND) })
|
||||
}
|
||||
// Resync
|
||||
if (message.isSyncFailed) {
|
||||
items += ActionItem(R.attr.menu_reply_icon, context.resources.getString(R.string.conversation_context__menu_resync_message), { handleActionItemClicked(Action.RESYNC) })
|
||||
}
|
||||
// Save media
|
||||
if (message.isMms && (message as MediaMmsMessageRecord).containsMediaSlide()) {
|
||||
items += ActionItem(R.attr.menu_save_icon, context.resources.getString(R.string.conversation_context_image__save_attachment), { handleActionItemClicked(Action.DOWNLOAD) },
|
||||
context.resources.getString(R.string.AccessibilityId_save_attachment))
|
||||
}
|
||||
backgroundView.visibility = VISIBLE
|
||||
foregroundView.visibility = VISIBLE
|
||||
return items
|
||||
}
|
||||
|
||||
private fun handleActionItemClicked(action: Action) {
|
||||
hideInternal(object : OnHideListener {
|
||||
override fun startHide() {
|
||||
onHideListener?.startHide()
|
||||
}
|
||||
|
||||
override fun onHide() {
|
||||
onHideListener?.onHide()
|
||||
onActionSelectedListener?.onActionSelected(action)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initAnimators() {
|
||||
val revealDuration = context.resources.getInteger(R.integer.reaction_scrubber_reveal_duration)
|
||||
val revealOffset = context.resources.getInteger(R.integer.reaction_scrubber_reveal_offset)
|
||||
val reveals = emojiViews.mapIndexed { idx: Int, v: EmojiImageView? ->
|
||||
AnimatorInflaterCompat.loadAnimator(context, R.animator.reactions_scrubber_reveal).apply {
|
||||
setTarget(v)
|
||||
startDelay = (idx * animationEmojiStartDelayFactor).toLong()
|
||||
}
|
||||
} + AnimatorInflaterCompat.loadAnimator(context, android.R.animator.fade_in).apply {
|
||||
setTarget(backgroundView)
|
||||
setDuration(revealDuration.toLong())
|
||||
startDelay = revealOffset.toLong()
|
||||
}
|
||||
revealAnimatorSet.interpolator = INTERPOLATOR
|
||||
revealAnimatorSet.playTogether(reveals)
|
||||
}
|
||||
|
||||
private fun newHideAnimatorSet(): AnimatorSet {
|
||||
val set = AnimatorSet()
|
||||
set.addListener(object : AnimationCompleteListener() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
visibility = GONE
|
||||
}
|
||||
})
|
||||
set.interpolator = INTERPOLATOR
|
||||
set.playTogether(newHideAnimators())
|
||||
return set
|
||||
}
|
||||
|
||||
private fun newHideAnimators(): List<Animator> {
|
||||
val duration = context.resources.getInteger(R.integer.reaction_scrubber_hide_duration).toLong()
|
||||
fun conversationItemAnimator(configure: ObjectAnimator.() -> Unit) = ObjectAnimator().apply {
|
||||
target = conversationItem
|
||||
setDuration(duration)
|
||||
configure()
|
||||
}
|
||||
return emojiViews.map {
|
||||
AnimatorInflaterCompat.loadAnimator(context, R.animator.reactions_scrubber_hide).apply { setTarget(it) }
|
||||
} + AnimatorInflaterCompat.loadAnimator(context, android.R.animator.fade_out).apply {
|
||||
setTarget(backgroundView)
|
||||
setDuration(duration)
|
||||
} + conversationItemAnimator {
|
||||
setProperty(SCALE_X)
|
||||
setFloatValues(1f)
|
||||
} + conversationItemAnimator {
|
||||
setProperty(SCALE_Y)
|
||||
setFloatValues(1f)
|
||||
} + conversationItemAnimator {
|
||||
setProperty(X)
|
||||
setFloatValues(selectedConversationModel.bubbleX)
|
||||
} + conversationItemAnimator {
|
||||
setProperty(Y)
|
||||
setFloatValues(selectedConversationModel.bubbleY - statusBarHeight)
|
||||
} + ValueAnimator.ofArgb(activity.window.statusBarColor, originalStatusBarColor).apply {
|
||||
setDuration(duration)
|
||||
addUpdateListener { animation: ValueAnimator -> WindowUtil.setStatusBarColor(activity.window, animation.animatedValue as Int) }
|
||||
} + ValueAnimator.ofArgb(activity.window.statusBarColor, originalNavigationBarColor).apply {
|
||||
setDuration(duration)
|
||||
addUpdateListener { animation: ValueAnimator -> WindowUtil.setNavigationBarColor(activity.window, animation.animatedValue as Int) }
|
||||
}
|
||||
}
|
||||
|
||||
interface OnHideListener {
|
||||
fun startHide()
|
||||
fun onHide()
|
||||
}
|
||||
|
||||
interface OnReactionSelectedListener {
|
||||
fun onReactionSelected(messageRecord: MessageRecord, emoji: String)
|
||||
fun onCustomReactionSelected(messageRecord: MessageRecord, hasAddedCustomEmoji: Boolean)
|
||||
}
|
||||
|
||||
interface OnActionSelectedListener {
|
||||
fun onActionSelected(action: Action)
|
||||
}
|
||||
|
||||
private class Boundary {
|
||||
private var min = 0f
|
||||
private var max = 0f
|
||||
|
||||
internal constructor()
|
||||
internal constructor(min: Float, max: Float) {
|
||||
update(min, max)
|
||||
}
|
||||
|
||||
fun update(min: Float, max: Float) {
|
||||
this.min = min
|
||||
this.max = max
|
||||
}
|
||||
|
||||
operator fun contains(value: Float): Boolean {
|
||||
return if (min < max) {
|
||||
min < value && max > value
|
||||
} else {
|
||||
min > value && max < value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class OverlayState {
|
||||
HIDDEN,
|
||||
UNINITAILIZED,
|
||||
DEADZONE,
|
||||
SCRUB,
|
||||
TAP
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
REPLY,
|
||||
RESEND,
|
||||
RESYNC,
|
||||
DOWNLOAD,
|
||||
COPY_MESSAGE,
|
||||
COPY_SESSION_ID,
|
||||
VIEW_INFO,
|
||||
SELECT,
|
||||
DELETE,
|
||||
BAN_USER,
|
||||
BAN_AND_DELETE_ALL
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LONG_PRESS_SCALE_FACTOR = 0.95f
|
||||
private val INTERPOLATOR: Interpolator = DecelerateInterpolator()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user