Swipe to reply

This commit is contained in:
Ammar Githam 2021-01-17 03:09:07 +09:00
parent 59aa14e2f6
commit 13d95523a3
24 changed files with 650 additions and 55 deletions

View File

@ -342,7 +342,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
public static class DirectItemOrHeader {
Date date;
DirectItem item;
public DirectItem item;
public boolean isHeader() {
return date != null;

View File

@ -10,6 +10,7 @@ import android.text.style.StyleSpan;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import java.util.List;
@ -86,4 +87,9 @@ public class DirectItemActionLogViewHolder extends DirectItemViewHolder {
protected boolean allowLongClick() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}

View File

@ -5,6 +5,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.backends.pipeline.Fresco;
@ -59,4 +60,9 @@ public class DirectItemAnimatedMediaViewHolder extends DirectItemViewHolder {
.setAutoPlayAnimations(true)
.build());
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}

View File

@ -3,6 +3,7 @@ package awais.instagrabber.adapters.viewholder.directmessages;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
@ -35,4 +36,9 @@ public class DirectItemDefaultViewHolder extends DirectItemViewHolder {
protected boolean allowLongClick() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}

View File

@ -1,6 +1,7 @@
package awais.instagrabber.adapters.viewholder.directmessages;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
@ -27,4 +28,9 @@ public class DirectItemLikeViewHolder extends DirectItemViewHolder {
protected boolean canForward() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}

View File

@ -7,6 +7,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
@ -36,6 +37,7 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
private final LayoutDmMediaShareBinding binding;
private final RoundingParams incomingRoundingParams;
private final RoundingParams outgoingRoundingParams;
private DirectItemType itemType;
public DirectItemMediaShareViewHolder(@NonNull final LayoutDmBaseBinding baseBinding,
@NonNull final LayoutDmMediaShareBinding binding,
@ -148,13 +150,14 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
@Nullable
private Media getMedia(@NonNull final DirectItem item) {
Media media = null;
if (item.getItemType() == DirectItemType.MEDIA_SHARE) {
itemType = item.getItemType();
if (itemType == DirectItemType.MEDIA_SHARE) {
media = item.getMediaShare();
} else if (item.getItemType() == DirectItemType.CLIP) {
} else if (itemType == DirectItemType.CLIP) {
final DirectItemClip clip = item.getClip();
if (clip == null) return null;
media = clip.getClip();
} else if (item.getItemType() == DirectItemType.FELIX_SHARE) {
} else if (itemType == DirectItemType.FELIX_SHARE) {
final DirectItemFelixShare felixShare = item.getFelixShare();
if (felixShare == null) return null;
media = felixShare.getVideo();
@ -166,4 +169,12 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
protected int getReactionsTranslationY() {
return reactionTranslationYType2;
}
@Override
public int getSwipeDirection() {
if (itemType != null && (itemType == DirectItemType.CLIP || itemType == DirectItemType.FELIX_SHARE)) {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
return super.getSwipeDirection();
}
}

View File

@ -1,6 +1,7 @@
package awais.instagrabber.adapters.viewholder.directmessages;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
@ -38,4 +39,9 @@ public class DirectItemPlaceholderViewHolder extends DirectItemViewHolder {
protected boolean allowLongClick() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}

View File

@ -4,6 +4,7 @@ import android.content.res.Resources;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.generic.RoundingParams;
@ -117,4 +118,9 @@ public class DirectItemProfileViewHolder extends DirectItemViewHolder {
binding.isVerified.setVisibility(View.GONE);
itemView.setOnClickListener(v -> openLocation(location.getPk()));
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}

View File

@ -5,6 +5,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
@ -110,4 +111,9 @@ public class DirectItemStoryShareViewHolder extends DirectItemViewHolder {
protected boolean canForward() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}

View File

@ -7,6 +7,7 @@ import android.text.style.ForegroundColorSpan;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import java.util.List;
@ -76,4 +77,9 @@ public class DirectItemVideoCallEventViewHolder extends DirectItemViewHolder {
protected boolean allowLongClick() {
return false;
}
@Override
public int getSwipeDirection() {
return ItemTouchHelper.ACTION_STATE_IDLE;
}
}

View File

@ -18,6 +18,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.widget.ImageViewCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.TransitionManager;
@ -33,6 +34,7 @@ import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemInternalLongClic
import awais.instagrabber.customviews.DirectItemContextMenu;
import awais.instagrabber.customviews.DirectItemFrameLayout;
import awais.instagrabber.customviews.RamboTextViewV2;
import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCallback.SwipeableViewHolder;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.models.enums.DirectItemType;
import awais.instagrabber.models.enums.MediaItemType;
@ -46,7 +48,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.DeepLinkParser;
import awais.instagrabber.utils.ResponseBodyUtils;
public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder implements SwipeableViewHolder {
private static final String TAG = DirectItemViewHolder.class.getSimpleName();
private final LayoutDmBaseBinding binding;
@ -72,6 +74,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
private DirectItemInternalLongClickListener longClickListener;
private DirectItem item;
private ViewPropertyAnimator shrinkGrowAnimator;
private MessageDirection messageDirection;
// private View.OnLayoutChangeListener layoutChangeListener;
public DirectItemViewHolder(@NonNull final LayoutDmBaseBinding binding,
@ -108,7 +111,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
public void bind(final int position, final DirectItem item) {
this.item = item;
final MessageDirection messageDirection = isSelf(item) ? MessageDirection.OUTGOING : MessageDirection.INCOMING;
messageDirection = isSelf(item) ? MessageDirection.OUTGOING : MessageDirection.INCOMING;
itemView.post(() -> bindBase(item, messageDirection, position));
itemView.post(() -> bindItem(item, messageDirection));
itemView.post(() -> setupLongClickListener(position, messageDirection));
@ -266,6 +269,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
// if (media == null) break;
// url = ResponseBodyUtils.getThumbUrl(media.getImageVersions2());
// break;
// case LOCATION
}
if (text == null && url == null) {
binding.quoteLine.setVisibility(View.GONE);
@ -568,6 +572,12 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder {
shrinkGrowAnimator.start();
}
@Override
public int getSwipeDirection() {
if (item == null || messageDirection == null) return ItemTouchHelper.ACTION_STATE_IDLE;
return messageDirection == MessageDirection.OUTGOING ? ItemTouchHelper.START : ItemTouchHelper.END;
}
public enum MessageDirection {
INCOMING,
OUTGOING

View File

@ -73,9 +73,20 @@ public class DirectItemFrameLayout extends FrameLayout {
touchY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
if (longPressed || Math.abs(touchX - ev.getRawX()) > touchSlop || Math.abs(touchY - ev.getRawY()) > touchSlop) {
final float diffX = touchX - ev.getRawX();
final float diffXAbs = Math.abs(diffX);
final boolean isMoved = diffXAbs > touchSlop || Math.abs(touchY - ev.getRawY()) > touchSlop;
if (longPressed || isMoved) {
handler.removeCallbacks(longPressStartRunnable);
handler.removeCallbacks(longPressRunnable);
if (!longPressed) {
if (onItemLongClickListener != null) {
onItemLongClickListener.onLongClickCancel(this);
}
}
// if (diffXAbs > touchSlop) {
// setTranslationX(-diffX);
// }
}
break;
case MotionEvent.ACTION_UP:
@ -91,6 +102,9 @@ public class DirectItemFrameLayout extends FrameLayout {
case MotionEvent.ACTION_CANCEL:
handler.removeCallbacks(longPressRunnable);
handler.removeCallbacks(longPressStartRunnable);
if (onItemLongClickListener != null) {
onItemLongClickListener.onLongClickCancel(this);
}
break;
}
final boolean dispatchTouchEvent = super.dispatchTouchEvent(ev);

View File

@ -0,0 +1,184 @@
package awais.instagrabber.customviews.helpers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.utils.Utils;
/**
* Thanks to https://github.com/izjumovfs/SwipeToReply/blob/master/swipetoreply/src/main/java/com/capybaralabs/swipetoreply/SwipeController.java
*/
public class SwipeAndRestoreItemTouchHelperCallback extends ItemTouchHelper.Callback {
private static final String TAG = "SwipeRestoreCallback";
private final float swipeThreshold;
private final float swipeAutoCancelThreshold;
private final OnSwipeListener onSwipeListener;
private final Drawable replyIcon;
// private final Drawable replyIconBackground;
private final int replyIconShowThreshold;
private final float replyIconMaxTranslation;
private final Rect replyIconBounds = new Rect();
private final float replyIconXOffset;
private final int replyIconSize;
private boolean mSwipeBack = false;
private boolean hasVibrated;
public SwipeAndRestoreItemTouchHelperCallback(final Context context, final OnSwipeListener onSwipeListener) {
this.onSwipeListener = onSwipeListener;
swipeThreshold = Utils.displayMetrics.widthPixels * 0.25f;
swipeAutoCancelThreshold = swipeThreshold + Utils.convertDpToPx(5);
replyIcon = AppCompatResources.getDrawable(context, R.drawable.ic_round_reply_24);
if (replyIcon == null) {
throw new IllegalArgumentException("reply icon is null");
}
replyIcon.setTint(context.getResources().getColor(R.color.white)); //todo need to update according to theme
replyIconShowThreshold = Utils.convertDpToPx(24);
replyIconMaxTranslation = swipeThreshold - replyIconShowThreshold;
// Log.d(TAG, "replyIconShowThreshold: " + replyIconShowThreshold + ", swipeThreshold: " + swipeThreshold);
replyIconSize = replyIconShowThreshold; // Utils.convertDpToPx(24);
replyIconXOffset = swipeThreshold * 0.25f /*Utils.convertDpToPx(20)*/;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
if (!(viewHolder instanceof SwipeableViewHolder)) {
return makeMovementFlags(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.ACTION_STATE_IDLE);
}
return makeMovementFlags(ItemTouchHelper.ACTION_STATE_IDLE, ((SwipeableViewHolder) viewHolder).getSwipeDirection());
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder viewHolder1) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {}
@Override
public int convertToAbsoluteDirection(int flags, int layoutDirection) {
if (mSwipeBack) {
mSwipeBack = false;
return 0;
}
return super.convertToAbsoluteDirection(flags, layoutDirection);
}
@Override
public void onChildDraw(@NonNull Canvas c,
@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
float dX,
float dY,
int actionState,
boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
setTouchListener(recyclerView, viewHolder);
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
drawReplyButton(c, viewHolder);
}
@SuppressLint("ClickableViewAccessibility")
private void setTouchListener(RecyclerView recyclerView, final RecyclerView.ViewHolder viewHolder) {
recyclerView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (Math.abs(viewHolder.itemView.getTranslationX()) >= swipeAutoCancelThreshold) {
if (!hasVibrated) {
viewHolder.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
hasVibrated = true;
}
// MotionEvent cancelEvent = MotionEvent.obtain(event);
// cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
// recyclerView.dispatchTouchEvent(cancelEvent);
// cancelEvent.recycle();
}
}
mSwipeBack = event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_UP;
if (mSwipeBack) {
hasVibrated = false;
if (Math.abs(viewHolder.itemView.getTranslationX()) >= swipeThreshold) {
if (onSwipeListener != null) {
onSwipeListener.onSwipe(viewHolder.getBindingAdapterPosition(), viewHolder);
}
}
}
return false;
});
}
public interface SwipeableViewHolder {
int getSwipeDirection();
}
public interface OnSwipeListener {
void onSwipe(final int adapterPosition, final RecyclerView.ViewHolder viewHolder);
}
private void drawReplyButton(Canvas canvas, final RecyclerView.ViewHolder viewHolder) {
if (!(viewHolder instanceof SwipeableViewHolder)) return;
final int swipeDirection = ((SwipeableViewHolder) viewHolder).getSwipeDirection();
if (swipeDirection != ItemTouchHelper.START && swipeDirection != ItemTouchHelper.END) return;
final View view = viewHolder.itemView;
float translationX = view.getTranslationX();
boolean show = false;
float progress;
final float translationXAbs = Math.abs(translationX);
if (translationXAbs >= replyIconShowThreshold) {
show = true;
}
if (show) {
// replyIconShowThreshold -> swipeThreshold <=> progress 0 -> 1
final float replyIconTranslation = translationXAbs - replyIconShowThreshold;
progress = replyIconTranslation / replyIconMaxTranslation;
if (progress > 1) {
progress = 1f;
}
if (progress < 0) {
progress = 0;
}
// Log.d(TAG, /*"translationX: " + translationX + ", replyIconTranslation: " + replyIconTranslation +*/ "progress: " + progress);
} else {
progress = 0f;
// Log.d(TAG, /*"translationX: " + translationX + ", replyIconTranslation: " + 0 +*/ "progress: " + progress);
}
if (progress > 0) {
// calculate the reply icon y position, then offset top, bottom with icon size
final int y = view.getTop() + (view.getMeasuredHeight() / 2);
final int tempIconSize = (int) (replyIconSize * progress);
final int tempIconSizeHalf = tempIconSize / 2;
final int xOffset = (int) (replyIconXOffset * progress);
final int left;
if (swipeDirection == ItemTouchHelper.END) {
// draw arrow of left side
left = xOffset;
} else {
// draw arrow of right side
left = view.getMeasuredWidth() - xOffset - tempIconSize;
}
final int right = tempIconSize + left;
replyIconBounds.set(left, y - tempIconSizeHalf, right, y + tempIconSizeHalf);
replyIcon.setBounds(replyIconBounds);
replyIcon.draw(canvas);
}
}
}

View File

@ -41,9 +41,11 @@ import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import androidx.transition.TransitionManager;
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
@ -73,6 +75,7 @@ import awais.instagrabber.customviews.emoji.Emoji;
import awais.instagrabber.customviews.helpers.HeaderItemDecoration;
import awais.instagrabber.customviews.helpers.HeightProvider;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge;
import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCallback;
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding;
import awais.instagrabber.dialogs.DirectItemReactionDialogFragment;
@ -81,16 +84,19 @@ import awais.instagrabber.fragments.PostViewV2Fragment;
import awais.instagrabber.fragments.UserSearchFragment;
import awais.instagrabber.fragments.UserSearchFragmentDirections;
import awais.instagrabber.models.Resource;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.Media;
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.DirectItemStoryShare;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.PermissionUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.AppStateViewModel;
@ -133,6 +139,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
private DirectItemReactionDialogFragment reactionDialogFragment;
private DirectItem itemToForward;
private MutableLiveData<Object> backStackSavedStateResultLiveData;
private int prevLength;
private final AppExecutors appExecutors = AppExecutors.getInstance();
private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() {
@ -252,7 +259,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
}
}
};
private final DirectItemLongClickListener directItemLongClickListener = position -> {
// viewModel.setSelectedPosition(position);
};
@ -280,6 +286,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
// clear result
backStackSavedStateResultLiveData.postValue(null);
};
private final MutableLiveData<Integer> inputLength = new MutableLiveData<>(0);
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
@ -491,6 +498,17 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
}
});
binding.chats.addItemDecoration(headerItemDecoration);
final SwipeAndRestoreItemTouchHelperCallback touchHelperCallback = new SwipeAndRestoreItemTouchHelperCallback(
context,
(adapterPosition, viewHolder) -> {
if (itemsAdapter == null) return;
final DirectItemOrHeader directItemOrHeader = itemsAdapter.getList().get(adapterPosition);
if (directItemOrHeader.isHeader()) return;
viewModel.setReplyToItem(directItemOrHeader.item);
}
);
final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(touchHelperCallback);
itemTouchHelper.attachToRecyclerView(binding.chats);
// final MentionClickListener mentionClickListener = (view, text, isHashtag, isLocation) -> searchUsername(text);
// final DialogInterface.OnClickListener onDialogListener = (dialogInterface, which) -> {
// if (which == 0) {
@ -632,6 +650,141 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
setupItemsAdapter(userThreadPair.first, userThreadPair.second);
});
viewModel.getItems().observe(getViewLifecycleOwner(), this::submitItemsToAdapter);
viewModel.getReplyToItem().observe(getViewLifecycleOwner(), item -> {
if (item == null) {
if (binding.input.length() == 0) {
showExtraInputOption(true);
}
binding.getRoot().post(() -> {
TransitionManager.beginDelayedTransition(binding.getRoot());
binding.replyBg.setVisibility(View.GONE);
binding.replyInfo.setVisibility(View.GONE);
binding.replyPreviewImage.setVisibility(View.GONE);
binding.replyCancel.setVisibility(View.GONE);
binding.replyPreviewText.setVisibility(View.GONE);
});
return;
}
showExtraInputOption(false);
binding.getRoot().postDelayed(() -> {
binding.replyBg.setVisibility(View.VISIBLE);
binding.replyInfo.setVisibility(View.VISIBLE);
binding.replyPreviewImage.setVisibility(View.VISIBLE);
binding.replyCancel.setVisibility(View.VISIBLE);
binding.replyPreviewText.setVisibility(View.VISIBLE);
if (item.getUserId() == viewModel.getViewerId()) {
binding.replyInfo.setText(R.string.replying_to_yourself);
} else {
final User user = viewModel.getUser(item.getUserId());
if (user != null) {
binding.replyInfo.setText(getString(R.string.replying_to_user, user.getFullName()));
} else {
binding.replyInfo.setVisibility(View.GONE);
}
}
final String previewText = getDirectItemPreviewText(item);
binding.replyPreviewText.setText(TextUtils.isEmpty(previewText) ? getString(R.string.message) : previewText);
final String previewImageUrl = getDirectItemPreviewImageUrl(item);
if (TextUtils.isEmpty(previewImageUrl)) {
binding.replyPreviewImage.setVisibility(View.GONE);
} else {
binding.replyPreviewImage.setImageURI(previewImageUrl);
}
binding.replyCancel.setOnClickListener(v -> viewModel.setReplyToItem(null));
}, 200);
});
inputLength.observe(getViewLifecycleOwner(), length -> {
if (length == null) return;
final boolean hasReplyToItem = viewModel.getReplyToItem().getValue() != null;
if (hasReplyToItem) {
prevLength = length;
return;
}
if ((prevLength == 0 && length != 0) || (prevLength != 0 && length == 0)) {
showExtraInputOption(length == 0);
}
prevLength = length;
});
}
private void showExtraInputOption(final boolean show) {
if (show) {
if (!binding.send.isListenForRecord()) {
binding.send.setListenForRecord(true);
startIconAnimation();
}
binding.gallery.setVisibility(View.VISIBLE);
binding.camera.setVisibility(View.VISIBLE);
return;
}
if (binding.send.isListenForRecord()) {
binding.send.setListenForRecord(false);
startIconAnimation();
}
binding.gallery.setVisibility(View.GONE);
binding.camera.setVisibility(View.GONE);
}
private String getDirectItemPreviewText(final DirectItem item) {
switch (item.getItemType()) {
case TEXT:
return item.getText();
case LINK:
return item.getLink().getText();
case MEDIA: {
final Media media = item.getMedia();
return getMediaPreviewTextString(media);
}
case RAVEN_MEDIA: {
final DirectItemVisualMedia visualMedia = item.getVisualMedia();
final Media media = visualMedia.getMedia();
return getMediaPreviewTextString(media);
}
case VOICE_MEDIA:
return getString(R.string.voice_message);
case MEDIA_SHARE:
return getString(R.string.post);
case REEL_SHARE:
return item.getReelShare().getText();
}
return "";
}
@NonNull
private String getMediaPreviewTextString(final Media media) {
final MediaItemType mediaType = media.getMediaType();
switch (mediaType) {
case MEDIA_TYPE_IMAGE:
return getString(R.string.photo);
case MEDIA_TYPE_VIDEO:
return getString(R.string.video);
default:
return "";
}
}
private String getDirectItemPreviewImageUrl(final DirectItem item) {
switch (item.getItemType()) {
case TEXT:
case LINK:
case VOICE_MEDIA:
case REEL_SHARE:
return null;
case MEDIA: {
final Media media = item.getMedia();
return ResponseBodyUtils.getThumbUrl(media);
}
case RAVEN_MEDIA: {
final DirectItemVisualMedia visualMedia = item.getVisualMedia();
final Media media = visualMedia.getMedia();
return ResponseBodyUtils.getThumbUrl(media);
}
case MEDIA_SHARE: {
final Media media = item.getMediaShare();
return ResponseBodyUtils.getThumbUrl(media);
}
}
return null;
}
private void setupBackStackResultObserver() {
@ -750,33 +903,29 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
binding.camera.setVisibility(View.VISIBLE);
});
binding.input.addTextChangedListener(new TextWatcherAdapter() {
int prevLength = 0;
// int prevLength = 0;
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
final int length = s.length();
if (prevLength != 0 && length == 0) {
binding.send.setListenForRecord(true);
startIconAnimation();
}
if (prevLength == 0 && length != 0) {
binding.send.setListenForRecord(false);
startIconAnimation();
}
binding.gallery.setVisibility(length == 0 ? View.VISIBLE : View.GONE);
binding.camera.setVisibility(length == 0 ? View.VISIBLE : View.GONE);
prevLength = length;
}
private void startIconAnimation() {
final Drawable icon = binding.send.getIcon();
if (icon instanceof Animatable) {
final Animatable animatable = (Animatable) icon;
if (animatable.isRunning()) {
animatable.stop();
}
animatable.start();
}
inputLength.postValue(length);
// boolean showExtraInputOptionsChanged = false;
// if (prevLength != 0 && length == 0) {
// inputLength.postValue(true);
// showExtraInputOptionsChanged = true;
// binding.send.setListenForRecord(true);
// startIconAnimation();
// }
// if (prevLength == 0 && length != 0) {
// inputLength.postValue(false);
// showExtraInputOptionsChanged = true;
// binding.send.setListenForRecord(false);
// startIconAnimation();
// }
// if (!showExtraInputOptionsChanged) {
// showExtraInputOptions.postValue(length == 0);
// }
// prevLength = length;
}
});
binding.send.setOnRecordClickListener(v -> {
@ -785,6 +934,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
final LiveData<Resource<DirectItem>> resourceLiveData = viewModel.sendText(text.toString());
resourceLiveData.observe(getViewLifecycleOwner(), resource -> handleSentMessage(resourceLiveData));
binding.input.setText("");
viewModel.setReplyToItem(null);
});
binding.send.setOnRecordLongClickListener(v -> {
Log.d(TAG, "setOnRecordLongClickListener");
@ -833,6 +983,17 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
});
}
private void startIconAnimation() {
final Drawable icon = binding.send.getIcon();
if (icon instanceof Animatable) {
final Animatable animatable = (Animatable) icon;
if (animatable.isRunning()) {
animatable.stop();
}
animatable.start();
}
}
private void navigateToImageEditFragment(final String path) {
navigateToImageEditFragment(Uri.fromFile(new File(path)));
}
@ -1168,6 +1329,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
ObjectAnimator.ofFloat(binding.gallery, TRANSLATION_Y, -height),
ObjectAnimator.ofFloat(binding.camera, TRANSLATION_Y, -height),
ObjectAnimator.ofFloat(binding.send, TRANSLATION_Y, -height),
ObjectAnimator.ofFloat(binding.replyBg, TRANSLATION_Y, -height),
ObjectAnimator.ofFloat(binding.replyInfo, TRANSLATION_Y, -height),
ObjectAnimator.ofFloat(binding.replyCancel, TRANSLATION_Y, -height),
ObjectAnimator.ofFloat(binding.replyPreviewImage, TRANSLATION_Y, -height),
ObjectAnimator.ofFloat(binding.replyPreviewText, TRANSLATION_Y, -height),
ObjectAnimator.ofFloat(binding.emojiPicker, TRANSLATION_Y, keyboardHeight - height)
);
// if (headerItemDecoration != null && headerItemDecoration.getCurrentHeader() != null) {

