activity: support for app inbox #114, better onclick, etc

This commit is contained in:
Austin Huang 2020-12-25 16:41:46 -05:00
parent d5161ac2ea
commit 49ba524305
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
13 changed files with 441 additions and 209 deletions

View File

@ -76,16 +76,23 @@ public final class NotificationsAdapter extends ListAdapter<NotificationModel, N
private List<NotificationModel> sort(final List<NotificationModel> list) {
final List<NotificationModel> listCopy = new ArrayList<>(list);
Collections.sort(listCopy, (o1, o2) -> {
if (o1.getType() == o2.getType()) return 0;
// keep requests at top
if (o1.getType() == NotificationType.REQUEST) return -1;
if (o2.getType() == NotificationType.REQUEST) return 1;
return 0;
if (o1.getType() == o2.getType()
&& o1.getType() == NotificationType.REQUEST
&& o2.getType() == NotificationType.REQUEST) return 0;
else if (o1.getType() == NotificationType.REQUEST) return -1;
else if (o2.getType() == NotificationType.REQUEST) return 1;
// timestamp
return o1.getTimestamp() > o2.getTimestamp() ? -1 : (o1.getTimestamp() == o2.getTimestamp() ? 0 : 1);
});
return listCopy;
}
public interface OnNotificationClickListener {
void onNotificationClick(final NotificationModel model);
void onProfileClick(final String username);
void onPreviewClick(final NotificationModel model);
}
}

View File

@ -24,10 +24,6 @@ public final class NotificationViewHolder extends RecyclerView.ViewHolder {
public void bind(final NotificationModel model,
final OnNotificationClickListener notificationClickListener) {
if (model == null) return;
itemView.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onNotificationClick(model);
});
int text = -1;
CharSequence subtext = null;
switch (model.getType()) {
@ -52,20 +48,47 @@ public final class NotificationViewHolder extends RecyclerView.ViewHolder {
text = R.string.request_notif;
subtext = model.getText();
break;
case COMMENT_LIKE:
case TAGGED_COMMENT:
case RESPONDED_STORY:
subtext = model.getText();
break;
}
binding.tvUsername.setText(model.getUsername());
binding.tvComment.setText(text);
binding.tvSubComment.setText(subtext, subtext instanceof Spannable ? TextView.BufferType.SPANNABLE : TextView.BufferType.NORMAL);
// binding.tvSubComment.setMentionClickListener(mentionClickListener);
if (text == -1 && subtext != null) {
binding.tvComment.setText(subtext);
binding.tvSubComment.setVisibility(View.GONE);
}
else if (text != -1) {
binding.tvComment.setText(text);
binding.tvSubComment.setText(subtext);
binding.tvSubComment.setVisibility(subtext == null ? View.GONE : View.VISIBLE);
}
if (model.getType() != NotificationType.REQUEST) {
binding.tvDate.setText(model.getDateTime());
}
binding.tvUsername.setText(model.getUsername());
binding.ivProfilePic.setImageURI(model.getProfilePic());
binding.ivProfilePic.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onProfileClick(model.getUsername());
});
if (TextUtils.isEmpty(model.getPreviewPic())) {
binding.ivPreviewPic.setVisibility(View.GONE);
binding.ivPreviewPic.setVisibility(View.INVISIBLE);
} else {
binding.ivPreviewPic.setVisibility(View.VISIBLE);
binding.ivPreviewPic.setImageURI(model.getPreviewPic());
binding.ivPreviewPic.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onPreviewClick(model);
});
}
itemView.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onNotificationClick(model);
});
}
}

View File

