videoVersions = media.getVideoVersions();
+ if (videoVersions != null && !videoVersions.isEmpty()) {
+ final MediaCandidate videoVersion = videoVersions.get(0);
+ videoUrl = videoVersion.getUrl();
+ }
+ final VideoPlayerViewHelper videoPlayerViewHelper = new VideoPlayerViewHelper(binding.getRoot().getContext(),
+ videoPost,
+ videoUrl,
+ vol,
+ aspectRatio,
+ ResponseBodyUtils.getThumbUrl(media),
+ false,
+ // null,
+ videoPlayerCallback);
+ videoPost.thumbnail.post(() -> {
+ if (media.getOriginalHeight() > 0.8 * Utils.displayMetrics.heightPixels) {
+ final ViewGroup.LayoutParams tLayoutParams = videoPost.thumbnail.getLayoutParams();
+ tLayoutParams.height = (int) (0.8 * Utils.displayMetrics.heightPixels);
+ videoPost.thumbnail.requestLayout();
+ }
+ });
+ }
+
+ public Media getCurrentFeedModel() {
+ return media;
+ }
+
+ // public void stopPlaying() {
+ // // Log.d(TAG, "Stopping post: " + feedModel.getPostId() + ", player: " + player + ", player.isPlaying: " + (player != null && player.isPlaying()));
+ // handler.removeCallbacks(loadRunnable);
+ // if (player != null) {
+ // player.release();
+ // }
+ // if (videoPost.root.getDisplayedChild() == 1) {
+ // videoPost.root.showPrevious();
+ // }
+ // }
+ //
+ // public void startPlaying() {
+ // handler.removeCallbacks(loadRunnable);
+ // handler.postDelayed(loadRunnable, 800);
+ // }
+}
diff --git a/app/src/main/java/awais/instagrabber/animations/CubicBezierInterpolator.java b/app/src/main/java/awais/instagrabber/animations/CubicBezierInterpolator.java
new file mode 100644
index 0000000..3c137d7
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/animations/CubicBezierInterpolator.java
@@ -0,0 +1,74 @@
+package awais.instagrabber.animations;
+
+import android.graphics.PointF;
+import android.view.animation.Interpolator;
+
+public class CubicBezierInterpolator implements Interpolator {
+
+ public static final CubicBezierInterpolator DEFAULT = new CubicBezierInterpolator(0.25, 0.1, 0.25, 1);
+ public static final CubicBezierInterpolator EASE_OUT = new CubicBezierInterpolator(0, 0, .58, 1);
+ public static final CubicBezierInterpolator EASE_OUT_QUINT = new CubicBezierInterpolator(.23, 1, .32, 1);
+ public static final CubicBezierInterpolator EASE_IN = new CubicBezierInterpolator(.42, 0, 1, 1);
+ public static final CubicBezierInterpolator EASE_BOTH = new CubicBezierInterpolator(.42, 0, .58, 1);
+
+ protected PointF start;
+ protected PointF end;
+ protected PointF a = new PointF();
+ protected PointF b = new PointF();
+ protected PointF c = new PointF();
+
+ public CubicBezierInterpolator(PointF start, PointF end) throws IllegalArgumentException {
+ if (start.x < 0 || start.x > 1) {
+ throw new IllegalArgumentException("startX value must be in the range [0, 1]");
+ }
+ if (end.x < 0 || end.x > 1) {
+ throw new IllegalArgumentException("endX value must be in the range [0, 1]");
+ }
+ this.start = start;
+ this.end = end;
+ }
+
+ public CubicBezierInterpolator(float startX, float startY, float endX, float endY) {
+ this(new PointF(startX, startY), new PointF(endX, endY));
+ }
+
+ public CubicBezierInterpolator(double startX, double startY, double endX, double endY) {
+ this((float) startX, (float) startY, (float) endX, (float) endY);
+ }
+
+ @Override
+ public float getInterpolation(float time) {
+ return getBezierCoordinateY(getXForTime(time));
+ }
+
+ protected float getBezierCoordinateY(float time) {
+ c.y = 3 * start.y;
+ b.y = 3 * (end.y - start.y) - c.y;
+ a.y = 1 - c.y - b.y;
+ return time * (c.y + time * (b.y + time * a.y));
+ }
+
+ protected float getXForTime(float time) {
+ float x = time;
+ float z;
+ for (int i = 1; i < 14; i++) {
+ z = getBezierCoordinateX(x) - time;
+ if (Math.abs(z) < 1e-3) {
+ break;
+ }
+ x -= z / getXDerivate(x);
+ }
+ return x;
+ }
+
+ private float getXDerivate(float t) {
+ return c.x + t * (2 * b.x + 3 * a.x * t);
+ }
+
+ private float getBezierCoordinateX(float time) {
+ c.x = 3 * start.x;
+ b.x = 3 * (end.x - start.x) - c.x;
+ a.x = 1 - c.x - b.x;
+ return time * (c.x + time * (b.x + time * a.x));
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/animations/FabAnimation.java b/app/src/main/java/awais/instagrabber/animations/FabAnimation.java
new file mode 100644
index 0000000..1df54dc
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/animations/FabAnimation.java
@@ -0,0 +1,61 @@
+package awais.instagrabber.animations;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.view.View;
+
+// https://medium.com/better-programming/animated-fab-button-with-more-options-2dcf7118fff6
+
+public class FabAnimation {
+ public static boolean rotateFab(final View v, boolean rotate) {
+ v.animate().setDuration(200)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ }
+ })
+ .rotation(rotate ? 135f : 0f);
+ return rotate;
+ }
+
+ public static void showIn(final View v) {
+ v.setVisibility(View.VISIBLE);
+ v.setAlpha(0f);
+ v.setTranslationY(v.getHeight());
+ v.animate()
+ .setDuration(200)
+ .translationY(0)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ }
+ })
+ .alpha(1f)
+ .start();
+ }
+
+ public static void showOut(final View v) {
+ v.setVisibility(View.VISIBLE);
+ v.setAlpha(1f);
+ v.setTranslationY(0);
+ v.animate()
+ .setDuration(200)
+ .translationY(v.getHeight())
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ v.setVisibility(View.GONE);
+ super.onAnimationEnd(animation);
+ }
+ }).alpha(0f)
+ .start();
+ }
+
+ public static void init(final View v) {
+ v.setVisibility(View.GONE);
+ v.setTranslationY(v.getHeight());
+ v.setAlpha(0f);
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/animations/ResizeAnimation.java b/app/src/main/java/awais/instagrabber/animations/ResizeAnimation.java
new file mode 100644
index 0000000..cec093a
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/animations/ResizeAnimation.java
@@ -0,0 +1,45 @@
+package awais.instagrabber.animations;
+
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+
+public class ResizeAnimation extends Animation {
+ private static final String TAG = "ResizeAnimation";
+
+ final View view;
+ final int startHeight;
+ final int targetHeight;
+ final int startWidth;
+ final int targetWidth;
+
+ public ResizeAnimation(final View view,
+ final int startHeight,
+ final int startWidth,
+ final int targetHeight,
+ final int targetWidth) {
+ this.view = view;
+ this.startHeight = startHeight;
+ this.targetHeight = targetHeight;
+ this.startWidth = startWidth;
+ this.targetWidth = targetWidth;
+ }
+
+ @Override
+ protected void applyTransformation(final float interpolatedTime, final Transformation t) {
+ // Log.d(TAG, "applyTransformation: interpolatedTime: " + interpolatedTime);
+ view.getLayoutParams().height = (int) (startHeight + (targetHeight - startHeight) * interpolatedTime);
+ view.getLayoutParams().width = (int) (startWidth + (targetWidth - startWidth) * interpolatedTime);
+ view.requestLayout();
+ }
+
+ @Override
+ public void initialize(final int width, final int height, final int parentWidth, final int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+ }
+
+ @Override
+ public boolean willChangeBounds() {
+ return true;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/animations/RevealOutlineAnimation.java b/app/src/main/java/awais/instagrabber/animations/RevealOutlineAnimation.java
new file mode 100644
index 0000000..1666171
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/animations/RevealOutlineAnimation.java
@@ -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);
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/animations/RoundedRectRevealOutlineProvider.java b/app/src/main/java/awais/instagrabber/animations/RoundedRectRevealOutlineProvider.java
new file mode 100644
index 0000000..c5cf043
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/animations/RoundedRectRevealOutlineProvider.java
@@ -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.
+ *
+ * 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);
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/animations/ScaleAnimation.java b/app/src/main/java/awais/instagrabber/animations/ScaleAnimation.java
new file mode 100644
index 0000000..c4e8193
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/animations/ScaleAnimation.java
@@ -0,0 +1,45 @@
+package awais.instagrabber.animations;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+
+public class ScaleAnimation {
+
+ private final View view;
+
+ public ScaleAnimation(View view) {
+ this.view = view;
+ }
+
+
+ public void start() {
+ AnimatorSet set = new AnimatorSet();
+ ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 2.0f);
+
+ ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 2.0f);
+ set.setDuration(150);
+ set.setInterpolator(new AccelerateDecelerateInterpolator());
+ set.playTogether(scaleY, scaleX);
+ set.start();
+ }
+
+ public void stop() {
+ AnimatorSet set = new AnimatorSet();
+ ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1.0f);
+ // scaleY.setDuration(250);
+ // scaleY.setInterpolator(new DecelerateInterpolator());
+
+
+ ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1.0f);
+ // scaleX.setDuration(250);
+ // scaleX.setInterpolator(new DecelerateInterpolator());
+
+
+ set.setDuration(150);
+ set.setInterpolator(new AccelerateDecelerateInterpolator());
+ set.playTogether(scaleY, scaleX);
+ set.start();
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/asyncs/DiscoverPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/DiscoverPostFetchService.java
new file mode 100644
index 0000000..0cf9c22
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/asyncs/DiscoverPostFetchService.java
@@ -0,0 +1,71 @@
+package awais.instagrabber.asyncs;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import awais.instagrabber.customviews.helpers.PostFetcher;
+import awais.instagrabber.interfaces.FetchListener;
+import awais.instagrabber.repositories.responses.Media;
+import awais.instagrabber.repositories.responses.discover.TopicalExploreFeedResponse;
+import awais.instagrabber.repositories.responses.WrappedMedia;
+import awais.instagrabber.webservices.DiscoverService;
+import awais.instagrabber.webservices.ServiceCallback;
+
+public class DiscoverPostFetchService implements PostFetcher.PostFetchService {
+ private static final String TAG = "DiscoverPostFetchService";
+ private final DiscoverService discoverService;
+ private final DiscoverService.TopicalExploreRequest topicalExploreRequest;
+ private boolean moreAvailable = false;
+
+ public DiscoverPostFetchService(final DiscoverService.TopicalExploreRequest topicalExploreRequest) {
+ this.topicalExploreRequest = topicalExploreRequest;
+ discoverService = DiscoverService.getInstance();
+ }
+
+ @Override
+ public void fetch(final FetchListener> fetchListener) {
+ discoverService.topicalExplore(topicalExploreRequest, new ServiceCallback() {
+ @Override
+ public void onSuccess(final TopicalExploreFeedResponse result) {
+ if (result == null) {
+ onFailure(new RuntimeException("result is null"));
+ return;
+ }
+ moreAvailable = result.getMoreAvailable();
+ topicalExploreRequest.setMaxId(result.getNextMaxId());
+ final List items = result.getItems();
+ final List posts;
+ if (items == null) {
+ posts = Collections.emptyList();
+ } else {
+ posts = items.stream()
+ .map(WrappedMedia::getMedia)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+ if (fetchListener != null) {
+ fetchListener.onResult(posts);
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ if (fetchListener != null) {
+ fetchListener.onFailure(t);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void reset() {
+ topicalExploreRequest.setMaxId(null);
+ }
+
+ @Override
+ public boolean hasNextPage() {
+ return moreAvailable;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java
new file mode 100644
index 0000000..9664333
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/asyncs/FeedPostFetchService.java
@@ -0,0 +1,74 @@
+package awais.instagrabber.asyncs;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import awais.instagrabber.customviews.helpers.PostFetcher;
+import awais.instagrabber.interfaces.FetchListener;
+import awais.instagrabber.repositories.responses.Media;
+import awais.instagrabber.repositories.responses.PostsFetchResponse;
+import awais.instagrabber.utils.Constants;
+import awais.instagrabber.utils.CookieUtils;
+import awais.instagrabber.webservices.FeedService;
+import awais.instagrabber.webservices.ServiceCallback;
+
+import static awais.instagrabber.utils.Utils.settingsHelper;
+
+public class FeedPostFetchService implements PostFetcher.PostFetchService {
+ private static final String TAG = "FeedPostFetchService";
+ private final FeedService feedService;
+ private String nextCursor;
+ private boolean hasNextPage;
+
+ public FeedPostFetchService() {
+ feedService = FeedService.getInstance();
+ }
+
+ @Override
+ public void fetch(final FetchListener> fetchListener) {
+ final List feedModels = new ArrayList<>();
+ final String cookie = settingsHelper.getString(Constants.COOKIE);
+ final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
+ final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID);
+ feedModels.clear();
+ feedService.fetch(csrfToken, deviceUuid, nextCursor, new ServiceCallback() {
+ @Override
+ public void onSuccess(final PostsFetchResponse result) {
+ if (result == null && feedModels.size() > 0) {
+ fetchListener.onResult(feedModels);
+ return;
+ } else if (result == null) return;
+ nextCursor = result.getNextCursor();
+ hasNextPage = result.getHasNextPage();
+
+ final List mediaResults = result.getFeedModels();
+ feedModels.addAll(mediaResults);
+
+ if (fetchListener != null) {
+ // if (feedModels.size() < 15 && hasNextPage) {
+ // feedService.fetch(csrfToken, nextCursor, this);
+ // } else {
+ fetchListener.onResult(feedModels);
+ // }
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ if (fetchListener != null) {
+ fetchListener.onFailure(t);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void reset() {
+ nextCursor = null;
+ }
+
+ @Override
+ public boolean hasNextPage() {
+ return hasNextPage;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java
new file mode 100644
index 0000000..7b26d79
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java
@@ -0,0 +1,75 @@
+package awais.instagrabber.asyncs;
+
+import java.util.List;
+
+import awais.instagrabber.customviews.helpers.PostFetcher;
+import awais.instagrabber.interfaces.FetchListener;
+import awais.instagrabber.repositories.responses.Hashtag;
+import awais.instagrabber.repositories.responses.Media;
+import awais.instagrabber.repositories.responses.PostsFetchResponse;
+import awais.instagrabber.utils.CoroutineUtilsKt;
+import awais.instagrabber.webservices.GraphQLRepository;
+import awais.instagrabber.webservices.ServiceCallback;
+import awais.instagrabber.webservices.TagsService;
+import kotlinx.coroutines.Dispatchers;
+
+public class HashtagPostFetchService implements PostFetcher.PostFetchService {
+ private final TagsService tagsService;
+ private final GraphQLRepository graphQLRepository;
+ private final Hashtag hashtagModel;
+ private String nextMaxId;
+ private boolean moreAvailable;
+ private final boolean isLoggedIn;
+
+ public HashtagPostFetchService(final Hashtag hashtagModel, final boolean isLoggedIn) {
+ this.hashtagModel = hashtagModel;
+ this.isLoggedIn = isLoggedIn;
+ tagsService = isLoggedIn ? TagsService.getInstance() : null;
+ graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
+ }
+
+ @Override
+ public void fetch(final FetchListener> fetchListener) {
+ final ServiceCallback cb = new ServiceCallback() {
+ @Override
+ public void onSuccess(final PostsFetchResponse result) {
+ if (result == null) return;
+ nextMaxId = result.getNextCursor();
+ moreAvailable = result.getHasNextPage();
+ if (fetchListener != null) {
+ fetchListener.onResult(result.getFeedModels());
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ // Log.e(TAG, "onFailure: ", t);
+ if (fetchListener != null) {
+ fetchListener.onFailure(t);
+ }
+ }
+ };
+ if (isLoggedIn) tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb);
+ else graphQLRepository.fetchHashtagPosts(
+ hashtagModel.getName().toLowerCase(),
+ nextMaxId,
+ CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
+ if (throwable != null) {
+ cb.onFailure(throwable);
+ return;
+ }
+ cb.onSuccess(postsFetchResponse);
+ }, Dispatchers.getIO())
+ );
+ }
+
+ @Override
+ public void reset() {
+ nextMaxId = null;
+ }
+
+ @Override
+ public boolean hasNextPage() {
+ return moreAvailable;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java
new file mode 100644
index 0000000..11f5dce
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/asyncs/LocationPostFetchService.java
@@ -0,0 +1,75 @@
+package awais.instagrabber.asyncs;
+
+import java.util.List;
+
+import awais.instagrabber.customviews.helpers.PostFetcher;
+import awais.instagrabber.interfaces.FetchListener;
+import awais.instagrabber.repositories.responses.Location;
+import awais.instagrabber.repositories.responses.Media;
+import awais.instagrabber.repositories.responses.PostsFetchResponse;
+import awais.instagrabber.utils.CoroutineUtilsKt;
+import awais.instagrabber.webservices.GraphQLRepository;
+import awais.instagrabber.webservices.LocationService;
+import awais.instagrabber.webservices.ServiceCallback;
+import kotlinx.coroutines.Dispatchers;
+
+public class LocationPostFetchService implements PostFetcher.PostFetchService {
+ private final LocationService locationService;
+ private final GraphQLRepository graphQLRepository;
+ private final Location locationModel;
+ private String nextMaxId;
+ private boolean moreAvailable;
+ private final boolean isLoggedIn;
+
+ public LocationPostFetchService(final Location locationModel, final boolean isLoggedIn) {
+ this.locationModel = locationModel;
+ this.isLoggedIn = isLoggedIn;
+ locationService = isLoggedIn ? LocationService.getInstance() : null;
+ graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
+ }
+
+ @Override
+ public void fetch(final FetchListener> fetchListener) {
+ final ServiceCallback cb = new ServiceCallback() {
+ @Override
+ public void onSuccess(final PostsFetchResponse result) {
+ if (result == null) return;
+ nextMaxId = result.getNextCursor();
+ moreAvailable = result.getHasNextPage();
+ if (fetchListener != null) {
+ fetchListener.onResult(result.getFeedModels());
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ // Log.e(TAG, "onFailure: ", t);
+ if (fetchListener != null) {
+ fetchListener.onFailure(t);
+ }
+ }
+ };
+ if (isLoggedIn) locationService.fetchPosts(locationModel.getPk(), nextMaxId, cb);
+ else graphQLRepository.fetchLocationPosts(
+ locationModel.getPk(),
+ nextMaxId,
+ CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
+ if (throwable != null) {
+ cb.onFailure(throwable);
+ return;
+ }
+ cb.onSuccess(postsFetchResponse);
+ }, Dispatchers.getIO())
+ );
+ }
+
+ @Override
+ public void reset() {
+ nextMaxId = null;
+ }
+
+ @Override
+ public boolean hasNextPage() {
+ return moreAvailable;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java
new file mode 100644
index 0000000..9bff389
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/asyncs/ProfilePostFetchService.java
@@ -0,0 +1,78 @@
+package awais.instagrabber.asyncs;
+
+import java.util.List;
+
+import awais.instagrabber.customviews.helpers.PostFetcher;
+import awais.instagrabber.interfaces.FetchListener;
+import awais.instagrabber.repositories.responses.Media;
+import awais.instagrabber.repositories.responses.PostsFetchResponse;
+import awais.instagrabber.repositories.responses.User;
+import awais.instagrabber.utils.CoroutineUtilsKt;
+import awais.instagrabber.webservices.GraphQLRepository;
+import awais.instagrabber.webservices.ProfileService;
+import awais.instagrabber.webservices.ServiceCallback;
+import kotlinx.coroutines.Dispatchers;
+
+public class ProfilePostFetchService implements PostFetcher.PostFetchService {
+ private static final String TAG = "ProfilePostFetchService";
+ private final ProfileService profileService;
+ private final GraphQLRepository graphQLRepository;
+ private final User profileModel;
+ private final boolean isLoggedIn;
+ private String nextMaxId;
+ private boolean moreAvailable;
+
+ public ProfilePostFetchService(final User profileModel, final boolean isLoggedIn) {
+ this.profileModel = profileModel;
+ this.isLoggedIn = isLoggedIn;
+ graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
+ profileService = isLoggedIn ? ProfileService.getInstance() : null;
+ }
+
+ @Override
+ public void fetch(final FetchListener> fetchListener) {
+ final ServiceCallback cb = new ServiceCallback() {
+ @Override
+ public void onSuccess(final PostsFetchResponse result) {
+ if (result == null) return;
+ nextMaxId = result.getNextCursor();
+ moreAvailable = result.getHasNextPage();
+ if (fetchListener != null) {
+ fetchListener.onResult(result.getFeedModels());
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ // Log.e(TAG, "onFailure: ", t);
+ if (fetchListener != null) {
+ fetchListener.onFailure(t);
+ }
+ }
+ };
+ if (isLoggedIn) profileService.fetchPosts(profileModel.getPk(), nextMaxId, cb);
+ else graphQLRepository.fetchProfilePosts(
+ profileModel.getPk(),
+ 30,
+ nextMaxId,
+ profileModel,
+ CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
+ if (throwable != null) {
+ cb.onFailure(throwable);
+ return;
+ }
+ cb.onSuccess(postsFetchResponse);
+ }, Dispatchers.getIO())
+ );
+ }
+
+ @Override
+ public void reset() {
+ nextMaxId = null;
+ }
+
+ @Override
+ public boolean hasNextPage() {
+ return moreAvailable;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java b/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java
new file mode 100644
index 0000000..e4650a7
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/asyncs/SavedPostFetchService.java
@@ -0,0 +1,94 @@
+package awais.instagrabber.asyncs;
+
+import java.util.List;
+
+import awais.instagrabber.customviews.helpers.PostFetcher;
+import awais.instagrabber.interfaces.FetchListener;
+import awais.instagrabber.models.enums.PostItemType;
+import awais.instagrabber.repositories.responses.Media;
+import awais.instagrabber.repositories.responses.PostsFetchResponse;
+import awais.instagrabber.utils.CoroutineUtilsKt;
+import awais.instagrabber.webservices.GraphQLRepository;
+import awais.instagrabber.webservices.ProfileService;
+import awais.instagrabber.webservices.ServiceCallback;
+import kotlinx.coroutines.Dispatchers;
+
+public class SavedPostFetchService implements PostFetcher.PostFetchService {
+ private final ProfileService profileService;
+ private final GraphQLRepository graphQLRepository;
+ private final long profileId;
+ private final PostItemType type;
+ private final boolean isLoggedIn;
+
+ private String nextMaxId;
+ private final String collectionId;
+ private boolean moreAvailable;
+
+ public SavedPostFetchService(final long profileId, final PostItemType type, final boolean isLoggedIn, final String collectionId) {
+ this.profileId = profileId;
+ this.type = type;
+ this.isLoggedIn = isLoggedIn;
+ this.collectionId = collectionId;
+ graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
+ profileService = isLoggedIn ? ProfileService.getInstance() : null;
+ }
+
+ @Override
+ public void fetch(final FetchListener> fetchListener) {
+ final ServiceCallback callback = new ServiceCallback() {
+ @Override
+ public void onSuccess(final PostsFetchResponse result) {
+ if (result == null) return;
+ nextMaxId = result.getNextCursor();
+ moreAvailable = result.getHasNextPage();
+ if (fetchListener != null) {
+ fetchListener.onResult(result.getFeedModels());
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable t) {
+ // Log.e(TAG, "onFailure: ", t);
+ if (fetchListener != null) {
+ fetchListener.onFailure(t);
+ }
+ }
+ };
+ switch (type) {
+ case LIKED:
+ profileService.fetchLiked(nextMaxId, callback);
+ break;
+ case TAGGED:
+ if (isLoggedIn) profileService.fetchTagged(profileId, nextMaxId, callback);
+ else graphQLRepository.fetchTaggedPosts(
+ profileId,
+ 30,
+ nextMaxId,
+ CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
+ if (throwable != null) {
+ callback.onFailure(throwable);
+ return;
+ }
+ callback.onSuccess(postsFetchResponse);
+ }, Dispatchers.getIO())
+ );
+ break;
+ case COLLECTION:
+ case SAVED:
+ profileService.fetchSaved(nextMaxId, collectionId, callback);
+ break;
+ default:
+ callback.onFailure(null);
+ }
+ }
+
+ @Override
+ public void reset() {
+ nextMaxId = null;
+ }
+
+ @Override
+ public boolean hasNextPage() {
+ return moreAvailable;
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/backup/BarinstaBackupAgent.kt b/app/src/main/java/awais/instagrabber/backup/BarinstaBackupAgent.kt
new file mode 100644
index 0000000..54b4958
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/backup/BarinstaBackupAgent.kt
@@ -0,0 +1,22 @@
+package awais.instagrabber.backup
+
+import android.app.backup.BackupAgent
+import android.app.backup.BackupDataInput
+import android.app.backup.BackupDataOutput
+import android.app.backup.FullBackupDataOutput
+import android.os.ParcelFileDescriptor
+import awais.instagrabber.fragments.settings.PreferenceKeys
+import awais.instagrabber.utils.Utils.settingsHelper
+
+class BarinstaBackupAgent : BackupAgent() {
+ override fun onFullBackup(data: FullBackupDataOutput?) {
+ super.onFullBackup(if (settingsHelper.getBoolean(PreferenceKeys.PREF_AUTO_BACKUP_ENABLED)) data else null)
+ }
+
+ // no key-value backups
+ override fun onBackup(oldState: ParcelFileDescriptor?,
+ data: BackupDataOutput?, newState: ParcelFileDescriptor?) {}
+
+ override fun onRestore(data: BackupDataInput, appVersionCode: Int,
+ newState: ParcelFileDescriptor) {}
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/broadcasts/DMRefreshBroadcastReceiver.java b/app/src/main/java/awais/instagrabber/broadcasts/DMRefreshBroadcastReceiver.java
new file mode 100644
index 0000000..3d2151a
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/broadcasts/DMRefreshBroadcastReceiver.java
@@ -0,0 +1,27 @@
+package awais.instagrabber.broadcasts;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class DMRefreshBroadcastReceiver extends BroadcastReceiver {
+ public static final String ACTION_REFRESH_DM = "action_refresh_dm";
+ private final OnDMRefreshCallback callback;
+
+ public DMRefreshBroadcastReceiver(final OnDMRefreshCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ if (callback == null) return;
+ final String action = intent.getAction();
+ if (action == null) return;
+ if (!action.equals(ACTION_REFRESH_DM)) return;
+ callback.onReceive();
+ }
+
+ public interface OnDMRefreshCallback {
+ void onReceive();
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/BarinstaFragmentNavigator.kt b/app/src/main/java/awais/instagrabber/customviews/BarinstaFragmentNavigator.kt
new file mode 100644
index 0000000..1220c0d
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/BarinstaFragmentNavigator.kt
@@ -0,0 +1,90 @@
+package awais.instagrabber.customviews
+
+import android.content.Context
+import androidx.fragment.app.FragmentManager
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavOptions
+import androidx.navigation.Navigator
+import androidx.navigation.fragment.FragmentNavigator
+import androidx.navigation.navOptions
+import awais.instagrabber.R
+import awais.instagrabber.fragments.settings.PreferenceKeys
+import awais.instagrabber.utils.Utils
+
+private val defaultNavOptions = navOptions {
+ anim {
+ enter = R.anim.slide_in_right
+ exit = R.anim.slide_out_left
+ popEnter = android.R.anim.slide_in_left
+ popExit = android.R.anim.slide_out_right
+ }
+}
+
+private val emptyNavOptions = navOptions {}
+
+/**
+ * Needs to replace FragmentNavigator and replacing is done with name in annotation.
+ * Navigation method will use defaults for fragments transitions animations.
+ */
+@Navigator.Name("fragment")
+class BarinstaFragmentNavigator(
+ context: Context,
+ fragmentManager: FragmentManager,
+ containerId: Int
+) : FragmentNavigator(context, fragmentManager, containerId) {
+
+ override fun navigate(
+ entries: List,
+ navOptions: NavOptions?,
+ navigatorExtras: Navigator.Extras?
+ ) {
+ val disableTransitions = Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_DISABLE_SCREEN_TRANSITIONS)
+ if (disableTransitions) {
+ super.navigate(entries, navOptions, navigatorExtras)
+ return
+ }
+ // this will try to fill in empty animations with defaults when no shared element transitions
+ // https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element
+ val hasSharedElements = navigatorExtras != null && navigatorExtras is Extras
+ val navOptions1 = if (hasSharedElements) navOptions else navOptions.fillEmptyAnimationsWithDefaults()
+ super.navigate(entries, navOptions1, navigatorExtras)
+ }
+
+ private fun NavOptions?.fillEmptyAnimationsWithDefaults(): NavOptions =
+ this?.copyNavOptionsWithDefaultAnimations() ?: defaultNavOptions
+
+ private fun NavOptions.copyNavOptionsWithDefaultAnimations(): NavOptions = let { originalNavOptions ->
+ navOptions {
+ launchSingleTop = originalNavOptions.shouldLaunchSingleTop()
+ popUpTo(originalNavOptions.popUpToId) {
+ inclusive = originalNavOptions.isPopUpToInclusive()
+ saveState = originalNavOptions.shouldPopUpToSaveState()
+ }
+ originalNavOptions.popUpToRoute?.let {
+ popUpTo(it) {
+ inclusive = originalNavOptions.isPopUpToInclusive()
+ saveState = originalNavOptions.shouldPopUpToSaveState()
+ }
+ }
+ restoreState = originalNavOptions.shouldRestoreState()
+ anim {
+ enter =
+ if (originalNavOptions.enterAnim == emptyNavOptions.enterAnim) defaultNavOptions.enterAnim
+ else originalNavOptions.enterAnim
+ exit =
+ if (originalNavOptions.exitAnim == emptyNavOptions.exitAnim) defaultNavOptions.exitAnim
+ else originalNavOptions.exitAnim
+ popEnter =
+ if (originalNavOptions.popEnterAnim == emptyNavOptions.popEnterAnim) defaultNavOptions.popEnterAnim
+ else originalNavOptions.popEnterAnim
+ popExit =
+ if (originalNavOptions.popExitAnim == emptyNavOptions.popExitAnim) defaultNavOptions.popExitAnim
+ else originalNavOptions.popExitAnim
+ }
+ }
+ }
+
+ private companion object {
+ private const val TAG = "FragmentNavigator"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/customviews/BarinstaNavHostFragment.kt b/app/src/main/java/awais/instagrabber/customviews/BarinstaNavHostFragment.kt
new file mode 100644
index 0000000..24979a9
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/BarinstaNavHostFragment.kt
@@ -0,0 +1,14 @@
+package awais.instagrabber.customviews
+
+import androidx.navigation.NavHostController
+import androidx.navigation.fragment.NavHostFragment
+
+class BarinstaNavHostFragment : NavHostFragment() {
+ override fun onCreateNavHostController(navHostController: NavHostController) {
+ super.onCreateNavHostController(navHostController)
+ navHostController.navigatorProvider.addNavigator(
+ // this replaces FragmentNavigator
+ BarinstaFragmentNavigator(requireContext(), childFragmentManager, id)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java b/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
new file mode 100644
index 0000000..7b50a06
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java
@@ -0,0 +1,174 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import awais.instagrabber.R;
+
+public class ChatMessageLayout extends FrameLayout {
+
+ private FrameLayout viewPartMain;
+ private View viewPartInfo;
+ private TypedArray a;
+
+ private int viewPartInfoWidth;
+ private int viewPartInfoHeight;
+
+ // private boolean withGroupHeader = false;
+
+ public ChatMessageLayout(@NonNull final Context context) {
+ super(context);
+ }
+
+ public ChatMessageLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ a = context.obtainStyledAttributes(attrs, R.styleable.ChatMessageLayout, 0, 0);
+ }
+
+ public ChatMessageLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ a = context.obtainStyledAttributes(attrs, R.styleable.ChatMessageLayout, defStyleAttr, 0);
+ }
+
+ public ChatMessageLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ a = context.obtainStyledAttributes(attrs, R.styleable.ChatMessageLayout, defStyleAttr, defStyleRes);
+ }
+
+ // public void setWithGroupHeader(boolean withGroupHeader) {
+ // this.withGroupHeader = withGroupHeader;
+ // }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ try {
+ viewPartMain = findViewById(a.getResourceId(R.styleable.ChatMessageLayout_viewPartMain, -1));
+ viewPartInfo = findViewById(a.getResourceId(R.styleable.ChatMessageLayout_viewPartInfo, -1));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize;
+ // heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (viewPartMain == null || viewPartInfo == null || widthSize <= 0) {
+ return;
+ }
+
+ final View firstChild = viewPartMain.getChildAt(0);
+ if (firstChild == null) return;
+
+ final int firstChildId = firstChild.getId();
+
+ int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
+ // int availableHeight = heightSize - getPaddingTop() - getPaddingBottom();
+
+ final LayoutParams viewPartMainLayoutParams = (LayoutParams) viewPartMain.getLayoutParams();
+ final int viewPartMainWidth = viewPartMain.getMeasuredWidth() + viewPartMainLayoutParams.leftMargin + viewPartMainLayoutParams.rightMargin;
+ final int viewPartMainHeight = viewPartMain.getMeasuredHeight() + viewPartMainLayoutParams.topMargin + viewPartMainLayoutParams.bottomMargin;
+
+ final LayoutParams viewPartInfoLayoutParams = (LayoutParams) viewPartInfo.getLayoutParams();
+ viewPartInfoWidth = viewPartInfo.getMeasuredWidth() + viewPartInfoLayoutParams.leftMargin + viewPartInfoLayoutParams.rightMargin;
+ viewPartInfoHeight = viewPartInfo.getMeasuredHeight() + viewPartInfoLayoutParams.topMargin + viewPartInfoLayoutParams.bottomMargin;
+
+ widthSize = getPaddingLeft() + getPaddingRight();
+ heightSize = getPaddingTop() + getPaddingBottom();
+ if (firstChildId == R.id.media_container) {
+ 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.ivAnimatedMessage || firstChildId == R.id.reel_share_container) {
+ widthSize += viewPartMainWidth;
+ heightSize += viewPartMainHeight + viewPartInfoHeight;
+ } else {
+ int viewPartMainLineCount = 1;
+ float viewPartMainLastLineWidth = 0;
+ final TextView textMessage;
+ if (firstChild instanceof TextView) {
+ textMessage = (TextView) firstChild;
+ }
+ else textMessage = null;
+ if (textMessage != null) {
+ viewPartMainLineCount = textMessage.getLineCount();
+ viewPartMainLastLineWidth = viewPartMainLineCount > 0
+ ? textMessage.getLayout().getLineWidth(viewPartMainLineCount - 1)
+ : 0;
+ // also include start left padding
+ viewPartMainLastLineWidth += textMessage.getPaddingLeft();
+ }
+
+ final float lastLineWithInfoWidth = viewPartMainLastLineWidth + viewPartInfoWidth;
+ if (viewPartMainLineCount > 1 && lastLineWithInfoWidth <= viewPartMain.getMeasuredWidth()) {
+ widthSize += viewPartMainWidth;
+ heightSize += viewPartMainHeight;
+ } else if (viewPartMainLineCount > 1 && (lastLineWithInfoWidth > availableWidth)) {
+ widthSize += viewPartMainWidth;
+ heightSize += viewPartMainHeight + viewPartInfoHeight;
+ } else if (viewPartMainLineCount == 1 && (viewPartMainWidth + viewPartInfoWidth > availableWidth)) {
+ widthSize += viewPartMain.getMeasuredWidth();
+ heightSize += viewPartMainHeight + viewPartInfoHeight;
+ } else {
+ heightSize += viewPartMainHeight;
+ widthSize += viewPartMainWidth + viewPartInfoWidth;
+ }
+
+ // if (isInEditMode()) {
+ // TextView wDebugView = (TextView) ((ViewGroup) this.getParent()).findViewWithTag("debug");
+ // wDebugView.setText(lastLineWithInfoWidth
+ // + "\n" + availableWidth
+ // + "\n" + viewPartMain.getMeasuredWidth()
+ // + "\n" + (lastLineWithInfoWidth <= viewPartMain.getMeasuredWidth())
+ // + "\n" + (lastLineWithInfoWidth > availableWidth)
+ // + "\n" + (viewPartMainWidth + viewPartInfoWidth > availableWidth));
+ // }
+ }
+ setMeasuredDimension(widthSize, heightSize);
+ super.onMeasure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));
+
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (viewPartMain == null || viewPartInfo == null) {
+ return;
+ }
+ // if (withGroupHeader) {
+ // viewPartMain.layout(
+ // getPaddingLeft(),
+ // getPaddingTop() - Utils.convertDpToPx(4),
+ // viewPartMain.getWidth() + getPaddingLeft(),
+ // viewPartMain.getHeight() + getPaddingTop());
+ //
+ // } else {
+ viewPartMain.layout(
+ getPaddingLeft(),
+ getPaddingTop(),
+ viewPartMain.getWidth() + getPaddingLeft(),
+ viewPartMain.getHeight() + getPaddingTop());
+
+ // }
+ viewPartInfo.layout(
+ right - left - viewPartInfoWidth - getPaddingRight(),
+ bottom - top - getPaddingBottom() - viewPartInfoHeight,
+ right - left - getPaddingRight(),
+ bottom - top - getPaddingBottom());
+ }
+}
diff --git a/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java b/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java
new file mode 100755
index 0000000..2f68786
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java
@@ -0,0 +1,63 @@
+package awais.instagrabber.customviews;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+
+import com.facebook.drawee.drawable.ScalingUtils;
+import com.facebook.drawee.generic.GenericDraweeHierarchy;
+import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
+import com.facebook.drawee.generic.GenericDraweeHierarchyInflater;
+import com.facebook.drawee.generic.RoundingParams;
+import com.facebook.drawee.view.SimpleDraweeView;
+
+import awais.instagrabber.R;
+
+public class CircularImageView extends SimpleDraweeView {
+ public CircularImageView(Context context, GenericDraweeHierarchy hierarchy) {
+ super(context);
+ setHierarchy(hierarchy);
+ }
+
+ public CircularImageView(final Context context) {
+ super(context);
+ inflateHierarchy(context, null);
+ }
+
+ public CircularImageView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ inflateHierarchy(context, attrs);
+ }
+
+ public CircularImageView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ inflateHierarchy(context, attrs);
+ }
+
+ protected void inflateHierarchy(Context context, @Nullable AttributeSet attrs) {
+ Resources resources = context.getResources();
+ final RoundingParams roundingParams = RoundingParams.asCircle();
+ GenericDraweeHierarchyBuilder builder = new GenericDraweeHierarchyBuilder(resources)
+ .setRoundingParams(roundingParams)
+ .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER);
+ GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs);
+ setAspectRatio(builder.getDesiredAspectRatio());
+ setHierarchy(builder.build());
+ setBackgroundResource(R.drawable.shape_oval_light);
+ }
+
+ /* types: 0 clear, 1 green (feed bestie / has story), 2 red (live) */
+ public void setStoriesBorder(final int type) {
+ // private final int borderSize = 8;
+ final int color = type == 2 ? Color.RED : Color.GREEN;
+ RoundingParams roundingParams = getHierarchy().getRoundingParams();
+ if (roundingParams == null) {
+ roundingParams = RoundingParams.asCircle().setRoundingMethod(RoundingParams.RoundingMethod.BITMAP_ONLY);
+ }
+ roundingParams.setBorder(color, type == 0 ? 0f : 5.0f);
+ getHierarchy().setRoundingParams(roundingParams);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/customviews/CommentMentionClickSpan.java b/app/src/main/java/awais/instagrabber/customviews/CommentMentionClickSpan.java
new file mode 100755
index 0000000..a68509c
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/CommentMentionClickSpan.java
@@ -0,0 +1,17 @@
+package awais.instagrabber.customviews;
+
+import android.text.TextPaint;
+import android.text.style.ClickableSpan;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+public final class CommentMentionClickSpan extends ClickableSpan {
+ @Override
+ public void onClick(@NonNull final View widget) { }
+
+ @Override
+ public void updateDrawState(@NonNull final TextPaint ds) {
+ ds.setColor(ds.linkColor);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java b/app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java
new file mode 100644
index 0000000..b13a3a4
--- /dev/null
+++ b/app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java
@@ -0,0 +1,477 @@
+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.content.res.Resources;
+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 java.util.function.Function;
+
+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 awais.instagrabber.repositories.responses.directmessages.DirectItem;
+
+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