Browse Source

greatly fix all the follower/ing stuff

closes #1334 (assuming)
closes #1340 (by implementing API search)
closes #1342
closes #1472
closes #1473
renovate/org.jetbrains.kotlinx-kotlinx-coroutines-test-1.x
Austin Huang 3 years ago
parent
commit
036edbea10
No known key found for this signature in database GPG Key ID: 84C23AA04587A91F
  1. 61
      app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java
  2. 11
      app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java
  3. 631
      app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.kt
  4. 49
      app/src/main/java/awais/instagrabber/models/FollowModel.kt
  5. 3
      app/src/main/java/awais/instagrabber/repositories/FriendshipService.kt
  6. 19
      app/src/main/java/awais/instagrabber/repositories/responses/FriendshipListFetchResponse.kt
  7. 170
      app/src/main/java/awais/instagrabber/viewmodels/FollowViewModel.kt
  8. 45
      app/src/main/java/awais/instagrabber/webservices/FriendshipRepository.kt
  9. 23
      app/src/main/java/thoughtbot/expandableadapter/ExpandableGroup.java
  10. 10
      app/src/main/java/thoughtbot/expandableadapter/ExpandableList.java
  11. 1
      app/src/main/res/layout/fragment_followers_viewer.xml

61
app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java

@ -12,12 +12,13 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import awais.instagrabber.R; import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.FollowsViewHolder; import awais.instagrabber.adapters.viewholder.FollowsViewHolder;
import awais.instagrabber.databinding.ItemFollowBinding; import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.interfaces.OnGroupClickListener; import awais.instagrabber.interfaces.OnGroupClickListener;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import thoughtbot.expandableadapter.ExpandableGroup; import thoughtbot.expandableadapter.ExpandableGroup;
import thoughtbot.expandableadapter.ExpandableList; import thoughtbot.expandableadapter.ExpandableList;
@ -27,28 +28,33 @@ import thoughtbot.expandableadapter.GroupViewHolder;
// thanks to ThoughtBot's ExpandableRecyclerViewAdapter // thanks to ThoughtBot's ExpandableRecyclerViewAdapter
// https://github.com/thoughtbot/expandable-recycler-view // https://github.com/thoughtbot/expandable-recycler-view
public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements OnGroupClickListener, Filterable { public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements OnGroupClickListener, Filterable {
private final View.OnClickListener onClickListener;
private final ExpandableList expandableListOriginal;
private final boolean hasManyGroups;
private ExpandableList expandableList;
private final Filter filter = new Filter() { private final Filter filter = new Filter() {
@Nullable @Nullable
@Override @Override
protected FilterResults performFiltering(final CharSequence filter) { protected FilterResults performFiltering(final CharSequence filter) {
if (expandableList.groups != null) {
final boolean isFilterEmpty = TextUtils.isEmpty(filter);
final String query = isFilterEmpty ? null : filter.toString().toLowerCase();
for (int x = 0; x < expandableList.groups.size(); ++x) {
final ExpandableGroup expandableGroup = expandableList.groups.get(x);
final List<FollowModel> items = expandableGroup.getItems(false);
final int itemCount = expandableGroup.getItemCount(false);
for (int i = 0; i < itemCount; ++i) {
final FollowModel followModel = items.get(i);
if (isFilterEmpty) followModel.setShown(true);
else followModel.setShown(hasKey(query, followModel.getUsername(), followModel.getFullName()));
}
}
}
return null;
final List<User> filteredItems = new ArrayList<User>();
if (expandableListOriginal.groups == null || TextUtils.isEmpty(filter)) return null;
final String query = filter.toString().toLowerCase();
final ArrayList<ExpandableGroup> groups = new ArrayList<ExpandableGroup>();
for (int x = 0; x < expandableListOriginal.groups.size(); ++x) {
final ExpandableGroup expandableGroup = expandableListOriginal.groups.get(x);
final String title = expandableGroup.getTitle();
final List<User> items = expandableGroup.getItems();
if (items != null) {
final List<User> toReturn = items.stream()
.filter(u -> hasKey(query, u.getUsername(), u.getFullName()))
.collect(Collectors.toList());
groups.add(new ExpandableGroup(title, toReturn));
}
}
final FilterResults filterResults = new FilterResults();
filterResults.values = new ExpandableList(groups, expandableList.expandedGroupIndexes);
return filterResults;
} }
private boolean hasKey(final String key, final String username, final String name) { private boolean hasKey(final String key, final String username, final String name) {
@ -60,15 +66,20 @@ public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
@Override @Override
protected void publishResults(final CharSequence constraint, final FilterResults results) { protected void publishResults(final CharSequence constraint, final FilterResults results) {
if (results == null) {
expandableList = expandableListOriginal;
}
else {
final ExpandableList filteredList = (ExpandableList) results.values;
expandableList = filteredList;
}
notifyDataSetChanged(); notifyDataSetChanged();
} }
}; };
private final View.OnClickListener onClickListener;
private final ExpandableList expandableList;
private final boolean hasManyGroups;
public FollowAdapter(final View.OnClickListener onClickListener, @NonNull final ArrayList<ExpandableGroup> groups) { public FollowAdapter(final View.OnClickListener onClickListener, @NonNull final ArrayList<ExpandableGroup> groups) {
this.expandableList = new ExpandableList(groups);
this.expandableListOriginal = new ExpandableList(groups);
expandableList = this.expandableListOriginal;
this.onClickListener = onClickListener; this.onClickListener = onClickListener;
this.hasManyGroups = groups.size() > 1; this.hasManyGroups = groups.size() > 1;
} }
@ -104,7 +115,7 @@ public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
gvh.toggle(isGroupExpanded(group)); gvh.toggle(isGroupExpanded(group));
return; return;
} }
final FollowModel model = group.getItems(true).get(hasManyGroups ? listPos.childPos : position);
final User model = group.getItems().get(hasManyGroups ? listPos.childPos : position);
((FollowsViewHolder) holder).bind(model, onClickListener); ((FollowsViewHolder) holder).bind(model, onClickListener);
} }
@ -124,7 +135,7 @@ public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
final int groupPos = listPosition.groupPos; final int groupPos = listPosition.groupPos;
final int positionStart = expandableList.getFlattenedGroupIndex(listPosition) + 1; final int positionStart = expandableList.getFlattenedGroupIndex(listPosition) + 1;
final int positionEnd = expandableList.groups.get(groupPos).getItemCount(true);
final int positionEnd = expandableList.groups.get(groupPos).getItemCount();
final boolean isExpanded = expandableList.expandedGroupIndexes[groupPos]; final boolean isExpanded = expandableList.expandedGroupIndexes[groupPos];
expandableList.expandedGroupIndexes[groupPos] = !isExpanded; expandableList.expandedGroupIndexes[groupPos] = !isExpanded;

