Merge branch 'master' into feature/multistack-navigation

This commit is contained in:
Ammar Githam 2021-07-11 02:56:24 +09:00
commit d7621e3d82
63 changed files with 750 additions and 909 deletions

View File

@ -139,7 +139,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
return new HeaderViewHolder(LayoutDmHeaderBinding.inflate(layoutInflater, parent, false));
}
final LayoutDmBaseBinding baseBinding = LayoutDmBaseBinding.inflate(layoutInflater, parent, false);
final DirectItemType directItemType = DirectItemType.Companion.getId(type);
final DirectItemType directItemType = DirectItemType.Companion.getTypeFromId(type);
final DirectItemViewHolder itemViewHolder = getItemViewHolder(layoutInflater, baseBinding, directItemType);
itemViewHolder.setLongClickListener(longClickListener);
return itemViewHolder;

View File

@ -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<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() {
@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<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()));
}
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));
}
}
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<RecyclerView.ViewH
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
if (results == null) {
expandableList = expandableListOriginal;
}
else {
final ExpandableList filteredList = (ExpandableList) results.values;
expandableList = filteredList;
}
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) {
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<RecyclerView.ViewH
gvh.toggle(isGroupExpanded(group));
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);
}
@ -124,7 +135,7 @@ public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
final int groupPos = listPosition.groupPos;
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];
expandableList.expandedGroupIndexes[groupPos] = !isExpanded;

View File

@ -24,12 +24,12 @@ public final class NotificationsAdapter extends ListAdapter<Notification, Notifi
private static final DiffUtil.ItemCallback<Notification> DIFF_CALLBACK = new DiffUtil.ItemCallback<Notification>() {
@Override
public boolean areItemsTheSame(final Notification oldItem, final Notification newItem) {
return oldItem != null && newItem != null && oldItem.getPk().equals(newItem.getPk());
return oldItem.getPk().equals(newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final Notification oldItem, @NonNull final Notification newItem) {
return oldItem.getPk().equals(newItem.getPk());
return oldItem.getPk().equals(newItem.getPk()) && oldItem.getType() == newItem.getType();
}
};

View File

@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User;
public final class FollowsViewHolder extends RecyclerView.ViewHolder {
@ -27,14 +26,4 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder {
binding.fullName.setText(model.getFullName());
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());
}
}

View File

@ -9,7 +9,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import awais.instagrabber.R;
@ -94,7 +93,7 @@ public class ChatMessageLayout extends FrameLayout {
heightSize += viewPartMainHeight;
} else if (firstChildId == R.id.raven_media_container || firstChildId == R.id.profile_container || firstChildId == R.id.voice_media
|| firstChildId == R.id.story_container || firstChildId == R.id.media_share_container || firstChildId == R.id.link_container
|| firstChildId == R.id.ivAnimatedMessage) {
|| firstChildId == R.id.ivAnimatedMessage || firstChildId == R.id.reel_share_container) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight + viewPartInfoHeight;
} else {
@ -104,12 +103,6 @@ public class ChatMessageLayout extends FrameLayout {
if (firstChild instanceof TextView) {
textMessage = (TextView) firstChild;
}
else if (firstChildId == R.id.reel_share_container) {
textMessage = (TextView) ((ConstraintLayout) firstChild).getChildAt(5);
}
else if (firstChildId == R.id.story_container) {
textMessage = (TextView) ((ConstraintLayout) firstChild).getChildAt(2);
}
else textMessage = null;
if (textMessage != null) {
viewPartMainLineCount = textMessage.getLineCount();

View File

@ -56,6 +56,7 @@ import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DownloadUtils;
@ -461,7 +462,9 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching());
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching())
);
}
private void navigateToProfile(final String username) {

View File

@ -1,456 +0,0 @@
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.NavDirections;
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) {
if (root != null) {
shouldRefresh = false;
return root;
}
binding = FragmentFollowersViewerBinding.inflate(getLayoutInflater());
root = binding.getRoot();
return root;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
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;
try {
final NavDirections action = FollowViewerFragmentDirections.actionToProfile().setUsername(model.getUsername());
NavHostFragment.findNavController(this).navigate(action);
} catch (Exception e) {
Log.e(TAG, "init: ", e);
}
}
};
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<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);
}
@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);
}
}
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);
}
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<FollowModel> followModels,
final ArrayList<FollowModel> followingModels,
final ArrayList<FollowModel> followersModels,
final ArrayList<FollowModel> allFollowing) {
loading = false;
final ArrayList<ExpandableGroup> groups = new 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));
} else if (followModels != null) {
groups.add(new ExpandableGroup(type, followModels));
} else return;
adapter = new FollowAdapter(clickListener, groups);
adapter.toggleGroup(0);
binding.rvFollow.setAdapter(adapter);
}
}

View File

@ -0,0 +1,233 @@
package awais.instagrabber.fragments
import android.os.Bundle
import android.view.*
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.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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.viewmodels.FollowViewModel
import thoughtbot.expandableadapter.ExpandableGroup
import java.util.*
class FollowViewerFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {
private var isFollowersList = false
private var isCompare = false
private var shouldRefresh = true
private var searching = false
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) {
shouldRefresh = false
return root!!
}
binding = FragmentFollowersViewerBinding.inflate(layoutInflater)
root = binding.root
return root!!
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!shouldRefresh) return
init()
shouldRefresh = false
}
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)
}
}
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)
}
}
}
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 fun onQueryTextChange(query: String): Boolean {
if (query.isEmpty()) {
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
}
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 {
isCompare = true
listCompare()
}
return true
}
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 (followingModels.isNotEmpty()) groups.add(
ExpandableGroup(
getString(
R.string.followers_not_following,
username
), followingModels
)
)
if (followersModels.isNotEmpty()) groups.add(
ExpandableGroup(
getString(
R.string.followers_not_follower,
namePost
), followersModels
)
)
if (allFollowing.isNotEmpty()) groups.add(
ExpandableGroup(
getString(R.string.followers_both_following),
allFollowing
)
)
} else if (followModels != null) {
groups.add(ExpandableGroup(getString(type), followModels))
} else return
adapter = FollowAdapter({ v ->
val tag = v.tag
if (tag is User) {
findNavController().navigate(FollowViewerFragmentDirections.actionToProfile().setUsername(tag.username))
}
}, groups).also {
it.toggleGroup(0)
binding.rvFollow.adapter = it
}
}
}

