Browse Source
greatly fix all the follower/ing stuff
greatly fix all the follower/ing stuff
closes #1334 (assuming) closes #1340 (by implementing API search) closes #1342 closes #1472 closes #1473renovate/org.jetbrains.kotlinx-kotlinx-coroutines-test-1.x
Austin Huang
3 years ago
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
11 changed files with 438 additions and 585 deletions
-
57app/src/main/java/awais/instagrabber/adapters/FollowAdapter.java
-
11app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java
-
633app/src/main/java/awais/instagrabber/fragments/FollowViewerFragment.kt
-
49app/src/main/java/awais/instagrabber/models/FollowModel.kt
-
3app/src/main/java/awais/instagrabber/repositories/FriendshipService.kt
-
19app/src/main/java/awais/instagrabber/repositories/responses/FriendshipListFetchResponse.kt
-
172app/src/main/java/awais/instagrabber/viewmodels/FollowViewModel.kt
-
45app/src/main/java/awais/instagrabber/webservices/FriendshipRepository.kt
-
23app/src/main/java/thoughtbot/expandableadapter/ExpandableGroup.java
-
10app/src/main/java/thoughtbot/expandableadapter/ExpandableList.java
-
1app/src/main/res/layout/fragment_followers_viewer.xml
@ -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); |
|
||||
|
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) |
||||
} |
} |
||||
|
|
||||
@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) { |
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 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; |
|
||||
} |
|
||||
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); |
|
||||
|
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) |
||||
} |
} |
||||
|
|
||||
@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; |
|
||||
} |
|
||||
cb.onSuccess(response); |
|
||||
}), Dispatchers.getIO()) |
|
||||
); |
|
||||
} else { |
|
||||
refreshAdapter(followModels, null, null, null); |
|
||||
layoutManager.scrollToPosition(0); |
|
||||
|
}) |
||||
|
binding.rvFollow.addOnScrollListener(lazyLoader) |
||||
|
binding.rvFollow.layoutManager = layoutManager |
||||
|
viewModel.getList(isFollowersList).observe(viewLifecycleOwner) { |
||||
|
binding.swipeRefreshLayout.isRefreshing = false |
||||
|
refreshAdapter(it, null, null, null) |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
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; |
|
||||
} |
|
||||
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); |
|
||||
|
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) |
||||
|
} |
||||
} |
} |
||||
|
|
||||
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 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 boolean onQueryTextChange(final String query) { |
|
||||
if (TextUtils.isEmpty(query)) { |
|
||||
searching = false; |
|
||||
// refreshAdapter(followModels, 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) |
||||
|
} |
||||
|
} |
||||
|
searching = false |
||||
|
return true |
||||
} |
} |
||||
// else filter.filter(query.toLowerCase()); |
|
||||
if (adapter != null) { |
|
||||
searching = true; |
|
||||
adapter.getFilter().filter(query); |
|
||||
|
searching = true |
||||
|
if (isCompare && adapter != null) { |
||||
|
adapter!!.filter.filter(query) |
||||
|
return true |
||||
} |
} |
||||
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 |
|
||||
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" |
||||
} |
} |
||||
} |
} |
@ -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 |
|
||||
} |
|
||||
} |
|
@ -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 |
|
||||
} |
|
||||
} |
} |
@ -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() } |
||||
|
|
||||
|
// 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 data |
||||
|
} |
||||
|
|
||||
|
fun getList(follower: Boolean): LiveData<List<User>> { |
||||
|
return if (follower) followers else followings |
||||
|
} |
||||
|
|
||||
public MutableLiveData<List<FollowModel>> getList() { |
|
||||
if (list == null) { |
|
||||
list = new MutableLiveData<>(); |
|
||||
|
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 list; |
|
||||
|
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>() |
||||
} |
} |
||||
} |
|
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue