Add like/reaction (WIP)

This commit is contained in:
Ammar Githam 2021-01-13 22:22:25 +09:00
parent 02a1a4a5f5
commit cf62d88531
33 changed files with 1406 additions and 149 deletions

View File

@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemActionLogViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemAnimatedMediaViewHolder;
@ -32,6 +33,7 @@ import awais.instagrabber.adapters.viewholder.directmessages.DirectItemTextViewH
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemVideoCallEventViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemVoiceMediaViewHolder;
import awais.instagrabber.customviews.emoji.Emoji;
import awais.instagrabber.databinding.LayoutDmActionLogBinding;
import awais.instagrabber.databinding.LayoutDmAnimatedMediaBinding;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
@ -57,11 +59,14 @@ import awais.instagrabber.utils.DateUtils;
public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = DirectItemsAdapter.class.getSimpleName();
private final User currentUser;
private List<DirectItem> items;
private DirectThread thread;
private DirectItemViewHolder selectedViewHolder;
private final User currentUser;
private final DirectItemCallback callback;
private final AsyncListDiffer<DirectItemOrHeader> differ;
private List<DirectItem> items;
private final DirectItemInternalLongClickListener longClickListener;
private static final DiffUtil.ItemCallback<DirectItemOrHeader> diffCallback = new DiffUtil.ItemCallback<DirectItemOrHeader>() {
@Override
@ -96,21 +101,30 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
if (bothHeaders) {
return oldItem.date.equals(newItem.date);
}
return oldItem.item.getTimestamp() == newItem.item.getTimestamp()
&& oldItem.item.isPending() == newItem.item.isPending(); // todo need to be more specific
final boolean timestampEqual = oldItem.item.getTimestamp() == newItem.item.getTimestamp();
final boolean bothPending = oldItem.item.isPending() == newItem.item.isPending();
final boolean reactionSame = Objects.equals(oldItem.item.getReactions(), newItem.item.getReactions());
return timestampEqual && bothPending && reactionSame;
}
};
public DirectItemsAdapter(@NonNull final User currentUser,
@NonNull final DirectThread thread,
@NonNull final DirectItemCallback callback) {
@NonNull final DirectItemCallback callback,
@NonNull final DirectItemLongClickListener itemLongClickListener) {
this.currentUser = currentUser;
this.thread = thread;
this.callback = callback;
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
// this.onClickListener = onClickListener;
// this.mentionClickListener = mentionClickListener;
longClickListener = (position, viewHolder) -> {
if (selectedViewHolder != null) {
selectedViewHolder.setSelected(false);
}
selectedViewHolder = viewHolder;
viewHolder.setSelected(true);
itemLongClickListener.onLongClick(position);
};
}
@NonNull
@ -123,7 +137,9 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
}
final LayoutDmBaseBinding baseBinding = LayoutDmBaseBinding.inflate(layoutInflater, parent, false);
final DirectItemType directItemType = DirectItemType.valueOf(type);
return getItemViewHolder(layoutInflater, baseBinding, directItemType);
final DirectItemViewHolder itemViewHolder = getItemViewHolder(layoutInflater, baseBinding, directItemType);
itemViewHolder.setLongClickListener(longClickListener);
return itemViewHolder;
}
@NonNull
@ -205,7 +221,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
return;
}
if (thread == null) return;
((DirectItemViewHolder) holder).bind(itemOrHeader.item);
((DirectItemViewHolder) holder).bind(position, itemOrHeader.item);
}
protected DirectItemOrHeader getItem(int position) {
@ -372,5 +388,15 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
void onMediaClick(Media media);
void onStoryClick(DirectItemStoryShare storyShare);
void onReaction(final DirectItem item, Emoji emoji);
}
public interface DirectItemInternalLongClickListener {
void onLongClick(int position, DirectItemViewHolder viewHolder);
}
public interface DirectItemLongClickListener {
void onLongClick(int position);
}
}

View File

@ -176,14 +176,20 @@ public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder {
subtitle = getMediaSpecificSubtitle(username, mediaType);
break;
}
case STORY_SHARE:
String format = "%s shared a story by @%s";
if (item.getStoryShare().getReelType().equals("highlight_reel")) {
format = "%s shared a story highlight by @%s";
case STORY_SHARE: {
final String reelType = item.getStoryShare().getReelType();
if (reelType == null) {
subtitle = item.getStoryShare().getTitle();
} else {
String format = "%s shared a story by @%s";
if (reelType.equals("highlight_reel")) {
format = "%s shared a story highlight by @%s";
}
subtitle = String.format(format, username != null ? username : "",
item.getStoryShare().getMedia().getUser().getUsername());
}
subtitle = String.format(format, username != null ? username : "",
item.getStoryShare().getMedia().getUser().getUsername());
break;
}
case VOICE_MEDIA:
subtitle = String.format("%s sent a voice message", username != null ? username : "");
break;

View File

@ -70,6 +70,7 @@ public class DirectItemLinkViewHolder extends DirectItemViewHolder {
setupRamboTextListeners(binding.text);
final View.OnClickListener onClickListener = v -> openURL(linkContext.getLinkUrl());
binding.preview.setOnClickListener(onClickListener);
// binding.preview.setOnLongClickListener(v -> itemView.performLongClick());
binding.title.setOnClickListener(onClickListener);
binding.summary.setOnClickListener(onClickListener);
binding.url.setOnClickListener(onClickListener);

View File

@ -12,6 +12,8 @@ import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.generic.RoundingParams;
import java.util.Objects;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
@ -29,6 +31,7 @@ import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
private static final String TAG = DirectItemMediaShareViewHolder.class.getSimpleName();
private final LayoutDmMediaShareBinding binding;
private final RoundingParams incomingRoundingParams;
@ -48,11 +51,6 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
@Override
public void bindItem(final DirectItem item, final MessageDirection messageDirection) {
final RoundingParams roundingParams = messageDirection == MessageDirection.INCOMING ? incomingRoundingParams : outgoingRoundingParams;
binding.mediaPreview.setHierarchy(new GenericDraweeHierarchyBuilder(itemView.getResources())
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.setRoundingParams(roundingParams)
.build());
binding.topBg.setBackgroundResource(messageDirection == MessageDirection.INCOMING
? R.drawable.bg_media_share_top_incoming
: R.drawable.bg_media_share_top_outgoing);
@ -67,10 +65,10 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
final MediaItemType mediaType = media.getMediaType();
setupTypeIndicator(mediaType);
if (mediaType == MediaItemType.MEDIA_TYPE_SLIDER) {
setupPreview(media.getCarouselMedia().get(0));
setupPreview(media.getCarouselMedia().get(0), messageDirection);
return;
}
setupPreview(media);
setupPreview(media, messageDirection);
});
itemView.setOnClickListener(v -> openMedia(media));
}
@ -87,7 +85,17 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
}
}
private void setupPreview(@NonNull final Media media) {
private void setupPreview(@NonNull final Media media,
final MessageDirection messageDirection) {
final String url = ResponseBodyUtils.getThumbUrl(media.getImageVersions2());
if (Objects.equals(url, binding.mediaPreview.getTag())) {
return;
}
final RoundingParams roundingParams = messageDirection == MessageDirection.INCOMING ? incomingRoundingParams : outgoingRoundingParams;
binding.mediaPreview.setHierarchy(new GenericDraweeHierarchyBuilder(itemView.getResources())
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.setRoundingParams(roundingParams)
.build());
final Pair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
@ -98,7 +106,7 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
binding.mediaPreview.requestLayout();
final String url = ResponseBodyUtils.getThumbUrl(media.getImageVersions2());
binding.mediaPreview.setTag(url);
binding.mediaPreview.setImageURI(url);
}
@ -153,4 +161,9 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
}
return media;
}
@Override
protected int getReactionsTranslationY() {
return reactionTranslationYType2;
}
}

View File

@ -1,18 +1,27 @@
package awais.instagrabber.adapters.viewholder.directmessages;
import android.annotation.SuppressLint;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.text.format.DateFormat;
import android.view.Gravity;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewPropertyAnimator;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.widget.ImageViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.TransitionManager;
import com.google.android.material.transition.MaterialFade;
import java.util.List;
import java.util.Locale;
@ -20,6 +29,9 @@ import java.util.stream.Collectors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemInternalLongClickListener;
import awais.instagrabber.customviews.DirectItemContextMenu;
import awais.instagrabber.customviews.DirectItemFrameLayout;
import awais.instagrabber.customviews.RamboTextViewV2;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.models.enums.DirectItemType;
@ -43,6 +55,8 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
private final int groupMessageWidth;
private final List<Long> userIds;
private final DirectItemCallback callback;
private final int reactionAdjustMargin;
private final AccelerateDecelerateInterpolator accelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator();
protected final int margin;
protected final int dmRadius;
@ -51,6 +65,14 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
protected final int mediaImageMaxHeight;
protected final int windowWidth;
protected final int mediaImageMaxWidth;
protected final int reactionTranslationYType1;
protected final int reactionTranslationYType2;
private boolean selected = false;
private DirectItemInternalLongClickListener longClickListener;
private DirectItem item;
private ViewPropertyAnimator shrinkGrowAnimator;
// private View.OnLayoutChangeListener layoutChangeListener;
public DirectItemViewHolder(@NonNull final LayoutDmBaseBinding binding,
@NonNull final User currentUser,
@ -75,16 +97,24 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
messageInfoPaddingSmall = resources.getDimensionPixelSize(R.dimen.dm_message_info_padding_small);
windowWidth = resources.getDisplayMetrics().widthPixels;
mediaImageMaxHeight = resources.getDimensionPixelSize(R.dimen.dm_media_img_max_height);
reactionAdjustMargin = resources.getDimensionPixelSize(R.dimen.dm_reaction_adjust_margin);
final int groupWidthCorrection = avatarSize + messageInfoPaddingSmall * 3;
mediaImageMaxWidth = windowWidth - margin - (thread.isGroup() ? groupWidthCorrection : 0);
mediaImageMaxWidth = windowWidth - margin - (thread.isGroup() ? groupWidthCorrection : messageInfoPaddingSmall * 2);
// messageInfoPaddingSmall is used cuz it's also 4dp, 1 avatar margin + 2 paddings = 3
groupMessageWidth = windowWidth - margin - groupWidthCorrection;
reactionTranslationYType1 = resources.getDimensionPixelSize(R.dimen.dm_reaction_translation_y_type_1);
reactionTranslationYType2 = resources.getDimensionPixelSize(R.dimen.dm_reaction_translation_y_type_2);
}
public void bind(final DirectItem item) {
public void bind(final int position, final DirectItem item) {
this.item = item;
final MessageDirection messageDirection = isSelf(item) ? MessageDirection.OUTGOING : MessageDirection.INCOMING;
itemView.post(() -> bindBase(item, messageDirection));
itemView.post(() -> bindItem(item, messageDirection));
itemView.post(() -> setupLongClickListener(position));
// bindBase(item, messageDirection);
// bindItem(item, messageDirection);
// setupLongClickListener(position);
}
private void bindBase(final DirectItem item, final MessageDirection messageDirection) {
@ -104,7 +134,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
binding.messageInfo.setPadding(0, 0, messageInfoPaddingSmall, dmRadiusSmall);
}
setupReply(item, messageDirection);
setReactions(item, thread.getUsers());
setReactions(item);
}
private void setBackground(final MessageDirection messageDirection) {
@ -304,31 +334,39 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
replyInfoLayoutParams.endToStart = isIncoming ? ConstraintLayout.LayoutParams.UNSET : quoteLineId;
}
private void setReactions(final DirectItem item, final List<User> users) {
final DirectItemReactions reactions = item.getReactions();
final List<DirectItemEmojiReaction> emojis = reactions != null ? reactions.getEmojis() : null;
if (emojis == null || emojis.isEmpty()) {
binding.reactions.setVisibility(View.GONE);
return;
}
binding.reactions.setVisibility(View.VISIBLE);
final String emojisJoined = emojis.stream()
.map(DirectItemEmojiReaction::getEmoji)
.collect(Collectors.joining());
final String text = String.format(Locale.ENGLISH, "%s %d", emojisJoined, emojis.size());
binding.emojis.setText(text);
// final List<DirectUser> reactedUsers = emojis.stream()
// .map(DirectItemEmojiReaction::getSenderId)
// .distinct()
// .map(userId -> getUser(userId, users))
// .collect(Collectors.toList());
// for (final DirectUser user : reactedUsers) {
// if (user == null) continue;
// final ProfilePicView profilePicView = new ProfilePicView(itemView.getContext());
// profilePicView.setSize(ProfilePicView.Size.TINY);
// profilePicView.setImageURI(user.getProfilePicUrl());
// binding.reactions.addView(profilePicView);
// }
private void setReactions(final DirectItem item) {
binding.getRoot().post(() -> {
MaterialFade materialFade = new MaterialFade();
materialFade.addTarget(binding.emojis);
TransitionManager.beginDelayedTransition(binding.getRoot(), materialFade);
final DirectItemReactions reactions = item.getReactions();
final List<DirectItemEmojiReaction> emojis = reactions != null ? reactions.getEmojis() : null;
if (emojis == null || emojis.isEmpty()) {
binding.container.setPadding(messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall);
binding.emojis.setVisibility(View.GONE);
return;
}
binding.emojis.setVisibility(View.VISIBLE);
binding.emojis.setTranslationY(getReactionsTranslationY());
binding.container.setPadding(messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall, reactionAdjustMargin);
final String emojisJoined = emojis.stream()
.map(DirectItemEmojiReaction::getEmoji)
.collect(Collectors.joining());
final String text = String.format(Locale.ENGLISH, "%s %d", emojisJoined, emojis.size());
binding.emojis.setText(text);
// final List<DirectUser> reactedUsers = emojis.stream()
// .map(DirectItemEmojiReaction::getSenderId)
// .distinct()
// .map(userId -> getUser(userId, users))
// .collect(Collectors.toList());
// for (final DirectUser user : reactedUsers) {
// if (user == null) continue;
// final ProfilePicView profilePicView = new ProfilePicView(itemView.getContext());
// profilePicView.setSize(ProfilePicView.Size.TINY);
// profilePicView.setImageURI(user.getProfilePicUrl());
// binding.reactions.addView(profilePicView);
// }
});
}
protected boolean isSelf(final DirectItem directItem) {
@ -370,7 +408,28 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
return true;
}
public void cleanup() {}
protected boolean allowLongClick() {
return true;
}
protected boolean allowReaction() {
return true;
}
protected List<DirectItemContextMenu.MenuItem> getLongClickOptions() {
return null;
}
protected int getReactionsTranslationY() {
return reactionTranslationYType1;
}
@CallSuper
public void cleanup() {
// if (layoutChangeListener != null) {
// binding.container.removeOnLayoutChangeListener(layoutChangeListener);
// }
}
protected void setupRamboTextListeners(@NonNull final RamboTextViewV2 textView) {
textView.addOnHashtagListener(autoLinkItem -> callback.onHashtagClick(autoLinkItem.getOriginalText().trim()));
@ -410,6 +469,73 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
}
}
@SuppressLint("ClickableViewAccessibility")
private void setupLongClickListener(final int position) {
if (!allowLongClick()) return;
binding.getRoot().setOnItemLongClickListener(new DirectItemFrameLayout.OnItemLongClickListener() {
@Override
public void onLongClickStart(final View view) {
itemView.post(() -> shrink());
}
@Override
public void onLongClickCancel(final View view) {
itemView.post(() -> grow());
}
@Override
public void onLongClick(final View view, final float x, final float y) {
// if (longClickListener == null) return false;
// longClickListener.onLongClick(position, this);
itemView.post(() -> grow());
setSelected(true);
showLongClickOptions(new Point((int) x, (int) y));
}
});
}
private void showLongClickOptions(final Point location) {
final DirectItemContextMenu menu = new DirectItemContextMenu(itemView.getContext(), allowReaction(), getLongClickOptions());
menu.setOnDismissListener(() -> setSelected(false));
menu.setOnReactionClickListener(emoji -> {
callback.onReaction(item, emoji);
});
menu.show(itemView, location);
}
public void setLongClickListener(final DirectItemInternalLongClickListener longClickListener) {
this.longClickListener = longClickListener;
}
public void setSelected(final boolean selected) {
this.selected = selected;
}
private void shrink() {
if (shrinkGrowAnimator != null) {
shrinkGrowAnimator.cancel();
}
shrinkGrowAnimator = itemView.animate()
.scaleX(0.8f)
.scaleY(0.8f)
.setInterpolator(accelerateDecelerateInterpolator)
.setDuration(ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout());
shrinkGrowAnimator.start();
}
private void grow() {
if (shrinkGrowAnimator != null) {
shrinkGrowAnimator.cancel();
}
shrinkGrowAnimator = itemView.animate()
.scaleX(1f)
.scaleY(1f)
.setInterpolator(accelerateDecelerateInterpolator)
.setDuration(200)
.withEndAction(() -> shrinkGrowAnimator = null);
shrinkGrowAnimator.start();
}
public enum MessageDirection {
INCOMING,
OUTGOING

View File

@ -154,6 +154,7 @@ public class DirectItemVoiceMediaViewHolder extends DirectItemViewHolder {
@Override
public void cleanup() {
super.cleanup();
if (handler != null && positionChecker != null) {
handler.removeCallbacks(positionChecker);
handler = null;

View File

@ -0,0 +1,84 @@
package awais.instagrabber.animations;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.graphics.Outline;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewOutlineProvider;
/**
* A {@link ViewOutlineProvider} that has helper functions to create reveal animations.
* This class should be extended so that subclasses can define the reveal shape as the
* animation progresses from 0 to 1.
*/
public abstract class RevealOutlineAnimation extends ViewOutlineProvider {
protected Rect mOutline;
protected float mOutlineRadius;
public RevealOutlineAnimation() {
mOutline = new Rect();
}
/**
* Returns whether elevation should be removed for the duration of the reveal animation.
*/
abstract boolean shouldRemoveElevationDuringAnimation();
/**
* Sets the progress, from 0 to 1, of the reveal animation.
*/
abstract void setProgress(float progress);
public ValueAnimator createRevealAnimator(final View revealView, boolean isReversed) {
ValueAnimator va =
isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f);
final float elevation = revealView.getElevation();
va.addListener(new AnimatorListenerAdapter() {
private boolean mIsClippedToOutline;
private ViewOutlineProvider mOldOutlineProvider;
public void onAnimationStart(Animator animation) {
mIsClippedToOutline = revealView.getClipToOutline();
mOldOutlineProvider = revealView.getOutlineProvider();
revealView.setOutlineProvider(RevealOutlineAnimation.this);
revealView.setClipToOutline(true);
if (shouldRemoveElevationDuringAnimation()) {
revealView.setTranslationZ(-elevation);
}
}
public void onAnimationEnd(Animator animation) {
revealView.setOutlineProvider(mOldOutlineProvider);
revealView.setClipToOutline(mIsClippedToOutline);
if (shouldRemoveElevationDuringAnimation()) {
revealView.setTranslationZ(0);
}
}
});
va.addUpdateListener(v -> {
float progress = (Float) v.getAnimatedValue();
setProgress(progress);
revealView.invalidateOutline();
});
return va;
}
@Override
public void getOutline(View v, Outline outline) {
outline.setRoundRect(mOutline, mOutlineRadius);
}
public float getRadius() {
return mOutlineRadius;
}
public void getOutline(Rect out) {
out.set(mOutline);
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package awais.instagrabber.animations;
import android.graphics.Rect;
/**
* A {@link RevealOutlineAnimation} that provides an outline that interpolates between two radii
* and two {@link Rect}s.
* <p>
* An example usage of this provider is an outline that starts out as a circle and ends
* as a rounded rectangle.
*/
public class RoundedRectRevealOutlineProvider extends RevealOutlineAnimation {
private final float mStartRadius;
private final float mEndRadius;
private final Rect mStartRect;
private final Rect mEndRect;
public RoundedRectRevealOutlineProvider(float startRadius, float endRadius, Rect startRect, Rect endRect) {
mStartRadius = startRadius;
mEndRadius = endRadius;
mStartRect = startRect;
mEndRect = endRect;
}
@Override
public boolean shouldRemoveElevationDuringAnimation() {
return false;
}
@Override
public void setProgress(float progress) {
mOutlineRadius = (1 - progress) * mStartRadius + progress * mEndRadius;
mOutline.left = (int) ((1 - progress) * mStartRect.left + progress * mEndRect.left);
mOutline.top = (int) ((1 - progress) * mStartRect.top + progress * mEndRect.top);
mOutline.right = (int) ((1 - progress) * mStartRect.right + progress * mEndRect.right);
mOutline.bottom = (int) ((1 - progress) * mStartRect.bottom + progress * mEndRect.bottom);
}
}

View File

@ -93,7 +93,8 @@ public class ChatMessageLayout extends FrameLayout {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight;
} else if (firstChildId == R.id.raven_media_container || firstChildId == R.id.profile_container || firstChildId == R.id.voice_media
|| firstChildId == R.id.story_container || firstChildId == R.id.media_share_container || firstChildId == R.id.link_container) {
|| firstChildId == R.id.story_container || firstChildId == R.id.media_share_container || firstChildId == R.id.link_container
|| firstChildId == R.id.ivAnimatedMessage) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight + viewPartInfoHeight;
} else {

View File

@ -0,0 +1,457 @@
package awais.instagrabber.customviews;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.ImageView;
import android.widget.PopupWindow;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.util.Pair;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.animations.RoundedRectRevealOutlineProvider;
import awais.instagrabber.customviews.emoji.Emoji;
import awais.instagrabber.customviews.emoji.ReactionsManager;
import awais.instagrabber.databinding.LayoutDirectItemOptionsBinding;
import static android.view.View.MeasureSpec.makeMeasureSpec;
public class DirectItemContextMenu extends PopupWindow {
private static final String TAG = DirectItemContextMenu.class.getSimpleName();
private static final int DO_NOT_UPDATE_FLAG = -1;
private static final int DURATION = 300;
private final Context context;
private final boolean showReactions;
private final ReactionsManager reactionsManager;
private final int emojiSize;
private final int emojiMargin;
private final int emojiMarginHalf;
private final Rect startRect = new Rect();
private final Rect endRect = new Rect();
private final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
private final AnimatorListenerAdapter exitAnimationListener;
private final TypedValue selectableItemBackgroundBorderless;
private final TypedValue selectableItemBackground;
private final int dividerHeight;
private final int optionHeight;
private final int optionPadding;
private final int addAdjust;
private final boolean hasOptions;
private final List<MenuItem> options;
/* = ImmutableList.of(
new MenuItem(R.id.reply, R.string.reply),
new MenuItem(R.id.unsend, R.string.dms_inbox_unsend)
);*/
private AnimatorSet openCloseAnimator;
private Point location;
private Point point;
private OnReactionClickListener onReactionClickListener;
private OnOptionSelectListener onOptionSelectListener;
private OnAddReactionClickListener onAddReactionListener;
public DirectItemContextMenu(@NonNull final Context context, final boolean showReactions, final List<MenuItem> options) {
super(context);
this.context = context;
this.showReactions = showReactions;
this.options = options;
if (!showReactions && (options == null || options.isEmpty())) {
throw new IllegalArgumentException("showReactions is set false and options are empty");
}
reactionsManager = ReactionsManager.getInstance();
emojiSize = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_emoji_size);
emojiMargin = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_emoji_margin);
emojiMarginHalf = emojiMargin / 2;
addAdjust = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_add_padding_adjustment);
dividerHeight = context.getResources().getDimensionPixelSize(R.dimen.horizontal_divider_height);
optionHeight = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_option_height);
optionPadding = context.getResources().getDimensionPixelSize(R.dimen.dm_message_card_radius);
exitAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
openCloseAnimator = null;
point = null;
getContentView().post(DirectItemContextMenu.super::dismiss);
}
};
selectableItemBackgroundBorderless = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, selectableItemBackgroundBorderless, true);
selectableItemBackground = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, selectableItemBackground, true);
hasOptions = options != null && !options.isEmpty();
}
public void show(@NonNull View rootView, @NonNull final Point location) {
final View content = createContentView();
content.measure(makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
setup(content);
// rootView.getParent().requestDisallowInterceptTouchEvent(true);
// final Point correctedLocation = new Point(location.x, location.y - emojiSize * 2);
this.location = location;
showAtLocation(rootView, Gravity.TOP | Gravity.START, location.x, location.y);
// fixPopupLocation(popupWindow, correctedLocation);
animateOpen();
}
private void setup(final View content) {
setContentView(content);
setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
setFocusable(true);
setOutsideTouchable(true);
setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
setBackgroundDrawable(null);
}
public void setOnOptionSelectListener(final OnOptionSelectListener onOptionSelectListener) {
this.onOptionSelectListener = onOptionSelectListener;
}
public void setOnReactionClickListener(final OnReactionClickListener onReactionClickListener) {
this.onReactionClickListener = onReactionClickListener;
}
public void setOnAddReactionListener(final OnAddReactionClickListener onAddReactionListener) {
this.onAddReactionListener = onAddReactionListener;
}
private void animateOpen() {
final View contentView = getContentView();
contentView.setVisibility(View.INVISIBLE);
contentView.post(() -> {
final AnimatorSet openAnim = new AnimatorSet();
// Rectangular reveal.
final ValueAnimator revealAnim = createOpenCloseOutlineProvider().createRevealAnimator(contentView, false);
revealAnim.setDuration(DURATION);
revealAnim.setInterpolator(revealInterpolator);
ValueAnimator fadeIn = ValueAnimator.ofFloat(0, 1);
fadeIn.setDuration(DURATION);
fadeIn.setInterpolator(revealInterpolator);
fadeIn.addUpdateListener(anim -> {
float alpha = (float) anim.getAnimatedValue();
contentView.setAlpha(revealAnim.isStarted() ? alpha : 0);
});
openAnim.play(fadeIn);
openAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
contentView.setAlpha(1f);
openCloseAnimator = null;
}
});
openCloseAnimator = openAnim;
openAnim.playSequentially(revealAnim);
contentView.setVisibility(View.VISIBLE);
openAnim.start();
});
}
protected void animateClose() {
endRect.setEmpty();
if (openCloseAnimator != null) {
openCloseAnimator.cancel();
}
final View contentView = getContentView();
final AnimatorSet closeAnim = new AnimatorSet();
// Rectangular reveal (reversed).
final ValueAnimator revealAnim = createOpenCloseOutlineProvider().createRevealAnimator(contentView, true);
revealAnim.setDuration(DURATION);
revealAnim.setInterpolator(revealInterpolator);
closeAnim.play(revealAnim);
ValueAnimator fadeOut = ValueAnimator.ofFloat(contentView.getAlpha(), 0);
fadeOut.setDuration(DURATION);
fadeOut.setInterpolator(revealInterpolator);
fadeOut.addUpdateListener(anim -> {
float alpha = (float) anim.getAnimatedValue();
contentView.setAlpha(revealAnim.isStarted() ? alpha : contentView.getAlpha());
});
closeAnim.playTogether(fadeOut);
closeAnim.addListener(exitAnimationListener);
openCloseAnimator = closeAnim;
closeAnim.start();
}
private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
final View contentView = getContentView();
final int radius = context.getResources().getDimensionPixelSize(R.dimen.dm_message_card_radius_small);
// Log.d(TAG, "createOpenCloseOutlineProvider: " + locationOnScreen(contentView) + " " + contentView.getMeasuredWidth() + " " + contentView
// .getMeasuredHeight());
if (point == null) {
point = locationOnScreen(contentView);
}
final int left = location.x - point.x;
final int top = location.y - point.y;
startRect.set(left, top, left, top);
endRect.set(0, 0, contentView.getMeasuredWidth(), contentView.getMeasuredHeight());
return new RoundedRectRevealOutlineProvider(radius, radius, startRect, endRect);
}
public void dismiss() {
animateClose();
}
private View createContentView() {
final LayoutInflater layoutInflater = LayoutInflater.from(context);
final LayoutDirectItemOptionsBinding binding = LayoutDirectItemOptionsBinding.inflate(layoutInflater, null, false);
Pair<View, View> firstLastEmojiView = null;
if (showReactions) {
firstLastEmojiView = addReactions(layoutInflater, binding.container);
}
if (hasOptions) {
View divider = null;
if (showReactions) {
if (firstLastEmojiView == null) {
throw new IllegalStateException("firstLastEmojiView is null even though reactions were added");
}
// add divider if reactions were added
divider = addDivider(binding.container,
firstLastEmojiView.first.getId(),
firstLastEmojiView.first.getId(),
firstLastEmojiView.second.getId());
((ConstraintLayout.LayoutParams) firstLastEmojiView.first.getLayoutParams()).bottomToTop = divider.getId();
}
addOptions(layoutInflater, binding.container, divider);
}
return binding.getRoot();
}
private Pair<View, View> addReactions(final LayoutInflater layoutInflater, final ConstraintLayout container) {
final List<Emoji> reactions = reactionsManager.getReactions();
AppCompatImageView prevSquareImageView = null;
View firstImageView = null;
View lastImageView = null;
for (int i = 0; i < reactions.size(); i++) {
final Emoji reaction = reactions.get(i);
final AppCompatImageView imageView = getEmojiImageView();
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) imageView.getLayoutParams();
if (i == 0 && !hasOptions) {
// only connect bottom to parent bottom if there are no options
layoutParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
}
if (i == 0) {
layoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
firstImageView = imageView;
layoutParams.setMargins(emojiMargin, emojiMargin, emojiMarginHalf, emojiMargin);
} else {
layoutParams.startToEnd = prevSquareImageView.getId();
final ConstraintLayout.LayoutParams prevViewLayoutParams = (ConstraintLayout.LayoutParams) prevSquareImageView.getLayoutParams();
prevViewLayoutParams.endToStart = imageView.getId();
// always connect the other image view's top and bottom to the first image view top and bottom
layoutParams.topToTop = firstImageView.getId();
layoutParams.bottomToBottom = firstImageView.getId();
layoutParams.setMargins(emojiMarginHalf, emojiMargin, emojiMarginHalf, emojiMargin);
}
imageView.setImageDrawable(reaction.getDrawable());
imageView.setOnClickListener(view -> {
if (onReactionClickListener != null) {
onReactionClickListener.onClick(reaction);
}
dismiss();
});
container.addView(imageView);
prevSquareImageView = imageView;
}
// add the + icon
if (prevSquareImageView != null) {
final AppCompatImageView imageView = getEmojiImageView();
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) imageView.getLayoutParams();
layoutParams.topToTop = firstImageView.getId();
layoutParams.bottomToBottom = firstImageView.getId();
layoutParams.startToEnd = prevSquareImageView.getId();
final ConstraintLayout.LayoutParams prevViewLayoutParams = (ConstraintLayout.LayoutParams) prevSquareImageView.getLayoutParams();
prevViewLayoutParams.endToStart = imageView.getId();
layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.setMargins(emojiMarginHalf - addAdjust, emojiMargin - addAdjust, emojiMargin - addAdjust, emojiMargin - addAdjust);
imageView.setImageResource(R.drawable.ic_add);
imageView.setOnClickListener(view -> {
if (onAddReactionListener != null) {
onAddReactionListener.onAdd();
}
dismiss();
});
lastImageView = imageView;
container.addView(imageView);
}
return new Pair<>(firstImageView, lastImageView);
}
@NonNull
private AppCompatImageView getEmojiImageView() {
final AppCompatImageView imageView = new AppCompatImageView(context);
final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(emojiSize, emojiSize);
imageView.setBackgroundResource(selectableItemBackgroundBorderless.resourceId);
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
imageView.setId(SquareImageView.generateViewId());
imageView.setLayoutParams(layoutParams);
return imageView;
}
private void addOptions(final LayoutInflater layoutInflater,
final ConstraintLayout container,
@Nullable final View divider) {
View prevOptionView = null;
for (int i = 0; i < options.size(); i++) {
final MenuItem menuItem = options.get(i);
final AppCompatTextView textView = getTextView();
final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) textView.getLayoutParams();
layoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
if (i == 0) {
if (divider != null) {
layoutParams.topToBottom = divider.getId();
} else {
// if divider is null mean reactions were not added, so connect top to top of parent
layoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
}
((ConstraintLayout.LayoutParams) divider.getLayoutParams()).bottomToTop = textView.getId();
} else {
layoutParams.topToBottom = prevOptionView.getId();
final ConstraintLayout.LayoutParams prevLayoutParams = (ConstraintLayout.LayoutParams) prevOptionView.getLayoutParams();
prevLayoutParams.bottomToTop = textView.getId();
}
if (i == options.size() - 1) {
layoutParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
layoutParams.bottomMargin = emojiMargin; // material design spec (https://material.io/components/menus#specs)
}
textView.setText(context.getString(menuItem.getTitleRes()));
textView.setOnClickListener(v -> {
if (onOptionSelectListener != null) {
onOptionSelectListener.onSelect(menuItem.getItemId());
}
dismiss();
});
container.addView(textView);
prevOptionView = textView;
}
}
private AppCompatTextView getTextView() {
final AppCompatTextView textView = new AppCompatTextView(context);
textView.setId(AppCompatEditText.generateViewId());
textView.setBackgroundResource(selectableItemBackground.resourceId);
textView.setGravity(Gravity.CENTER_VERTICAL);
textView.setPaddingRelative(optionPadding, 0, optionPadding, 0);
textView.setTextAppearance(context, R.style.TextAppearance_MaterialComponents_Body1);
final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_CONSTRAINT,
optionHeight);
textView.setLayoutParams(layoutParams);
return textView;
}
private View addDivider(final ConstraintLayout container,
final int topViewId,
final int startViewId,
final int endViewId) {
final View dividerView = new View(context);
dividerView.setId(View.generateViewId());
dividerView.setBackgroundResource(R.drawable.pref_list_divider_material);
final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_CONSTRAINT,
dividerHeight);
layoutParams.topToBottom = topViewId;
layoutParams.startToStart = startViewId;
layoutParams.endToEnd = endViewId;
dividerView.setLayoutParams(layoutParams);
container.addView(dividerView);
return dividerView;
}
@NonNull
private Point locationOnScreen(@NonNull final View view) {
final int[] location = new int[2];
view.getLocationOnScreen(location);
return new Point(location[0], location[1]);
}
public static class MenuItem {
@IdRes
private final int itemId;
@StringRes
private final int titleRes;
public MenuItem(@IdRes final int itemId, @StringRes final int titleRes) {
this.itemId = itemId;
this.titleRes = titleRes;
}
public int getItemId() {
return itemId;
}
public int getTitleRes() {
return titleRes;
}
}
public interface OnOptionSelectListener {
void onSelect(int itemId);
}
public interface OnReactionClickListener {
void onClick(Emoji emoji);
}
public interface OnAddReactionClickListener {
void onAdd();
}
// @NonNull
// private Rect getGlobalVisibleRect(@NonNull final View view) {
// final Rect rect = new Rect();
// view.getGlobalVisibleRect(rect);
// return rect;
// }
// private void fixPopupLocation(@NonNull final PopupWindow popupWindow, @NonNull final Point desiredLocation) {
// popupWindow.getContentView().post(() -> {
// final Point actualLocation = locationOnScreen(popupWindow.getContentView());
//
// if (!(actualLocation.x == desiredLocation.x && actualLocation.y == desiredLocation.y)) {
// final int differenceX = actualLocation.x - desiredLocation.x;
// final int differenceY = actualLocation.y - desiredLocation.y;
//
// final int fixedOffsetX;
// final int fixedOffsetY;
//
// if (actualLocation.x > desiredLocation.x) {
// fixedOffsetX = desiredLocation.x - differenceX;
// } else {
// fixedOffsetX = desiredLocation.x + differenceX;
// }
//
// if (actualLocation.y > desiredLocation.y) {
// fixedOffsetY = desiredLocation.y - differenceY;
// } else {
// fixedOffsetY = desiredLocation.y + differenceY;
// }
//
// popupWindow.update(fixedOffsetX, fixedOffsetY, DO_NOT_UPDATE_FLAG, DO_NOT_UPDATE_FLAG);
// }
// });
// }
}

View File

@ -0,0 +1,110 @@
package awais.instagrabber.customviews;
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class DirectItemFrameLayout extends FrameLayout {
private static final String TAG = DirectItemFrameLayout.class.getSimpleName();
private boolean longPressed = false;
private float touchX;
private float touchY;
private OnItemLongClickListener onItemLongClickListener;
private int touchSlop;
private final Handler handler = new Handler();
private final Runnable longPressRunnable = () -> {
longPressed = true;
if (onItemLongClickListener != null) {
onItemLongClickListener.onLongClick(this, touchX, touchY);
}
};
private final Runnable longPressStartRunnable = () -> {
if (onItemLongClickListener != null) {
onItemLongClickListener.onLongClickStart(this);
}
};
public DirectItemFrameLayout(@NonNull final Context context) {
super(context);
init(context);
}
public DirectItemFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
init(context);
}
public DirectItemFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public DirectItemFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(final Context context) {
ViewConfiguration vc = ViewConfiguration.get(context);
touchSlop = vc.getScaledTouchSlop();
}
public void setOnItemLongClickListener(final OnItemLongClickListener onItemLongClickListener) {
this.onItemLongClickListener = onItemLongClickListener;
}
@Override
public boolean dispatchTouchEvent(final MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
longPressed = false;
handler.postDelayed(longPressRunnable, ViewConfiguration.getLongPressTimeout());
handler.postDelayed(longPressStartRunnable, ViewConfiguration.getTapTimeout());
touchX = ev.getRawX();
touchY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
if (longPressed || Math.abs(touchX - ev.getRawX()) > touchSlop || Math.abs(touchY - ev.getRawY()) > touchSlop) {
handler.removeCallbacks(longPressStartRunnable);
handler.removeCallbacks(longPressRunnable);
}
break;
case MotionEvent.ACTION_UP:
handler.removeCallbacks(longPressRunnable);
handler.removeCallbacks(longPressStartRunnable);
if (longPressed) {
return true;
}
if (onItemLongClickListener != null) {
onItemLongClickListener.onLongClickCancel(this);
}
break;
case MotionEvent.ACTION_CANCEL:
handler.removeCallbacks(longPressRunnable);
handler.removeCallbacks(longPressStartRunnable);
break;
}
final boolean dispatchTouchEvent = super.dispatchTouchEvent(ev);
if (ev.getAction() == MotionEvent.ACTION_DOWN && !dispatchTouchEvent) {
return true;
}
return dispatchTouchEvent;
}
public interface OnItemLongClickListener {
void onLongClickStart(View view);
void onLongClickCancel(View view);
void onLongClick(View view, float x, float y);
}
}

