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) { private List<NotificationModel> sort(final List<NotificationModel> list) {
final List<NotificationModel> listCopy = new ArrayList<>(list); final List<NotificationModel> listCopy = new ArrayList<>(list);
Collections.sort(listCopy, (o1, o2) -> { Collections.sort(listCopy, (o1, o2) -> {
if (o1.getType() == o2.getType()) return 0;
// keep requests at top // keep requests at top
if (o1.getType() == NotificationType.REQUEST) return -1; if (o1.getType() == o2.getType()
if (o2.getType() == NotificationType.REQUEST) return 1; && o1.getType() == NotificationType.REQUEST
return 0; && 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; return listCopy;
} }
public interface OnNotificationClickListener { public interface OnNotificationClickListener {
void onNotificationClick(final NotificationModel model); 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, public void bind(final NotificationModel model,
final OnNotificationClickListener notificationClickListener) { final OnNotificationClickListener notificationClickListener) {
if (model == null) return; if (model == null) return;
itemView.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onNotificationClick(model);
});
int text = -1; int text = -1;
CharSequence subtext = null; CharSequence subtext = null;
switch (model.getType()) { switch (model.getType()) {
@ -52,20 +48,47 @@ public final class NotificationViewHolder extends RecyclerView.ViewHolder {
text = R.string.request_notif; text = R.string.request_notif;
subtext = model.getText(); subtext = model.getText();
break; break;
case COMMENT_LIKE:
case TAGGED_COMMENT:
case RESPONDED_STORY:
subtext = model.getText();
break;
} }
binding.tvUsername.setText(model.getUsername()); if (text == -1 && subtext != null) {
binding.tvComment.setText(text); binding.tvComment.setText(subtext);
binding.tvSubComment.setText(subtext, subtext instanceof Spannable ? TextView.BufferType.SPANNABLE : TextView.BufferType.NORMAL); binding.tvSubComment.setVisibility(View.GONE);
// binding.tvSubComment.setMentionClickListener(mentionClickListener); }
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) { if (model.getType() != NotificationType.REQUEST) {
binding.tvDate.setText(model.getDateTime()); binding.tvDate.setText(model.getDateTime());
} }
binding.tvUsername.setText(model.getUsername());
binding.ivProfilePic.setImageURI(model.getProfilePic()); binding.ivProfilePic.setImageURI(model.getProfilePic());
binding.ivProfilePic.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onProfileClick(model.getUsername());
});
if (TextUtils.isEmpty(model.getPreviewPic())) { if (TextUtils.isEmpty(model.getPreviewPic())) {
binding.ivPreviewPic.setVisibility(View.GONE); binding.ivPreviewPic.setVisibility(View.INVISIBLE);
} else { } else {
binding.ivPreviewPic.setVisibility(View.VISIBLE); binding.ivPreviewPic.setVisibility(View.VISIBLE);
binding.ivPreviewPic.setImageURI(model.getPreviewPic()); 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> { public class GetActivityAsyncTask extends AsyncTask<String, Void, GetActivityAsyncTask.NotificationCounts> {
private static final String TAG = "GetActivityAsyncTask"; private static final String TAG = "GetActivityAsyncTask";
private OnTaskCompleteListener onTaskCompleteListener; private final OnTaskCompleteListener onTaskCompleteListener;
public GetActivityAsyncTask(final OnTaskCompleteListener onTaskCompleteListener) { public GetActivityAsyncTask(final OnTaskCompleteListener onTaskCompleteListener) {
this.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) { protected NotificationCounts doInBackground(final String... cookiesArray) {
if (cookiesArray == null) return null; if (cookiesArray == null) return null;
final String cookie = cookiesArray[0]; final String cookie = cookiesArray[0];
@ -70,11 +79,11 @@ public class GetActivityAsyncTask extends AsyncTask<String, Void, GetActivityAsy
} }
public static class NotificationCounts { public static class NotificationCounts {
private int relationshipsCount; private final int relationshipsCount;
private int userTagsCount; private final int userTagsCount;
private int commentsCount; private final int commentsCount;
private int commentLikesCount; private final int commentLikesCount;
private int likesCount; private final int likesCount;
public NotificationCounts(final int relationshipsCount, public NotificationCounts(final int relationshipsCount,
final int userTagsCount, final int userTagsCount,

View File

@ -3,21 +3,14 @@ package awais.instagrabber.asyncs;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.util.Log; 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.ArrayList;
import java.util.List; import java.util.List;
import awais.instagrabber.BuildConfig; import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.NotificationModel; import awais.instagrabber.models.NotificationModel;
import awais.instagrabber.models.enums.NotificationType; import awais.instagrabber.webservices.NewsService;
import awais.instagrabber.utils.Constants; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.utils.LocaleUtils;
import awais.instagrabber.utils.NetworkUtils;
import awaisomereport.LogCollector; import awaisomereport.LogCollector;
import static awais.instagrabber.utils.Utils.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 static final String TAG = "NotificationsFetcher";
private final FetchListener<List<NotificationModel>> fetchListener; 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; this.fetchListener = fetchListener;
newsService = NewsService.getInstance();
} }
@Override @Override
protected List<NotificationModel> doInBackground(final Void... voids) { protected List<NotificationModel> doInBackground(final Void... voids) {
List<NotificationModel> result = new ArrayList<>(); List<NotificationModel> notificationModels = 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();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { newsService.fetchAppInbox(markAsSeen, new ServiceCallback<List<NotificationModel>>() {
final JSONObject page = new JSONObject(NetworkUtils.readFromConnection(conn)) @Override
.getJSONObject("graphql") public void onSuccess(final List<NotificationModel> result) {
.getJSONObject("user"); if (result == null) return;
final JSONObject ewaf = page.getJSONObject("activity_feed") notificationModels.addAll(result);
.optJSONObject("edge_web_activity_feed"); if (fetchedWeb) {
final JSONObject efr = page.optJSONObject("edge_follow_requests"); fetchListener.onResult(notificationModels);
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));
}
} }
else {
if (efr != null fetchedWeb = true;
&& (media = efr.optJSONArray("edges")) != null newsService.fetchWebInbox(markAsSeen, this);
&& 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));
}
} }
} }
conn.disconnect();
} catch (final Exception e) { @Override
if (logCollector != null) public void onFailure(final Throwable t) {
logCollector.appendException(e, LogCollector.LogFile.ASYNC_NOTIFICATION_FETCHER, "doInBackground"); // Log.e(TAG, "onFailure: ", t);
if (BuildConfig.DEBUG) Log.e(TAG, "", e); if (fetchListener != null) {
} fetchListener.onFailure(t);
return result; }
}
});
return notificationModels;
} }
@Override @Override
protected void onPreExecute() { protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore(); 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.appcompat.app.AlertDialog;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavDirections; import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
@ -30,8 +32,11 @@ import awais.instagrabber.adapters.NotificationsAdapter.OnNotificationClickListe
import awais.instagrabber.asyncs.NotificationsFetcher; import awais.instagrabber.asyncs.NotificationsFetcher;
import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.asyncs.PostFetcher;
import awais.instagrabber.databinding.FragmentNotificationsViewerBinding; import awais.instagrabber.databinding.FragmentNotificationsViewerBinding;
import awais.instagrabber.dialogs.ProfilePicDialogFragment;
import awais.instagrabber.fragments.settings.MorePreferencesFragmentDirections; import awais.instagrabber.fragments.settings.MorePreferencesFragmentDirections;
import awais.instagrabber.interfaces.MentionClickListener; import awais.instagrabber.interfaces.MentionClickListener;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.NotificationModel;
import awais.instagrabber.models.enums.NotificationType; import awais.instagrabber.models.enums.NotificationType;
import awais.instagrabber.repositories.responses.FriendshipRepoChangeRootResponse; import awais.instagrabber.repositories.responses.FriendshipRepoChangeRootResponse;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
@ -40,8 +45,10 @@ import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.NotificationViewModel; import awais.instagrabber.viewmodels.NotificationViewModel;
import awais.instagrabber.webservices.FriendshipService; import awais.instagrabber.webservices.FriendshipService;
import awais.instagrabber.webservices.MediaService;
import awais.instagrabber.webservices.NewsService; import awais.instagrabber.webservices.NewsService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesService;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
@ -53,96 +60,153 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
private boolean shouldRefresh = true; private boolean shouldRefresh = true;
private NotificationViewModel notificationViewModel; private NotificationViewModel notificationViewModel;
private FriendshipService friendshipService; private FriendshipService friendshipService;
private MediaService mediaService;
private StoriesService storiesService;
private String userId; private String userId;
private String csrfToken; private String csrfToken;
private NewsService newsService; private NewsService newsService;
private Context context;
private final OnNotificationClickListener clickListener = model -> { private final OnNotificationClickListener clickListener = new OnNotificationClickListener() {
if (model == null) return; @Override
final String username = model.getUsername(); public void onProfileClick(final String username) {
final SpannableString title = new SpannableString(username + (TextUtils.isEmpty(model.getText()) ? "" : (":\n" + model.getText()))); openProfile(username);
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)};
} }
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 @Override
public void onFailure(final Throwable t) { public void onPreviewClick(final NotificationModel model) {
Log.e(TAG, "approve: onFailure: ", t); if (model.getType() == NotificationType.RESPONDED_STORY) {
} showProfilePicDialog(model);
}); }
return; else {
} mediaService.fetch(model.getPostId(), new ServiceCallback<FeedModel>() {
final AlertDialog alertDialog = new AlertDialog.Builder(context) @Override
.setCancelable(false) public void onSuccess(final FeedModel feedModel) {
.setView(R.layout.dialog_opening_post)
.create();
alertDialog.show();
new PostFetcher(model.getShortCode(), feedModel -> {
final PostViewV2Fragment fragment = PostViewV2Fragment final PostViewV2Fragment fragment = PostViewV2Fragment
.builder(feedModel) .builder(feedModel)
.build(); .build();
fragment.setOnShowListener(dialog1 -> alertDialog.dismiss());
fragment.show(getChildFragmentManager(), "post_view"); fragment.show(getChildFragmentManager(), "post_view");
}).execute(); }
break;
case 2: @Override
friendshipService.ignore(userId, model.getUserId(), csrfToken, new ServiceCallback<FriendshipRepoChangeRootResponse>() { public void onFailure(final Throwable t) {
@Override Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
public void onSuccess(final FriendshipRepoChangeRootResponse result) { }
// Log.d(TAG, "onSuccess: " + result); });
if (result.getStatus().equals("ok")) { }
onRefresh(); }
@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; 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 @Override
public void onFailure(final Throwable t) { public void onFailure(final Throwable t) {
Log.e(TAG, "ignore: onFailure: ", t); Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
} }
}); });
break; 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) -> { private final MentionClickListener mentionClickListener = (view, text, isHashtag, isLocation) -> {
if (getContext() == null) return; if (getContext() == null) return;
@ -158,7 +222,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
@Override @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final Context context = getContext(); context = getContext();
if (context == null) return; if (context == null) return;
NotificationManagerCompat.from(context.getApplicationContext()).cancel(Constants.ACTIVITY_NOTIFICATION_ID); NotificationManagerCompat.from(context.getApplicationContext()).cancel(Constants.ACTIVITY_NOTIFICATION_ID);
final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); final String cookie = Utils.settingsHelper.getString(Constants.COOKIE);
@ -167,6 +231,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
} }
friendshipService = FriendshipService.getInstance(); friendshipService = FriendshipService.getInstance();
newsService = NewsService.getInstance(); newsService = NewsService.getInstance();
mediaService = MediaService.getInstance();
userId = CookieUtils.getUserIdFromCookie(cookie); userId = CookieUtils.getUserIdFromCookie(cookie);
csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
} }
@ -205,22 +270,9 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
@Override @Override
public void onRefresh() { public void onRefresh() {
binding.swipeRefreshLayout.setRefreshing(true); binding.swipeRefreshLayout.setRefreshing(true);
new NotificationsFetcher(notificationModels -> { new NotificationsFetcher(true, notificationModels -> {
binding.swipeRefreshLayout.setRefreshing(false); binding.swipeRefreshLayout.setRefreshing(false);
notificationViewModel.getList().postValue(notificationModels); 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); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
@ -229,4 +281,15 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe
.actionGlobalProfileFragment("@" + username); .actionGlobalProfileFragment("@" + username);
NavHostFragment.findNavController(this).navigate(action); 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 userId;
private final String username; private final String username;
private final String profilePicUrl; private final String profilePicUrl;
private final String shortCode; private final String postId;
private final String previewUrl; private final String previewUrl;
private final NotificationType type; private final NotificationType type;
private final CharSequence text; private final CharSequence text;
@ -25,7 +25,7 @@ public final class NotificationModel {
final String userId, final String userId,
final String username, final String username,
final String profilePicUrl, final String profilePicUrl,
final String shortCode, final String postId,
final String previewUrl, final String previewUrl,
final NotificationType type) { final NotificationType type) {
this.id = id; this.id = id;
@ -34,7 +34,7 @@ public final class NotificationModel {
this.userId = userId; this.userId = userId;
this.username = username; this.username = username;
this.profilePicUrl = profilePicUrl; this.profilePicUrl = profilePicUrl;
this.shortCode = shortCode; this.postId = postId;
this.previewUrl = previewUrl; this.previewUrl = previewUrl;
this.type = type; this.type = type;
} }
@ -47,6 +47,10 @@ public final class NotificationModel {
return text; return text;
} }
public long getTimestamp() {
return timestamp;
}
@NonNull @NonNull
public String getDateTime() { public String getDateTime() {
return Utils.datetimeParser.format(new Date(timestamp * 1000L)); return Utils.datetimeParser.format(new Date(timestamp * 1000L));
@ -64,8 +68,8 @@ public final class NotificationModel {
return profilePicUrl; return profilePicUrl;
} }
public String getShortCode() { public String getPostId() {
return shortCode; return postId;
} }
public String getPreviewPic() { public String getPreviewPic() {

View File

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

View File

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

View File

@ -6,16 +6,19 @@ import awais.instagrabber.utils.Constants;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.http.FieldMap; import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded; import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header; import retrofit2.http.Header;
import retrofit2.http.Headers; import retrofit2.http.Headers;
import retrofit2.http.POST; import retrofit2.http.POST;
import retrofit2.http.Query;
public interface NewsRepository { public interface NewsRepository {
Call<String> inbox();
@FormUrlEncoded
@Headers("User-Agent: " + Constants.USER_AGENT) @Headers("User-Agent: " + Constants.USER_AGENT)
@POST("https://www.instagram.com/web/activity/mark_checked/") @GET("https://www.instagram.com/accounts/activity/?__a=1")
Call<String> markChecked(@Header("x-csrftoken") String csrfToken, @FieldMap Map<String, String> map); 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.Map;
import java.util.UUID; import java.util.UUID;
import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.repositories.MediaRepository; import awais.instagrabber.repositories.MediaRepository;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
@ -46,6 +48,37 @@ public class MediaService extends BaseService {
return instance; 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, public void like(final String mediaId,
final String userId, final String userId,
final String csrfToken, final String csrfToken,

View File

@ -1,14 +1,25 @@
package awais.instagrabber.webservices; package awais.instagrabber.webservices;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; 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.repositories.NewsRepository;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.NetworkUtils;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
@ -35,25 +46,34 @@ public class NewsService extends BaseService {
return instance; return instance;
} }
public void markChecked(final String timestamp, public void fetchAppInbox(final boolean markAsSeen,
final String csrfToken, final ServiceCallback<List<NotificationModel>> callback) {
final ServiceCallback<Boolean> callback) { final List<NotificationModel> result = new ArrayList<>();
final Map<String, String> map = new HashMap<>(); final Call<String> request = repository.appInbox(markAsSeen);
map.put("timestamp", timestamp);
final Call<String> request = repository.markChecked(csrfToken, map);
request.enqueue(new Callback<String>() { request.enqueue(new Callback<String>() {
@Override @Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
final String body = response.body(); final String body = response.body();
if (body == null) { if (body == null) {
callback.onSuccess(false); callback.onSuccess(null);
return; return;
} }
try { try {
final JSONObject jsonObject = new JSONObject(body); final JSONObject jsonObject = new JSONObject(body);
final String status = jsonObject.optString("status"); final JSONArray oldStories = jsonObject.getJSONArray("old_stories"),
callback.onSuccess(status.equals("ok")); 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) { } catch (JSONException e) {
callback.onFailure(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:gravity="end"
android:paddingStart="8dp" android:paddingStart="8dp"
android:paddingLeft="8dp" android:paddingLeft="8dp"
android:paddingEnd="16dp" android:paddingEnd="4dp"
android:paddingRight="16dp" android:paddingRight="16dp"
android:singleLine="true" android:singleLine="true"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:textStyle="italic" android:textStyle="italic"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@id/ivPreviewPic"
app:layout_constraintStart_toEndOf="@id/ivProfilePic" app:layout_constraintStart_toEndOf="@id/ivProfilePic"
app:layout_constraintTop_toBottomOf="@id/tvSubComment" app:layout_constraintTop_toBottomOf="@id/tvSubComment"
tools:text="some long long long long long date" /> 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_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="quick_access_confirm_delete">Are you sure you want to delete \'%s\'?</string>
<string name="open_profile">Open profile</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="view_pfp">View profile picture</string>
<string name="direct_messages_you">You</string> <string name="direct_messages_you">You</string>
<string name="direct_messages_sent_link">Shared a link</string> <string name="direct_messages_sent_link">Shared a link</string>