diff --git a/app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java b/app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java index b11e0411..d8368aa5 100755 --- a/app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java +++ b/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.List; +import java.util.stream.Collectors; import awais.instagrabber.R; import awais.instagrabber.adapters.viewholder.FollowsViewHolder; import awais.instagrabber.databinding.ItemFollowBinding; import awais.instagrabber.interfaces.OnGroupClickListener; -import awais.instagrabber.models.FollowModel; +import awais.instagrabber.repositories.responses.User; import awais.instagrabber.utils.TextUtils; import thoughtbot.expandableadapter.ExpandableGroup; import thoughtbot.expandableadapter.ExpandableList; @@ -27,28 +28,33 @@ import thoughtbot.expandableadapter.GroupViewHolder; // thanks to ThoughtBot's ExpandableRecyclerViewAdapter // https://github.com/thoughtbot/expandable-recycler-view public final class FollowAdapter extends RecyclerView.Adapter 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() { @Nullable @Override 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 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())); - } + final List filteredItems = new ArrayList(); + if (expandableListOriginal.groups == null || TextUtils.isEmpty(filter)) return null; + final String query = filter.toString().toLowerCase(); + final ArrayList groups = new ArrayList(); + for (int x = 0; x < expandableListOriginal.groups.size(); ++x) { + final ExpandableGroup expandableGroup = expandableListOriginal.groups.get(x); + final String title = expandableGroup.getTitle(); + final List items = expandableGroup.getItems(); + if (items != null) { + final List toReturn = items.stream() + .filter(u -> hasKey(query, u.getUsername(), u.getFullName())) + .collect(Collectors.toList()); + groups.add(new ExpandableGroup(title, toReturn)); } } - return null; + 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) { @@ -60,15 +66,20 @@ public final class FollowAdapter extends RecyclerView.Adapter groups) { - this.expandableList = new ExpandableList(groups); + this.expandableListOriginal = new ExpandableList(groups); + expandableList = this.expandableListOriginal; this.onClickListener = onClickListener; this.hasManyGroups = groups.size() > 1; } @@ -104,7 +115,7 @@ public final class FollowAdapter extends RecyclerView.Adapter = ArrayList() + private val followingModels: ArrayList = ArrayList() + private val followersModels: ArrayList = ArrayList() + private val allFollowing: ArrayList = ArrayList() + 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 - private final ArrayList followModels = new ArrayList<>(); - private final ArrayList followingModels = new ArrayList<>(); - private final ArrayList followersModels = new ArrayList<>(); - private final ArrayList 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 followingFetchCb = new ServiceCallback() { - @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 followersFetchCb = new ServiceCallback() { - @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); + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + fragmentActivity = activity as AppCompatActivity + viewModel = ViewModelProvider(this).get(FollowViewModel::class.java) + setHasOptionsMenu(true) } - @NonNull - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { 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"; + 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 fun onResume() { + super.onResume() + setTitle(username) + setSubtitle(type) + } + + private fun setTitle(title: String?) { + val actionBar: ActionBar = fragmentActivity.supportActionBar ?: return + actionBar.title = title + } + + private fun setSubtitle(subtitleRes: Int) { + val actionBar: ActionBar = fragmentActivity.supportActionBar ?: return + actionBar.setSubtitle(subtitleRes) + } + + override fun onRefresh() { + lazyLoader.resetState() + viewModel.clearProgress() + if (isCompare) listCompare() + else viewModel.fetch(isFollowersList, null) + } + + 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) + } + }) + binding.rvFollow.addOnScrollListener(lazyLoader) + binding.rvFollow.layoutManager = layoutManager + viewModel.getList(isFollowersList).observe(viewLifecycleOwner) { + binding.swipeRefreshLayout.isRefreshing = false + refreshAdapter(it, null, null, null) } - 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); + } + + 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) } - }; - binding.swipeRefreshLayout.setOnRefreshListener(this); - onRefresh(); + } } - @Override - public void 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 void setSubtitle(final String subtitle) { - final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - if (actionBar == null) return; - actionBar.setSubtitle(subtitle); - } - - private void setSubtitle(@SuppressWarnings("SameParameterValue") final int subtitleRes) { - final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - if (actionBar == null) return; - actionBar.setSubtitle(subtitleRes); - } - - @Override - public void onRefresh() { - if (isCompare) listCompare(); - else listFollows(); - endCursor = null; - lazyLoader.resetState(); - } - - private void listFollows() { - type = resources.getString(isFollowersList ? R.string.followers_type_followers : R.string.followers_type_following); - setSubtitle(type); - final ServiceCallback cb = new ServiceCallback() { - @Override - public void onSuccess(final FriendshipListFetchResponse result) { - if (result == null) { - binding.swipeRefreshLayout.setRefreshing(false); - return; - } - 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 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 } - @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); - } - }; - 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; + 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) } - cb.onSuccess(response); - }), Dispatchers.getIO()) - ); + } + searching = false + return true + } + searching = true + if (isCompare && adapter != null) { + adapter!!.filter.filter(query) + return true + } + 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 + } + }) + } + + 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 { - refreshAdapter(followModels, null, null, null); - layoutManager.scrollToPosition(0); + isCompare = true + listCompare() } + return true } - 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 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 callback = isFollowersList ? followingFetchCb : followersFetchCb; - if (throwable != null) { - callback.onFailure(throwable); - return; - } - 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 - 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); - } - // else filter.filter(query.toLowerCase()); - if (adapter != null) { - searching = true; - adapter.getFilter().filter(query); - } - 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(); - } else { - isCompare = true; - listCompare(); - } - return true; - } - - private void refreshAdapter(final ArrayList followModels, - final ArrayList followingModels, - final ArrayList followersModels, - final ArrayList allFollowing) { - loading = false; - final ArrayList groups = new ArrayList<>(1); - + private fun refreshAdapter( + followModels: List?, + allFollowing: List?, + followingModels: List?, + followersModels: List? + ) { + val groups: ArrayList = ArrayList(1) 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) { - 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" } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/FollowModel.kt b/app/src/main/java/awais/instagrabber/models/FollowModel.kt deleted file mode 100644 index d71196e1..00000000 --- a/app/src/main/java/awais/instagrabber/models/FollowModel.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/FriendshipService.kt b/app/src/main/java/awais/instagrabber/repositories/FriendshipService.kt index 6e783dfa..7bff0c75 100644 --- a/app/src/main/java/awais/instagrabber/repositories/FriendshipService.kt +++ b/app/src/main/java/awais/instagrabber/repositories/FriendshipService.kt @@ -1,6 +1,7 @@ package awais.instagrabber.repositories import awais.instagrabber.repositories.responses.FriendshipChangeResponse +import awais.instagrabber.repositories.responses.FriendshipListFetchResponse import awais.instagrabber.repositories.responses.FriendshipRestrictResponse import retrofit2.http.* @@ -25,7 +26,7 @@ interface FriendshipService { @Path("userId") userId: Long, @Path("type") type: String, // following or followers @QueryMap(encoded = true) queryParams: Map, - ): String + ): FriendshipListFetchResponse @FormUrlEncoded @POST("/api/v1/friendships/{action}/") diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/FriendshipListFetchResponse.kt b/app/src/main/java/awais/instagrabber/repositories/responses/FriendshipListFetchResponse.kt index a9ff7c9b..25145aa6 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/FriendshipListFetchResponse.kt +++ b/app/src/main/java/awais/instagrabber/repositories/responses/FriendshipListFetchResponse.kt @@ -1,27 +1,10 @@ package awais.instagrabber.repositories.responses -import awais.instagrabber.models.FollowModel - data class FriendshipListFetchResponse( var nextMaxId: String?, var status: String?, - var items: List? + var users: List? ) { val isMoreAvailable: Boolean 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): FriendshipListFetchResponse { - this.items = items - return this - } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/viewmodels/FollowViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/FollowViewModel.kt index dcd65d4a..f2a0a428 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/FollowViewModel.kt +++ b/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() + private val followers = MutableLiveData>() + private val followings = MutableLiveData>() + private val searchResults = MutableLiveData>() -import awais.instagrabber.models.FollowModel; + // cursors + private val followersMaxId = MutableLiveData("") + private val followingMaxId = MutableLiveData("") + private val searchingMaxId = MutableLiveData("") + private val searchQuery = MutableLiveData() -public class FollowViewModel extends ViewModel { - private MutableLiveData> list; - - public MutableLiveData> getList() { - if (list == null) { - list = new MutableLiveData<>(); + // comparison + val status: LiveData> = object : MediatorLiveData>() { + 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) + } + } } - return list; + val comparison: LiveData, List, List>> = + object : MediatorLiveData, List, List>>() { + init { + addSource(status) { + if (it.first && it.second) { + val followersList = followers.value!! + val followingList = followings.value!! + val allUsers: MutableList = mutableListOf() + allUsers.addAll(followersList) + allUsers.addAll(followingList) + val followersMap = followersList.groupBy { it.pk } + val followingMap = followingList.groupBy { it.pk } + val mutual: MutableList = mutableListOf() + val onlyFollowing: MutableList = mutableListOf() + val onlyFollowers: MutableList = 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() } + + // fetch: supply max ID for continuous fetch + fun fetch(follower: Boolean, nextMaxId: String?): LiveData> { + val data = MutableLiveData>() + 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 data } -} + + fun getList(follower: Boolean): LiveData> { + return if (follower) followers else followings + } + + fun search(follower: Boolean): LiveData> { + val data = MutableLiveData>() + 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> { + 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() + followers.value = listOf() + searchResults.value = listOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/webservices/FriendshipRepository.kt b/app/src/main/java/awais/instagrabber/webservices/FriendshipRepository.kt index d286ed09..7736399f 100644 --- a/app/src/main/java/awais/instagrabber/webservices/FriendshipRepository.kt +++ b/app/src/main/java/awais/instagrabber/webservices/FriendshipRepository.kt @@ -1,15 +1,11 @@ package awais.instagrabber.webservices -import awais.instagrabber.models.FollowModel import awais.instagrabber.repositories.FriendshipService import awais.instagrabber.repositories.responses.FriendshipChangeResponse import awais.instagrabber.repositories.responses.FriendshipListFetchResponse import awais.instagrabber.repositories.responses.FriendshipRestrictResponse import awais.instagrabber.utils.Utils import awais.instagrabber.webservices.RetrofitFactory.retrofit -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject class FriendshipRepository(private val service: FriendshipService) { @@ -113,43 +109,12 @@ class FriendshipRepository(private val service: FriendshipService) { follower: Boolean, targetUserId: Long, maxId: String?, + query: String? ): 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 { - if (items == null) { - return emptyList() - } - val followModels = mutableListOf() - 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 = 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 { diff --git a/app/src/main/java/thoughtbot/expandableadapter/ExpandableGroup.java b/app/src/main/java/thoughtbot/expandableadapter/ExpandableGroup.java index 0e499ba8..f1163550 100755 --- a/app/src/main/java/thoughtbot/expandableadapter/ExpandableGroup.java +++ b/app/src/main/java/thoughtbot/expandableadapter/ExpandableGroup.java @@ -3,13 +3,13 @@ package thoughtbot.expandableadapter; import java.util.ArrayList; import java.util.List; -import awais.instagrabber.models.FollowModel; +import awais.instagrabber.repositories.responses.User; public class ExpandableGroup { private final String title; - private final List items; + private final List items; - public ExpandableGroup(final String title, final List items) { + public ExpandableGroup(final String title, final List items) { this.title = title; this.items = items; } @@ -18,22 +18,13 @@ public class ExpandableGroup { return title; } - public List getItems(final boolean filtered) { - if (!filtered) return items; - final ArrayList followModels = new ArrayList<>(); - for (final FollowModel followModel : items) if (followModel.isShown()) followModels.add(followModel); - return followModels; + public List getItems() { + return items; } - public int getItemCount(final boolean filtered) { + public int getItemCount() { 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; } diff --git a/app/src/main/java/thoughtbot/expandableadapter/ExpandableList.java b/app/src/main/java/thoughtbot/expandableadapter/ExpandableList.java index 408e46cf..5006fb67 100755 --- a/app/src/main/java/thoughtbot/expandableadapter/ExpandableList.java +++ b/app/src/main/java/thoughtbot/expandableadapter/ExpandableList.java @@ -1,6 +1,7 @@ package thoughtbot.expandableadapter; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.ArrayList; @@ -15,6 +16,13 @@ public final class ExpandableList { this.expandedGroupIndexes = new boolean[groupsSize]; } + public ExpandableList(@NonNull final ArrayList groups, + @Nullable final boolean[] expandedGroupIndexes) { + this.groups = groups; + this.groupsSize = groups.size(); + this.expandedGroupIndexes = expandedGroupIndexes; + } + public int getVisibleItemCount() { int count = 0; 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) { - 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) { diff --git a/app/src/main/res/layout/fragment_followers_viewer.xml b/app/src/main/res/layout/fragment_followers_viewer.xml index 3d5a21ae..6452c078 100644 --- a/app/src/main/res/layout/fragment_followers_viewer.xml +++ b/app/src/main/res/layout/fragment_followers_viewer.xml @@ -16,6 +16,5 @@ android:paddingLeft="8dp" android:paddingEnd="8dp" android:paddingRight="8dp" - app:layoutManager="LinearLayoutManager" tools:listitem="@layout/item_follow" /> \ No newline at end of file