View File

@ -1,55 +0,0 @@
package awais.instagrabber.customviews;
import android.app.Dialog;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.IBinder;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import awais.instagrabber.utils.Utils;
/**
* https://stackoverflow.com/a/15766097/1436766
*/
public class PopupDialog extends Dialog {
private final Context context;
public PopupDialog(Context context) {
super(context);
this.context = context;
requestWindowFeature(Window.FEATURE_NO_TITLE);
}
public void showAtLocation(final IBinder token, final int gravity, int x, int y) {
final Window window = getWindow();
if (window == null) return;
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.gravity = gravity;
layoutParams.x = x;
layoutParams.y = y;
// layoutParams.token = token;
show();
}
public void showAsDropDown(View view) {
float density = Utils.displayMetrics.density;
final Window window = getWindow();
if (window == null) return;
WindowManager.LayoutParams layoutParams = window.getAttributes();
int[] location = new int[2];
view.getLocationInWindow(location);
layoutParams.gravity = Gravity.TOP | Gravity.START;
layoutParams.x = location[0] + (int) (view.getWidth() / density);
layoutParams.y = location[1] + (int) (view.getHeight() / density);
show();
}
public void setBackgroundDrawable(final Drawable drawable) {
final Window window = getWindow();
if (window == null) return;
window.setBackgroundDrawable(drawable);
}
}