@ -16,12 +16,21 @@ import awais.instagrabber.utils.TextUtils;
public class GetActivityAsyncTask extends AsyncTask<String, Void, GetActivityAsyncTask.NotificationCounts> {
private static final String TAG = "GetActivityAsyncTask";
private OnTaskCompleteListener onTaskCompleteListener;
private final OnTaskCompleteListener onTaskCompleteListener;
public GetActivityAsyncTask(final OnTaskCompleteListener onTaskCompleteListener) {
this.onTaskCompleteListener = onTaskCompleteListener;
}
/*
This needs to be redone to fetch i inbox instead
Within inbox, data is (body JSON => counts)
Then we have these counts:
new_posts, activity_feed_dot_badge, relationships, campaign_notification
usertags, likes, comment_likes, shopping_notification, comments
photos_of_you (not sure about difference to usertags), requests
*/
protected NotificationCounts doInBackground(final String... cookiesArray) {
if (cookiesArray == null) return null;
final String cookie = cookiesArray[0];
@ -70,11 +79,11 @@ public class GetActivityAsyncTask extends AsyncTask<String, Void, GetActivityAsy
}
public static class NotificationCounts {
private int relationshipsCount;
private int userTagsCount;
private int commentsCount;
private int commentLikesCount;
private int likesCount;
private final int relationshipsCount;
private final int userTagsCount;
private final int commentsCount;
private final int commentLikesCount;
private final int likesCount;
public NotificationCounts(final int relationshipsCount,
final int userTagsCount,

View File

@ -3,21 +3,14 @@ package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.NotificationModel;
import awais.instagrabber.models.enums.NotificationType;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.LocaleUtils;
import awais.instagrabber.utils.NetworkUtils;
import awais.instagrabber.webservices.NewsService;
import awais.instagrabber.webservices.ServiceCallback;
import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Utils.logCollector;
@ -26,89 +19,48 @@ public final class NotificationsFetcher extends AsyncTask<Void, Void, List<Notif
private static final String TAG = "NotificationsFetcher";
private final FetchListener<List<NotificationModel>> fetchListener;
private final NewsService newsService;
private final boolean markAsSeen;
private boolean fetchedWeb = false;
public NotificationsFetcher(final FetchListener<List<NotificationModel>> fetchListener) {
public NotificationsFetcher(final boolean markAsSeen,
final FetchListener<List<NotificationModel>> fetchListener) {
this.markAsSeen = markAsSeen;
this.fetchListener = fetchListener;
newsService = NewsService.getInstance();
}
@Override
protected List<NotificationModel> doInBackground(final Void... voids) {
List<NotificationModel> result = new ArrayList<>();
final String url = "https://www.instagram.com/accounts/activity/?__a=1";
try {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setInstanceFollowRedirects(false);
conn.setUseCaches(false);
conn.setRequestProperty("Accept-Language", LocaleUtils.getCurrentLocale().getLanguage() + ",en-US;q=0.8");
conn.connect();
List<NotificationModel> notificationModels = new ArrayList<>();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
final JSONObject page = new JSONObject(NetworkUtils.readFromConnection(conn))
.getJSONObject("graphql")
.getJSONObject("user");
final JSONObject ewaf = page.getJSONObject("activity_feed")
.optJSONObject("edge_web_activity_feed");
final JSONObject efr = page.optJSONObject("edge_follow_requests");
JSONObject data;
JSONArray media;
if (ewaf != null
&& (media = ewaf.optJSONArray("edges")) != null
&& media.length() > 0
&& media.optJSONObject(0).optJSONObject("node") != null) {
for (int i = 0; i < media.length(); ++i) {
data = media.optJSONObject(i).optJSONObject("node");
if (data == null) continue;
final String type = data.getString("__typename");
final NotificationType notificationType = NotificationType.valueOfType(type);
if (notificationType == null) continue;
final JSONObject user = data.getJSONObject("user");
result.add(new NotificationModel(
data.getString(Constants.EXTRAS_ID),
data.optString("text"), // comments or mentions
data.getLong("timestamp"),
user.getString("id"),
user.getString("username"),
user.getString("profile_pic_url"),
!data.isNull("media") ? data.getJSONObject("media").getString("shortcode") : null,
!data.isNull("media") ? data.getJSONObject("media").getString("thumbnail_src") : null, notificationType));
}
newsService.fetchAppInbox(markAsSeen, new ServiceCallback<List<NotificationModel>>() {
@Override
public void onSuccess(final List<NotificationModel> result) {
if (result == null) return;
notificationModels.addAll(result);
if (fetchedWeb) {
fetchListener.onResult(notificationModels);
}
if (efr != null
&& (media = efr.optJSONArray("edges")) != null
&& media.length() > 0
&& media.optJSONObject(0).optJSONObject("node") != null) {
for (int i = 0; i < media.length(); ++i) {
data = media.optJSONObject(i).optJSONObject("node");
if (data == null) continue;
result.add(new NotificationModel(
data.getString(Constants.EXTRAS_ID),
data.optString("full_name"),
0L,
data.getString(Constants.EXTRAS_ID),
data.getString("username"),
data.getString("profile_pic_url"),
null,
null, NotificationType.REQUEST));
}
else {
fetchedWeb = true;
newsService.fetchWebInbox(markAsSeen, this);
}
}
conn.disconnect();
} catch (final Exception e) {
if (logCollector != null)
logCollector.appendException(e, LogCollector.LogFile.ASYNC_NOTIFICATION_FETCHER, "doInBackground");
if (BuildConfig.DEBUG) Log.e(TAG, "", e);
}
return result;
@Override
public void onFailure(final Throwable t) {
// Log.e(TAG, "onFailure: ", t);
if (fetchListener != null) {
fetchListener.onFailure(t);
}
}
});
return notificationModels;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final List<NotificationModel> result) {
if (fetchListener != null) fetchListener.onResult(result);
}
}

View File

@ -18,6 +18,8 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.NotificationManagerCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
@ -30,8 +32,11 @@ import awais.instagrabber.adapters.NotificationsAdapter.OnNotificationClickListe
import awais.instagrabber.asyncs.NotificationsFetcher;
import awais.instagrabber.asyncs.PostFetcher;
import awais.instagrabber.databinding.FragmentNotificationsViewerBinding;
import awais.instagrabber.dialogs.ProfilePicDialogFragment;
import awais.instagrabber.fragments.settings.MorePreferencesFragmentDirections;
import awais.instagrabber.interfaces.MentionClickListener;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.NotificationModel;
import awais.instagrabber.models.enums.NotificationType;
import awais.instagrabber.repositories.responses.FriendshipRepoChangeRootResponse;
import awais.instagrabber.utils.Constants;
@ -40,8 +45,10 @@ import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.NotificationViewModel;
import awais.instagrabber.webservices.FriendshipService;
import awais.instagrabber.webservices.MediaService;
import awais.instagrabber.webservices.NewsService;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesService;
import static awais.instagrabber.utils.Utils.settingsHelper;
@ -53,96 +60,153 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
private boolean shouldRefresh = true;
private NotificationViewModel notificationViewModel;
private FriendshipService friendshipService;
private MediaService mediaService;
private StoriesService storiesService;
private String userId;
private String csrfToken;
private NewsService newsService;
private Context context;
private final OnNotificationClickListener clickListener = model -> {
if (model == null) return;
final String username = model.getUsername();
final SpannableString title = new SpannableString(username + (TextUtils.isEmpty(model.getText()) ? "" : (":\n" + model.getText())));
title.setSpan(new RelativeSizeSpan(1.23f), 0, username.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
String[] commentDialogList;
if (model.getShortCode() != null) {
commentDialogList = new String[]{
getString(R.string.open_profile),
getString(R.string.view_post)
};
} else if (model.getType() == NotificationType.REQUEST) {
commentDialogList = new String[]{
getString(R.string.open_profile),
getString(R.string.request_approve),
getString(R.string.request_reject)
};
} else {
commentDialogList = new String[]{getString(R.string.open_profile)};
private final OnNotificationClickListener clickListener = new OnNotificationClickListener() {
@Override
public void onProfileClick(final String username) {
openProfile(username);
}
final Context context = getContext();
if (context == null) return;
final DialogInterface.OnClickListener profileDialogListener = (dialog, which) -> {
switch (which) {
case 0:
openProfile(model.getUsername());
break;
case 1:
if (model.getType() == NotificationType.REQUEST) {
friendshipService.approve(userId, model.getUserId(), csrfToken, new ServiceCallback<FriendshipRepoChangeRootResponse>() {
@Override
public void onSuccess(final FriendshipRepoChangeRootResponse result) {
// Log.d(TAG, "onSuccess: " + result);
if (result.getStatus().equals("ok")) {
onRefresh();
return;
}
Log.e(TAG, "approve: status was not ok!");
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "approve: onFailure: ", t);
}
});
return;
}
final AlertDialog alertDialog = new AlertDialog.Builder(context)
.setCancelable(false)
.setView(R.layout.dialog_opening_post)
.create();
alertDialog.show();
new PostFetcher(model.getShortCode(), feedModel -> {
@Override
public void onPreviewClick(final NotificationModel model) {
if (model.getType() == NotificationType.RESPONDED_STORY) {
showProfilePicDialog(model);
}
else {
mediaService.fetch(model.getPostId(), new ServiceCallback<FeedModel>() {
@Override
public void onSuccess(final FeedModel feedModel) {
final PostViewV2Fragment fragment = PostViewV2Fragment
.builder(feedModel)
.build();
fragment.setOnShowListener(dialog1 -> alertDialog.dismiss());
fragment.show(getChildFragmentManager(), "post_view");
}).execute();
break;
case 2:
friendshipService.ignore(userId, model.getUserId(), csrfToken, new ServiceCallback<FriendshipRepoChangeRootResponse>() {
@Override
public void onSuccess(final FriendshipRepoChangeRootResponse result) {
// Log.d(TAG, "onSuccess: " + result);
if (result.getStatus().equals("ok")) {
onRefresh();
}
@Override
public void onFailure(final Throwable t) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
}
});
}
}
@Override
public void onNotificationClick(final NotificationModel model) {
if (model == null) return;
final String username = model.getUsername();
if (model.getType() == NotificationType.FOLLOW) {
openProfile(username);
}
else {
final SpannableString title = new SpannableString(username + (TextUtils.isEmpty(model.getText()) ? "" : (":\n" + model.getText())));
title.setSpan(new RelativeSizeSpan(1.23f), 0, username.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
String[] commentDialogList;
if (model.getType() == NotificationType.RESPONDED_STORY) {
commentDialogList = new String[]{
getString(R.string.open_profile),
getString(R.string.view_story)
};
}
else if (model.getPostId() != null) {
commentDialogList = new String[]{
getString(R.string.open_profile),
getString(R.string.view_post)
};
}
else if (model.getType() == NotificationType.REQUEST) {
commentDialogList = new String[]{
getString(R.string.open_profile),
getString(R.string.request_approve),
getString(R.string.request_reject)
};
}
else commentDialogList = null; // shouldn't happen
final Context context = getContext();
if (context == null) return;
final DialogInterface.OnClickListener profileDialogListener = (dialog, which) -> {
switch (which) {
case 0:
openProfile(username);
break;
case 1:
if (model.getType() == NotificationType.REQUEST) {
friendshipService.approve(userId, model.getUserId(), csrfToken, new ServiceCallback<FriendshipRepoChangeRootResponse>() {
@Override
public void onSuccess(final FriendshipRepoChangeRootResponse result) {
// Log.d(TAG, "onSuccess: " + result);
if (result.getStatus().equals("ok")) {
onRefresh();
return;
}
Log.e(TAG, "approve: status was not ok!");
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "approve: onFailure: ", t);
}
});
return;
}
Log.e(TAG, "ignore: status was not ok!");
}
else if (model.getType() == NotificationType.RESPONDED_STORY) {
showProfilePicDialog(model);
return;
}
final AlertDialog alertDialog = new AlertDialog.Builder(context)
.setCancelable(false)
.setView(R.layout.dialog_opening_post)
.create();
alertDialog.show();
mediaService.fetch(model.getPostId(), new ServiceCallback<FeedModel>() {
@Override
public void onSuccess(final FeedModel feedModel) {
final PostViewV2Fragment fragment = PostViewV2Fragment
.builder(feedModel)
.build();
fragment.setOnShowListener(dialog1 -> alertDialog.dismiss());
fragment.show(getChildFragmentManager(), "post_view");
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "ignore: onFailure: ", t);
}
});
break;
@Override
public void onFailure(final Throwable t) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
}
});
break;
case 2:
friendshipService.ignore(userId, model.getUserId(), csrfToken, new ServiceCallback<FriendshipRepoChangeRootResponse>() {
@Override
public void onSuccess(final FriendshipRepoChangeRootResponse result) {
// Log.d(TAG, "onSuccess: " + result);
if (result.getStatus().equals("ok")) {
onRefresh();
return;
}
Log.e(TAG, "ignore: status was not ok!");
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "ignore: onFailure: ", t);
}
});
break;
}
};
new AlertDialog.Builder(context)
.setTitle(title)
.setItems(commentDialogList, profileDialogListener)
.setNegativeButton(R.string.cancel, null)
.show();
}
};
new AlertDialog.Builder(context)
.setTitle(title)
.setItems(commentDialogList, profileDialogListener)
.setNegativeButton(R.string.cancel, null)
.show();
}
};
private final MentionClickListener mentionClickListener = (view, text, isHashtag, isLocation) -> {
if (getContext() == null) return;
@ -158,7 +222,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = getContext();
context = getContext();
if (context == null) return;
NotificationManagerCompat.from(context.getApplicationContext()).cancel(Constants.ACTIVITY_NOTIFICATION_ID);
final String cookie = Utils.settingsHelper.getString(Constants.COOKIE);
@ -167,6 +231,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
}
friendshipService = FriendshipService.getInstance();
newsService = NewsService.getInstance();
mediaService = MediaService.getInstance();
userId = CookieUtils.getUserIdFromCookie(cookie);
csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
}
@ -205,22 +270,9 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
@Override
public void onRefresh() {
binding.swipeRefreshLayout.setRefreshing(true);
new NotificationsFetcher(notificationModels -> {
new NotificationsFetcher(true, notificationModels -> {
binding.swipeRefreshLayout.setRefreshing(false);
notificationViewModel.getList().postValue(notificationModels);
final String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
newsService.markChecked(timestamp, csrfToken, new ServiceCallback<Boolean>() {
@Override
public void onSuccess(@NonNull final Boolean result) {
// Log.d(TAG, "onResponse: body: " + result);
if (!result) Log.e(TAG, "onSuccess: Error marking activity checked, response is false");
}
@Override
public void onFailure(final Throwable t) {
Log.e(TAG, "onFailure: Error marking activity checked", t);
}
});
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@ -229,4 +281,15 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
.actionGlobalProfileFragment("@" + username);
NavHostFragment.findNavController(this).navigate(action);
}
private void showProfilePicDialog(final NotificationModel model) {
final FragmentManager fragmentManager = getParentFragmentManager();
final ProfilePicDialogFragment fragment = new ProfilePicDialogFragment(model.getPostId(),
model.getUsername(),
model.getPreviewPic());
final FragmentTransaction ft = fragmentManager.beginTransaction();
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.add(fragment, "profilePicDialog")
.commit();
}
}

View File

@ -13,7 +13,7 @@ public final class NotificationModel {
private final String userId;
private final String username;
private final String profilePicUrl;
private final String shortCode;
private final String postId;
private final String previewUrl;
private final NotificationType type;
private final CharSequence text;
@ -25,7 +25,7 @@ public final class NotificationModel {
final String userId,
final String username,
final String profilePicUrl,
final String shortCode,
final String postId,
final String previewUrl,
final NotificationType type) {
this.id = id;
@ -34,7 +34,7 @@ public final class NotificationModel {
this.userId = userId;
this.username = username;
this.profilePicUrl = profilePicUrl;
this.shortCode = shortCode;
this.postId = postId;
this.previewUrl = previewUrl;
this.type = type;
}
@ -47,6 +47,10 @@ public final class NotificationModel {
return text;
}
public long getTimestamp() {
return timestamp;
}
@NonNull
public String getDateTime() {
return Utils.datetimeParser.format(new Date(timestamp * 1000L));
@ -64,8 +68,8 @@ public final class NotificationModel {
return profilePicUrl;
}
public String getShortCode() {
return shortCode;
public String getPostId() {
return postId;
}
public String getPreviewPic() {

View File

@ -5,11 +5,17 @@ import java.util.HashMap;
import java.util.Map;
public enum NotificationType implements Serializable {
// web
LIKE("GraphLikeAggregatedStory"),
FOLLOW("GraphFollowAggregatedStory"),
COMMENT("GraphCommentMediaStory"),
MENTION("GraphMentionStory"),
TAGGED("GraphUserTaggedStory"),
// app story_type
COMMENT_LIKE("13"),
TAGGED_COMMENT("14"),
RESPONDED_STORY("213"),
// efr
REQUEST("REQUEST");
private final String itemType;

View File

@ -12,6 +12,9 @@ import retrofit2.http.Path;
import retrofit2.http.QueryMap;
public interface MediaRepository {
@GET("/api/v1/media/{mediaId}/info/")
Call<String> fetch(@Path("mediaId") final String mediaId);
@GET("/api/v1/media/{mediaId}/likers/")
Call<String> fetchLikes(@Path("mediaId") final String mediaId);

View File

@ -6,16 +6,19 @@ import awais.instagrabber.utils.Constants;
import retrofit2.Call;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface NewsRepository {
Call<String> inbox();
@FormUrlEncoded
@Headers("User-Agent: " + Constants.USER_AGENT)
@POST("https://www.instagram.com/web/activity/mark_checked/")
Call<String> markChecked(@Header("x-csrftoken") String csrfToken, @FieldMap Map<String, String> map);
@GET("https://www.instagram.com/accounts/activity/?__a=1")
Call<String> webInbox();
@Headers("User-Agent: " + Constants.I_USER_AGENT)
@GET("/api/v1/news/inbox/")
Call<String> appInbox(@Query(value = "mark_as_seen", encoded = true) boolean markAsSeen);
}

View File

@ -16,9 +16,11 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.repositories.MediaRepository;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
import retrofit2.Call;
import retrofit2.Callback;
@ -46,6 +48,37 @@ public class MediaService extends BaseService {
return instance;
}
public void fetch(final String mediaId,
final ServiceCallback<FeedModel> callback) {
final Call<String> request = repository.fetch(mediaId);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call,
@NonNull final Response<String> response) {
if (callback == null) return;
final String body = response.body();
if (body == null) {
callback.onSuccess(null);
return;
}
try {
final JSONObject itemJson = new JSONObject(body).getJSONArray("items").getJSONObject(0);
callback.onSuccess(ResponseBodyUtils.parseItem(itemJson));
} catch (JSONException e) {
callback.onFailure(e);
}
}
@Override
public void onFailure(@NonNull final Call<String> call,
@NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
public void like(final String mediaId,
final String userId,
final String csrfToken,

View File

@ -1,14 +1,25 @@
package awais.instagrabber.webservices;
import android.util.Log;
import androidx.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import awais.instagrabber.models.NotificationModel;
import awais.instagrabber.models.enums.NotificationType;
import awais.instagrabber.repositories.NewsRepository;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.NetworkUtils;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -35,25 +46,34 @@ public class NewsService extends BaseService {
return instance;
}
public void markChecked(final String timestamp,
final String csrfToken,
final ServiceCallback<Boolean> callback) {
final Map<String, String> map = new HashMap<>();
map.put("timestamp", timestamp);
final Call<String> request = repository.markChecked(csrfToken, map);
public void fetchAppInbox(final boolean markAsSeen,
final ServiceCallback<List<NotificationModel>> callback) {
final List<NotificationModel> result = new ArrayList<>();
final Call<String> request = repository.appInbox(markAsSeen);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
final String body = response.body();
if (body == null) {
callback.onSuccess(false);
callback.onSuccess(null);
return;
}
try {
final JSONObject jsonObject = new JSONObject(body);
final String status = jsonObject.optString("status");
callback.onSuccess(status.equals("ok"));
final JSONArray oldStories = jsonObject.getJSONArray("old_stories"),
newStories = jsonObject.getJSONArray("new_stories");
for (int j = 0; j < newStories.length(); ++j) {
final NotificationModel newsItem = parseNewsItem(newStories.getJSONObject(j));
if (newsItem != null) result.add(newsItem);
}
for (int i = 0; i < oldStories.length(); ++i) {
final NotificationModel newsItem = parseNewsItem(oldStories.getJSONObject(i));
if (newsItem != null) result.add(newsItem);
}
callback.onSuccess(result);
} catch (JSONException e) {
callback.onFailure(e);
}
@ -66,4 +86,112 @@ public class NewsService extends BaseService {
}
});
}
public void fetchWebInbox(final boolean markAsSeen,
final ServiceCallback<List<NotificationModel>> callback) {
final List<NotificationModel> result = new ArrayList<>();
final Call<String> request = repository.webInbox();
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
final String body = response.body();
if (body == null) {
callback.onSuccess(null);
return;
}
try {
final JSONObject page = new JSONObject(body)
.getJSONObject("graphql")
.getJSONObject("user");
final JSONObject ewaf = page.getJSONObject("activity_feed")
.optJSONObject("edge_web_activity_feed");
final JSONObject efr = page.optJSONObject("edge_follow_requests");
JSONObject data;
JSONArray media;
if (ewaf != null
&& (media = ewaf.optJSONArray("edges")) != null
&& media.length() > 0
&& media.optJSONObject(0).optJSONObject("node") != null) {
for (int i = 0; i < media.length(); ++i) {
data = media.optJSONObject(i).optJSONObject("node");
if (data == null) continue;
final String type = data.getString("__typename");
final NotificationType notificationType = NotificationType.valueOfType(type);
if (notificationType == null) continue;
final JSONObject user = data.getJSONObject("user");
result.add(new NotificationModel(
data.getString(Constants.EXTRAS_ID),
data.optString("text"), // comments or mentions
data.getLong("timestamp"),
user.getString("id"),
user.getString("username"),
user.getString("profile_pic_url"),
!data.isNull("media") ? data.getJSONObject("media").getString("id") : null,
!data.isNull("media") ? data.getJSONObject("media").getString("thumbnail_src") : null, notificationType));
}
}
if (efr != null
&& (media = efr.optJSONArray("edges")) != null
&& media.length() > 0
&& media.optJSONObject(0).optJSONObject("node") != null) {
for (int i = 0; i < media.length(); ++i) {
data = media.optJSONObject(i).optJSONObject("node");
if (data == null) continue;
result.add(new NotificationModel(
data.getString(Constants.EXTRAS_ID),
data.optString("full_name"),
0L,
data.getString(Constants.EXTRAS_ID),
data.getString("username"),
data.getString("profile_pic_url"),
null,
null, NotificationType.REQUEST));
}
}
callback.onSuccess(result);
} catch (JSONException e) {
callback.onFailure(e);
}
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
callback.onFailure(t);
// Log.e(TAG, "onFailure: ", t);
}
});
}
private NotificationModel parseNewsItem(final JSONObject itemJson) throws JSONException {
if (itemJson == null) return null;
final String type = itemJson.getString("story_type");
final NotificationType notificationType = NotificationType.valueOfType(type);
if (notificationType == null) {
Log.d("austin_debug", "unhandled news type: "+itemJson);
return null;
}
final JSONObject data = itemJson.getJSONObject("args");
return new NotificationModel(
data.getString("tuuid"),
data.has("text") ? data.getString("text") : cleanRichText(data.optString("rich_text", "")),
data.getLong("timestamp"),
data.getString("profile_id"),
data.getString("profile_name"),
data.getString("profile_image"),
!data.isNull("media") ? data.getJSONArray("media").getJSONObject(0).getString("id") : null,
!data.isNull("media") ? data.getJSONArray("media").getJSONObject(0).getString("image") : null,
notificationType);
}
private String cleanRichText(final String raw) {
final Matcher matcher = Pattern.compile("\\{[\\p{L}\\d._]+\\|000000\\|1\\|user\\?id=\\d+\\}").matcher(raw);
String result = raw;
while (matcher.find()) {
final String richObject = raw.substring(matcher.start(), matcher.end());
final String username = richObject.split("\\|")[0].substring(1);
result = result.replace(richObject, username);
}
return result;
}
}

View File

@ -105,13 +105,13 @@
android:gravity="end"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingEnd="16dp"
android:paddingEnd="4dp"
android:paddingRight="16dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:textStyle="italic"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivPreviewPic"
app:layout_constraintStart_toEndOf="@id/ivProfilePic"
app:layout_constraintTop_toBottomOf="@id/tvSubComment"
tools:text="some long long long long long date" />

View File

@ -145,6 +145,7 @@
<string name="quick_access_cannot_delete_curr">Cannot delete currently in use account</string>
<string name="quick_access_confirm_delete">Are you sure you want to delete \'%s\'?</string>
<string name="open_profile">Open profile</string>
<string name="view_story">View story</string>
<string name="view_pfp">View profile picture</string>
<string name="direct_messages_you">You</string>
<string name="direct_messages_sent_link">Shared a link</string>