11
app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java

@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.databinding.ItemFollowBinding; import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
public final class FollowsViewHolder extends RecyclerView.ViewHolder { public final class FollowsViewHolder extends RecyclerView.ViewHolder {
@ -27,14 +26,4 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder {
binding.fullName.setText(model.getFullName()); binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl()); binding.profilePic.setImageURI(model.getProfilePicUrl());
} }
public void bind(final FollowModel model,
final View.OnClickListener onClickListener) {
if (model == null) return;
itemView.setTag(model);
itemView.setOnClickListener(onClickListener);
binding.username.setUsername("@" + model.getUsername());
binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl());
}
} }

631
app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.kt

@ -1,453 +1,260 @@
package awais.instagrabber.fragments;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
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.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.util.ArrayList;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FollowAdapter;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader;
import awais.instagrabber.databinding.FragmentFollowersViewerBinding;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.FriendshipRepository;
import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
import thoughtbot.expandableadapter.ExpandableGroup;
public final class FollowViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "FollowViewerFragment";
private final ArrayList<FollowModel> followModels = new ArrayList<>();
private final ArrayList<FollowModel> followingModels = new ArrayList<>();
private final ArrayList<FollowModel> followersModels = new ArrayList<>();
private final ArrayList<FollowModel> allFollowing = new ArrayList<>();
private boolean moreAvailable = true, isFollowersList, isCompare = false, loading = false, shouldRefresh = true, searching = false;
private long profileId;
private String username;
private String namePost;
private String type;
private String endCursor;
private Resources resources;
private LinearLayoutManager layoutManager;
private RecyclerLazyLoader lazyLoader;
private FollowModel model;
private FollowAdapter adapter;
private View.OnClickListener clickListener;
private FragmentFollowersViewerBinding binding;
private SwipeRefreshLayout root;
private FriendshipRepository friendshipRepository;
private AppCompatActivity fragmentActivity;
final ServiceCallback<FriendshipListFetchResponse> followingFetchCb = new ServiceCallback<FriendshipListFetchResponse>() {
@Override
public void onSuccess(final FriendshipListFetchResponse result) {
if (result != null && isCompare) {
followingModels.addAll(result.getItems());
if (!isFollowersList) followModels.addAll(result.getItems());
if (result.isMoreAvailable()) {
endCursor = result.getNextMaxId();
friendshipRepository.getList(
false,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
onFailure(throwable);
return;
}
onSuccess(response);
}), Dispatchers.getIO())
);
} else if (followersModels.size() == 0) {
if (!isFollowersList) moreAvailable = false;
friendshipRepository.getList(
true,
profileId,
null,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
followingFetchCb.onFailure(throwable);
return;
}
followingFetchCb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
if (!isFollowersList) moreAvailable = false;
showCompare();
}
} else if (isCompare) binding.swipeRefreshLayout.setRefreshing(false);
}
@Override
public void onFailure(final Throwable t) {
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (double, following)", t);
}
};
final ServiceCallback<FriendshipListFetchResponse> followersFetchCb = new ServiceCallback<FriendshipListFetchResponse>() {
@Override
public void onSuccess(final FriendshipListFetchResponse result) {
if (result != null && isCompare) {
followersModels.addAll(result.getItems());
if (isFollowersList) followModels.addAll(result.getItems());
if (result.isMoreAvailable()) {
endCursor = result.getNextMaxId();
friendshipRepository.getList(
true,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
onFailure(throwable);
return;
}
onSuccess(response);
}), Dispatchers.getIO())
);
} else if (followingModels.size() == 0) {
if (isFollowersList) moreAvailable = false;
friendshipRepository.getList(
false,
profileId,
null,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
followingFetchCb.onFailure(throwable);
return;
}
followingFetchCb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
if (isFollowersList) moreAvailable = false;
showCompare();
}
} else if (isCompare) binding.swipeRefreshLayout.setRefreshing(false);
}
@Override
public void onFailure(final Throwable t) {
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (double, follower)", t);
}
};
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
friendshipRepository = FriendshipRepository.Companion.getInstance();
fragmentActivity = (AppCompatActivity) getActivity();
setHasOptionsMenu(true);
}
@NonNull
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
package awais.instagrabber.fragments
import android.content.Context
import android.content.res.Resources
import android.os.Bundle
import android.util.Log
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.annotation.NonNull
import androidx.annotation.Nullable
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.NavHostFragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.util.ArrayList
import awais.instagrabber.R
import awais.instagrabber.adapters.FollowAdapter
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader
import awais.instagrabber.databinding.FragmentFollowersViewerBinding
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.utils.AppExecutors
import awais.instagrabber.viewmodels.FollowViewModel
import thoughtbot.expandableadapter.ExpandableGroup
class FollowViewerFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {
private val followModels: ArrayList<User> = ArrayList<User>()
private val followingModels: ArrayList<User> = ArrayList<User>()
private val followersModels: ArrayList<User> = ArrayList<User>()
private val allFollowing: ArrayList<User> = ArrayList<User>()
private val moreAvailable = true
private var isFollowersList = false
private var isCompare = false
private var shouldRefresh = true
private var searching = false
private var profileId: Long = 0
private var username: String? = null
private var namePost: String? = null
private var type = 0
private var root: SwipeRefreshLayout? = null
private var adapter: FollowAdapter? = null
private lateinit var lazyLoader: RecyclerLazyLoader
private lateinit var fragmentActivity: AppCompatActivity
private lateinit var viewModel: FollowViewModel
private lateinit var binding: FragmentFollowersViewerBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fragmentActivity = activity as AppCompatActivity
viewModel = ViewModelProvider(this).get(FollowViewModel::class.java)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
if (root != null) { if (root != null) {
shouldRefresh = false;
return root;
shouldRefresh = false
return root!!
} }
binding = FragmentFollowersViewerBinding.inflate(getLayoutInflater());
root = binding.getRoot();
return root;
binding = FragmentFollowersViewerBinding.inflate(layoutInflater)
root = binding.root
return root!!
} }
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
if (!shouldRefresh) return;
init();
shouldRefresh = false;
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!shouldRefresh) return
init()
shouldRefresh = false
} }
private void init() {
if (getArguments() == null) return;
final FollowViewerFragmentArgs fragmentArgs = FollowViewerFragmentArgs.fromBundle(getArguments());
profileId = fragmentArgs.getProfileId();
isFollowersList = fragmentArgs.getIsFollowersList();
username = fragmentArgs.getUsername();
namePost = username;
if (TextUtils.isEmpty(username)) {
// this usually should not occur
username = "You";
namePost = "You're";
}
setTitle(username);
resources = getResources();
clickListener = v -> {
final Object tag = v.getTag();
if (tag instanceof FollowModel) {
model = (FollowModel) tag;
final FollowViewerFragmentDirections.ActionFollowViewerFragmentToProfileFragment action = FollowViewerFragmentDirections
.actionFollowViewerFragmentToProfileFragment();
action.setUsername("@" + model.getUsername());
NavHostFragment.findNavController(this).navigate(action);
}
};
binding.swipeRefreshLayout.setOnRefreshListener(this);
onRefresh();
private fun init() {
val args = arguments ?: return
val fragmentArgs = FollowViewerFragmentArgs.fromBundle(args)
viewModel.userId.value = fragmentArgs.profileId
isFollowersList = fragmentArgs.isFollowersList
username = fragmentArgs.username
namePost = username
setTitle(username)
binding.swipeRefreshLayout.setOnRefreshListener(this)
if (isCompare) listCompare() else listFollows()
viewModel.fetch(isFollowersList, null)
} }
@Override
public void onResume() {
super.onResume();
setTitle(username);
setSubtitle(type);
override fun onResume() {
super.onResume()
setTitle(username)
setSubtitle(type)
} }
private void setTitle(final String title) {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar == null) return;
actionBar.setTitle(title);
private fun setTitle(title: String?) {
val actionBar: ActionBar = fragmentActivity.supportActionBar ?: return
actionBar.title = title
} }
private void setSubtitle(final String subtitle) {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar == null) return;
actionBar.setSubtitle(subtitle);
private fun setSubtitle(subtitleRes: Int) {
val actionBar: ActionBar = fragmentActivity.supportActionBar ?: return
actionBar.setSubtitle(subtitleRes)
} }
private void setSubtitle(@SuppressWarnings("SameParameterValue") final int subtitleRes) {
final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar == null) return;
actionBar.setSubtitle(subtitleRes);
override fun onRefresh() {
lazyLoader.resetState()
viewModel.clearProgress()
if (isCompare) listCompare()
else viewModel.fetch(isFollowersList, null)
} }
@Override
public void onRefresh() {
if (isCompare) listCompare();
else listFollows();
endCursor = null;
lazyLoader.resetState();
private fun listFollows() {
viewModel.comparison.removeObservers(viewLifecycleOwner)
viewModel.status.removeObservers(viewLifecycleOwner)
type = if (isFollowersList) R.string.followers_type_followers else R.string.followers_type_following
setSubtitle(type)
val layoutManager = LinearLayoutManager(context)
lazyLoader = RecyclerLazyLoader(layoutManager, { _, totalItemsCount ->
binding.swipeRefreshLayout.isRefreshing = true
val liveData = if (searching) viewModel.search(isFollowersList)
else viewModel.fetch(isFollowersList, null)
liveData.observe(viewLifecycleOwner) {
binding.swipeRefreshLayout.isRefreshing = it.status != Resource.Status.SUCCESS
layoutManager.scrollToPosition(totalItemsCount)
} }
private void listFollows() {
type = resources.getString(isFollowersList ? R.string.followers_type_followers : R.string.followers_type_following);
setSubtitle(type);
final ServiceCallback<FriendshipListFetchResponse> cb = new ServiceCallback<FriendshipListFetchResponse>() {
@Override
public void onSuccess(final FriendshipListFetchResponse result) {
if (result == null) {
binding.swipeRefreshLayout.setRefreshing(false);
return;
})
binding.rvFollow.addOnScrollListener(lazyLoader)
binding.rvFollow.layoutManager = layoutManager
viewModel.getList(isFollowersList).observe(viewLifecycleOwner) {
binding.swipeRefreshLayout.isRefreshing = false
refreshAdapter(it, null, null, null)
} }
int oldSize = followModels.size() == 0 ? 0 : followModels.size() - 1;
followModels.addAll(result.getItems());
if (result.isMoreAvailable()) {
moreAvailable = true;
endCursor = result.getNextMaxId();
} else moreAvailable = false;
binding.swipeRefreshLayout.setRefreshing(false);
if (isFollowersList) followersModels.addAll(result.getItems());
else followingModels.addAll(result.getItems());
refreshAdapter(followModels, null, null, null);
layoutManager.scrollToPosition(oldSize);
} }
@Override
public void onFailure(final Throwable t) {
try {
binding.swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show();
} catch (Throwable ignored) {}
Log.e(TAG, "Error fetching list (single)", t);
private fun listCompare() {
viewModel.getList(isFollowersList).removeObservers(viewLifecycleOwner)
binding.rvFollow.clearOnScrollListeners()
binding.swipeRefreshLayout.isRefreshing = true
setSubtitle(R.string.followers_compare)
viewModel.status.observe(viewLifecycleOwner) {}
viewModel.comparison.observe(viewLifecycleOwner) {
if (it != null) {
binding.swipeRefreshLayout.isRefreshing = false
refreshAdapter(null, it.first, it.second, it.third)
} }
};
layoutManager = new LinearLayoutManager(getContext());
lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
if (!TextUtils.isEmpty(endCursor) && !searching) {
binding.swipeRefreshLayout.setRefreshing(true);
layoutManager.setStackFromEnd(true);
friendshipRepository.getList(
isFollowersList,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(response);
}), Dispatchers.getIO())
);
endCursor = null;
}
});
binding.rvFollow.addOnScrollListener(lazyLoader);
binding.rvFollow.setLayoutManager(layoutManager);
if (moreAvailable) {
binding.swipeRefreshLayout.setRefreshing(true);
friendshipRepository.getList(
isFollowersList,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(response);
}), Dispatchers.getIO())
);
} else {
refreshAdapter(followModels, null, null, null);
layoutManager.scrollToPosition(0);
} }
} }
private void listCompare() {
layoutManager.setStackFromEnd(false);
binding.rvFollow.clearOnScrollListeners();
loading = true;
setSubtitle(R.string.followers_compare);
allFollowing.clear();
if (moreAvailable) {
binding.swipeRefreshLayout.setRefreshing(true);
Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show();
friendshipRepository.getList(
isFollowersList,
profileId,
endCursor,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
final ServiceCallback<FriendshipListFetchResponse> callback = isFollowersList ? followersFetchCb : followingFetchCb;
if (throwable != null) {
callback.onFailure(throwable);
return;
}
callback.onSuccess(response);
}), Dispatchers.getIO())
);
} else if (followersModels.size() == 0 || followingModels.size() == 0) {
binding.swipeRefreshLayout.setRefreshing(true);
Toast.makeText(getContext(), R.string.follower_start_compare, Toast.LENGTH_LONG).show();
friendshipRepository.getList(
!isFollowersList,
profileId,
null,
CoroutineUtilsKt.getContinuation((response, throwable) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
final ServiceCallback<FriendshipListFetchResponse> callback = isFollowersList ? followingFetchCb : followersFetchCb;
if (throwable != null) {
callback.onFailure(throwable);
return;
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.follow, menu)
val menuSearch = menu.findItem(R.id.action_search)
val searchView = menuSearch.actionView as SearchView
searchView.queryHint = resources.getString(R.string.action_search)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return false
} }
callback.onSuccess(response);
}), Dispatchers.getIO()));
} else showCompare();
}
private void showCompare() {
allFollowing.addAll(followersModels);
allFollowing.retainAll(followingModels);
for (final FollowModel followModel : allFollowing) {
followersModels.remove(followModel);
followingModels.remove(followModel);
}
allFollowing.trimToSize();
followersModels.trimToSize();
followingModels.trimToSize();
binding.swipeRefreshLayout.setRefreshing(false);
refreshAdapter(null, followingModels, followersModels, allFollowing);
override fun onQueryTextChange(query: String): Boolean {
if (query.isNullOrEmpty()) {
if (!isCompare && searching) {
viewModel.setQuery(null, isFollowersList)
viewModel.getSearch().removeObservers(viewLifecycleOwner)
viewModel.getList(isFollowersList).observe(viewLifecycleOwner) {
refreshAdapter(it, null, null, null)
} }
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.follow, menu);
final MenuItem menuSearch = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) menuSearch.getActionView();
searchView.setQueryHint(getResources().getString(R.string.action_search));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(final String query) {
return false;
} }
@Override
public boolean onQueryTextChange(final String query) {
if (TextUtils.isEmpty(query)) {
searching = false;
// refreshAdapter(followModels, followingModels, followersModels, allFollowing);
searching = false
return true
}
searching = true
if (isCompare && adapter != null) {
adapter!!.filter.filter(query)
return true
} }
// else filter.filter(query.toLowerCase());
if (adapter != null) {
searching = true;
adapter.getFilter().filter(query);
viewModel.getList(isFollowersList).removeObservers(viewLifecycleOwner)
binding.swipeRefreshLayout.isRefreshing = true
viewModel.setQuery(query, isFollowersList)
viewModel.getSearch().observe(viewLifecycleOwner) {
binding.swipeRefreshLayout.isRefreshing = false
refreshAdapter(it, null, null, null)
} }
return true;
return true
} }
});
})
} }
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() != R.id.action_compare) return super.onOptionsItemSelected(item);
binding.rvFollow.setAdapter(null);
final Context context = getContext();
if (loading) Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_LONG).show();
else if (isCompare) {
isCompare = false;
listFollows();
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId != R.id.action_compare) return super.onOptionsItemSelected(item)
binding.rvFollow.adapter = null
if (isCompare) {
isCompare = false
listFollows()
} else { } else {
isCompare = true;
listCompare();
isCompare = true
listCompare()
} }
return true;
return true
} }
private void refreshAdapter(final ArrayList<FollowModel> followModels,
final ArrayList<FollowModel> followingModels,
final ArrayList<FollowModel> followersModels,
final ArrayList<FollowModel> allFollowing) {
loading = false;
final ArrayList<ExpandableGroup> groups = new ArrayList<>(1);
private fun refreshAdapter(
followModels: List<User>?,
allFollowing: List<User>?,
followingModels: List<User>?,
followersModels: List<User>?
) {
val groups: ArrayList<ExpandableGroup> = ArrayList<ExpandableGroup>(1)
if (isCompare && followingModels != null && followersModels != null && allFollowing != null) { if (isCompare && followingModels != null && followersModels != null && allFollowing != null) {
if (followingModels.size() > 0)
groups.add(new ExpandableGroup(resources.getString(R.string.followers_not_following, username), followingModels));
if (followersModels.size() > 0)
groups.add(new ExpandableGroup(resources.getString(R.string.followers_not_follower, namePost), followersModels));
if (allFollowing.size() > 0)
groups.add(new ExpandableGroup(resources.getString(R.string.followers_both_following), allFollowing));
if (followingModels.size > 0) groups.add(
ExpandableGroup(
getString(
R.string.followers_not_following,
username
), followingModels
)
)
if (followersModels.size > 0) groups.add(
ExpandableGroup(
getString(
R.string.followers_not_follower,
namePost
), followersModels
)
)
if (allFollowing.size > 0) groups.add(
ExpandableGroup(
getString(R.string.followers_both_following),
allFollowing
)
)
} else if (followModels != null) { } else if (followModels != null) {
groups.add(new ExpandableGroup(type, followModels));
} else return;
adapter = new FollowAdapter(clickListener, groups);
adapter.toggleGroup(0);
binding.rvFollow.setAdapter(adapter);
groups.add(ExpandableGroup(getString(type), followModels))
} else return
adapter = FollowAdapter({ v ->
val tag = v.tag
if (tag is User) {
val model = tag
val bundle = Bundle()
bundle.putString("username", model.username)
NavHostFragment.findNavController(this).navigate(R.id.action_global_profileFragment, bundle)
}
}, groups)
adapter!!.toggleGroup(0)
binding.rvFollow.adapter = adapter!!
}
companion object {
private const val TAG = "FollowViewerFragment"
} }
} }

