1
0
mirror of https://github.com/KokaKiwi/BarInsta synced 2024-11-18 04:37:30 +00:00

version up, close #266, address half of #259

instagram is not returning most data in graphql anymore so mitigation has been implemented, but again they need to stop trampling on the rights of anonymous users
This commit is contained in:
Austin Huang 2020-11-13 14:19:26 -05:00
parent 133abcca85
commit f67d3a023c
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
10 changed files with 280 additions and 167 deletions

View File

@ -10,8 +10,8 @@ android {
minSdkVersion 21
targetSdkVersion 29
versionCode 52
versionName '19.0.0'
versionCode 53
versionName '19.0.1'
multiDexEnabled true

View File

@ -64,7 +64,7 @@ public class FeedGridItemViewHolder extends RecyclerView.ViewHolder {
binding.postImage.setAspectRatio(1);
}
if (layoutPreferences.isAvatarVisible()) {
binding.profilePic.setVisibility(View.VISIBLE);
binding.profilePic.setVisibility(TextUtils.isEmpty(feedModel.getProfileModel().getSdProfilePic()) ? View.GONE : View.VISIBLE);
binding.profilePic.setImageURI(feedModel.getProfileModel().getSdProfilePic());
final ViewGroup.LayoutParams layoutParams = binding.profilePic.getLayoutParams();
@DimenRes final int dimenRes;
@ -88,7 +88,7 @@ public class FeedGridItemViewHolder extends RecyclerView.ViewHolder {
binding.profilePic.setVisibility(View.GONE);
}
if (layoutPreferences.isNameVisible()) {
binding.name.setVisibility(View.VISIBLE);
binding.name.setVisibility(TextUtils.isEmpty(feedModel.getProfileModel().getUsername()) ? View.GONE : View.VISIBLE);
binding.name.setText(feedModel.getProfileModel().getUsername());
} else {
binding.name.setVisibility(View.GONE);

View File

@ -14,16 +14,36 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService {
private final TagsService tagsService;
private final HashtagModel hashtagModel;
private String nextMaxId;
private boolean moreAvailable;
private boolean moreAvailable, isLoggedIn;
public HashtagPostFetchService(final HashtagModel hashtagModel) {
public HashtagPostFetchService(final HashtagModel hashtagModel, final boolean isLoggedIn) {
this.hashtagModel = hashtagModel;
this.isLoggedIn = isLoggedIn;
tagsService = TagsService.getInstance();
}
@Override
public void fetch(final FetchListener<List<FeedModel>> fetchListener) {
tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, new ServiceCallback<TagPostsFetchResponse>() {
if (isLoggedIn) tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, new ServiceCallback<TagPostsFetchResponse>() {
@Override
public void onSuccess(final TagPostsFetchResponse result) {
if (result == null) return;
nextMaxId = result.getNextMaxId();
moreAvailable = result.isMoreAvailable();
if (fetchListener != null) {
fetchListener.onResult(result.getItems());
}
}
@Override
public void onFailure(final Throwable t) {
// Log.e(TAG, "onFailure: ", t);
if (fetchListener != null) {
fetchListener.onFailure(t);
}
}
});
else tagsService.fetchGraphQLPosts(hashtagModel.getName().toLowerCase(), nextMaxId, new ServiceCallback<TagPostsFetchResponse>() {
@Override
public void onSuccess(final TagPostsFetchResponse result) {
if (result == null) return;

View File

@ -352,7 +352,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe
private void setupPosts() {
binding.posts.setViewModelStoreOwner(this)
.setLifeCycleOwner(this)
.setPostFetchService(new HashtagPostFetchService(hashtagModel))
.setPostFetchService(new HashtagPostFetchService(hashtagModel, isLoggedIn))
.setLayoutPreferences(PostsLayoutPreferences.fromJson(settingsHelper.getString(Constants.PREF_HASHTAG_POSTS_LAYOUT)))
.addFetchStatusChangeListener(fetching -> updateSwipeRefreshState())
.setFeedItemCallback(feedItemCallback)

View File

@ -112,7 +112,7 @@ public class StoryViewerFragment extends Fragment {
private StoryModel currentStory;
private int slidePos;
private int lastSlidePos;
private String url, username;
private String url;
private PollModel poll;
private QuestionModel question;
private String[] mentions;
@ -498,7 +498,7 @@ public class StoryViewerFragment extends Fragment {
}
} else if (!TextUtils.isEmpty(fragmentArgs.getProfileId()) && !TextUtils.isEmpty(fragmentArgs.getUsername())) {
currentStoryMediaId = fragmentArgs.getProfileId();
username = fragmentArgs.getUsername();
currentStoryUsername = fragmentArgs.getUsername();
}
isHashtag = fragmentArgs.getIsHashtag();
isLoc = fragmentArgs.getIsLoc();
@ -534,7 +534,7 @@ public class StoryViewerFragment extends Fragment {
}
};
storiesService.getUserStory(currentStoryMediaId,
username,
currentStoryUsername,
isLoc,
isHashtag,
isHighlight,

View File

@ -39,10 +39,10 @@ public class AboutFragment extends BasePreferencesFragment {
//thirdPartyCategory.setSummary(R.string.about_category_3pt_summary);
thirdPartyCategory.setIconSpaceReserved(false);
// alphabetical order!!!
thirdPartyCategory.addPreference(getACIPreference());
thirdPartyCategory.addPreference(getAutolinkPreference());
thirdPartyCategory.addPreference(getExoPlayerPreference());
thirdPartyCategory.addPreference(getFrescoPreference());
thirdPartyCategory.addPreference(getIcafePreference());
thirdPartyCategory.addPreference(getJsoupPreference());
thirdPartyCategory.addPreference(getMDIPreference());
thirdPartyCategory.addPreference(getRetrofitPreference());
@ -101,7 +101,7 @@ public class AboutFragment extends BasePreferencesFragment {
if (context == null) return null;
final Preference preference = new Preference(context);
preference.setTitle("Retrofit");
preference.setSummary("Copyright 2013 Square, Inc. Apache Version 2.0.");
preference.setSummary("Copyright 2013 Square, Inc. Apache 2.0.");
preference.setIconSpaceReserved(false);
preference.setOnPreferenceClickListener(p -> {
final Intent intent = new Intent(Intent.ACTION_VIEW);
@ -149,7 +149,7 @@ public class AboutFragment extends BasePreferencesFragment {
if (context == null) return null;
final Preference preference = new Preference(context);
preference.setTitle("ExoPlayer");
preference.setSummary("Copyright (C) 2016 The Android Open Source Project. Apache Version 2.0.");
preference.setSummary("Copyright (C) 2016 The Android Open Source Project. Apache 2.0.");
preference.setIconSpaceReserved(false);
preference.setOnPreferenceClickListener(p -> {
final Intent intent = new Intent(Intent.ACTION_VIEW);
@ -165,7 +165,7 @@ public class AboutFragment extends BasePreferencesFragment {
if (context == null) return null;
final Preference preference = new Preference(context);
preference.setTitle("Material Design Icons");
preference.setSummary("Copyright (C) 2014 Austin Andrews & Google LLC. Apache Version 2.0.");
preference.setSummary("Copyright (C) 2014 Austin Andrews & Google LLC. Apache 2.0.");
preference.setIconSpaceReserved(false);
preference.setOnPreferenceClickListener(p -> {
final Intent intent = new Intent(Intent.ACTION_VIEW);
@ -181,7 +181,7 @@ public class AboutFragment extends BasePreferencesFragment {
if (context == null) return null;
final Preference preference = new Preference(context);
preference.setTitle("AutoLinkTextViewV2");
preference.setSummary("Copyright (C) 2019 Arman Chatikyan. Apache Version 2.0.");
preference.setSummary("Copyright (C) 2019 Arman Chatikyan. Apache 2.0.");
preference.setIconSpaceReserved(false);
preference.setOnPreferenceClickListener(p -> {
final Intent intent = new Intent(Intent.ACTION_VIEW);
@ -192,16 +192,16 @@ public class AboutFragment extends BasePreferencesFragment {
return preference;
}
private Preference getIcafePreference() {
private Preference getACIPreference() {
final Context context = getContext();
if (context == null) return null;
final Preference preference = new Preference(context);
preference.setTitle("ICAFE");
preference.setSummary("Copyright (C) 2014-2019 Wen Yu. Eclipse Version 2.0.");
preference.setTitle("Apache Commons Imaging");
preference.setSummary("Copyright 2007-2020 The Apache Software Foundation. Apache 2.0.");
preference.setIconSpaceReserved(false);
preference.setOnPreferenceClickListener(p -> {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://github.com/dragon66/icafe"));
intent.setData(Uri.parse("https://commons.apache.org/proper/commons-imaging/"));
startActivity(intent);
return true;
});

View File

@ -24,4 +24,7 @@ public interface TagsRepository {
@GET("/api/v1/feed/tag/{tag}/")
Call<String> fetchPosts(@Path("tag") final String tag,
@QueryMap Map<String, String> queryParams);
@GET("/graphql/query/")
Call<String> fetchGraphQLPosts(@QueryMap(encoded = true) Map<String, String> queryParams);
}

View File

@ -665,6 +665,124 @@ public final class ResponseBodyUtils {
return feedModelBuilder.build();
}
public static FeedModel parseGraphQLItem(final JSONObject itemJson) throws JSONException {
if (itemJson == null) {
return null;
}
final JSONObject feedItem = itemJson.getJSONObject("node");
final String mediaType = feedItem.optString("__typename");
if (mediaType.isEmpty() || "GraphSuggestedUserFeedUnit".equals(mediaType))
return null;
final boolean isVideo = feedItem.optBoolean("is_video");
final long videoViews = feedItem.optLong("video_view_count", 0);
final String displayUrl = feedItem.optString("display_url");
if (TextUtils.isEmpty(displayUrl)) return null;
final String resourceUrl;
if (isVideo && feedItem.has("video_url")) {
resourceUrl = feedItem.getString("video_url");
} else {
resourceUrl = feedItem.has("display_resources") ? ResponseBodyUtils.getHighQualityImage(feedItem) : displayUrl;
}
ProfileModel profileModel = null;
if (feedItem.has("owner")) {
final JSONObject owner = feedItem.getJSONObject("owner");
profileModel = new ProfileModel(
owner.optBoolean("is_private"),
false, // if you can see it then you def follow
owner.optBoolean("is_verified"),
owner.getString(Constants.EXTRAS_ID),
owner.optString(Constants.EXTRAS_USERNAME),
owner.optString("full_name"),
null,
null,
owner.optString("profile_pic_url"),
null,
0,
0,
0,
false,
false,
false,
false);
}
JSONObject tempJsonObject = feedItem.optJSONObject("edge_media_preview_comment");
final long commentsCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0;
tempJsonObject = feedItem.optJSONObject("edge_media_preview_like");
final long likesCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0;
tempJsonObject = feedItem.optJSONObject("edge_media_to_caption");
final JSONArray captions = tempJsonObject != null ? tempJsonObject.getJSONArray("edges") : null;
String captionText = null;
if (captions != null && captions.length() > 0) {
if ((tempJsonObject = captions.optJSONObject(0)) != null &&
(tempJsonObject = tempJsonObject.optJSONObject("node")) != null) {
captionText = tempJsonObject.getString("text");
}
}
final JSONObject location = feedItem.optJSONObject("location");
// Log.d(TAG, "location: " + (location == null ? null : location.toString()));
String locationId = null;
String locationName = null;
if (location != null) {
locationName = location.optString("name");
if (location.has("id")) {
locationId = location.getString("id");
} else if (location.has("pk")) {
locationId = location.getString("pk");
}
// Log.d(TAG, "locationId: " + locationId);
}
int height = 0;
int width = 0;
final JSONObject dimensions = feedItem.optJSONObject("dimensions");
if (dimensions != null) {
height = dimensions.optInt("height");
width = dimensions.optInt("width");
}
String thumbnailUrl = null;
try {
thumbnailUrl = feedItem.getJSONArray("display_resources")
.getJSONObject(0)
.getString("src");
} catch (JSONException ignored) {}
final FeedModel.Builder feedModelBuilder = new FeedModel.Builder()
.setProfileModel(profileModel)
.setItemType(isVideo ? MediaItemType.MEDIA_TYPE_VIDEO
: MediaItemType.MEDIA_TYPE_IMAGE)
.setViewCount(videoViews)
.setPostId(feedItem.getString(Constants.EXTRAS_ID))
.setDisplayUrl(resourceUrl)
.setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl : displayUrl)
.setShortCode(feedItem.getString(Constants.EXTRAS_SHORTCODE))
.setPostCaption(captionText)
.setCommentsCount(commentsCount)
.setTimestamp(feedItem.optLong("taken_at_timestamp", -1))
.setLiked(feedItem.optBoolean("viewer_has_liked"))
.setBookmarked(feedItem.optBoolean("viewer_has_saved"))
.setLikesCount(likesCount)
.setLocationName(locationName)
.setLocationId(locationId)
.setImageHeight(height)
.setImageWidth(width);
final boolean isSlider = "GraphSidecar".equals(mediaType) && feedItem.has("edge_sidecar_to_children");
if (isSlider) {
feedModelBuilder.setItemType(MediaItemType.MEDIA_TYPE_SLIDER);
final JSONObject sidecar = feedItem.optJSONObject("edge_sidecar_to_children");
if (sidecar != null) {
final JSONArray children = sidecar.optJSONArray("edges");
if (children != null) {
final List<PostChild> sliderItems = getSliderItems(children);
feedModelBuilder.setSliderItems(sliderItems);
}
}
}
return feedModelBuilder.build();
}
private static List<PostChild> getChildPosts(final JSONObject mediaJson) throws JSONException {
if (mediaJson == null) {
return Collections.emptyList();
@ -708,4 +826,42 @@ public final class ResponseBodyUtils {
.setWidth(childJson.optInt("original_width"))
.build();
}
// this is for graphql
@NonNull
private static List<PostChild> getSliderItems(final JSONArray children) throws JSONException {
final List<PostChild> sliderItems = new ArrayList<>();
for (int j = 0; j < children.length(); ++j) {
final JSONObject childNode = children.optJSONObject(j).getJSONObject("node");
final boolean isChildVideo = childNode.optBoolean("is_video");
int height = 0;
int width = 0;
final JSONObject dimensions = childNode.optJSONObject("dimensions");
if (dimensions != null) {
height = dimensions.optInt("height");
width = dimensions.optInt("width");
}
String thumbnailUrl = null;
try {
thumbnailUrl = childNode.getJSONArray("display_resources")
.getJSONObject(0)
.getString("src");
} catch (JSONException ignored) {}
final PostChild sliderItem = new PostChild.Builder()
.setItemType(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO
: MediaItemType.MEDIA_TYPE_IMAGE)
.setPostId(childNode.getString(Constants.EXTRAS_ID))
.setDisplayUrl(isChildVideo ? childNode.getString("video_url")
: childNode.getString("display_url"))
.setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl
: childNode.getString("display_url"))
.setVideoViews(childNode.optLong("video_view_count", 0))
.setHeight(height)
.setWidth(width)
.build();
// Log.d(TAG, "getSliderItems: sliderItem: " + sliderItem);
sliderItems.add(sliderItem);
}
return sliderItems;
}
}

View File

@ -150,157 +150,13 @@ public class FeedService extends BaseService {
final JSONArray feedItems = timelineFeed.getJSONArray("edges");
for (int i = 0; i < feedItems.length(); ++i) {
final JSONObject feedItem = feedItems.getJSONObject(i).getJSONObject("node");
final String mediaType = feedItem.optString("__typename");
if (mediaType.isEmpty() || "GraphSuggestedUserFeedUnit".equals(mediaType))
final JSONObject itemJson = feedItems.optJSONObject(i);
if (itemJson == null) {
continue;
final boolean isVideo = feedItem.optBoolean("is_video");
final long videoViews = feedItem.optLong("video_view_count", 0);
final String displayUrl = feedItem.optString("display_url");
if (TextUtils.isEmpty(displayUrl)) continue;
final String resourceUrl;
if (isVideo) {
resourceUrl = feedItem.getString("video_url");
} else {
resourceUrl = feedItem.has("display_resources") ? ResponseBodyUtils.getHighQualityImage(feedItem) : displayUrl;
}
ProfileModel profileModel = null;
if (feedItem.has("owner")) {
final JSONObject owner = feedItem.getJSONObject("owner");
profileModel = new ProfileModel(
owner.optBoolean("is_private"),
false, // if you can see it then you def follow
owner.optBoolean("is_verified"),
owner.getString(Constants.EXTRAS_ID),
owner.getString(Constants.EXTRAS_USERNAME),
owner.optString("full_name"),
null,
null,
owner.getString("profile_pic_url"),
null,
0,
0,
0,
false,
false,
false,
false);
}
JSONObject tempJsonObject = feedItem.optJSONObject("edge_media_preview_comment");
final long commentsCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0;
tempJsonObject = feedItem.optJSONObject("edge_media_preview_like");
final long likesCount = tempJsonObject != null ? tempJsonObject.optLong("count") : 0;
tempJsonObject = feedItem.optJSONObject("edge_media_to_caption");
final JSONArray captions = tempJsonObject != null ? tempJsonObject.getJSONArray("edges") : null;
String captionText = null;
if (captions != null && captions.length() > 0) {
if ((tempJsonObject = captions.optJSONObject(0)) != null &&
(tempJsonObject = tempJsonObject.optJSONObject("node")) != null) {
captionText = tempJsonObject.getString("text");
}
}
final JSONObject location = feedItem.optJSONObject("location");
// Log.d(TAG, "location: " + (location == null ? null : location.toString()));
String locationId = null;
String locationName = null;
if (location != null) {
locationName = location.optString("name");
if (location.has("id")) {
locationId = location.getString("id");
} else if (location.has("pk")) {
locationId = location.getString("pk");
}
// Log.d(TAG, "locationId: " + locationId);
}
int height = 0;
int width = 0;
final JSONObject dimensions = feedItem.optJSONObject("dimensions");
if (dimensions != null) {
height = dimensions.optInt("height");
width = dimensions.optInt("width");
}
String thumbnailUrl = null;
try {
thumbnailUrl = feedItem.getJSONArray("display_resources")
.getJSONObject(0)
.getString("src");
} catch (JSONException ignored) {}
final FeedModel.Builder feedModelBuilder = new FeedModel.Builder()
.setProfileModel(profileModel)
.setItemType(isVideo ? MediaItemType.MEDIA_TYPE_VIDEO
: MediaItemType.MEDIA_TYPE_IMAGE)
.setViewCount(videoViews)
.setPostId(feedItem.getString(Constants.EXTRAS_ID))
.setDisplayUrl(resourceUrl)
.setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl : displayUrl)
.setShortCode(feedItem.getString(Constants.EXTRAS_SHORTCODE))
.setPostCaption(captionText)
.setCommentsCount(commentsCount)
.setTimestamp(feedItem.optLong("taken_at_timestamp", -1))
.setLiked(feedItem.getBoolean("viewer_has_liked"))
.setBookmarked(feedItem.getBoolean("viewer_has_saved"))
.setLikesCount(likesCount)
.setLocationName(locationName)
.setLocationId(locationId)
.setImageHeight(height)
.setImageWidth(width);
final boolean isSlider = "GraphSidecar".equals(mediaType) && feedItem.has("edge_sidecar_to_children");
if (isSlider) {
feedModelBuilder.setItemType(MediaItemType.MEDIA_TYPE_SLIDER);
final JSONObject sidecar = feedItem.optJSONObject("edge_sidecar_to_children");
if (sidecar != null) {
final JSONArray children = sidecar.optJSONArray("edges");
if (children != null) {
final List<PostChild> sliderItems = getSliderItems(children);
feedModelBuilder.setSliderItems(sliderItems);
}
}
}
final FeedModel feedModel = feedModelBuilder.build();
final FeedModel feedModel = ResponseBodyUtils.parseItem(itemJson);
feedModels.add(feedModel);
}
return new PostsFetchResponse(feedModels, hasNextPage, endCursor);
}
@NonNull
private List<PostChild> getSliderItems(final JSONArray children) throws JSONException {
final List<PostChild> sliderItems = new ArrayList<>();
for (int j = 0; j < children.length(); ++j) {
final JSONObject childNode = children.optJSONObject(j).getJSONObject("node");
final boolean isChildVideo = childNode.optBoolean("is_video");
int height = 0;
int width = 0;
final JSONObject dimensions = childNode.optJSONObject("dimensions");
if (dimensions != null) {
height = dimensions.optInt("height");
width = dimensions.optInt("width");
}
String thumbnailUrl = null;
try {
thumbnailUrl = childNode.getJSONArray("display_resources")
.getJSONObject(0)
.getString("src");
} catch (JSONException ignored) {}
final PostChild sliderItem = new PostChild.Builder()
.setItemType(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO
: MediaItemType.MEDIA_TYPE_IMAGE)
.setPostId(childNode.getString(Constants.EXTRAS_ID))
.setDisplayUrl(isChildVideo ? childNode.getString("video_url")
: childNode.getString("display_url"))
.setThumbnailUrl(thumbnailUrl != null ? thumbnailUrl
: childNode.getString("display_url"))
.setVideoViews(childNode.optLong("video_view_count", 0))
.setHeight(height)
.setWidth(width)
.build();
// Log.d(TAG, "getSliderItems: sliderItem: " + sliderItem);
sliderItems.add(sliderItem);
}
return sliderItems;
}
}

View File

@ -12,7 +12,9 @@ import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import awais.instagrabber.models.FeedModel;
@ -186,6 +188,82 @@ public class TagsService extends BaseService {
return feedModels;
}
public void fetchGraphQLPosts(@NonNull final String tag,
final String maxId,
final ServiceCallback<TagPostsFetchResponse> callback) {
final Map<String, String> queryMap = new HashMap<>();
queryMap.put("query_hash", "9b498c08113f1e09617a1703c22b2f32");
queryMap.put("variables", "{" +
"\"tag_name\":\"" + tag + "\"," +
"\"first\":25," +
"\"after\":\"" + (maxId == null ? "" : maxId) + "\"" +
"}");
final Call<String> request = webRepository.fetchGraphQLPosts(queryMap);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
try {
if (callback == null) {
return;
}
final String body = response.body();
if (TextUtils.isEmpty(body)) {
callback.onSuccess(null);
return;
}
final TagPostsFetchResponse tagPostsFetchResponse = parseGraphQLResponse(body);
callback.onSuccess(tagPostsFetchResponse);
} catch (JSONException e) {
Log.e(TAG, "onResponse", e);
callback.onFailure(e);
}
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
private TagPostsFetchResponse parseGraphQLResponse(@NonNull final String body) throws JSONException {
final JSONObject rootroot = new JSONObject(body);
final JSONObject root = rootroot.getJSONObject("data").getJSONObject("hashtag").getJSONObject("edge_hashtag_to_media");
final boolean moreAvailable = root.getJSONObject("page_info").optBoolean("has_next_page");
final String nextMaxId = root.getJSONObject("page_info").optString("end_cursor");
final int numResults = root.optInt("count");
final String status = rootroot.optString("status");
final JSONArray itemsJson = root.optJSONArray("edges");
final List<FeedModel> items = parseGraphQLItems(itemsJson);
return new TagPostsFetchResponse(
moreAvailable,
nextMaxId,
numResults,
status,
items
);
}
private List<FeedModel> parseGraphQLItems(final JSONArray items) throws JSONException {
if (items == null) {
return Collections.emptyList();
}
final List<FeedModel> feedModels = new ArrayList<>();
for (int i = 0; i < items.length(); i++) {
final JSONObject itemJson = items.optJSONObject(i);
if (itemJson == null) {
continue;
}
final FeedModel feedModel = ResponseBodyUtils.parseGraphQLItem(itemJson);
if (feedModel != null) {
feedModels.add(feedModel);
}
}
return feedModels;
}
public static class TagPostsFetchResponse {
private boolean moreAvailable;
private String nextMaxId;