View File

@ -1,7 +1,5 @@
package awais.instagrabber.models.enums;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
@ -65,7 +63,6 @@ public enum DirectItemType implements Serializable {
return map.get(id);
}
@Nullable
public String getName() {
switch (this) {
case TEXT:
@ -102,8 +99,7 @@ public enum DirectItemType implements Serializable {
return "felix_share";
case LOCATION:
return "location";
default:
return null;
}
return null;
}
}

View File

@ -13,6 +13,9 @@ public abstract class BroadcastOptions {
private final ThreadIdOrUserIds threadIdOrUserIds;
private final BroadcastItemType itemType;
private String repliedToItemId;
private String repliedToClientContext;
public BroadcastOptions(final String clientContext,
@NonNull final ThreadIdOrUserIds threadIdOrUserIds,
@NonNull final BroadcastItemType itemType) {
@ -39,6 +42,22 @@ public abstract class BroadcastOptions {
public abstract Map<String, String> getFormMap();
public String getRepliedToItemId() {
return repliedToItemId;
}
public void setRepliedToItemId(final String repliedToItemId) {
this.repliedToItemId = repliedToItemId;
}
public String getRepliedToClientContext() {
return repliedToClientContext;
}
public void setRepliedToClientContext(final String repliedToClientContext) {
this.repliedToClientContext = repliedToClientContext;
}
public static final class ThreadIdOrUserIds {
private final String threadId;
private final List<String> userIds;

View File

@ -11,7 +11,7 @@ import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
public class DirectItem implements Cloneable {
private final String itemId;
private String itemId;
private final long userId;
private long timestamp;
private final DirectItemType itemType;
@ -213,6 +213,10 @@ public class DirectItem implements Cloneable {
return date;
}
public void setItemId(final String itemId) {
this.itemId = itemId;
}
public boolean isPending() {
return isPending;
}

View File

@ -20,7 +20,8 @@ public class DirectItemFactory {
public static DirectItem createText(final long userId,
final String clientContext,
final String text) {
final String text,
final DirectItem repliedToMessage) {
return new DirectItem(
UUID.randomUUID().toString(),
userId,
@ -44,7 +45,7 @@ public class DirectItemFactory {
null,
null,
null,
null,
repliedToMessage,
null,
null,
0,

View File

@ -62,8 +62,8 @@ public final class Utils {
public static SettingsHelper settingsHelper;
public static boolean sessionVolumeFull = false;
public static final MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
public static final DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
public static ClipboardManager clipboardManager;
public static DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
public static SimpleDateFormat datetimeParser;
public static SimpleCache simpleCache;
private static int statusBarHeight;
@ -73,9 +73,7 @@ public final class Utils {
private static int defaultStatusBarColor;
public static int convertDpToPx(final float dp) {
if (displayMetrics == null)
displayMetrics = Resources.getSystem().getDisplayMetrics();
return Math.round((dp * displayMetrics.densityDpi) / 160.0f);
return Math.round((dp * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT);
}
public static void copyText(@NonNull final Context context, final CharSequence string) {

View File

@ -81,6 +81,7 @@ public class DirectThreadViewModel extends AndroidViewModel {
private final MutableLiveData<Boolean> fetching = new MutableLiveData<>(false);
private final MutableLiveData<List<User>> users = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<User>> leftUsers = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<DirectItem> replyToItem = new MutableLiveData<>();
private final DirectMessagesService service;
private final ContentResolver contentResolver;
@ -285,7 +286,7 @@ public class DirectThreadViewModel extends AndroidViewModel {
return index;
}
private void updateItemSent(final String clientContext, final long timestamp) {
private void updateItemSent(final String clientContext, final long timestamp, final String itemId) {
if (clientContext == null) return;
List<DirectItem> list = this.items.getValue();
list = list == null ? new LinkedList<>() : new LinkedList<>(list);
@ -297,6 +298,7 @@ public class DirectThreadViewModel extends AndroidViewModel {
final DirectItem directItem = list.get(index);
try {
final DirectItem itemClone = (DirectItem) directItem.clone();
itemClone.setItemId(itemId);
itemClone.setPending(false);
itemClone.setTimestamp(timestamp);
list.set(index, itemClone);
@ -322,6 +324,10 @@ public class DirectThreadViewModel extends AndroidViewModel {
return leftUsers;
}
public LiveData<DirectItem> getReplyToItem() {
return replyToItem;
}
public void fetchChats() {
final Boolean isFetching = fetching.getValue();
if ((isFetching != null && isFetching) || !hasOlder) return;
@ -392,12 +398,21 @@ public class DirectThreadViewModel extends AndroidViewModel {
final Long userId = handleCurrentUser(data);
if (userId == null) return data;
final String clientContext = UUID.randomUUID().toString();
final DirectItem directItem = DirectItemFactory.createText(userId, clientContext, text);
final DirectItem replyToItemValue = replyToItem.getValue();
final DirectItem directItem = DirectItemFactory.createText(userId, clientContext, text, replyToItemValue);
// Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId());
directItem.setPending(true);
addItems(0, Collections.singletonList(directItem));
data.postValue(Resource.loading(directItem));
final Call<DirectThreadBroadcastResponse> request = service.broadcastText(clientContext, threadIdOrUserIds, text);
final String repliedToItemId = replyToItemValue != null ? replyToItemValue.getItemId() : null;
final String repliedToClientContext = replyToItemValue != null ? replyToItemValue.getClientContext() : null;
final Call<DirectThreadBroadcastResponse> request = service.broadcastText(
clientContext,
threadIdOrUserIds,
text,
repliedToItemId,
repliedToClientContext
);
enqueueRequest(request, data, directItem);
return data;
}
@ -848,6 +863,7 @@ public class DirectThreadViewModel extends AndroidViewModel {
}
final String payloadClientContext;
final long timestamp;
final String itemId;
final DirectThreadBroadcastResponsePayload payload = broadcastResponse.getPayload();
if (payload == null) {
final List<DirectThreadBroadcastResponseMessageMetadata> messageMetadata = broadcastResponse.getMessageMetadata();
@ -857,12 +873,14 @@ public class DirectThreadViewModel extends AndroidViewModel {
}
final DirectThreadBroadcastResponseMessageMetadata metadata = messageMetadata.get(0);
payloadClientContext = metadata.getClientContext();
itemId = metadata.getItemId();
timestamp = metadata.getTimestamp();
} else {
payloadClientContext = payload.getClientContext();
timestamp = payload.getTimestamp();
itemId = payload.getItemId();
}
updateItemSent(payloadClientContext, timestamp);
updateItemSent(payloadClientContext, timestamp, itemId);
data.postValue(Resource.success(directItem));
return;
}
@ -987,8 +1005,13 @@ public class DirectThreadViewModel extends AndroidViewModel {
private void forward(@NonNull final DirectThread thread, @NonNull final DirectItem itemToForward) {
final DirectItemType itemType = itemToForward.getItemType();
final String itemTypeName = itemType.getName();
if (itemTypeName == null) {
Log.e(TAG, "forward: itemTypeName was null!");
return;
}
final Call<DirectThreadBroadcastResponse> request = service.forward(thread.getThreadId(),
itemType.getName(),
itemTypeName,
threadId,
itemToForward.getItemId());
request.enqueue(new Callback<DirectThreadBroadcastResponse>() {
@ -1019,4 +1042,9 @@ public class DirectThreadViewModel extends AndroidViewModel {
}
});
}
public void setReplyToItem(final DirectItem item) {
// Log.d(TAG, "setReplyToItem: " + item);
replyToItem.postValue(item);
}
}

View File

@ -119,19 +119,29 @@ public class DirectMessagesService extends BaseService {
public Call<DirectThreadBroadcastResponse> broadcastText(final String clientContext,
final ThreadIdOrUserIds threadIdOrUserIds,
final String text) {
final String text,
final String repliedToItemId,
final String repliedToClientContext) {
final List<String> urls = TextUtils.extractUrls(text);
if (!urls.isEmpty()) {
return broadcastLink(clientContext, threadIdOrUserIds, text, urls);
return broadcastLink(clientContext, threadIdOrUserIds, text, urls, repliedToItemId, repliedToClientContext);
}
return broadcast(new TextBroadcastOptions(clientContext, threadIdOrUserIds, text));
final TextBroadcastOptions broadcastOptions = new TextBroadcastOptions(clientContext, threadIdOrUserIds, text);
broadcastOptions.setRepliedToItemId(repliedToItemId);
broadcastOptions.setRepliedToClientContext(repliedToClientContext);
return broadcast(broadcastOptions);
}
public Call<DirectThreadBroadcastResponse> broadcastLink(final String clientContext,
final ThreadIdOrUserIds threadIdOrUserIds,
final String linkText,
final List<String> urls) {
return broadcast(new LinkBroadcastOptions(clientContext, threadIdOrUserIds, linkText, urls));
final List<String> urls,
final String repliedToItemId,
final String repliedToClientContext) {
final LinkBroadcastOptions broadcastOptions = new LinkBroadcastOptions(clientContext, threadIdOrUserIds, linkText, urls);
broadcastOptions.setRepliedToItemId(repliedToItemId);
broadcastOptions.setRepliedToClientContext(repliedToClientContext);
return broadcast(broadcastOptions);
}
public Call<DirectThreadBroadcastResponse> broadcastPhoto(final String clientContext,
@ -187,6 +197,10 @@ public class DirectMessagesService extends BaseService {
form.put("__uuid", deviceUuid);
form.put("client_context", broadcastOptions.getClientContext());
form.put("mutation_token", broadcastOptions.getClientContext());
if (!TextUtils.isEmpty(broadcastOptions.getRepliedToItemId()) && !TextUtils.isEmpty(broadcastOptions.getRepliedToClientContext())) {
form.put("replied_to_item_id", broadcastOptions.getRepliedToItemId());
form.put("replied_to_client_context", broadcastOptions.getRepliedToClientContext());
}
form.putAll(broadcastOptions.getFormMap());
form.put("action", "send_item");
final Map<String, String> signedForm = Utils.sign(form);

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M10,9V7.41c0,-0.89 -1.08,-1.34 -1.71,-0.71L3.7,11.29c-0.39,0.39 -0.39,1.02 0,1.41l4.59,4.59c0.63,0.63 1.71,0.19 1.71,-0.7V14.9c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z"/>
</vector>

View File

@ -11,12 +11,98 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbars="none"
app:layout_constraintBottom_toTopOf="@id/input"
app:layout_constraintBottom_toTopOf="@id/reply_info"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/layout_dm_base" />
<View
android:id="@+id/reply_bg"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/bg_input"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/input_bg"
app:layout_constraintEnd_toEndOf="@id/input_bg"
app:layout_constraintStart_toStartOf="@id/input_bg"
app:layout_constraintTop_toTopOf="@id/reply_info"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/reply_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="4dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/reply_preview_text"
app:layout_constraintEnd_toStartOf="@id/reply_preview_image"
app:layout_constraintStart_toStartOf="@id/input_bg"
app:layout_constraintTop_toBottomOf="@id/chats"
tools:text="Replying to yourself"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiAppCompatTextView
android:id="@+id/reply_preview_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingStart="16dp"
android:paddingTop="4dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/input_bg"
app:layout_constraintEnd_toStartOf="@id/reply_preview_image"
app:layout_constraintStart_toStartOf="@id/input_bg"
app:layout_constraintTop_toBottomOf="@id/reply_info"
app:layout_goneMarginTop="8dp"
tools:text="Post"
tools:visibility="visible" />
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/reply_preview_image"
android:layout_width="@dimen/dm_inbox_avatar_size_small"
android:layout_height="@dimen/dm_inbox_avatar_size_small"
android:layout_marginEnd="8dp"
android:visibility="gone"
app:actualImageScaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="@id/reply_preview_text"
app:layout_constraintEnd_toStartOf="@id/reply_cancel"
app:layout_constraintStart_toEndOf="@id/reply_preview_text"
app:layout_constraintTop_toTopOf="@id/reply_info"
tools:background="@mipmap/ic_launcher"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/reply_cancel"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/reply_preview_text"
app:layout_constraintEnd_toEndOf="@id/input_bg"
app:layout_constraintStart_toEndOf="@id/reply_preview_image"
app:layout_constraintTop_toTopOf="@id/reply_info"
app:srcCompat="@drawable/ic_close_24"
tools:visibility="visible" />
<!--<androidx.constraintlayout.widget.Group-->
<!-- android:id="@+id/reply_group"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="0dp"-->
<!-- android:visibility="gone"-->
<!-- app:constraint_referenced_ids="reply_bg,reply_cancel,reply_info,reply_item_type,reply_preview"-->
<!-- tools:visibility="visible" />-->
<View
android:id="@+id/input_bg"
android:layout_width="0dp"
@ -65,7 +151,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/camera"
app:layout_constraintStart_toEndOf="@id/emoji_toggle"
app:layout_constraintTop_toBottomOf="@id/chats"
app:layout_constraintTop_toBottomOf="@id/reply_preview_text"
app:layout_goneMarginBottom="4dp"
app:layout_goneMarginEnd="24dp" />

View File

@ -401,4 +401,10 @@
<string name="forward">Forward</string>
<string name="add">Add</string>
<string name="send">Send</string>
<string name="replying_to_yourself">Replying to yourself</string>
<string name="replying_to_user">Replying to %s</string>
<string name="photo">Photo</string>
<string name="video">Video</string>
<string name="voice_message">Voice message</string>
<string name="post">Post</string>
</resources>