49
app/src/main/java/awais/instagrabber/models/FollowModel.kt

@ -1,49 +0,0 @@
package awais.instagrabber.models
import java.io.Serializable
class FollowModel(
val id: String,
val username: String,
val fullName: String,
val profilePicUrl: String
) : Serializable {
private var hasNextPage = false
get() = endCursor != null && field
var isShown = true
var endCursor: String? = null
private set
fun setPageCursor(hasNextPage: Boolean, endCursor: String?) {
this.endCursor = endCursor
this.hasNextPage = hasNextPage
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FollowModel
if (id != other.id) return false
if (username != other.username) return false
if (fullName != other.fullName) return false
if (profilePicUrl != other.profilePicUrl) return false
if (isShown != other.isShown) return false
if (endCursor != other.endCursor) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + username.hashCode()
result = 31 * result + fullName.hashCode()
result = 31 * result + profilePicUrl.hashCode()
result = 31 * result + isShown.hashCode()
result = 31 * result + (endCursor?.hashCode() ?: 0)
return result
}
}

3
app/src/main/java/awais/instagrabber/repositories/FriendshipService.kt

@ -1,6 +1,7 @@
package awais.instagrabber.repositories package awais.instagrabber.repositories
import awais.instagrabber.repositories.responses.FriendshipChangeResponse import awais.instagrabber.repositories.responses.FriendshipChangeResponse
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse import awais.instagrabber.repositories.responses.FriendshipRestrictResponse
import retrofit2.http.* import retrofit2.http.*
@ -25,7 +26,7 @@ interface FriendshipService {
@Path("userId") userId: Long, @Path("userId") userId: Long,
@Path("type") type: String, // following or followers @Path("type") type: String, // following or followers
@QueryMap(encoded = true) queryParams: Map<String, String>, @QueryMap(encoded = true) queryParams: Map<String, String>,
): String
): FriendshipListFetchResponse
@FormUrlEncoded @FormUrlEncoded
@POST("/api/v1/friendships/{action}/") @POST("/api/v1/friendships/{action}/")