View File

@ -0,0 +1,76 @@
package awais.instagrabber.customviews.emoji;
import android.util.Log;
import com.google.common.collect.ImmutableList;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.utils.emoji.EmojiParser;
import static awais.instagrabber.utils.Constants.PREF_REACTIONS;
public class ReactionsManager {
private static final String TAG = ReactionsManager.class.getSimpleName();
private static final Object LOCK = new Object();
private final AppExecutors appExecutors = AppExecutors.getInstance();
private final List<Emoji> reactions = new ArrayList<>();
private static ReactionsManager instance;
public static ReactionsManager getInstance() {
if (instance == null) {
synchronized (LOCK) {
if (instance == null) {
instance = new ReactionsManager();
}
}
}
return instance;
}
private ReactionsManager() {
String reactionsJson = Utils.settingsHelper.getString(PREF_REACTIONS);
if (TextUtils.isEmpty(reactionsJson)) {
final ImmutableList<String> list = ImmutableList.of("❤️", "\uD83D\uDE02", "\uD83D\uDE2E", "\uD83D\uDE22", "\uD83D\uDE21", "\uD83D\uDC4D");
reactionsJson = new JSONArray(list).toString();
}
final EmojiParser emojiParser = EmojiParser.getInstance();
final Map<String, Emoji> allEmojis = emojiParser.getAllEmojis();
try {
final JSONArray reactionsJsonArray = new JSONArray(reactionsJson);
for (int i = 0; i < reactionsJsonArray.length(); i++) {
final String emojiUnicode = reactionsJsonArray.optString(i);
if (emojiUnicode == null) continue;
final Emoji emoji = allEmojis.get(emojiUnicode);
if (emoji == null) continue;
reactions.add(emoji);
}
} catch (JSONException e) {
Log.e(TAG, "ReactionsManager: ", e);
}
}
public List<Emoji> getReactions() {
return reactions;
}
// public void setVariant(final String parent, final String variant) {
// if (parent == null || variant == null) return;
// selectedVariantMap.put(parent, variant);
// appExecutors.tasksThread().execute(() -> {
// final JSONObject jsonObject = new JSONObject(selectedVariantMap);
// final String json = jsonObject.toString();
// Utils.settingsHelper.putString(PREF_EMOJI_VARIANTS, json);
// });
// }
}