View File

@ -390,7 +390,6 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
}
setTitle();
setupPosts();
// fetchStories();
if (isLoggedIn) {
hashtagDetailsBinding.btnFollowTag.setVisibility(View.VISIBLE);
hashtagDetailsBinding.btnFollowTag.setText(hashtagModel.getFollowing() == FollowingType.FOLLOWING
@ -559,7 +558,9 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching());
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching())
);
}
private void navigateToProfile(final String username) {

View File

@ -61,7 +61,6 @@ import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.LocationService;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesRepository;
import kotlinx.coroutines.Dispatchers;
import static awais.instagrabber.utils.Utils.settingsHelper;
@ -78,11 +77,11 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
private long locationId;
private Location locationModel;
private ActionMode actionMode;
private StoriesRepository storiesRepository;
// private StoriesRepository storiesRepository;
private GraphQLRepository graphQLRepository;
private LocationService locationService;
private boolean isLoggedIn;
private boolean storiesFetching;
// private boolean storiesFetching;
private Set<Media> selectedFeedModels;
private PostsLayoutPreferences layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_LOCATION_POSTS_LAYOUT);
private LayoutLocationDetailsBinding locationDetailsBinding;
@ -280,7 +279,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0;
locationService = isLoggedIn ? LocationService.getInstance() : null;
storiesRepository = StoriesRepository.Companion.getInstance();
// storiesRepository = StoriesRepository.Companion.getInstance();
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
setHasOptionsMenu(true);
}
@ -584,7 +583,9 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching() || storiesFetching);
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching())
);
}
private void navigateToProfile(final String username) {

View File

@ -758,15 +758,19 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme
popupMenu.setOnMenuItemClickListener(item -> {
final int itemId = item.getItemId();
if (itemId == R.id.share_dm) {
if (profileModel.isPrivate()) {
Toast.makeText(context, R.string.share_private_post, Toast.LENGTH_SHORT).show();
}
final NavDirections actionGlobalUserSearch = PostViewV2FragmentDirections
.actionToUserSearch()
.setTitle(getString(R.string.share))
.setActionLabel(getString(R.string.send))
.setShowGroups(true)
.setMultiple(true)
.setSearchMode(UserSearchMode.RAVEN);
final NavController navController = NavHostFragment.findNavController(PostViewV2Fragment.this);
try {
final NavDirections actionGlobalUserSearch = PostViewV2FragmentDirections
.actionToUserSearch()
.setTitle(getString(R.string.share))
.setActionLabel(getString(R.string.send))
.setShowGroups(true)
.setMultiple(true)
.setSearchMode(UserSearchMode.RAVEN);
NavHostFragment.findNavController(this).navigate(actionGlobalUserSearch);
navController.navigate(actionGlobalUserSearch);
} catch (Exception e) {
Log.e(TAG, "setupShare: ", e);
}

View File

@ -39,6 +39,7 @@ import awais.instagrabber.models.enums.PostItemType;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DownloadUtils;
@ -327,7 +328,9 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching());
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching())
);
}
private void navigateToProfile(final String username) {

View File

@ -18,9 +18,7 @@ import androidx.appcompat.widget.PopupMenu
import androidx.core.view.GestureDetectorCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
@ -40,9 +38,7 @@ import awais.instagrabber.models.enums.StoryPaginationType
import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.repositories.responses.stories.*
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.DownloadUtils.download
import awais.instagrabber.utils.TextUtils.epochSecondToString
import awais.instagrabber.utils.ResponseBodyUtils
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG
@ -78,7 +74,6 @@ class StoryViewerFragment : Fragment() {
private var gestureDetector: GestureDetectorCompat? = null
private val storiesRepository: StoriesRepository? = null
private val mediaRepository: MediaRepository? = null
private var live: Broadcast? = null
private var menuProfile: MenuItem? = null
private var profileVisible: Boolean = false
private var player: SimpleExoPlayer? = null
@ -218,7 +213,7 @@ class StoryViewerFragment : Fragment() {
}
binding.storiesList.adapter = storiesAdapter
storiesViewModel.getCurrentStory().observe(fragmentActivity, {
if (it?.items != null) {
if (it?.items != null && it.items.size > 1) {
val storyMedias = it.items.toMutableList()
val newItem = storyMedias.get(0)
newItem.isCurrentSlide = true
@ -230,6 +225,7 @@ class StoryViewerFragment : Fragment() {
else View.GONE
}
else {
if (it?.items != null) storiesViewModel.setMedia(0)
binding.listToggle.isEnabled = false
binding.storiesList.visibility = View.GONE
}
@ -266,65 +262,28 @@ class StoryViewerFragment : Fragment() {
@SuppressLint("ClickableViewAccessibility")
private fun setupListeners() {
var liveModels: LiveData<List<Story>?>? = null
if (currentFeedStoryIndex >= 0) {
val type = options!!.type
when (type) {
StoryViewerOptions.Type.HIGHLIGHT -> {
storiesViewModel.fetchHighlights(options!!.id)
liveModels = storiesViewModel.getHighlights()
storiesViewModel.highlights.observe(fragmentActivity) {
setupMultipage(it)
}
}
StoryViewerOptions.Type.FEED_STORY_POSITION -> {
val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
liveModels = feedStoriesViewModel!!.list
setupMultipage(feedStoriesViewModel!!.list.value)
}
StoryViewerOptions.Type.STORY_ARCHIVE -> {
val archivesViewModel = listViewModel as ArchivesViewModel?
liveModels = archivesViewModel!!.list
setupMultipage(archivesViewModel!!.list.value)
}
StoryViewerOptions.Type.USER -> {
resetView()
}
}
}
if (liveModels != null) liveModels.observe(viewLifecycleOwner, { models ->
storiesViewModel.getPagination().observe(fragmentActivity, {
if (models != null) {
when (it) {
StoryPaginationType.FORWARD -> {
if (currentFeedStoryIndex == models.size - 1)
Toast.makeText(
context,
R.string.no_more_stories,
Toast.LENGTH_SHORT
).show()
else paginateStories(false, currentFeedStoryIndex == models.size - 2)
}
StoryPaginationType.BACKWARD -> {
if (currentFeedStoryIndex == 0)
Toast.makeText(
context,
R.string.no_more_stories,
Toast.LENGTH_SHORT
).show()
else paginateStories(true, false)
}
StoryPaginationType.ERROR -> {
Toast.makeText(
context,
R.string.downloader_unknown_error,
Toast.LENGTH_SHORT
).show()
}
}
}
})
if (models != null && !models.isEmpty()) {
binding.btnBackward.isEnabled = currentFeedStoryIndex != 0
binding.btnForward.isEnabled = currentFeedStoryIndex != models.size - 1
resetView()
}
})
val context = context ?: return
swipeEvent = SwipeEvent { isRightSwipe: Boolean ->
@ -357,9 +316,46 @@ class StoryViewerFragment : Fragment() {
binding.imageViewer.setTapListener(simpleOnGestureListener)
}
private fun setupMultipage(models: List<Story>?) {
if (models == null) return
storiesViewModel.getPagination().observe(fragmentActivity, {
when (it) {
StoryPaginationType.FORWARD -> {
if (currentFeedStoryIndex == models.size - 1)
Toast.makeText(
context,
R.string.no_more_stories,
Toast.LENGTH_SHORT
).show()
else paginateStories(false, currentFeedStoryIndex == models.size - 2)
}
StoryPaginationType.BACKWARD -> {
if (currentFeedStoryIndex == 0)
Toast.makeText(
context,
R.string.no_more_stories,
Toast.LENGTH_SHORT
).show()
else paginateStories(true, false)
}
StoryPaginationType.ERROR -> {
Toast.makeText(
context,
R.string.downloader_unknown_error,
Toast.LENGTH_SHORT
).show()
}
}
})
if (!models.isEmpty()) {
binding.btnBackward.isEnabled = currentFeedStoryIndex != 0
binding.btnForward.isEnabled = currentFeedStoryIndex != models.size - 1
resetView()
}
}
private fun resetView() {
val context = context ?: return
live = null
if (menuProfile != null) menuProfile!!.isVisible = false
binding.imageViewer.controller = null
releasePlayer()
@ -367,7 +363,7 @@ class StoryViewerFragment : Fragment() {
var fetchOptions: StoryViewerOptions? = null
when (type) {
StoryViewerOptions.Type.HIGHLIGHT -> {
val models = storiesViewModel.getHighlights().value
val models = storiesViewModel.highlights.value
if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show()
return
@ -378,10 +374,15 @@ class StoryViewerFragment : Fragment() {
val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
val models = feedStoriesViewModel!!.list.value
if (models == null || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) return
val (_, _, _, _, user, _, _, _, _, _, _, broadcast) = models[currentFeedStoryIndex]
currentStoryUsername = user!!.username
fetchOptions = StoryViewerOptions.forUser(user.pk, currentStoryUsername)
live = broadcast
val userStory = models[currentFeedStoryIndex]
currentStoryUsername = userStory.user!!.username
fetchOptions = StoryViewerOptions.forUser(userStory.user.pk, currentStoryUsername)
val live = userStory.broadcast
if (live != null) {
storiesViewModel.setStory(userStory)
refreshLive(live)
return
}
}
StoryViewerOptions.Type.STORY_ARCHIVE -> {
val archivesViewModel = listViewModel as ArchivesViewModel?
@ -404,11 +405,7 @@ class StoryViewerFragment : Fragment() {
storiesViewModel.fetchSingleMedia(options!!.id)
return
}
if (live != null) {
refreshLive()
return
}
storiesViewModel.fetchStory(fetchOptions).observe(fragmentActivity, {
storiesViewModel.fetchStory(fetchOptions).observe(viewLifecycleOwner, {
if (it.status == Resource.Status.ERROR) {
Toast.makeText(context, "Error: " + it.message, Toast.LENGTH_SHORT).show()
}
@ -416,18 +413,14 @@ class StoryViewerFragment : Fragment() {
}
@Synchronized
private fun refreshLive() {
private fun refreshLive(live: Broadcast) {
binding.btnDownload.isEnabled = false
binding.stickers.isEnabled = false
binding.listToggle.isEnabled = false
binding.btnShare.isEnabled = false
binding.btnReply.isEnabled = false
releasePlayer()
setupLive(live!!.dashPlaybackUrl ?: live!!.dashAbrPlaybackUrl ?: return)
val actionBar = fragmentActivity.supportActionBar
actionBarSubtitle = epochSecondToString(live!!.publishedTime!!)
if (actionBar != null) {
try {
actionBar.setSubtitle(actionBarSubtitle)
} catch (e: Exception) {
Log.e(TAG, "refreshLive: ", e)
}
}
setupLive(live.dashPlaybackUrl ?: live.dashAbrPlaybackUrl ?: return)
}
@Synchronized
@ -446,14 +439,19 @@ class StoryViewerFragment : Fragment() {
binding.btnReply.isEnabled = currentStory.canReply
if (itemType === MediaItemType.MEDIA_TYPE_VIDEO) setupVideo(url) else setupImage(url)
// if (Utils.settingsHelper.getBoolean(MARK_AS_SEEN)) storiesRepository!!.seen(
// csrfToken,
// userId,
// deviceId,
// currentStory!!.id!!,
// currentStory!!.takenAt,
// System.currentTimeMillis() / 1000
// )
if (options!!.type == StoryViewerOptions.Type.FEED_STORY_POSITION
&& Utils.settingsHelper.getBoolean(PreferenceKeys.MARK_AS_SEEN)) {
val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
storiesViewModel.markAsSeen(currentStory).observe(viewLifecycleOwner) { m ->
if (m.status == Resource.Status.SUCCESS && m.data != null) {
val liveModels: MutableLiveData<List<Story>> = feedStoriesViewModel!!.list
val models = liveModels.value
val modelsCopy: MutableList<Story> = models!!.toMutableList()
modelsCopy.set(currentFeedStoryIndex, m.data)
liveModels.postValue(modelsCopy)
}
}
}
}
private fun downloadStory() {
@ -797,6 +795,13 @@ class StoryViewerFragment : Fragment() {
}
private fun shareStoryViaDm() {
val story = storiesViewModel.getCurrentStory().value ?: return
val context = context
if (story.user?.isPrivate == true && context != null) {
Toast.makeText(context, R.string.share_private_post, Toast.LENGTH_SHORT).show()
}
val actionBar = fragmentActivity.supportActionBar
if (actionBar != null) actionBar.subtitle = null
val actionGlobalUserSearch = StoryViewerFragmentDirections.actionToUserSearch().apply {
title = getString(R.string.share)
actionLabel = getString(R.string.send)

View File

@ -52,6 +52,7 @@ import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.discover.TopicCluster;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
@ -377,7 +378,9 @@ public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.O
}
private void updateSwipeRefreshState() {
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching());
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.swipeRefreshLayout.setRefreshing(binding.posts.isFetching())
);
}
private void navigateToProfile(final String username) {

View File

@ -330,12 +330,6 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
return super.onOptionsItemSelected(item);
}
@Override
public void onResume() {
super.onResume();
binding.getRoot().postDelayed(feedStoriesAdapter::notifyDataSetChanged, 1000);
}
@Override
public void onRefresh() {
binding.feedRecyclerView.refresh();
@ -370,7 +364,9 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
}
private void updateSwipeRefreshState() {
binding.feedSwipeRefreshLayout.setRefreshing(binding.feedRecyclerView.isFetching() || storiesFetching);
AppExecutors.INSTANCE.getMainThread().execute(() ->
binding.feedSwipeRefreshLayout.setRefreshing(binding.feedRecyclerView.isFetching() || storiesFetching)
);
}
private void setupFeedStories() {
@ -381,7 +377,7 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
storiesRecyclerView = binding.header;
storiesRecyclerView.setLayoutManager(new LinearLayoutManager(context, RecyclerView.HORIZONTAL, false));
storiesRecyclerView.setAdapter(feedStoriesAdapter);
feedStoriesViewModel.getList().observe(getViewLifecycleOwner(), feedStoriesAdapter::submitList);
feedStoriesViewModel.getList().observe(fragmentActivity, feedStoriesAdapter::submitList);
fetchStories();
}
@ -401,8 +397,6 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
storiesFetching = false;
//noinspection unchecked
feedStoriesViewModel.getList().postValue((List<Story>) feedStoryModels);
//noinspection unchecked
feedStoriesAdapter.submitList((List<Story>) feedStoryModels);
if (storyListMenu != null) storyListMenu.setVisible(true);
updateSwipeRefreshState();
}), Dispatchers.getIO())

View File

@ -875,7 +875,11 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall
.setLifeCycleOwner(this)
.setPostFetchService(ProfilePostFetchService(profile, currentUser != null))
.setLayoutPreferences(layoutPreferences)
.addFetchStatusChangeListener { binding.swipeRefreshLayout.isRefreshing = it }
.addFetchStatusChangeListener {
AppExecutors.mainThread.execute {
binding.swipeRefreshLayout.isRefreshing = it
}
}
.setFeedItemCallback(feedItemCallback)
.setSelectionModeCallback(selectionModeCallback)
.init()

View File

@ -240,6 +240,7 @@ public class SearchFragment extends Fragment implements SearchCategoryFragment.O
switch (resource.status) {
case SUCCESS:
viewModel.search("", type);
viewModel.search("", FavoriteType.TOP);
liveData.removeObserver(this);
break;
case ERROR:

View File

@ -104,7 +104,7 @@ public class DownloadsPreferencesFragment extends BasePreferencesFragment {
Utils.setupSelectedDir(context, data);
String path;
try {
path = URLDecoder.decode(data.getData().toString(), StandardCharsets.UTF_8.toString());
path = URLDecoder.decode(data.getData().toString(), StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
path = data.getData().toString();
}

View File

@ -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
}
}

View File

@ -64,8 +64,8 @@ enum class DirectItemType(val id: Int) : Serializable {
private val map: MutableMap<Int, DirectItemType> = mutableMapOf()
@JvmStatic
fun getId(id: Int): DirectItemType? {
return map[id]
fun getTypeFromId(id: Int): DirectItemType {
return map[id] ?: UNKNOWN
}
fun getName(directItemType: DirectItemType): String? {

View File

@ -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, String>,
): String
): FriendshipListFetchResponse
@FormUrlEncoded
@POST("/api/v1/friendships/{action}/")

View File

@ -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<FollowModel>?
var users: List<User>?
) {
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<FollowModel>): FriendshipListFetchResponse {
this.items = items
return this
}
}

View File

@ -1,9 +1,9 @@
package awais.instagrabber.repositories.responses.notification
class NotificationCounts(val commentLikesCount: Int,
val userTagsCount: Int,
val likesCount: Int,
val commentsCount: Int,
val relationshipsCount: Int,
val pOYCount: Int,
val requestsCount: Int)
class NotificationCounts(val commentLikes: Int,
val usertags: Int,
val likes: Int,
val comments: Int,
val relationships: Int,
val photosOfYou: Int,
val requests: Int)

View File

@ -11,7 +11,7 @@ data class Story(
val latestReelMedia: Long? = null, // = timestamp
val mediaCount: Int? = null,
// for stories and highlights
var seen: Long? = null,
val seen: Long? = null,
val user: User? = null,
// for stories
val muted: Boolean? = null,

View File

@ -54,10 +54,9 @@ public class ActivityCheckerService extends Service {
public void onSuccess(final NotificationCounts result) {
try {
if (result == null) return;
final String notification = getNotificationString(result);
final List<String> notification = getNotificationString(result);
if (notification == null) return;
final String notificationString = getString(R.string.activity_count_prefix) + " " + notification + ".";
showNotification(notificationString);
showNotification(notification);
} finally {
handler.postDelayed(runnable, DELAY_MILLIS);
}
@ -88,42 +87,54 @@ public class ActivityCheckerService extends Service {
handler.removeCallbacks(runnable);
}
private String getNotificationString(final NotificationCounts result) {
private List<String> getNotificationString(final NotificationCounts result) {
final List<String> toReturn = new ArrayList<>(2);
final List<String> list = new ArrayList<>();
if (result.getRelationshipsCount() != 0) {
list.add(getString(R.string.activity_count_relationship, result.getRelationshipsCount()));
int count = 0;
if (result.getRelationships() != 0) {
list.add(getString(R.string.activity_count_relationship, result.getRelationships()));
count += result.getRelationships();
}
if (result.getRequestsCount() != 0) {
list.add(getString(R.string.activity_count_requests, result.getRequestsCount()));
if (result.getRequests() != 0) {
list.add(getString(R.string.activity_count_requests, result.getRequests()));
count += result.getRequests();
}
if (result.getUserTagsCount() != 0) {
list.add(getString(R.string.activity_count_usertags, result.getUserTagsCount()));
if (result.getUsertags() != 0) {
list.add(getString(R.string.activity_count_usertags, result.getUsertags()));
count += result.getUsertags();
}
if (result.getPOYCount() != 0) {
list.add(getString(R.string.activity_count_poy, result.getPOYCount()));
if (result.getPhotosOfYou() != 0) {
list.add(getString(R.string.activity_count_poy, result.getPhotosOfYou()));
count += result.getPhotosOfYou();
}
if (result.getCommentsCount() != 0) {
list.add(getString(R.string.activity_count_comments, result.getCommentsCount()));
if (result.getComments() != 0) {
list.add(getString(R.string.activity_count_comments, result.getComments()));
count += result.getComments();
}
if (result.getCommentLikesCount() != 0) {
list.add(getString(R.string.activity_count_commentlikes, result.getCommentLikesCount()));
if (result.getCommentLikes() != 0) {
list.add(getString(R.string.activity_count_commentlikes, result.getCommentLikes()));
count += result.getCommentLikes();
}
if (result.getLikesCount() != 0) {
list.add(getString(R.string.activity_count_likes, result.getLikesCount()));
if (result.getLikes() != 0) {
list.add(getString(R.string.activity_count_likes, result.getLikes()));
count += result.getLikes();
}
if (list.isEmpty()) return null;
return TextUtils.join(", ", list);
toReturn.add(TextUtils.join(", ", list));
toReturn.add(getResources().getQuantityString(R.plurals.activity_count_total, count, count));
return toReturn;
}
private void showNotification(final String notificationString) {
private void showNotification(final List<String> notificationString) {
final Notification notification = new NotificationCompat.Builder(this, Constants.ACTIVITY_CHANNEL_ID)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setSmallIcon(R.drawable.ic_notif)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentTitle(getString(R.string.action_notif))
.setContentText(notificationString)
.setContentTitle(notificationString.get(1))
.setContentText(notificationString.get(0))
.setStyle(new NotificationCompat.BigTextStyle().bigText(notificationString.get(0)))
.setContentIntent(getPendingIntent())
.build();
notificationManager.notify(Constants.ACTIVITY_NOTIFICATION_ID, notification);

View File

@ -15,48 +15,23 @@ import java.io.FileDescriptor;
public final class MediaUtils {
private static final String TAG = MediaUtils.class.getSimpleName();
private static final String[] PROJECTION_VIDEO = {
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.WIDTH,
MediaStore.Video.Media.HEIGHT,
MediaStore.Video.Media.SIZE
};
private static final String[] PROJECTION_AUDIO = {
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.SIZE
};
public static void getVideoInfo(@NonNull final ContentResolver contentResolver,
@NonNull final Uri uri,
@NonNull final OnInfoLoadListener<VideoInfo> listener) {
AppExecutors.INSTANCE.getTasksThread().submit(() -> {
try (Cursor cursor = MediaStore.Video.query(contentResolver, uri, PROJECTION_VIDEO)) {
if (cursor == null) {
listener.onLoad(null);
return;
}
int durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION);
int widthColumn = cursor.getColumnIndex(MediaStore.Video.Media.WIDTH);
int heightColumn = cursor.getColumnIndex(MediaStore.Video.Media.HEIGHT);
int sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE);
if (cursor.moveToNext()) {
listener.onLoad(new VideoInfo(
cursor.getLong(durationColumn),
cursor.getInt(widthColumn),
cursor.getInt(heightColumn),
cursor.getLong(sizeColumn)
));
}
} catch (Exception e) {
Log.e(TAG, "getVideoInfo: ", e);
listener.onFailure(e);
}
});
getInfo(contentResolver, uri, listener, true);
}
public static void getVoiceInfo(@NonNull final ContentResolver contentResolver,
@NonNull final Uri uri,
@NonNull final OnInfoLoadListener<VideoInfo> listener) {
getInfo(contentResolver, uri, listener, false);
}
private static void getInfo(@NonNull final ContentResolver contentResolver,
@NonNull final Uri uri,
@NonNull final OnInfoLoadListener<VideoInfo> listener,
@NonNull final Boolean isVideo) {
AppExecutors.INSTANCE.getTasksThread().submit(() -> {
try (ParcelFileDescriptor parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")) {
if (parcelFileDescriptor == null) {
@ -68,6 +43,23 @@ public final class MediaUtils {
mediaMetadataRetriever.setDataSource(fileDescriptor);
String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
if (TextUtils.isEmpty(duration)) duration = "0";
if (isVideo) {
String width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
if (TextUtils.isEmpty(width)) width = "1";
String height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
if (TextUtils.isEmpty(height)) height = "1";
final Cursor cursor = contentResolver.query(uri, new String[]{MediaStore.Video.Media.SIZE}, null, null, null);
cursor.moveToFirst();
final long fileSize = cursor.getLong(0);
cursor.close();
listener.onLoad(new VideoInfo(
Long.parseLong(duration),
Integer.valueOf(width),
Integer.valueOf(height),
fileSize
));
return;
}
listener.onLoad(new VideoInfo(
Long.parseLong(duration),
0,
@ -75,7 +67,7 @@ public final class MediaUtils {
0
));
} catch (Exception e) {
Log.e(TAG, "getVoiceInfo: ", e);
Log.e(TAG, "getInfo: ", e);
listener.onFailure(e);
}
});

View File

@ -1,19 +0,0 @@
package awais.instagrabber.viewmodels;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.List;
import awais.instagrabber.models.FollowModel;
public class FollowViewModel extends ViewModel {
private MutableLiveData<List<FollowModel>> list;
public MutableLiveData<List<FollowModel>> getList() {
if (list == null) {
list = new MutableLiveData<>();
}
return list;
}
}

View File

@ -0,0 +1,163 @@
package awais.instagrabber.viewmodels
import androidx.lifecycle.*
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.webservices.FriendshipRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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>>()
// cursors
private val followersMaxId = MutableLiveData<String?>("")
private val followingMaxId = MutableLiveData<String?>("")
private val searchingMaxId = MutableLiveData<String?>("")
private val searchQuery = MutableLiveData<String?>()
// 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
}
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>()
}
}

View File

@ -222,7 +222,7 @@ class ProfileFragmentViewModel(
private suspend fun fetchUser(
currentUser: User?,
stateUsername: String,
): User {
): User? {
if (currentUser != null) {
// logged in
val tempUser = userRepository.getUsernameInfo(stateUsername)

View File

@ -242,7 +242,7 @@ public class SearchFragmentViewModel extends AppStateViewModel {
@Override
public void onFailure(@NonNull final Throwable t) {
if (!TextUtils.isEmpty(tempQuery)) return;
topResults.postValue(Resource.success(Collections.emptyList()));
liveData.postValue(Resource.success(Collections.emptyList()));
Log.e(TAG, "onFailure: ", t);
}
}, AppExecutors.INSTANCE.getMainThread());

View File

@ -7,19 +7,22 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import awais.instagrabber.R
import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.models.enums.StoryPaginationType
import awais.instagrabber.models.Resource
import awais.instagrabber.models.Resource.Companion.error
import awais.instagrabber.models.Resource.Companion.loading
import awais.instagrabber.models.Resource.Companion.success
import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.models.enums.StoryPaginationType
import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.repositories.responses.stories.*
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.utils.*
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.getCsrfTokenFromCookie
import awais.instagrabber.utils.getUserIdFromCookie
import awais.instagrabber.webservices.MediaRepository
import awais.instagrabber.webservices.StoriesRepository
import com.google.common.collect.ImmutableList
@ -60,21 +63,22 @@ class StoryFragmentViewModel : ViewModel() {
private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() }
// for highlights ONLY
private val highlights = MutableLiveData<List<Story>?>()
val highlights = MutableLiveData<List<Story>?>()
/* set functions */
fun setStory(story: Story) {
if (story.items == null || story.items.size == 0) {
pagination.postValue(StoryPaginationType.ERROR)
return
}
currentStory.postValue(story)
storyTitle.postValue(story.title ?: story.user?.username)
if (story.broadcast != null) {
date.postValue(story.dateTime)
type.postValue(MediaItemType.MEDIA_TYPE_LIVE)
pagination.postValue(StoryPaginationType.DO_NOTHING)
return
}
if (story.items == null || story.items.size == 0) {
pagination.postValue(StoryPaginationType.ERROR)
return
}
}
@ -184,10 +188,6 @@ class StoryFragmentViewModel : ViewModel() {
/* get functions */
fun getHighlights(): LiveData<List<Story>?> {
return highlights
}
fun getCurrentStory(): LiveData<Story?> {
return currentStory
}
@ -472,4 +472,28 @@ class StoryFragmentViewModel : ViewModel() {
}
return data
}
fun markAsSeen(storyMedia: StoryMedia): LiveData<Resource<Story?>> {
val data = MutableLiveData<Resource<Story?>>()
data.postValue(loading(null))
val oldStory = currentStory.value!!
if (oldStory.seen != null && oldStory.seen >= storyMedia.takenAt) data.postValue(success(null))
else viewModelScope.launch(Dispatchers.IO) {
try {
storiesRepository.seen(
csrfToken!!,
userId,
deviceId,
storyMedia.id,
storyMedia.takenAt,
System.currentTimeMillis() / 1000
)
val newStory = oldStory.copy(seen = storyMedia.takenAt)
data.postValue(success(newStory))
} catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
}

View File

@ -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<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 {

View File

@ -178,51 +178,59 @@ open class GraphQLRepository(private val service: GraphQLService) {
// TODO convert string response to a response class
open suspend fun fetchUser(
username: String,
): User {
): User? {
val response = service.getUser(username)
val body = JSONObject(response
.split("<script type=\"text/javascript\">window._sharedData = ").get(1)
.split("</script>").get(0)
.trim().replace(Regex("\\};$"), "}"))
val userJson = body
.getJSONObject("entry_data")
.getJSONArray("ProfilePage")
.getJSONObject(0)
.getJSONObject("graphql")
.getJSONObject(Constants.EXTRAS_USER)
val isPrivate = userJson.getBoolean("is_private")
val id = userJson.optLong(Constants.EXTRAS_ID, 0)
val timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media")
// if (timelineMedia.has("edges")) {
// final JSONArray edges = timelineMedia.getJSONArray("edges");
// }
var url: String? = userJson.optString("external_url")
if (url.isNullOrBlank()) url = null
return User(
id,
username,
userJson.getString("full_name"),
isPrivate,
userJson.getString("profile_pic_url_hd"),
userJson.getBoolean("is_verified"),
friendshipStatus = FriendshipStatus(
userJson.optBoolean("followed_by_viewer"),
userJson.optBoolean("follows_viewer"),
userJson.optBoolean("blocked_by_viewer"),
false,
try {
val body = JSONObject(
response
.split("<script type=\"text/javascript\">window._sharedData = ").get(1)
.split("</script>").get(0)
.trim().replace(Regex("\\};$"), "}")
)
val userJson = body
.getJSONObject("entry_data")
.getJSONArray("ProfilePage")
.getJSONObject(0)
.getJSONObject("graphql")
.getJSONObject(Constants.EXTRAS_USER)
val isPrivate = userJson.getBoolean("is_private")
val id = userJson.optLong(Constants.EXTRAS_ID, 0)
val timelineMedia = userJson.getJSONObject("edge_owner_to_timeline_media")
// if (timelineMedia.has("edges")) {
// final JSONArray edges = timelineMedia.getJSONArray("edges");
// }
var url: String? = userJson.optString("external_url")
if (url.isNullOrBlank()) url = null
return User(
id,
username,
userJson.getString("full_name"),
isPrivate,
userJson.optBoolean("has_requested_viewer"),
userJson.optBoolean("requested_by_viewer"),
false,
userJson.optBoolean("restricted_by_viewer"),
false
),
mediaCount = timelineMedia.getLong("count"),
followerCount = userJson.getJSONObject("edge_followed_by").getLong("count"),
followingCount = userJson.getJSONObject("edge_follow").getLong("count"),
biography = userJson.getString("biography"),
externalUrl = url,
)
userJson.getString("profile_pic_url_hd"),
userJson.getBoolean("is_verified"),
friendshipStatus = FriendshipStatus(
userJson.optBoolean("followed_by_viewer"),
userJson.optBoolean("follows_viewer"),
userJson.optBoolean("blocked_by_viewer"),
false,
isPrivate,
userJson.optBoolean("has_requested_viewer"),
userJson.optBoolean("requested_by_viewer"),
false,
userJson.optBoolean("restricted_by_viewer"),
false
),
mediaCount = timelineMedia.getLong("count"),
followerCount = userJson.getJSONObject("edge_followed_by").getLong("count"),
followingCount = userJson.getJSONObject("edge_follow").getLong("count"),
biography = userJson.getString("biography"),
externalUrl = url,
)
}
catch (e: Exception) {
Log.e(TAG, "fetchUser failed", e)
return null
}
}
// TODO convert string response to a response class

View File

@ -1,15 +1,14 @@
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<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.items = items;
}
@ -18,22 +17,13 @@ public class ExpandableGroup {
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) {
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;
}

View File

@ -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<ExpandableGroup> 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) {

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
@ -16,6 +15,5 @@
android:paddingLeft="8dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_follow" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -253,7 +253,6 @@
<string name="action_ayml">Suggested users</string>
<string name="select_picture">Select Picture</string>
<string name="uploading">Uploading…</string>
<string name="activity_count_prefix">You have:</string>
<string name="activity_count_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Usuaris suggerits</string>
<string name="select_picture">Seleccionar imatge</string>
<string name="uploading">S\'està pujant…</string>
<string name="activity_count_prefix">Tens:</string>
<string name="activity_count_relationship">%d seguidors</string>
<string name="activity_count_comments">%d comentaris</string>
<string name="activity_count_commentlikes">%d m\'agrades al comentari</string>

View File

@ -245,7 +245,6 @@
<string name="action_ayml">Navrhovaní uživatelé</string>
<string name="select_picture">Vybrat obrázek</string>
<string name="uploading">Nahrávání…</string>
<string name="activity_count_prefix">Máte:</string>
<string name="activity_count_relationship">%d sleduje</string>
<string name="activity_count_comments">%d komentářů</string>
<string name="activity_count_commentlikes">%d lajků komentáře</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Vorgeschlagene Benutzer</string>
<string name="select_picture">Bild auswählen</string>
<string name="uploading">Hochladen…</string>
<string name="activity_count_prefix">Du hast:</string>
<string name="activity_count_relationship">%d Abonnenten</string>
<string name="activity_count_comments">%d Kommentare</string>
<string name="activity_count_commentlikes">%d gelikte Kommentare</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Προτεινόμενοι χρήστες</string>
<string name="select_picture">Επιλογή εικόνας</string>
<string name="uploading">Μεταφόρτωση…</string>
<string name="activity_count_prefix">Έχετε:</string>
<string name="activity_count_relationship">%d ακόλουθοι</string>
<string name="activity_count_comments">%d σχόλια</string>
<string name="activity_count_commentlikes">Το σχόλιο αρέσει σε %d</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Usuarios sugeridos</string>
<string name="select_picture">Seleccionar imagen</string>
<string name="uploading">Subiendo…</string>
<string name="activity_count_prefix">Tienes:</string>
<string name="activity_count_relationship">%d sigue</string>
<string name="activity_count_comments">%d comentarios</string>
<string name="activity_count_commentlikes">%d me gustas en comentarios</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Iradokitutako erabiltzaileak</string>
<string name="select_picture">Hautatu irudia</string>
<string name="uploading">Igotzen…</string>
<string name="activity_count_prefix">Duzuna:</string>
<string name="activity_count_relationship">%d jarraitzaile</string>
<string name="activity_count_comments">%d iruzkin</string>
<string name="activity_count_commentlikes">%d iruzkin-atsegite</string>

View File

@ -238,7 +238,6 @@
<string name="action_ayml">Suggested users</string>
<string name="select_picture">انتخاب تصویر</string>
<string name="uploading">Uploading…</string>
<string name="activity_count_prefix">شما باید:</string>
<string name="activity_count_relationship">%d دنبال کننده‌</string>
<string name="activity_count_comments">%d دیدگاه</string>
<string name="activity_count_commentlikes">%d پسند دیدگاه</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Utilisateurs suggérés</string>
<string name="select_picture">Sélectionnez une image</string>
<string name="uploading">Envoi en cours…</string>
<string name="activity_count_prefix">Vous avez :</string>
<string name="activity_count_relationship">%d abonné(e)s</string>
<string name="activity_count_comments">%d commentaires</string>
<string name="activity_count_commentlikes">%d j\'aime(s) sur le commentaire</string>

View File

@ -238,7 +238,6 @@
<string name="action_ayml">सुझायें ऊपयोगकर्ता</string>
<string name="select_picture">चित्र का चयन करें</string>
<string name="uploading">अपलोड हो रहा है...</string>
<string name="activity_count_prefix">आपके पास है:</string>
<string name="activity_count_relationship">%d अनुगामी</string>
<string name="activity_count_comments">%d टिप्पणियाँ</string>
<string name="activity_count_commentlikes">%d टिप्पणीयाँ पसन्दीत</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">Pengguna yang disarankan</string>
<string name="select_picture">Pilih Gambar</string>
<string name="uploading">Mengunggah…</string>
<string name="activity_count_prefix">Anda memiliki:</string>
<string name="activity_count_relationship">%d mengikuti</string>
<string name="activity_count_comments">%d komentar</string>
<string name="activity_count_commentlikes">%d suka komentar</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Utenti suggeriti</string>
<string name="select_picture">Seleziona Immagine</string>
<string name="uploading">Caricamento…</string>
<string name="activity_count_prefix">Hai:</string>
<string name="activity_count_relationship">%d seguaci</string>
<string name="activity_count_comments">%d commenti</string>
<string name="activity_count_commentlikes">%d mi piace al commento</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">おすすめのユーザー</string>
<string name="select_picture">画像を選択</string>
<string name="uploading">アップロード中…</string>
<string name="activity_count_prefix">あなたのステータス:</string>
<string name="activity_count_relationship">%d 人のフォロワー</string>
<string name="activity_count_comments">%d コメント</string>
<string name="activity_count_commentlikes">%d 個のコメントへのいいね!</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">프로필 추천</string>
<string name="select_picture">사진 선택</string>
<string name="uploading">업로드 중…</string>
<string name="activity_count_prefix">You have:</string>
<string name="activity_count_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Препорачани кориснчки сметки</string>
<string name="select_picture">Селектирај слика</string>
<string name="uploading">Се Прикачува…</string>
<string name="activity_count_prefix">Вие имате:</string>
<string name="activity_count_relationship">%d следачи</string>
<string name="activity_count_comments">%d коментари</string>
<string name="activity_count_commentlikes">%d лајкови на коментари</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Voorgestelde gebruikers</string>
<string name="select_picture">Selecteer Afbeelding</string>
<string name="uploading">Bezig met uploaden…</string>
<string name="activity_count_prefix">Je hebt:</string>
<string name="activity_count_relationship">%d volgers</string>
<string name="activity_count_comments">%d opmerkingen</string>
<string name="activity_count_commentlikes">%d opmerking-likes</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Suggested users</string>
<string name="select_picture">Select Picture</string>
<string name="uploading">Uploading…</string>
<string name="activity_count_prefix">You have:</string>
<string name="activity_count_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</string>

View File

@ -245,7 +245,6 @@
<string name="action_ayml">Proponowani użytkownicy</string>
<string name="select_picture">Wybierz obraz</string>
<string name="uploading">Przesyłanie…</string>
<string name="activity_count_prefix">Masz:</string>
<string name="activity_count_relationship">%d obserwujących</string>
<string name="activity_count_comments">%d komentarzy</string>
<string name="activity_count_commentlikes">%d polubionych komentarzy</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Usuários sugeridos</string>
<string name="select_picture">Selecionar imagem</string>
<string name="uploading">Enviando…</string>
<string name="activity_count_prefix">Você tem:</string>
<string name="activity_count_relationship">%d seguidores</string>
<string name="activity_count_comments">%d comentários</string>
<string name="activity_count_commentlikes">%d comentários curtidos</string>

View File

@ -245,7 +245,6 @@
<string name="action_ayml">Предлагаемые пользователи</string>
<string name="select_picture">Выберите изображение</string>
<string name="uploading">Загрузка…</string>
<string name="activity_count_prefix">У вас есть:</string>
<string name="activity_count_relationship">%d подписано</string>
<string name="activity_count_comments">%d комментариев</string>
<string name="activity_count_commentlikes">%d симпатий к комментарию</string>

View File

@ -245,7 +245,6 @@
<string name="action_ayml">Používatelia ktorých možno poznáte</string>
<string name="select_picture">Vybrať fotografiu</string>
<string name="uploading">Nahráva sa…</string>
<string name="activity_count_prefix">Máš:</string>
<string name="activity_count_relationship">%d sledovaní</string>
<string name="activity_count_comments">%d komentárov</string>
<string name="activity_count_commentlikes">%d komentárov ktoré sa niekomu páčia</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Suggested users</string>
<string name="select_picture">Välj bild</string>
<string name="uploading">Laddar upp…</string>
<string name="activity_count_prefix">Du har:</string>
<string name="activity_count_relationship">%d följer</string>
<string name="activity_count_comments">%d kommentarer</string>
<string name="activity_count_commentlikes">%d gillade kommentarer</string>

View File

@ -237,7 +237,6 @@
<string name="action_ayml">Önerilen kullanıcılar</string>
<string name="select_picture">Resim Seç</string>
<string name="uploading">Yükleniyor…</string>
<string name="activity_count_prefix">Sahip olduğun:</string>
<string name="activity_count_relationship">%d takip</string>
<string name="activity_count_comments">%d yorum</string>
<string name="activity_count_commentlikes">%d yorum beğenisi</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">Người dùng được đề xuất</string>
<string name="select_picture">Chọn hình ảnh</string>
<string name="uploading">Đang tải lên…</string>
<string name="activity_count_prefix">Bạn có:</string>
<string name="activity_count_relationship">%d người theo dõi</string>
<string name="activity_count_comments">%d bình luận</string>
<string name="activity_count_commentlikes">%d lượt thích bình luận</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">推荐用户</string>
<string name="select_picture">选择图片</string>
<string name="uploading">上传中...</string>
<string name="activity_count_prefix">您有:</string>
<string name="activity_count_relationship">%d 位新粉丝</string>
<string name="activity_count_comments">%d 个评论回复</string>
<string name="activity_count_commentlikes">%d 个评论点赞</string>

View File

@ -233,7 +233,6 @@
<string name="action_ayml">推薦用戶</string>
<string name="select_picture">選擇圖片</string>
<string name="uploading">上傳中…</string>
<string name="activity_count_prefix">您有</string>
<string name="activity_count_relationship">%d 個追蹤者</string>
<string name="activity_count_comments">%d 個評論</string>
<string name="activity_count_commentlikes">%d 個評論的讚</string>

View File

@ -242,7 +242,10 @@
<string name="liability" translatable="false">This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</string>
<string name="select_picture">Select Picture</string>
<string name="uploading">Uploading…</string>
<string name="activity_count_prefix">You have:</string>
<plurals name="activity_count_total">
<item quantity="one">You have %d notification</item>
<item quantity="other">You have %d notifications</item>
</plurals>
<string name="activity_count_relationship">%d follows</string>
<string name="activity_count_comments">%d comments</string>
<string name="activity_count_commentlikes">%d comment likes</string>
@ -486,7 +489,7 @@
<string name="crash_report_subject">Barinsta Crash Report</string>
<string name="crash_report_title">Select an email app to send crash logs</string>
<string name="not_found">Not found!</string>
<string name="rate_limit">Your IP has been rate limited by Instagram. Wait for an hour and try again. &lt;a href=\"https://redd.it/msxlko\">Learn more.&lt;/a></string>
<string name="rate_limit">Your IP has been rate limited by Instagram. &lt;a href=\"https://barinsta.austinhuang.me/en/latest/faq.html#ratelimits\">Learn more.&lt;/a></string>
<string name="skip_update">Skip this update</string>
<string name="on_latest_version">You\'re already on the latest version</string>
<string name="tab_order">Screen order</string>