19
app/src/main/java/awais/instagrabber/repositories/responses/FriendshipListFetchResponse.kt

@ -1,27 +1,10 @@
package awais.instagrabber.repositories.responses package awais.instagrabber.repositories.responses
import awais.instagrabber.models.FollowModel
data class FriendshipListFetchResponse( data class FriendshipListFetchResponse(
var nextMaxId: String?, var nextMaxId: String?,
var status: String?, var status: String?,
var items: List<FollowModel>?
var users: List<User>?
) { ) {
val isMoreAvailable: Boolean val isMoreAvailable: Boolean
get() = !nextMaxId.isNullOrBlank() get() = !nextMaxId.isNullOrBlank()
fun setNextMaxId(nextMaxId: String): FriendshipListFetchResponse {
this.nextMaxId = nextMaxId
return this
}
fun setStatus(status: String): FriendshipListFetchResponse {
this.status = status
return this
}
fun setItems(items: List<FollowModel>): FriendshipListFetchResponse {
this.items = items
return this
}
} }

170
app/src/main/java/awais/instagrabber/viewmodels/FollowViewModel.kt

@ -1,19 +1,167 @@
package awais.instagrabber.viewmodels;
package awais.instagrabber.viewmodels
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.webservices.FriendshipRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.List;
class FollowViewModel : ViewModel() {
// data
val userId = MutableLiveData<Long>()
private val followers = MutableLiveData<List<User>>()
private val followings = MutableLiveData<List<User>>()
private val searchResults = MutableLiveData<List<User>>()
import awais.instagrabber.models.FollowModel;
// cursors
private val followersMaxId = MutableLiveData<String?>("")
private val followingMaxId = MutableLiveData<String?>("")
private val searchingMaxId = MutableLiveData<String?>("")
private val searchQuery = MutableLiveData<String?>()
public class FollowViewModel extends ViewModel {
private MutableLiveData<List<FollowModel>> list;
// comparison
val status: LiveData<Pair<Boolean, Boolean>> = object : MediatorLiveData<Pair<Boolean, Boolean>>() {
init {
postValue(Pair(false, false))
addSource(followersMaxId) {
if (it == null) {
postValue(Pair(true, value!!.second))
}
else fetch(true, it)
}
addSource(followingMaxId) {
if (it == null) {
postValue(Pair(value!!.first, true))
}
else fetch(false, it)
}
}
}
val comparison: LiveData<Triple<List<User>, List<User>, List<User>>> =
object : MediatorLiveData<Triple<List<User>, List<User>, List<User>>>() {
init {
addSource(status) {
if (it.first && it.second) {
val followersList = followers.value!!
val followingList = followings.value!!
val allUsers: MutableList<User> = mutableListOf()
allUsers.addAll(followersList)
allUsers.addAll(followingList)
val followersMap = followersList.groupBy { it.pk }
val followingMap = followingList.groupBy { it.pk }
val mutual: MutableList<User> = mutableListOf()
val onlyFollowing: MutableList<User> = mutableListOf()
val onlyFollowers: MutableList<User> = mutableListOf()
allUsers.forEach {
val isFollowing = followingMap.get(it.pk) != null
val isFollower = followersMap.get(it.pk) != null
if (isFollowing && isFollower) mutual.add(it)
else if (isFollowing) onlyFollowing.add(it)
else if (isFollower) onlyFollowers.add(it)
}
postValue(Triple(mutual, onlyFollowing, onlyFollowers))
}
}
}
}
private val friendshipRepository: FriendshipRepository by lazy { FriendshipRepository.getInstance() }
public MutableLiveData<List<FollowModel>> getList() {
if (list == null) {
list = new MutableLiveData<>();
// fetch: supply max ID for continuous fetch
fun fetch(follower: Boolean, nextMaxId: String?): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(Resource.loading(null))
val maxId = if (follower) followersMaxId else followingMaxId
if (maxId.value == null && nextMaxId == null) data.postValue(Resource.success(null))
else if (userId.value == null) data.postValue(Resource.error("No user ID supplied!", null))
else viewModelScope.launch(Dispatchers.IO) {
try {
val tempList = friendshipRepository.getList(
follower,
userId.value!!,
nextMaxId ?: maxId.value,
null
)
if (!tempList.status.equals("ok")) {
data.postValue(Resource.error("Status not ok!", null))
}
else {
if (tempList.users != null) {
val liveData = if (follower) followers else followings
val currentList = if (liveData.value != null) liveData.value!!.toMutableList()
else mutableListOf()
currentList.addAll(tempList.users!!)
liveData.postValue(currentList.toList())
}
maxId.postValue(tempList.nextMaxId)
data.postValue(Resource.success(null))
}
} catch (e: Exception) {
data.postValue(Resource.error(e.message, null))
}
} }
return list;
return data
}
fun getList(follower: Boolean): LiveData<List<User>> {
return if (follower) followers else followings
}
fun search(follower: Boolean): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(Resource.loading(null))
val query = searchQuery.value
if (searchingMaxId.value == null) data.postValue(Resource.success(null))
else if (userId.value == null) data.postValue(Resource.error("No user ID supplied!", null))
else if (query.isNullOrEmpty()) data.postValue(Resource.error("No query supplied!", null))
else viewModelScope.launch(Dispatchers.IO) {
try {
val tempList = friendshipRepository.getList(
follower,
userId.value!!,
searchingMaxId.value,
query
)
if (!tempList.status.equals("ok")) {
data.postValue(Resource.error("Status not ok!", null))
}
else {
if (tempList.users != null) {
val currentList = if (searchResults.value != null) searchResults.value!!.toMutableList()
else mutableListOf()
currentList.addAll(tempList.users!!)
searchResults.postValue(currentList.toList())
}
searchingMaxId.postValue(tempList.nextMaxId)
data.postValue(Resource.success(null))
}
} catch (e: Exception) {
data.postValue(Resource.error(e.message, null))
}
}
return data
}
fun getSearch(): LiveData<List<User>> {
return searchResults
}
fun setQuery(query: String?, follower: Boolean) {
searchQuery.value = query
if (!query.isNullOrEmpty()) search(follower)
}
fun clearProgress() {
followersMaxId.value = ""
followingMaxId.value = ""
searchingMaxId.value = ""
followings.value = listOf<User>()
followers.value = listOf<User>()
searchResults.value = listOf<User>()
} }
} }

45
app/src/main/java/awais/instagrabber/webservices/FriendshipRepository.kt

@ -1,15 +1,11 @@
package awais.instagrabber.webservices package awais.instagrabber.webservices
import awais.instagrabber.models.FollowModel
import awais.instagrabber.repositories.FriendshipService import awais.instagrabber.repositories.FriendshipService
import awais.instagrabber.repositories.responses.FriendshipChangeResponse import awais.instagrabber.repositories.responses.FriendshipChangeResponse
import awais.instagrabber.repositories.responses.FriendshipListFetchResponse import awais.instagrabber.repositories.responses.FriendshipListFetchResponse
import awais.instagrabber.repositories.responses.FriendshipRestrictResponse import awais.instagrabber.repositories.responses.FriendshipRestrictResponse
import awais.instagrabber.utils.Utils import awais.instagrabber.utils.Utils
import awais.instagrabber.webservices.RetrofitFactory.retrofit import awais.instagrabber.webservices.RetrofitFactory.retrofit
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
class FriendshipRepository(private val service: FriendshipService) { class FriendshipRepository(private val service: FriendshipService) {
@ -113,43 +109,12 @@ class FriendshipRepository(private val service: FriendshipService) {
follower: Boolean, follower: Boolean,
targetUserId: Long, targetUserId: Long,
maxId: String?, maxId: String?,
query: String?
): FriendshipListFetchResponse { ): FriendshipListFetchResponse {
val queryMap = if (maxId != null) mapOf("max_id" to maxId) else emptyMap()
val response = service.getList(targetUserId, if (follower) "followers" else "following", queryMap)
return parseListResponse(response)
}
@Throws(JSONException::class)
private fun parseListResponse(body: String): FriendshipListFetchResponse {
val root = JSONObject(body)
val nextMaxId = root.optString("next_max_id")
val status = root.optString("status")
val itemsJson = root.optJSONArray("users")
val items = parseItems(itemsJson)
return FriendshipListFetchResponse(
nextMaxId,
status,
items
)
}
@Throws(JSONException::class)
private fun parseItems(items: JSONArray?): List<FollowModel> {
if (items == null) {
return emptyList()
}
val followModels = mutableListOf<FollowModel>()
for (i in 0 until items.length()) {
val itemJson = items.optJSONObject(i) ?: continue
val followModel = FollowModel(
itemJson.getString("pk"),
itemJson.getString("username"),
itemJson.optString("full_name"),
itemJson.getString("profile_pic_url")
)
followModels.add(followModel)
}
return followModels
val queryMap: MutableMap<String, String> = mutableMapOf()
if (!maxId.isNullOrEmpty()) queryMap.set("max_id", maxId)
if (!query.isNullOrEmpty()) queryMap.set("query", query)
return service.getList(targetUserId, if (follower) "followers" else "following", queryMap.toMap())
} }
companion object { companion object {

23
app/src/main/java/thoughtbot/expandableadapter/ExpandableGroup.java

@ -3,13 +3,13 @@ package thoughtbot.expandableadapter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User;
public class ExpandableGroup { public class ExpandableGroup {
private final String title; private final String title;
private final List<FollowModel> items;
private final List<User> items;
public ExpandableGroup(final String title, final List<FollowModel> items) {
public ExpandableGroup(final String title, final List<User> items) {
this.title = title; this.title = title;
this.items = items; this.items = items;
} }
@ -18,22 +18,13 @@ public class ExpandableGroup {
return title; return title;
} }
public List<FollowModel> getItems(final boolean filtered) {
if (!filtered) return items;
final ArrayList<FollowModel> followModels = new ArrayList<>();
for (final FollowModel followModel : items) if (followModel.isShown()) followModels.add(followModel);
return followModels;
public List<User> getItems() {
return items;
} }
public int getItemCount(final boolean filtered) {
public int getItemCount() {
if (items != null) { if (items != null) {
final int size = items.size();
if (filtered) {
int finalSize = 0;
for (int i = 0; i < size; ++i) if (items.get(i).isShown()) ++finalSize;
return finalSize;
}
return size;
return items.size();
} }
return 0; return 0;
} }

10
app/src/main/java/thoughtbot/expandableadapter/ExpandableList.java

@ -1,6 +1,7 @@
package thoughtbot.expandableadapter; package thoughtbot.expandableadapter;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
@ -15,6 +16,13 @@ public final class ExpandableList {
this.expandedGroupIndexes = new boolean[groupsSize]; this.expandedGroupIndexes = new boolean[groupsSize];
} }
public ExpandableList(@NonNull final ArrayList<ExpandableGroup> groups,
@Nullable final boolean[] expandedGroupIndexes) {
this.groups = groups;
this.groupsSize = groups.size();
this.expandedGroupIndexes = expandedGroupIndexes;
}
public int getVisibleItemCount() { public int getVisibleItemCount() {
int count = 0; int count = 0;
for (int i = 0; i < groupsSize; i++) count = count + numberOfVisibleItemsInGroup(i); for (int i = 0; i < groupsSize; i++) count = count + numberOfVisibleItemsInGroup(i);
@ -36,7 +44,7 @@ public final class ExpandableList {
} }
private int numberOfVisibleItemsInGroup(final int group) { private int numberOfVisibleItemsInGroup(final int group) {
return expandedGroupIndexes[group] ? groups.get(group).getItemCount(true) + 1 : 1;
return expandedGroupIndexes[group] ? groups.get(group).getItemCount() + 1 : 1;
} }
public int getFlattenedGroupIndex(@NonNull final ExpandableListPosition listPosition) { public int getFlattenedGroupIndex(@NonNull final ExpandableListPosition listPosition) {

1
app/src/main/res/layout/fragment_followers_viewer.xml

@ -16,6 +16,5 @@
android:paddingLeft="8dp" android:paddingLeft="8dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingRight="8dp" android:paddingRight="8dp"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_follow" /> tools:listitem="@layout/item_follow" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
Loading…
Cancel
Save