From 2931f2d3ab71c60e38333332fecef372d962bd8b Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sun, 1 Nov 2020 20:34:42 +0900 Subject: [PATCH] Add Posts view to Hashtag fragment --- .../asyncs/HashtagPostFetchService.java | 56 +++ .../customviews/PostsRecyclerView.java | 11 +- .../fragments/HashTagFragment.java | 472 ++++++++++++------ .../fragments/main/FeedFragment.java | 1 - .../instagrabber/models/HashtagModel.java | 4 +- .../awais/instagrabber/models/StoryModel.java | 13 +- .../repositories/TagsRepository.java | 8 + .../awais/instagrabber/utils/Constants.java | 1 + .../instagrabber/utils/ResponseBodyUtils.java | 123 +++++ .../instagrabber/utils/SettingsHelper.java | 3 +- .../webservices/DiscoverService.java | 119 +---- .../instagrabber/webservices/TagsService.java | 186 ++++++- app/src/main/res/layout/fragment_hashtag.xml | 66 +-- app/src/main/res/menu/hashtag_menu.xml | 9 + .../res/navigation/comments_nav_graph.xml | 2 +- .../main/res/navigation/hashtag_nav_graph.xml | 21 +- 16 files changed, 748 insertions(+), 347 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java create mode 100644 app/src/main/res/menu/hashtag_menu.xml 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 00000000..b341eb4a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/asyncs/HashtagPostFetchService.java @@ -0,0 +1,56 @@ +package awais.instagrabber.asyncs; + +import java.util.List; + +import awais.instagrabber.customviews.helpers.PostFetcher; +import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.HashtagModel; +import awais.instagrabber.webservices.ServiceCallback; +import awais.instagrabber.webservices.TagsService; +import awais.instagrabber.webservices.TagsService.TagPostsFetchResponse; + +public class HashtagPostFetchService implements PostFetcher.PostFetchService { + private final TagsService tagsService; + private final HashtagModel hashtagModel; + private String nextMaxId; + private boolean moreAvailable; + + public HashtagPostFetchService(final HashtagModel hashtagModel) { + this.hashtagModel = hashtagModel; + tagsService = TagsService.getInstance(); + } + + @Override + public void fetch(final FetchListener> fetchListener) { + tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, new ServiceCallback() { + @Override + public void onSuccess(final TagPostsFetchResponse result) { + if (result == null) return; + nextMaxId = result.getNextMaxId(); + moreAvailable = result.isMoreAvailable(); + if (fetchListener != null) { + fetchListener.onResult(result.getItems()); + } + } + + @Override + public void onFailure(final Throwable t) { + // Log.e(TAG, "onFailure: ", t); + if (fetchListener != null) { + fetchListener.onFailure(t); + } + } + }); + } + + @Override + public void reset() { + nextMaxId = null; + } + + @Override + public boolean hasNextPage() { + return moreAvailable; + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java index f5b1acde..113914ab 100644 --- a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java +++ b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java @@ -44,6 +44,9 @@ public class PostsRecyclerView extends RecyclerView { private GridSpacingItemDecoration gridSpacingItemDecoration; private RecyclerLazyLoaderAtBottom lazyLoader; private FeedAdapterV2.FeedItemCallback feedItemCallback; + private boolean shouldScrollToTop; + + private final List fetchStatusChangeListeners = new ArrayList<>(); private final FetchListener> fetchListener = new FetchListener>() { @Override @@ -51,6 +54,7 @@ public class PostsRecyclerView extends RecyclerView { final int currentPage = lazyLoader.getCurrentPage(); if (currentPage == 0) { feedViewModel.getList().postValue(result); + shouldScrollToTop = true; dispatchFetchStatus(); return; } @@ -66,7 +70,6 @@ public class PostsRecyclerView extends RecyclerView { Log.e(TAG, "onFailure: ", t); } }; - private final List fetchStatusChangeListeners = new ArrayList<>(); public PostsRecyclerView(@NonNull final Context context) { super(context); @@ -158,7 +161,11 @@ public class PostsRecyclerView extends RecyclerView { private void initSelf() { feedViewModel = new ViewModelProvider(viewModelStoreOwner).get(FeedViewModel.class); - feedViewModel.getList().observe(lifeCycleOwner, feedAdapter::submitList); + feedViewModel.getList().observe(lifeCycleOwner, list -> feedAdapter.submitList(list, () -> { + if (!shouldScrollToTop) return; + smoothScrollToPosition(0); + shouldScrollToTop = false; + })); postFetcher = new PostFetcher(postFetchService, fetchListener); if (layoutPreferences.getHasGap()) { addItemDecoration(gridSpacingItemDecoration); diff --git a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java index 07ccc788..ce1110d7 100644 --- a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java @@ -4,24 +4,27 @@ import android.content.Context; import android.graphics.Typeface; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; import android.text.SpannableStringBuilder; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.util.Log; import android.view.ActionMode; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; -import androidx.activity.OnBackPressedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; +import androidx.core.content.PermissionChecker; import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -29,73 +32,67 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; -import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.List; import awais.instagrabber.R; import awais.instagrabber.activities.MainActivity; -import awais.instagrabber.adapters.PostsAdapter; +import awais.instagrabber.adapters.FeedAdapterV2; import awais.instagrabber.asyncs.HashtagFetcher; -import awais.instagrabber.asyncs.PostsFetcher; +import awais.instagrabber.asyncs.HashtagPostFetchService; import awais.instagrabber.customviews.PrimaryActionModeCallback; -import awais.instagrabber.customviews.helpers.GridAutofitLayoutManager; -import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; import awais.instagrabber.customviews.helpers.NestedCoordinatorLayout; -import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; import awais.instagrabber.databinding.FragmentHashtagBinding; -import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment; +import awais.instagrabber.fragments.main.FeedFragmentDirections; +import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.HashtagModel; -import awais.instagrabber.models.PostModel; +import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.models.StoryModel; -import awais.instagrabber.models.enums.DownloadMethod; import awais.instagrabber.models.enums.FavoriteType; -import awais.instagrabber.models.enums.PostItemType; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DataBox; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; -import awais.instagrabber.viewmodels.PostsViewModel; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.StoriesService; import awais.instagrabber.webservices.TagsService; import awaisomereport.LogCollector; +import static androidx.core.content.PermissionChecker.checkSelfPermission; +import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; import static awais.instagrabber.utils.Utils.logCollector; import static awais.instagrabber.utils.Utils.settingsHelper; public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "HashTagFragment"; + private static final int STORAGE_PERM_REQUEST_CODE = 8020; public static final String ARG_HASHTAG = "hashtag"; private MainActivity fragmentActivity; private FragmentHashtagBinding binding; private NestedCoordinatorLayout root; - private boolean shouldRefresh = true, hasStories = false; + private boolean shouldRefresh = true; + private boolean hasStories = false; private String hashtag; private HashtagModel hashtagModel; - private PostsViewModel postsViewModel; - private PostsAdapter postsAdapter; private ActionMode actionMode; private StoriesService storiesService; - private boolean hasNextPage; - private String endCursor; private AsyncTask currentlyExecuting; private boolean isLoggedIn; private TagsService tagsService; - private boolean isPullToRefresh; + private boolean storiesFetching; private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { setEnabled(false); remove(); - if (postsAdapter == null) return; - postsAdapter.clearSelection(); + // if (postsAdapter == null) return; + // postsAdapter.clearSelection(); } }; private final PrimaryActionModeCallback multiSelectAction = new PrimaryActionModeCallback( @@ -109,47 +106,132 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) { if (item.getItemId() == R.id.action_download) { - if (postsAdapter == null || hashtag == null) { - return false; - } - final Context context = getContext(); - if (context == null) return false; - DownloadUtils.batchDownload(context, - hashtag, - DownloadMethod.DOWNLOAD_MAIN, - postsAdapter.getSelectedModels()); - checkAndResetAction(); + // if (postsAdapter == null || hashtag == null) { + // return false; + // } + // final Context context = getContext(); + // if (context == null) return false; + // DownloadUtils.batchDownload(context, + // hashtag, + // DownloadMethod.DOWNLOAD_MAIN, + // postsAdapter.getSelectedModels()); + // checkAndResetAction(); return true; } return false; } }); - private final FetchListener> postsFetchListener = new FetchListener>() { + // private final FetchListener> postsFetchListener = new FetchListener>() { + // @Override + // public void onResult(final List result) { + // binding.swipeRefreshLayout.setRefreshing(false); + // if (result == null) return; + // binding.mainPosts.post(() -> binding.mainPosts.setVisibility(View.VISIBLE)); + // final List postModels = postsViewModel.getList().getValue(); + // List finalList = postModels == null || postModels.isEmpty() + // ? new ArrayList<>() + // : new ArrayList<>(postModels); + // if (isPullToRefresh) { + // finalList = result; + // isPullToRefresh = false; + // } else { + // finalList.addAll(result); + // } + // finalList.addAll(result); + // postsViewModel.getList().postValue(finalList); + // PostModel model = null; + // if (!result.isEmpty()) { + // model = result.get(result.size() - 1); + // } + // if (model == null) return; + // endCursor = model.getEndCursor(); + // hasNextPage = model.hasNextPage(); + // model.setPageCursor(false, null); + // } + // }; + private final FeedAdapterV2.FeedItemCallback feedItemCallback = new FeedAdapterV2.FeedItemCallback() { @Override - public void onResult(final List result) { - binding.swipeRefreshLayout.setRefreshing(false); - if (result == null) return; - binding.mainPosts.post(() -> binding.mainPosts.setVisibility(View.VISIBLE)); - final List postModels = postsViewModel.getList().getValue(); - List finalList = postModels == null || postModels.isEmpty() - ? new ArrayList<>() - : new ArrayList<>(postModels); - if (isPullToRefresh) { - finalList = result; - isPullToRefresh = false; - } else { - finalList.addAll(result); + public void onPostClick(final FeedModel feedModel, final View profilePicView, final View mainPostImage) { + openPostDialog(feedModel, profilePicView, mainPostImage, -1); + } + + @Override + public void onSliderClick(final FeedModel feedModel, final int position) { + openPostDialog(feedModel, null, null, position); + } + + @Override + public void onCommentsClick(final FeedModel feedModel) { + final NavDirections commentsAction = HashTagFragmentDirections.actionGlobalCommentsViewerFragment( + feedModel.getShortCode(), + feedModel.getPostId(), + feedModel.getProfileModel().getId() + ); + NavHostFragment.findNavController(HashTagFragment.this).navigate(commentsAction); + } + + @Override + public void onDownloadClick(final FeedModel feedModel) { + final Context context = getContext(); + if (context == null) return; + if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { + showDownloadDialog(feedModel); + return; } - finalList.addAll(result); - postsViewModel.getList().postValue(finalList); - PostModel model = null; - if (!result.isEmpty()) { - model = result.get(result.size() - 1); + requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); + } + + @Override + public void onHashtagClick(final String hashtag) { + final NavDirections action = FeedFragmentDirections.actionGlobalHashTagFragment(hashtag); + NavHostFragment.findNavController(HashTagFragment.this).navigate(action); + } + + @Override + public void onLocationClick(final FeedModel feedModel) { + final NavDirections action = FeedFragmentDirections.actionGlobalLocationFragment(feedModel.getLocationId()); + NavHostFragment.findNavController(HashTagFragment.this).navigate(action); + } + + @Override + public void onMentionClick(final String mention) { + navigateToProfile(mention.trim()); + } + + @Override + public void onNameClick(final FeedModel feedModel, final View profilePicView) { + navigateToProfile("@" + feedModel.getProfileModel().getUsername()); + } + + @Override + public void onProfilePicClick(final FeedModel feedModel, final View profilePicView) { + navigateToProfile("@" + feedModel.getProfileModel().getUsername()); + } + + @Override + public void onURLClick(final String url) { + Utils.openURL(getContext(), url); + } + + @Override + public void onEmailClick(final String emailId) { + Utils.openEmailAddress(getContext(), emailId); + } + + private void openPostDialog(final FeedModel feedModel, + final View profilePicView, + final View mainPostImage, + final int position) { + final PostViewV2Fragment.Builder builder = PostViewV2Fragment + .builder(feedModel); + if (position >= 0) { + builder.setPosition(position); } - if (model == null) return; - endCursor = model.getEndCursor(); - hasNextPage = model.hasNextPage(); - model.setPageCursor(false, null); + final PostViewV2Fragment fragment = builder + .setSharedProfilePicElement(profilePicView) + .setSharedMainPostElement(mainPostImage) + .build(); + fragment.show(getChildFragmentManager(), "post_view"); } }; @@ -159,6 +241,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe fragmentActivity = (MainActivity) requireActivity(); tagsService = TagsService.getInstance(); storiesService = StoriesService.getInstance(); + setHasOptionsMenu(true); } @Nullable @@ -183,9 +266,8 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onRefresh() { - isPullToRefresh = true; - endCursor = null; - fetchHashtagModel(); + binding.posts.refresh(); + fetchStories(); } @Override @@ -195,11 +277,17 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe } @Override - public void onDestroy() { - super.onDestroy(); - if (postsViewModel != null) { - postsViewModel.getList().postValue(Collections.emptyList()); + public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { + inflater.inflate(R.menu.topic_posts_menu, menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + if (item.getItemId() == R.id.layout) { + showPostsLayoutPreferences(); + return true; } + return super.onOptionsItemSelected(item); } private void init() { @@ -208,114 +296,96 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null; final HashTagFragmentArgs fragmentArgs = HashTagFragmentArgs.fromBundle(getArguments()); hashtag = fragmentArgs.getHashtag(); - // setTitle(); - setupPosts(); fetchHashtagModel(); } - private void setupPosts() { - postsViewModel = new ViewModelProvider(this).get(PostsViewModel.class); - final Context context = getContext(); - if (context == null) return; - final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(context, Utils.convertDpToPx(110)); - binding.mainPosts.setLayoutManager(layoutManager); - binding.mainPosts.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4))); - postsAdapter = new PostsAdapter((postModel, position) -> { - if (postsAdapter.isSelecting()) { - if (actionMode == null) return; - final String title = getString(R.string.number_selected, postsAdapter.getSelectedModels().size()); - actionMode.setTitle(title); - return; - } - if (checkAndResetAction()) return; - final List postModels = postsViewModel.getList().getValue(); - if (postModels == null || postModels.size() == 0) return; - if (postModels.get(0) == null) return; - final String postId = postModels.get(0).getPostId(); - final boolean isId = postId != null && isLoggedIn; - final String[] idsOrShortCodes = new String[postModels.size()]; - for (int i = 0; i < postModels.size(); i++) { - idsOrShortCodes[i] = isId ? postModels.get(i).getPostId() - : postModels.get(i).getShortCode(); - } - final NavDirections action = HashTagFragmentDirections.actionGlobalPostViewFragment( - position, - idsOrShortCodes, - isId); - NavHostFragment.findNavController(this).navigate(action); - - }, (model, position) -> { - if (!postsAdapter.isSelecting()) { - checkAndResetAction(); - return true; - } - if (onBackPressedCallback.isEnabled()) { - return true; - } - final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher(); - onBackPressedCallback.setEnabled(true); - actionMode = fragmentActivity.startActionMode(multiSelectAction); - final String title = getString(R.string.number_selected, 1); - actionMode.setTitle(title); - onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback); - return true; - }); - postsViewModel.getList().observe(fragmentActivity, postsAdapter::submitList); - binding.mainPosts.setAdapter(postsAdapter); - final RecyclerLazyLoader lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { - if (!hasNextPage || getContext() == null) return; - binding.swipeRefreshLayout.setRefreshing(true); - fetchPosts(); - endCursor = null; - }); - binding.mainPosts.addOnScrollListener(lazyLoader); - } - private void fetchHashtagModel() { stopCurrentExecutor(); binding.swipeRefreshLayout.setRefreshing(true); currentlyExecuting = new HashtagFetcher(hashtag.substring(1), result -> { - final Context context = getContext(); - if (context == null) return; hashtagModel = result; binding.swipeRefreshLayout.setRefreshing(false); + final Context context = getContext(); + if (context == null) return; if (hashtagModel == null) { Toast.makeText(context, R.string.error_loading_profile, Toast.LENGTH_SHORT).show(); return; } setTitle(); - fetchPosts(); + setHashtagDetails(); + setupPosts(); + fetchStories(); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - private void fetchPosts() { - stopCurrentExecutor(); + private void setupPosts() { + binding.posts.setViewModelStoreOwner(this) + .setLifeCycleOwner(this) + .setPostFetchService(new HashtagPostFetchService(hashtagModel)) + .setLayoutPreferences(PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_HASHTAG_POSTS_LAYOUT))) + .addFetchStatusChangeListener(fetching -> updateSwipeRefreshState()) + .setFeedItemCallback(feedItemCallback) + .init(); binding.swipeRefreshLayout.setRefreshing(true); - if (TextUtils.isEmpty(hashtag)) return; - currentlyExecuting = new PostsFetcher(hashtag.substring(1), PostItemType.HASHTAG, endCursor, postsFetchListener) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - final Context context = getContext(); - if (context == null) return; + // postsViewModel = new ViewModelProvider(this).get(PostsViewModel.class); + // final Context context = getContext(); + // if (context == null) return; + // final GridAutofitLayoutManager layoutManager = new GridAutofitLayoutManager(context, Utils.convertDpToPx(110)); + // binding.mainPosts.setLayoutManager(layoutManager); + // binding.mainPosts.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(4))); + // postsAdapter = new PostsAdapter((postModel, position) -> { + // if (postsAdapter.isSelecting()) { + // if (actionMode == null) return; + // final String title = getString(R.string.number_selected, postsAdapter.getSelectedModels().size()); + // actionMode.setTitle(title); + // return; + // } + // if (checkAndResetAction()) return; + // final List postModels = postsViewModel.getList().getValue(); + // if (postModels == null || postModels.size() == 0) return; + // if (postModels.get(0) == null) return; + // final String postId = postModels.get(0).getPostId(); + // final boolean isId = postId != null && isLoggedIn; + // final String[] idsOrShortCodes = new String[postModels.size()]; + // for (int i = 0; i < postModels.size(); i++) { + // idsOrShortCodes[i] = isId ? postModels.get(i).getPostId() + // : postModels.get(i).getShortCode(); + // } + // final NavDirections action = HashTagFragmentDirections.actionGlobalPostViewFragment( + // position, + // idsOrShortCodes, + // isId); + // NavHostFragment.findNavController(this).navigate(action); + // + // }, (model, position) -> { + // if (!postsAdapter.isSelecting()) { + // checkAndResetAction(); + // return true; + // } + // if (onBackPressedCallback.isEnabled()) { + // return true; + // } + // final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher(); + // onBackPressedCallback.setEnabled(true); + // actionMode = fragmentActivity.startActionMode(multiSelectAction); + // final String title = getString(R.string.number_selected, 1); + // actionMode.setTitle(title); + // onBackPressedDispatcher.addCallback(getViewLifecycleOwner(), onBackPressedCallback); + // return true; + // }); + // postsViewModel.getList().observe(fragmentActivity, postsAdapter::submitList); + // binding.mainPosts.setAdapter(postsAdapter); + // final RecyclerLazyLoader lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { + // if (!hasNextPage || getContext() == null) return; + // binding.swipeRefreshLayout.setRefreshing(true); + // fetchPosts(); + // endCursor = null; + // }); + // binding.mainPosts.addOnScrollListener(lazyLoader); + } + + private void setHashtagDetails() { if (isLoggedIn) { - storiesService.getUserStory(hashtagModel.getName(), - null, - false, - true, - false, - new ServiceCallback>() { - @Override - public void onSuccess(final List storyModels) { - if (storyModels != null && !storyModels.isEmpty()) { - binding.mainHashtagImage.setStoriesBorder(); - hasStories = true; - } - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error", t); - } - }); binding.btnFollowTag.setVisibility(View.VISIBLE); binding.btnFollowTag.setText(hashtagModel.getFollowing() ? R.string.unfollow : R.string.follow); binding.btnFollowTag.setChipIconResource(hashtagModel.getFollowing() @@ -422,15 +492,43 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe binding.mainTagPostCount.setText(span); binding.mainTagPostCount.setVisibility(View.VISIBLE); binding.mainHashtagImage.setOnClickListener(v -> { - if (hasStories) { - // show stories - final NavDirections action = HashTagFragmentDirections - .actionHashtagFragmentToStoryViewerFragment(-1, null, true, false, hashtagModel.getName(), hashtagModel.getName()); - NavHostFragment.findNavController(this).navigate(action); - } + if (!hasStories) return; + // show stories + final NavDirections action = HashTagFragmentDirections + .actionHashtagFragmentToStoryViewerFragment(-1, null, true, false, hashtagModel.getName(), hashtagModel.getName()); + NavHostFragment.findNavController(this).navigate(action); }); } + private void fetchStories() { + if (!isLoggedIn) return; + storiesFetching = true; + storiesService.getUserStory( + hashtagModel.getName(), + null, + false, + true, + false, + new ServiceCallback>() { + @Override + public void onSuccess(final List storyModels) { + if (storyModels != null && !storyModels.isEmpty()) { + binding.mainHashtagImage.setStoriesBorder(); + hasStories = true; + } else { + hasStories = false; + } + storiesFetching = false; + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error", t); + storiesFetching = false; + } + }); + } + public void stopCurrentExecutor() { if (currentlyExecuting != null) { try { @@ -453,6 +551,59 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe } } + private void updateSwipeRefreshState() { + binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching() || storiesFetching); + } + + private void showDownloadDialog(final FeedModel feedModel) { + final Context context = getContext(); + if (context == null) return; + DownloadUtils.download(context, feedModel); + // switch (feedModel.getItemType()) { + // case MEDIA_TYPE_IMAGE: + // case MEDIA_TYPE_VIDEO: + // break; + // case MEDIA_TYPE_SLIDER: + // break; + // } + // final List postModelsToDownload = new ArrayList<>(); + // // if (!session) { + // final DialogInterface.OnClickListener clickListener = (dialog, which) -> { + // if (which == DialogInterface.BUTTON_NEGATIVE) { + // postModelsToDownload.addAll(postModels); + // } else if (which == DialogInterface.BUTTON_POSITIVE) { + // postModelsToDownload.add(postModels.get(childPosition)); + // } else { + // session = true; + // postModelsToDownload.add(postModels.get(childPosition)); + // } + // if (postModelsToDownload.size() > 0) { + // DownloadUtils.batchDownload(context, + // username, + // DownloadMethod.DOWNLOAD_POST_VIEWER, + // postModelsToDownload); + // } + // }; + // new AlertDialog.Builder(context) + // .setTitle(R.string.post_viewer_download_dialog_title) + // .setMessage(R.string.post_viewer_download_message) + // .setNeutralButton(R.string.post_viewer_download_session, clickListener) + // .setPositiveButton(R.string.post_viewer_download_current, clickListener) + // .setNegativeButton(R.string.post_viewer_download_album, clickListener).show(); + // } else { + // DownloadUtils.batchDownload(context, + // username, + // DownloadMethod.DOWNLOAD_POST_VIEWER, + // Collections.singletonList(postModels.get(childPosition))); + } + + private void navigateToProfile(final String username) { + final NavController navController = NavHostFragment.findNavController(this); + final Bundle bundle = new Bundle(); + bundle.putString("username", username); + navController.navigate(R.id.action_global_profileFragment, bundle); + } + private boolean checkAndResetAction() { if (!onBackPressedCallback.isEnabled() && actionMode == null) { return false; @@ -467,4 +618,11 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe } return true; } + + private void showPostsLayoutPreferences() { + final PostsLayoutPreferencesDialogFragment fragment = new PostsLayoutPreferencesDialogFragment( + Constants.PREF_HASHTAG_POSTS_LAYOUT, + preferences -> new Handler().postDelayed(() -> binding.posts.setLayoutPreferences(preferences), 200)); + fragment.show(getChildFragmentManager(), "posts_layout_preferences"); + } } diff --git a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java index e79967c9..055c36c1 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -232,7 +232,6 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre .setFeedItemCallback(feedItemCallback) .init(); binding.feedSwipeRefreshLayout.setRefreshing(true); - // feedAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); // if (shouldAutoPlay) { // videoAwareRecyclerScroller = new VideoAwareRecyclerScroller(); // binding.feedRecyclerView.addOnScrollListener(videoAwareRecyclerScroller); diff --git a/app/src/main/java/awais/instagrabber/models/HashtagModel.java b/app/src/main/java/awais/instagrabber/models/HashtagModel.java index d45d6917..fbcac9db 100755 --- a/app/src/main/java/awais/instagrabber/models/HashtagModel.java +++ b/app/src/main/java/awais/instagrabber/models/HashtagModel.java @@ -5,7 +5,9 @@ import java.io.Serializable; public final class HashtagModel implements Serializable { private final boolean following; private final long postCount; - private final String id, name, sdProfilePic; + private final String id; + private final String name; + private final String sdProfilePic; public HashtagModel(final String id, final String name, final String sdProfilePic, final long postCount, final boolean following) { this.id = id; diff --git a/app/src/main/java/awais/instagrabber/models/StoryModel.java b/app/src/main/java/awais/instagrabber/models/StoryModel.java index a2962e2b..5cf43fbc 100755 --- a/app/src/main/java/awais/instagrabber/models/StoryModel.java +++ b/app/src/main/java/awais/instagrabber/models/StoryModel.java @@ -9,17 +9,24 @@ import awais.instagrabber.models.stickers.QuizModel; import awais.instagrabber.models.stickers.SwipeUpModel; public final class StoryModel implements Serializable { - private final String storyMediaId, storyUrl, username, userId; + private final String storyMediaId; + private final String storyUrl; + private final String username; + private final String userId; private final MediaItemType itemType; private final long timestamp; - private String videoUrl, tappableShortCode, tappableId, spotify; + private String videoUrl; + private String tappableShortCode; + private String tappableId; + private String spotify; private PollModel poll; private QuestionModel question; private QuizModel quiz; private SwipeUpModel swipeUp; private String[] mentions; private int position; - private boolean isCurrentSlide = false, canReply = false; + private boolean isCurrentSlide = false; + private boolean canReply = false; public StoryModel(final String storyMediaId, final String storyUrl, final MediaItemType itemType, final long timestamp, final String username, final String userId, final boolean canReply) { diff --git a/app/src/main/java/awais/instagrabber/repositories/TagsRepository.java b/app/src/main/java/awais/instagrabber/repositories/TagsRepository.java index 13cb4857..4db3efbd 100644 --- a/app/src/main/java/awais/instagrabber/repositories/TagsRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/TagsRepository.java @@ -1,9 +1,13 @@ package awais.instagrabber.repositories; +import java.util.Map; + import retrofit2.Call; +import retrofit2.http.GET; import retrofit2.http.Header; import retrofit2.http.POST; import retrofit2.http.Path; +import retrofit2.http.QueryMap; public interface TagsRepository { @@ -16,4 +20,8 @@ public interface TagsRepository { Call unfollow(@Header("User-Agent") String userAgent, @Header("x-csrftoken") String csrfToken, @Path("tag") String tag); + + @GET("/api/v1/feed/tag/{tag}/") + Call fetchPosts(@Path("tag") final String tag, + @QueryMap Map queryParams); } diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index 7ad2a35e..8ead842b 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -90,4 +90,5 @@ public final class Constants { public static final String PREF_POSTS_LAYOUT = "posts_layout"; public static final String PREF_PROFILE_POSTS_LAYOUT = "profile_posts_layout"; public static final String PREF_TOPIC_POSTS_LAYOUT = "topic_posts_layout"; + public static final String PREF_HASHTAG_POSTS_LAYOUT = "hashtag_posts_layout"; } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java index 122d781c..62cdc833 100644 --- a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java @@ -7,11 +7,16 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import awais.instagrabber.BuildConfig; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.PostChild; import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.direct_messages.DirectItemModel; import awais.instagrabber.models.direct_messages.InboxThreadModel; @@ -580,4 +585,122 @@ public final class ResponseBodyUtils { //if ("raven_unknown".equals(type)) [default?] return RavenExpiringMediaType.RAVEN_UNKNOWN; } + + public static FeedModel parseItem(final JSONObject itemJson) throws JSONException { + if (itemJson == null) { + return null; + } + ProfileModel profileModel = null; + if (itemJson.has("user")) { + final JSONObject user = itemJson.getJSONObject("user"); + final JSONObject friendshipStatus = user.optJSONObject("friendship_status"); + boolean following = false; + boolean restricted = false; + boolean requested = false; + if (friendshipStatus != null) { + following = friendshipStatus.optBoolean("following"); + requested = friendshipStatus.optBoolean("outgoing_request"); + restricted = friendshipStatus.optBoolean("is_restricted"); + } + profileModel = new ProfileModel( + user.optBoolean("is_private"), + false, // if you can see it then you def follow + user.optBoolean("is_verified"), + user.getString("pk"), + user.getString(Constants.EXTRAS_USERNAME), + user.optString("full_name"), + null, + null, + user.getString("profile_pic_url"), + null, + 0, + 0, + 0, + following, + restricted, + false, + requested); + } + final JSONObject captionJson = itemJson.optJSONObject("caption"); + final JSONObject locationJson = itemJson.optJSONObject("location"); + final MediaItemType mediaType = ResponseBodyUtils.getMediaItemType(itemJson.optInt("media_type")); + if (mediaType == null) { + return null; + } + final FeedModel.Builder feedModelBuilder = new FeedModel.Builder() + .setItemType(mediaType) + .setProfileModel(profileModel) + .setPostId(itemJson.getString(Constants.EXTRAS_ID)) + .setThumbnailUrl(mediaType != MediaItemType.MEDIA_TYPE_SLIDER ? ResponseBodyUtils.getLowQualityImage(itemJson) : null) + .setShortCode(itemJson.getString("code")) + .setPostCaption(captionJson != null ? captionJson.optString("text") : null) + .setCommentsCount(itemJson.optInt("comment_count")) + .setTimestamp(itemJson.optLong("taken_at", -1)) + .setLiked(itemJson.optBoolean("has_liked")) + // .setBookmarked() + .setLikesCount(itemJson.optInt("like_count")) + .setLocationName(locationJson != null ? locationJson.optString("name") : null) + .setLocationId(locationJson != null ? String.valueOf(locationJson.optLong("pk")) : null) + .setImageHeight(itemJson.optInt("original_height")) + .setImageWidth(itemJson.optInt("original_width")); + switch (mediaType) { + case MEDIA_TYPE_VIDEO: + final long videoViews = itemJson.optLong("view_count", 0); + feedModelBuilder.setViewCount(videoViews) + .setDisplayUrl(ResponseBodyUtils.getVideoUrl(itemJson)); + break; + case MEDIA_TYPE_IMAGE: + feedModelBuilder.setDisplayUrl(ResponseBodyUtils.getHighQualityImage(itemJson)); + break; + case MEDIA_TYPE_SLIDER: + final List childPosts = getChildPosts(itemJson); + feedModelBuilder.setSliderItems(childPosts); + break; + } + return feedModelBuilder.build(); + } + + private static List getChildPosts(final JSONObject mediaJson) throws JSONException { + if (mediaJson == null) { + return Collections.emptyList(); + } + final JSONArray carouselMedia = mediaJson.optJSONArray("carousel_media"); + if (carouselMedia == null) { + return Collections.emptyList(); + } + final List children = new ArrayList<>(); + for (int i = 0; i < carouselMedia.length(); i++) { + final JSONObject childJson = carouselMedia.optJSONObject(i); + final PostChild childPost = getChildPost(childJson); + if (childPost != null) { + children.add(childPost); + } + } + return children; + } + + private static PostChild getChildPost(final JSONObject childJson) throws JSONException { + if (childJson == null) { + return null; + } + final MediaItemType mediaType = ResponseBodyUtils.getMediaItemType(childJson.optInt("media_type")); + if (mediaType == null) { + return null; + } + final PostChild.Builder builder = new PostChild.Builder(); + switch (mediaType) { + case MEDIA_TYPE_VIDEO: + builder.setDisplayUrl(ResponseBodyUtils.getVideoUrl(childJson)); + break; + case MEDIA_TYPE_IMAGE: + builder.setDisplayUrl(ResponseBodyUtils.getHighQualityImage(childJson)); + break; + } + return builder.setItemType(mediaType) + .setPostId(childJson.getString("id")) + .setThumbnailUrl(ResponseBodyUtils.getLowQualityImage(childJson)) + .setHeight(childJson.optInt("original_height")) + .setWidth(childJson.optInt("original_width")) + .build(); + } } diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index 52903987..f9ffdb15 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -29,6 +29,7 @@ import static awais.instagrabber.utils.Constants.INSTADP; import static awais.instagrabber.utils.Constants.MARK_AS_SEEN; import static awais.instagrabber.utils.Constants.MUTED_VIDEOS; import static awais.instagrabber.utils.Constants.PREF_DARK_THEME; +import static awais.instagrabber.utils.Constants.PREF_HASHTAG_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_LIGHT_THEME; import static awais.instagrabber.utils.Constants.PREF_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_PROFILE_POSTS_LAYOUT; @@ -117,7 +118,7 @@ public final class SettingsHelper { @StringDef( {APP_LANGUAGE, APP_THEME, COOKIE, FOLDER_PATH, DATE_TIME_FORMAT, DATE_TIME_SELECTION, CUSTOM_DATE_TIME_FORMAT, DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT, - PREF_TOPIC_POSTS_LAYOUT}) + PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT}) public @interface StringSettings {} @StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS, diff --git a/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java b/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java index 107eb49c..1e1a9f4a 100644 --- a/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java +++ b/app/src/main/java/awais/instagrabber/webservices/DiscoverService.java @@ -2,7 +2,7 @@ package awais.instagrabber.webservices; import androidx.annotation.NonNull; -import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableMap; import org.json.JSONArray; import org.json.JSONException; @@ -14,7 +14,6 @@ import java.util.List; import java.util.Objects; import awais.instagrabber.models.FeedModel; -import awais.instagrabber.models.PostChild; import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.TopicCluster; import awais.instagrabber.models.enums.MediaItemType; @@ -51,7 +50,7 @@ public class DiscoverService extends BaseService { public void topicalExplore(@NonNull final TopicalExploreRequest request, final ServiceCallback callback) { - final ImmutableBiMap.Builder builder = ImmutableBiMap.builder() + final ImmutableMap.Builder builder = ImmutableMap.builder() .put("module", "explore_popular"); if (!TextUtils.isEmpty(request.getModule())) { builder.put("module", request.getModule()); @@ -204,7 +203,7 @@ public class DiscoverService extends BaseService { continue; } final JSONObject mediaJson = itemJson.optJSONObject("media"); - final FeedModel feedModel = parseClusterItemMedia(mediaJson); + final FeedModel feedModel = ResponseBodyUtils.parseItem(mediaJson); if (feedModel != null) { feedModels.add(feedModel); } @@ -212,118 +211,6 @@ public class DiscoverService extends BaseService { return feedModels; } - private FeedModel parseClusterItemMedia(final JSONObject mediaJson) throws JSONException { - if (mediaJson == null) { - return null; - } - ProfileModel profileModel = null; - if (mediaJson.has("user")) { - final JSONObject user = mediaJson.getJSONObject("user"); - final JSONObject friendshipStatus = user.optJSONObject("friendship_status"); - boolean following = false; - boolean restricted = false; - boolean requested = false; - if (friendshipStatus != null) { - following = friendshipStatus.optBoolean("following"); - requested = friendshipStatus.optBoolean("outgoing_request"); - restricted = friendshipStatus.optBoolean("is_restricted"); - } - profileModel = new ProfileModel( - user.optBoolean("is_private"), - false, // if you can see it then you def follow - user.optBoolean("is_verified"), - user.getString("pk"), - user.getString(Constants.EXTRAS_USERNAME), - user.optString("full_name"), - null, - null, - user.getString("profile_pic_url"), - null, - 0, - 0, - 0, - following, - restricted, - false, - requested); - } - final JSONObject captionJson = mediaJson.optJSONObject("caption"); - final JSONObject locationJson = mediaJson.optJSONObject("location"); - final MediaItemType mediaType = ResponseBodyUtils.getMediaItemType(mediaJson.optInt("media_type")); - final FeedModel.Builder feedModelBuilder = new FeedModel.Builder() - .setItemType(mediaType) - .setProfileModel(profileModel) - .setPostId(mediaJson.getString(Constants.EXTRAS_ID)) - .setThumbnailUrl(mediaType != MediaItemType.MEDIA_TYPE_SLIDER ? ResponseBodyUtils.getLowQualityImage(mediaJson) : null) - .setShortCode(mediaJson.getString("code")) - .setPostCaption(captionJson != null ? captionJson.optString("text") : null) - .setCommentsCount(mediaJson.optInt("comment_count")) - .setTimestamp(mediaJson.optLong("taken_at", -1)) - .setLiked(mediaJson.optBoolean("has_liked")) - // .setBookmarked() - .setLikesCount(mediaJson.optInt("like_count")) - .setLocationName(locationJson != null ? locationJson.optString("name") : null) - .setLocationId(locationJson != null ? String.valueOf(locationJson.optInt("pk")) : null) - .setImageHeight(mediaJson.optInt("original_height")) - .setImageWidth(mediaJson.optInt("original_width")); - switch (mediaType) { - case MEDIA_TYPE_VIDEO: - final long videoViews = mediaJson.optLong("view_count", 0); - feedModelBuilder.setViewCount(videoViews) - .setDisplayUrl(ResponseBodyUtils.getVideoUrl(mediaJson)); - break; - case MEDIA_TYPE_IMAGE: - feedModelBuilder.setDisplayUrl(ResponseBodyUtils.getHighQualityImage(mediaJson)); - break; - case MEDIA_TYPE_SLIDER: - final List childPosts = getChildPosts(mediaJson); - feedModelBuilder.setSliderItems(childPosts); - break; - } - return feedModelBuilder.build(); - } - - private List getChildPosts(final JSONObject mediaJson) throws JSONException { - if (mediaJson == null) { - return Collections.emptyList(); - } - final JSONArray carouselMedia = mediaJson.optJSONArray("carousel_media"); - if (carouselMedia == null) { - return Collections.emptyList(); - } - final List children = new ArrayList<>(); - for (int i = 0; i < carouselMedia.length(); i++) { - final JSONObject childJson = carouselMedia.optJSONObject(i); - final PostChild childPost = getChildPost(childJson); - if (childPost != null) { - children.add(childPost); - } - } - return children; - } - - private PostChild getChildPost(final JSONObject childJson) throws JSONException { - if (childJson == null) { - return null; - } - final MediaItemType mediaType = ResponseBodyUtils.getMediaItemType(childJson.optInt("media_type")); - final PostChild.Builder builder = new PostChild.Builder(); - switch (mediaType) { - case MEDIA_TYPE_VIDEO: - builder.setDisplayUrl(ResponseBodyUtils.getVideoUrl(childJson)); - break; - case MEDIA_TYPE_IMAGE: - builder.setDisplayUrl(ResponseBodyUtils.getHighQualityImage(childJson)); - break; - } - return builder.setItemType(mediaType) - .setPostId(childJson.getString("id")) - .setThumbnailUrl(ResponseBodyUtils.getLowQualityImage(childJson)) - .setHeight(childJson.optInt("original_height")) - .setWidth(childJson.optInt("original_width")) - .build(); - } - public static class TopicalExploreRequest { private String module; diff --git a/app/src/main/java/awais/instagrabber/webservices/TagsService.java b/app/src/main/java/awais/instagrabber/webservices/TagsService.java index 2834a570..8a7eea6d 100644 --- a/app/src/main/java/awais/instagrabber/webservices/TagsService.java +++ b/app/src/main/java/awais/instagrabber/webservices/TagsService.java @@ -4,11 +4,22 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.google.common.collect.ImmutableMap; + +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import awais.instagrabber.models.FeedModel; import awais.instagrabber.repositories.TagsRepository; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.ResponseBodyUtils; +import awais.instagrabber.utils.TextUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -18,16 +29,20 @@ public class TagsService extends BaseService { private static final String TAG = "TagsService"; - // web for www.instagram.com - private final TagsRepository webRepository; - private static TagsService instance; + private final TagsRepository webRepository; + private final TagsRepository repository; + private TagsService() { final Retrofit webRetrofit = getRetrofitBuilder() .baseUrl("https://www.instagram.com/") .build(); webRepository = webRetrofit.create(TagsRepository.class); + final Retrofit retrofit = getRetrofitBuilder() + .baseUrl("https://i.instagram.com/") + .build(); + repository = retrofit.create(TagsRepository.class); } public static TagsService getInstance() { @@ -98,4 +113,169 @@ public class TagsService extends BaseService { } }); } + + public void fetchPosts(@NonNull final String tag, + final String maxId, + final ServiceCallback callback) { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + if (!TextUtils.isEmpty(maxId)) { + builder.put("max_id", maxId); + } + final Call request = repository.fetchPosts(tag, builder.build()); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + try { + if (callback == null) { + return; + } + final String body = response.body(); + if (TextUtils.isEmpty(body)) { + callback.onSuccess(null); + return; + } + final TagPostsFetchResponse tagPostsFetchResponse = parseResponse(body); + callback.onSuccess(tagPostsFetchResponse); + } catch (JSONException e) { + Log.e(TAG, "onResponse", e); + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + + private TagPostsFetchResponse parseResponse(@NonNull final String body) throws JSONException { + final JSONObject root = new JSONObject(body); + final boolean moreAvailable = root.optBoolean("more_available"); + final String nextMaxId = root.optString("next_max_id"); + final int numResults = root.optInt("num_results"); + final String status = root.optString("status"); + final JSONArray itemsJson = root.optJSONArray("items"); + final List items = parseItems(itemsJson); + return new TagPostsFetchResponse( + moreAvailable, + nextMaxId, + numResults, + status, + items + ); + } + + private List parseItems(final JSONArray items) throws JSONException { + if (items == null) { + return Collections.emptyList(); + } + final List feedModels = new ArrayList<>(); + for (int i = 0; i < items.length(); i++) { + final JSONObject itemJson = items.optJSONObject(i); + if (itemJson == null) { + continue; + } + final FeedModel feedModel = ResponseBodyUtils.parseItem(itemJson); + if (feedModel != null) { + feedModels.add(feedModel); + } + } + return feedModels; + } + + public static class TagPostsFetchResponse { + private boolean moreAvailable; + private String nextMaxId; + private int numResults; + private String status; + private List items; + + public TagPostsFetchResponse(final boolean moreAvailable, + final String nextMaxId, + final int numResults, + final String status, + final List items) { + this.moreAvailable = moreAvailable; + this.nextMaxId = nextMaxId; + this.numResults = numResults; + this.status = status; + this.items = items; + } + + public boolean isMoreAvailable() { + return moreAvailable; + } + + public TagPostsFetchResponse setMoreAvailable(final boolean moreAvailable) { + this.moreAvailable = moreAvailable; + return this; + } + + public String getNextMaxId() { + return nextMaxId; + } + + public TagPostsFetchResponse setNextMaxId(final String nextMaxId) { + this.nextMaxId = nextMaxId; + return this; + } + + public int getNumResults() { + return numResults; + } + + public TagPostsFetchResponse setNumResults(final int numResults) { + this.numResults = numResults; + return this; + } + + public String getStatus() { + return status; + } + + public TagPostsFetchResponse setStatus(final String status) { + this.status = status; + return this; + } + + public List getItems() { + return items; + } + + public TagPostsFetchResponse setItems(final List items) { + this.items = items; + return this; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final TagPostsFetchResponse that = (TagPostsFetchResponse) o; + return moreAvailable == that.moreAvailable && + numResults == that.numResults && + Objects.equals(nextMaxId, that.nextMaxId) && + Objects.equals(status, that.status) && + Objects.equals(items, that.items); + } + + @Override + public int hashCode() { + return Objects.hash(moreAvailable, nextMaxId, numResults, status, items); + } + + @Override + public String toString() { + return "TagPostsFetchResponse{" + + "moreAvailable=" + moreAvailable + + ", nextMaxId='" + nextMaxId + '\'' + + ", numResults=" + numResults + + ", status='" + status + '\'' + + ", items=" + items + + '}'; + } + } } diff --git a/app/src/main/res/layout/fragment_hashtag.xml b/app/src/main/res/layout/fragment_hashtag.xml index 262a2f22..32aaf043 100644 --- a/app/src/main/res/layout/fragment_hashtag.xml +++ b/app/src/main/res/layout/fragment_hashtag.xml @@ -51,7 +51,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" android:text="@string/follow" android:visibility="gone" app:chipBackgroundColor="@null" @@ -67,7 +66,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" android:text="@string/add_to_favorites" android:visibility="gone" app:chipBackgroundColor="@null" @@ -77,74 +75,20 @@ app:layout_constraintStart_toEndOf="@id/btnFollowTag" app:layout_constraintTop_toBottomOf="@id/mainTagPostCount" app:rippleColor="@color/yellow_400" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + android:clipToPadding="false" /> \ No newline at end of file diff --git a/app/src/main/res/menu/hashtag_menu.xml b/app/src/main/res/menu/hashtag_menu.xml new file mode 100644 index 00000000..1ced72b6 --- /dev/null +++ b/app/src/main/res/menu/hashtag_menu.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/comments_nav_graph.xml b/app/src/main/res/navigation/comments_nav_graph.xml index f9d0c121..91502f40 100644 --- a/app/src/main/res/navigation/comments_nav_graph.xml +++ b/app/src/main/res/navigation/comments_nav_graph.xml @@ -5,7 +5,7 @@ android:id="@+id/comments_nav_graph" app:startDestination="@id/commentsViewerFragment"> - + + + + + + + + + + app:nullable="true" />