View File

@ -57,11 +57,14 @@ import awais.instagrabber.R;
import awais.instagrabber.activities.CameraActivity;
import awais.instagrabber.activities.MainActivity;
import awais.instagrabber.adapters.DirectItemsAdapter;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemLongClickListener;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemOrHeader;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder;
import awais.instagrabber.animations.CubicBezierInterpolator;
import awais.instagrabber.customviews.RecordView;
import awais.instagrabber.customviews.Tooltip;
import awais.instagrabber.customviews.emoji.Emoji;
import awais.instagrabber.customviews.helpers.HeaderItemDecoration;
import awais.instagrabber.customviews.helpers.HeightProvider;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge;
@ -133,7 +136,7 @@ public class DirectMessageThreadFragment extends Fragment {
setMicToSendIcon();
}
};
private final DirectItemsAdapter.DirectItemCallback directItemCallback = new DirectItemsAdapter.DirectItemCallback() {
private final DirectItemCallback directItemCallback = new DirectItemCallback() {
@Override
public void onHashtagClick(final String hashtag) {
final NavDirections action = DirectMessageThreadFragmentDirections.actionGlobalHashTagFragment(hashtag);
@ -188,6 +191,18 @@ public class DirectMessageThreadFragment extends Fragment {
Log.e(TAG, "onStoryClick: ", e);
}
}
@Override
public void onReaction(final DirectItem item, final Emoji emoji) {
if (item == null) return;
final LiveData<Resource<DirectItem>> resourceLiveData = viewModel.sendReaction(item, emoji);
if (resourceLiveData != null) {
resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData));
}
}
};
private final DirectItemLongClickListener directItemLongClickListener = position -> {
// viewModel.setSelectedPosition(position);
};
@Override
@ -226,6 +241,7 @@ public class DirectMessageThreadFragment extends Fragment {
init();
binding.send.post(() -> initialSendX = binding.send.getX());
shouldRefresh = false;
setObservers();
}
@Override
@ -309,7 +325,6 @@ public class DirectMessageThreadFragment extends Fragment {
binding.input.post(this::showKeyboard);
wasKbShowing = false;
}
setObservers();
if (initialSendX != 0) {
binding.send.setX(initialSendX);
}
@ -354,7 +369,6 @@ public class DirectMessageThreadFragment extends Fragment {
setupList();
root.post(this::setupInput);
root.post(this::getInitialData);
setObservers();
}
private void getInitialData() {
@ -578,7 +592,7 @@ public class DirectMessageThreadFragment extends Fragment {
itemsAdapter.setThread(thread);
return;
}
itemsAdapter = new DirectItemsAdapter(currentUser, thread, directItemCallback);
itemsAdapter = new DirectItemsAdapter(currentUser, thread, directItemCallback, directItemLongClickListener);
itemsAdapter.setHasStableIds(true);
itemsAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
binding.chats.setAdapter(itemsAdapter);
@ -694,7 +708,8 @@ public class DirectMessageThreadFragment extends Fragment {
binding.send.setOnRecordClickListener(v -> {
final Editable text = binding.input.getText();
if (TextUtils.isEmpty(text)) return;
viewModel.sendText(text.toString()).observe(getViewLifecycleOwner(), this::handleSentMessage);
final LiveData<Resource<DirectItem>> resourceLiveData = viewModel.sendText(text.toString());
resourceLiveData.observe(getViewLifecycleOwner(), resource -> handleSentMessage(resourceLiveData));
binding.input.setText("");
});
binding.send.setOnRecordLongClickListener(v -> {
@ -754,16 +769,21 @@ public class DirectMessageThreadFragment extends Fragment {
navController.navigate(navDirections);
}
private void handleSentMessage(@NonNull final Resource<DirectItem> resource) {
private void handleSentMessage(final LiveData<Resource<DirectItem>> resourceLiveData) {
final Resource<DirectItem> resource = resourceLiveData.getValue();
if (resource == null) return;
final Resource.Status status = resource.status;
switch (status) {
case SUCCESS:
resourceLiveData.removeObservers(getViewLifecycleOwner());
break;
case LOADING:
break;
case ERROR:
if (resource.message != null) {
Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
}
resourceLiveData.removeObservers(getViewLifecycleOwner());
break;
}
}
@ -1093,6 +1113,10 @@ public class DirectMessageThreadFragment extends Fragment {
animatorSet.start();
}
private void showLongClickOptions(final View itemView) {
}
public static class ItemsAdapterDataMerger extends MediatorLiveData<Pair<User, DirectThread>> {
private User user;
private DirectThread thread;

View File

@ -4,17 +4,21 @@ import java.util.HashMap;
import java.util.Map;
import awais.instagrabber.models.enums.BroadcastItemType;
import awais.instagrabber.utils.TextUtils;
public class ReactionBroadcastOptions extends BroadcastOptions {
private final String itemId;
private final String emoji;
private final boolean delete;
public ReactionBroadcastOptions(final String clientContext,
final ThreadIdOrUserIds threadIdOrUserIds,
final String itemId,
final String emoji,
final boolean delete) {
super(clientContext, threadIdOrUserIds, BroadcastItemType.REACTION);
this.itemId = itemId;
this.emoji = emoji;
this.delete = delete;
}
@ -24,6 +28,9 @@ public class ReactionBroadcastOptions extends BroadcastOptions {
form.put("item_id", itemId);
form.put("reaction_status", delete ? "deleted" : "created");
form.put("reaction_type", "like");
if (!TextUtils.isEmpty(emoji)) {
form.put("emoji", emoji);
}
return form;
}
}

View File

@ -32,7 +32,7 @@ public class DirectItem implements Cloneable {
private final DirectItemFelixShare felixShare;
private final DirectItemVisualMedia visualMedia;
private final DirectItemAnimatedMedia animatedMedia;
private final DirectItemReactions reactions;
private DirectItemReactions reactions;
private final DirectItem repliedToMessage;
private final DirectItemVoiceMedia voiceMedia;
private final Location location;
@ -218,6 +218,10 @@ public class DirectItem implements Cloneable {
isPending = pending;
}
public void setReactions(final DirectItemReactions reactions) {
this.reactions = reactions;
}
@NonNull
@Override
public Object clone() throws CloneNotSupportedException {

View File

@ -1,5 +1,7 @@
package awais.instagrabber.repositories.responses.directmessages;
import java.util.Objects;
public class DirectItemEmojiReaction {
private final long senderId;
private final long timestamp;
@ -28,4 +30,20 @@ public class DirectItemEmojiReaction {
public String getSuperReactType() {
return superReactType;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final DirectItemEmojiReaction that = (DirectItemEmojiReaction) o;
return senderId == that.senderId &&
timestamp == that.timestamp &&
Objects.equals(emoji, that.emoji) &&
Objects.equals(superReactType, that.superReactType);
}
@Override
public int hashCode() {
return Objects.hash(senderId, timestamp, emoji, superReactType);
}
}

View File

@ -1,10 +1,13 @@
package awais.instagrabber.repositories.responses.directmessages;
import java.util.List;
import androidx.annotation.NonNull;
public class DirectItemReactions {
private final List<DirectItemEmojiReaction> emojis;
private final List<DirectItemEmojiReaction> likes;
import java.util.List;
import java.util.Objects;
public class DirectItemReactions implements Cloneable {
private List<DirectItemEmojiReaction> emojis;
private List<DirectItemEmojiReaction> likes;
public DirectItemReactions(final List<DirectItemEmojiReaction> emojis,
final List<DirectItemEmojiReaction> likes) {
@ -19,4 +22,32 @@ public class DirectItemReactions {
public List<DirectItemEmojiReaction> getLikes() {
return likes;
}
public void setLikes(final List<DirectItemEmojiReaction> likes) {
this.likes = likes;
}
public void setEmojis(final List<DirectItemEmojiReaction> emojis) {
this.emojis = emojis;
}
@NonNull
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final DirectItemReactions that = (DirectItemReactions) o;
return Objects.equals(emojis, that.emojis) &&
Objects.equals(likes, that.likes);
}
@Override
public int hashCode() {
return Objects.hash(emojis, likes);
}
}

View File

@ -95,4 +95,5 @@ public final class Constants {
public static final String PREF_TAGGED_POSTS_LAYOUT = "tagged_posts_layout";
public static final String PREF_SAVED_POSTS_LAYOUT = "saved_posts_layout";
public static final String PREF_EMOJI_VARIANTS = "emoji_variants";
public static final String PREF_REACTIONS = "reactions";
}

View File

@ -34,6 +34,7 @@ import static awais.instagrabber.utils.Constants.PREF_LIKED_POSTS_LAYOUT;
import static awais.instagrabber.utils.Constants.PREF_LOCATION_POSTS_LAYOUT;
import static awais.instagrabber.utils.Constants.PREF_POSTS_LAYOUT;
import static awais.instagrabber.utils.Constants.PREF_PROFILE_POSTS_LAYOUT;
import static awais.instagrabber.utils.Constants.PREF_REACTIONS;
import static awais.instagrabber.utils.Constants.PREF_SAVED_POSTS_LAYOUT;
import static awais.instagrabber.utils.Constants.PREF_TAGGED_POSTS_LAYOUT;
import static awais.instagrabber.utils.Constants.PREF_TOPIC_POSTS_LAYOUT;
@ -123,7 +124,7 @@ public final class SettingsHelper {
{APP_LANGUAGE, APP_THEME, COOKIE, FOLDER_PATH, DATE_TIME_FORMAT, DATE_TIME_SELECTION, CUSTOM_DATE_TIME_FORMAT,
DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT,
PREF_PROFILE_POSTS_LAYOUT, PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT, PREF_LOCATION_POSTS_LAYOUT,
PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT, STORY_SORT, PREF_EMOJI_VARIANTS})
PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT, STORY_SORT, PREF_EMOJI_VARIANTS, PREF_REACTIONS})
public @interface StringSettings {}
@StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS,

View File

@ -14,6 +14,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
@ -51,6 +52,7 @@ public final class EmojiParser {
private static final UnicodeSet SKIN_TONE_MODIFIERS = new UnicodeSet("[🏻-🏿]").freeze();
private static final String SKIN_TONE_PATTERN = SKIN_TONE_MODIFIERS.toPattern(true);
private static final Map<EmojiCategoryType, EmojiCategory> CATEGORY_MAP = new LinkedHashMap<>();
private static final Map<String, Emoji> ALL_EMOJIS = new HashMap<>();
// private final UnicodeMap<String> emojiToMajorCategory = new UnicodeMap<>();
// private final UnicodeMap<String> emojiToMinorCategory = new UnicodeMap<>();
@ -201,6 +203,7 @@ public final class EmojiParser {
spacePos = comment.indexOf(' ', spacePos + 1); // get second space
final String name = comment.substring(spacePos + 1).trim();
final Emoji emoji = new Emoji(original, name);
ALL_EMOJIS.put(original, emoji);
String minimal = original.replace(EMOJI_VARIANT, "");
//noinspection deprecation
boolean singleton = CharSequences.getSingleCodePoint(minimal) != Integer.MAX_VALUE;
@ -263,6 +266,10 @@ public final class EmojiParser {
return categories;
}
public Map<String, Emoji> getAllEmojis() {
return ALL_EMOJIS;
}
// public String getMinorCategory(String emoji) {
// String minorCat = emojiToMinorCategory.get(emoji);
// if (minorCat == null) {

View File

@ -30,12 +30,15 @@ import java.util.Locale;
import java.util.UUID;
import java.util.stream.Collectors;
import awais.instagrabber.customviews.emoji.Emoji;
import awais.instagrabber.models.Resource;
import awais.instagrabber.models.UploadVideoOptions;
import awais.instagrabber.repositories.requests.UploadFinishOptions;
import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction;
import awais.instagrabber.repositories.responses.directmessages.DirectItemReactions;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponseMessageMetadata;
@ -74,6 +77,7 @@ public class DirectThreadViewModel extends AndroidViewModel {
private final MutableLiveData<String> threadTitle = new MutableLiveData<>("");
private final MutableLiveData<Boolean> fetching = new MutableLiveData<>(false);
private final MutableLiveData<List<User>> users = new MutableLiveData<>(new ArrayList<>());
private final DirectMessagesService service;
private final ContentResolver contentResolver;
private final MediaService mediaService;
@ -154,6 +158,74 @@ public class DirectThreadViewModel extends AndroidViewModel {
this.items.postValue(list);
}
private void addReaction(final DirectItem item, final Emoji emoji) {
if (item == null || emoji == null || currentUser == null) return;
final boolean isLike = emoji.getUnicode().equals("❤️");
DirectItemReactions reactions = item.getReactions();
if (reactions == null) {
reactions = new DirectItemReactions(null, null);
} else {
try {
reactions = (DirectItemReactions) reactions.clone();
} catch (CloneNotSupportedException e) {
Log.e(TAG, "addReaction: ", e);
return;
}
}
if (isLike) {
final List<DirectItemEmojiReaction> likes = addEmoji(reactions.getLikes(), null, false);
reactions.setLikes(likes);
}
final List<DirectItemEmojiReaction> emojis = addEmoji(reactions.getEmojis(), emoji.getUnicode(), true);
reactions.setEmojis(emojis);
List<DirectItem> list = this.items.getValue();
list = list == null ? new LinkedList<>() : new LinkedList<>(list);
int index = -1;
for (int i = 0; i < list.size(); i++) {
final DirectItem directItem = list.get(i);
if (directItem.getItemId().equals(item.getItemId())) {
index = i;
break;
}
}
if (index >= 0) {
try {
final DirectItem clone = (DirectItem) list.get(index).clone();
clone.setReactions(reactions);
list.set(index, clone);
} catch (CloneNotSupportedException e) {
Log.e(TAG, "addReaction: error cloning", e);
}
}
this.items.postValue(list);
}
private List<DirectItemEmojiReaction> addEmoji(final List<DirectItemEmojiReaction> reactionList,
final String emoji,
final boolean shouldReplaceIfAlreadyReacted) {
final List<DirectItemEmojiReaction> temp = reactionList == null ? new ArrayList<>() : new ArrayList<>(reactionList);
int index = -1;
for (int i = 0; i < temp.size(); i++) {
final DirectItemEmojiReaction directItemEmojiReaction = temp.get(i);
if (directItemEmojiReaction.getSenderId() == currentUser.getPk()) {
index = i;
break;
}
}
final DirectItemEmojiReaction reaction = new DirectItemEmojiReaction(
currentUser.getPk(),
System.currentTimeMillis() * 1000,
emoji,
"none"
);
if (index < 0) {
temp.add(reaction);
} else if (shouldReplaceIfAlreadyReacted) {
temp.set(index, reaction);
}
return temp;
}
private void updateItemSent(final String clientContext, final long timestamp) {
if (clientContext == null) return;
List<DirectItem> list = this.items.getValue();
@ -551,6 +623,48 @@ public class DirectThreadViewModel extends AndroidViewModel {
});
}
public LiveData<Resource<DirectItem>> sendReaction(final DirectItem item, final Emoji emoji) {
final MutableLiveData<Resource<DirectItem>> data = new MutableLiveData<>();
final Long userId = handleCurrentUser(data);
if (userId == null) return data;
final String clientContext = UUID.randomUUID().toString();
// Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId());
data.postValue(Resource.loading(item));
addReaction(item, emoji);
String emojiUnicode = null;
if (!emoji.getUnicode().equals("❤️")) {
emojiUnicode = emoji.getUnicode();
}
final Call<DirectThreadBroadcastResponse> request = service.broadcastReaction(
clientContext, threadIdOrUserIds, item.getItemId(), emojiUnicode, false);
request.enqueue(new Callback<DirectThreadBroadcastResponse>() {
@Override
public void onResponse(@NonNull final Call<DirectThreadBroadcastResponse> call,
@NonNull final Response<DirectThreadBroadcastResponse> response) {
if (!response.isSuccessful()) {
if (response.errorBody() != null) {
handleErrorBody(call, response, data, item);
return;
}
data.postValue(Resource.error("request was not successful and response error body was null", item));
return;
}
final DirectThreadBroadcastResponse body = response.body();
if (body == null) {
data.postValue(Resource.error("Response is null!", item));
}
// otherwise nothing to do? maybe update the timestamp in the emoji?
}
@Override
public void onFailure(@NonNull final Call<DirectThreadBroadcastResponse> call, @NonNull final Throwable t) {
data.postValue(Resource.error(t.getMessage(), item));
Log.e(TAG, "enqueueRequest: onFailure: ", t);
}
});
return data;
}
public void setCurrentUser(final User currentUser) {
this.currentUser = currentUser;
}

View File

@ -19,6 +19,7 @@ import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions;
import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds;
import awais.instagrabber.repositories.requests.directmessages.LinkBroadcastOptions;
import awais.instagrabber.repositories.requests.directmessages.PhotoBroadcastOptions;
import awais.instagrabber.repositories.requests.directmessages.ReactionBroadcastOptions;
import awais.instagrabber.repositories.requests.directmessages.StoryReplyBroadcastOptions;
import awais.instagrabber.repositories.requests.directmessages.TextBroadcastOptions;
import awais.instagrabber.repositories.requests.directmessages.VideoBroadcastOptions;
@ -158,6 +159,15 @@ public class DirectMessagesService extends BaseService {
return broadcast(new StoryReplyBroadcastOptions(UUID.randomUUID().toString(), threadIdOrUserIds, text, mediaId, reelId));
}
public Call<DirectThreadBroadcastResponse> broadcastReaction(final String clientContext,
final ThreadIdOrUserIds threadIdOrUserIds,
final String itemId,
final String emoji,
final boolean delete) {
return broadcast(new ReactionBroadcastOptions(clientContext, threadIdOrUserIds, itemId, emoji, delete));
}
private Call<DirectThreadBroadcastResponse> broadcast(@NonNull final BroadcastOptions broadcastOptions) {
if (TextUtils.isEmpty(broadcastOptions.getClientContext())) {
throw new IllegalArgumentException("Broadcast requires a valid client context value");

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?colorSurface" />
<padding
android:bottom="4dp"
android:left="4dp"
android:right="4dp"
android:top="4dp" />
<corners android:radius="8dp" />
</shape>

View File

@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v5h5c0.55,0 1,0.45 1,1s-0.45,1 -1,1z"/>
</vector>

View File

@ -143,4 +143,14 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<View
android:id="@+id/long_click_backdrop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:elevation="5dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_height="@dimen/horizontal_divider_height"
android:background="@drawable/pref_list_divider_material" />

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!--<androidx.appcompat.widget.AppCompatImageView-->
<!-- android:id="@+id/img1"-->
<!-- android:layout_width="48dp"-->
<!-- android:layout_height="0dp"-->
<!-- android:padding="4dp"-->
<!-- android:scaleType="fitCenter"-->
<!-- app:layout_constraintBottom_toTopOf="@id/divider"-->
<!-- app:layout_constraintDimensionRatio="1"-->
<!-- app:layout_constraintEnd_toStartOf="@id/img2"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- tools:srcCompat="@mipmap/ic_launcher" />-->
<!--<androidx.appcompat.widget.AppCompatImageView-->
<!-- android:id="@+id/img2"-->
<!-- android:layout_width="48dp"-->
<!-- android:layout_height="0dp"-->
<!-- android:padding="4dp"-->
<!-- android:scaleType="fitCenter"-->
<!-- app:layout_constraintDimensionRatio="1"-->
<!-- app:layout_constraintEnd_toStartOf="@id/img3"-->
<!-- app:layout_constraintStart_toEndOf="@id/img1"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- tools:srcCompat="@mipmap/ic_launcher" />-->
<!--<awais.instagrabber.customviews.SquareImageView-->
<!-- android:id="@+id/img3"-->
<!-- android:layout_width="48dp"-->
<!-- android:layout_height="48dp"-->
<!-- android:padding="4dp"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toEndOf="@id/img2"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- tools:srcCompat="@mipmap/ic_launcher" />-->
<!--<include-->
<!-- android:id="@+id/divider"-->
<!-- layout="@layout/item_pref_divider"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="1dp"-->
<!-- app:layout_constraintEnd_toEndOf="@id/img3"-->
<!-- app:layout_constraintStart_toStartOf="@id/img1"-->
<!-- app:layout_constraintTop_toBottomOf="@id/img1" />-->
<!--<androidx.appcompat.widget.AppCompatTextView-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="test"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toBottomOf="@id/divider" />-->
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<awais.instagrabber.customviews.DirectItemFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -10,7 +10,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:padding="4dp">
android:clipToPadding="false"
android:paddingStart="4dp"
android:paddingTop="4dp"
android:paddingEnd="4dp"
tools:layout_gravity="end"
tools:paddingBottom="@dimen/dm_reaction_adjust_margin">
<View
android:id="@+id/quote_line"
@ -114,11 +119,11 @@
tools:text="@string/app_name"
tools:visibility="visible" />
<!--app:layout_constraintBottom_toTopOf="@id/reactions"-->
<awais.instagrabber.customviews.ChatMessageLayout
android:id="@+id/chat_message_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/reactions"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/ivProfilePic"
app:layout_constraintTop_toBottomOf="@id/tvUsername"
@ -129,7 +134,9 @@
<FrameLayout
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
tools:layout_height="200dp"
tools:layout_width="100dp" />
<LinearLayout
android:id="@+id/message_info"
@ -158,7 +165,7 @@
android:layout_gravity="bottom"
android:layout_marginStart="4dp"
android:gravity="center_vertical"
android:visibility="visible"
android:visibility="gone"
app:srcCompat="@drawable/ic_check_all_24"
app:tint="@color/grey_500"
tools:visibility="visible" />
@ -166,24 +173,49 @@
</awais.instagrabber.customviews.ChatMessageLayout>
<FrameLayout
android:id="@+id/reactions"
<androidx.emoji.widget.EmojiAppCompatTextView
android:id="@+id/emojis"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginBottom="4dp"
android:background="@drawable/bg_rounded_corner"
android:elevation="1dp"
android:maxLines="1"
android:padding="4dp"
android:textColor="?android:textColorPrimary"
android:textSize="18sp"
android:translationY="@dimen/dm_reaction_translation_y_type_1"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/chat_message_layout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@id/chat_message_layout"
app:layout_constraintTop_toBottomOf="@id/chat_message_layout"
tools:visibility="visible">
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintWidth_max="wrap"
tools:text="😀"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/emojis"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:textColor="?android:textColorPrimary"
tools:text="😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" />
</FrameLayout>
<!--<FrameLayout-->
<!-- android:id="@+id/reactions"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:background="@drawable/bg_rounded_corner"-->
<!-- android:elevation="1dp"-->
<!-- android:visibility="gone"-->
<!-- app:layout_constraintEnd_toEndOf="@id/chat_message_layout"-->
<!-- app:layout_constraintStart_toStartOf="@id/chat_message_layout"-->
<!-- app:layout_constraintTop_toBottomOf="@id/chat_message_layout"-->
<!-- tools:visibility="visible">-->
<!-- <androidx.appcompat.widget.AppCompatTextView-->
<!-- android:id="@+id/emojis"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:padding="4dp"-->
<!-- android:textColor="?android:textColorPrimary"-->
<!-- tools:text="😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" />-->
<!--</FrameLayout>-->
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</awais.instagrabber.customviews.DirectItemFrameLayout>

View File

@ -3,9 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/media_share_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Barrier
android:id="@+id/top_barrier"

View File

@ -32,7 +32,17 @@
<dimen name="dm_message_item_margin">80dp</dimen>
<dimen name="dm_message_item_avatar_size">48dp</dimen>
<dimen name="dm_message_info_padding_small">4dp</dimen>
<dimen name="dm_reaction_adjust_margin">22dp</dimen>
<dimen name="dm_reaction_translation_y_type_1">6dp</dimen>
<dimen name="dm_reaction_translation_y_type_2">-12dp</dimen>
<dimen name="feed_item_bottom_icon_size">32dp</dimen>
<dimen name="keyboard_height">200dp</dimen>
<dimen name="reaction_picker_emoji_size">40dp</dimen>
<dimen name="reaction_picker_emoji_margin">8dp</dimen>
<dimen name="reaction_picker_option_height">48dp</dimen>
<dimen name="reaction_picker_add_padding_adjustment">4dp</dimen>
<dimen name="horizontal_divider_height">1dp</dimen>
</resources>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="reply" type="id" />
<item name="unsend" type="id" />
</resources>

View File

@ -396,4 +396,5 @@
<string name="dms_action_remove_admin">Remove as Admin</string>
<string name="edit_unsuccessful">Edit was unsuccessful</string>
<string name="message">Message</string>
<string name="reply">Reply</string>
</resources>