session-android/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay...

902 lines
36 KiB
Java

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,
}
}