diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectMessageInboxAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectMessageInboxAdapter.java index 2ccb5df3..18c1ba17 100644 --- a/app/src/main/java/awais/instagrabber/adapters/DirectMessageInboxAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/DirectMessageInboxAdapter.java @@ -9,6 +9,7 @@ import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; import java.util.List; +import java.util.Objects; import awais.instagrabber.adapters.viewholder.directmessages.DirectInboxItemViewHolder; import awais.instagrabber.databinding.LayoutDmInboxItemBinding; @@ -29,6 +30,8 @@ public final class DirectMessageInboxAdapter extends ListAdapter oldItems = oldThread.getItems(); final List newItems = newThread.getItems(); if (oldItems == null || newItems == null) return false; diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index 313e8e12..82cb2d6f 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -58,7 +58,6 @@ import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import awais.instagrabber.ProfileNavGraphDirections; import awais.instagrabber.R; @@ -100,6 +99,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectItemVisual import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; import awais.instagrabber.utils.AppExecutors; +import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.PermissionUtils; import awais.instagrabber.utils.ResponseBodyUtils; @@ -146,6 +146,17 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private int prevLength; private BadgeDrawable pendingRequestCountBadgeDrawable; private boolean isPendingRequestCountBadgeAttached = false; + private ItemTouchHelper itemTouchHelper; + private LiveData pendingLiveData; + private LiveData threadLiveData; + private LiveData inputModeLiveData; + private LiveData threadTitleLiveData; + private LiveData> fetchingLiveData; + private LiveData> itemsLiveData; + private LiveData replyToItemLiveData; + private LiveData pendingRequestsCountLiveData; + private LiveData> usersLiveData; + private boolean autoMarkAsSeen = false; private final AppExecutors appExecutors = AppExecutors.getInstance(); private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() { @@ -306,22 +317,13 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact backStackSavedStateResultLiveData.postValue(null); }; private final MutableLiveData inputLength = new MutableLiveData<>(0); - private ItemTouchHelper itemTouchHelper; - private LiveData pendingLiveData; - private LiveData threadLiveData; - private LiveData inputModeLiveData; - private LiveData threadTitleLiveData; - private LiveData> fetchingLiveData; - private LiveData> itemsLiveData; - private LiveData replyToItemLiveData; - private LiveData pendingRequestsCountLiveData; - private LiveData> usersLiveData; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); fragmentActivity = (MainActivity) requireActivity(); appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class); + autoMarkAsSeen = Utils.settingsHelper.getBoolean(Constants.DM_MARK_AS_SEEN); final Bundle arguments = getArguments(); if (arguments == null) return; final DirectMessageThreadFragmentArgs fragmentArgs = DirectMessageThreadFragmentArgs.fromBundle(arguments); @@ -895,6 +897,9 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } private void submitItemsToAdapter(final List items) { + if (autoMarkAsSeen) { + binding.chats.post(() -> viewModel.markAsSeen()); + } if (itemsAdapter == null) return; itemsAdapter.submitList(items, () -> { itemOrHeaders = itemsAdapter.getList(); diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java index 21581079..0f8678e2 100644 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java @@ -22,6 +22,7 @@ import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -45,12 +46,15 @@ import awais.instagrabber.repositories.responses.directmessages.DirectInbox; import awais.instagrabber.repositories.responses.directmessages.DirectItem; import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction; import awais.instagrabber.repositories.responses.directmessages.DirectItemReactions; +import awais.instagrabber.repositories.responses.directmessages.DirectItemSeenResponse; +import awais.instagrabber.repositories.responses.directmessages.DirectItemSeenResponse.DirectItemSeenResponsePayload; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponseMessageMetadata; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponsePayload; import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; import awais.instagrabber.repositories.responses.giphy.GiphyGif; @@ -1187,7 +1191,7 @@ public final class ThreadManager { private void handleErrorBody(@NonNull final Call call, @NonNull final Response response, - @NonNull final MutableLiveData> data) { + final MutableLiveData> data) { try { final String string = response.errorBody() != null ? response.errorBody().string() : ""; final String msg = String.format(Locale.US, @@ -1195,10 +1199,14 @@ public final class ThreadManager { call.request().url().toString(), response.code(), string); - data.postValue(Resource.error(msg, null)); + if (data != null) { + data.postValue(Resource.error(msg, null)); + } Log.e(TAG, msg); } catch (IOException e) { - data.postValue(Resource.error(e.getMessage(), null)); + if (data != null) { + data.postValue(Resource.error(e.getMessage(), null)); + } Log.e(TAG, "onResponse: ", e); } } @@ -1794,6 +1802,39 @@ public final class ThreadManager { return inviter; } + public void markAsSeen(@NonNull final DirectItem directItem) { + final Call request = service.markAsSeen(threadId, directItem); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (!response.isSuccessful()) { + handleErrorBody(call, response, null); + return; + } + final DirectItemSeenResponse seenResponse = response.body(); + if (seenResponse == null) return; + inboxManager.fetchUnseenCount(); + final DirectItemSeenResponsePayload payload = seenResponse.getPayload(); + if (payload == null) return; + final String timestamp = payload.getTimestamp(); + final DirectThread thread = ThreadManager.this.thread.getValue(); + if (thread == null) return; + Map lastSeenAt = thread.getLastSeenAt(); + lastSeenAt = lastSeenAt == null ? new HashMap<>() : new HashMap<>(lastSeenAt); + lastSeenAt.put(currentUser.getPk(), new DirectThreadLastSeenAt(timestamp, directItem.getItemId())); + thread.setLastSeenAt(lastSeenAt); + setThread(thread, true); + } + + @Override + public void onFailure(@NonNull final Call call, + @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + } + }); + } + private interface OnSuccessAction { void onSuccess(); } diff --git a/app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java b/app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java index 0db1c42e..bdb8c29f 100644 --- a/app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java @@ -4,6 +4,7 @@ import java.util.Map; import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; +import awais.instagrabber.repositories.responses.directmessages.DirectItemSeenResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; @@ -145,4 +146,10 @@ public interface DirectMessagesRepository { @POST("/api/v1/direct_v2/threads/{threadId}/decline/") Call declineRequest(@Path("threadId") String threadId, @FieldMap final Map form); + + @FormUrlEncoded + @POST("/api/v1/direct_v2/threads/{threadId}/items/{itemId}/seen/") + Call markItemSeen(@Path("threadId") String threadId, + @Path("itemId") String itemId, + @FieldMap final Map form); } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemSeenResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemSeenResponse.java new file mode 100644 index 00000000..e1c88dab --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemSeenResponse.java @@ -0,0 +1,95 @@ +package awais.instagrabber.repositories.responses.directmessages; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +public class DirectItemSeenResponse { + private final String action; + private final DirectItemSeenResponsePayload payload; + private final String status; + + public DirectItemSeenResponse(final String action, final DirectItemSeenResponsePayload payload, final String status) { + this.action = action; + this.payload = payload; + this.status = status; + } + + public String getAction() { + return action; + } + + public DirectItemSeenResponsePayload getPayload() { + return payload; + } + + public String getStatus() { + return status; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemSeenResponse that = (DirectItemSeenResponse) o; + return Objects.equals(action, that.action) && + Objects.equals(payload, that.payload) && + Objects.equals(status, that.status); + } + + @Override + public int hashCode() { + return Objects.hash(action, payload, status); + } + + @NonNull + @Override + public String toString() { + return "DirectItemSeenResponse{" + + "action='" + action + '\'' + + ", payload=" + payload + + ", status='" + status + '\'' + + '}'; + } + + public static class DirectItemSeenResponsePayload { + private final int count; + private final String timestamp; + + public DirectItemSeenResponsePayload(final int count, final String timestamp) { + this.count = count; + this.timestamp = timestamp; + } + + public int getCount() { + return count; + } + + public String getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemSeenResponsePayload that = (DirectItemSeenResponsePayload) o; + return count == that.count && + Objects.equals(timestamp, that.timestamp); + } + + @Override + public int hashCode() { + return Objects.hash(count, timestamp); + } + + @NonNull + @Override + public String toString() { + return "DirectItemSeenResponsePayload{" + + "count=" + count + + ", timestamp='" + timestamp + '\'' + + '}'; + } + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java index 5be30e3a..67b58db4 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java @@ -36,7 +36,7 @@ public class DirectThread implements Serializable, Cloneable { private final User inviter; private final boolean hasOlder; private final boolean hasNewer; - private final Map lastSeenAt; + private Map lastSeenAt; private final String newestCursor; private final String oldestCursor; private final boolean isSpam; @@ -248,6 +248,10 @@ public class DirectThread implements Serializable, Cloneable { return lastSeenAt; } + public void setLastSeenAt(final Map lastSeenAt) { + this.lastSeenAt = lastSeenAt; + } + public String getNewestCursor() { return newestCursor; } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java index fc1b4893..27cf10e4 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java @@ -15,6 +15,9 @@ import androidx.lifecycle.Transformations; import java.io.File; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -25,6 +28,7 @@ import awais.instagrabber.models.Resource; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.directmessages.DirectItem; import awais.instagrabber.repositories.responses.directmessages.DirectThread; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; import awais.instagrabber.repositories.responses.giphy.GiphyGif; import awais.instagrabber.utils.Constants; @@ -273,4 +277,28 @@ public class DirectThreadViewModel extends AndroidViewModel { public LiveData> declineRequest() { return threadManager.declineRequest(); } + + public void markAsSeen() { + final DirectThread thread = getThread().getValue(); + if (thread == null) return; + final List items = thread.getItems(); + if (items == null || items.isEmpty()) return; + final Optional itemOptional = items.stream() + .filter(item -> item.getUserId() != currentUser.getPk()) + .findFirst(); + if (!itemOptional.isPresent()) return; + final DirectItem directItem = itemOptional.get(); + final Map lastSeenAt = thread.getLastSeenAt(); + if (lastSeenAt != null) { + final DirectThreadLastSeenAt seenAt = lastSeenAt.get(currentUser.getPk()); + try { + if (seenAt != null + && (Objects.equals(seenAt.getItemId(), directItem.getItemId()) + || Long.parseLong(seenAt.getTimestamp()) >= directItem.getTimestamp())) { + return; + } + } catch (Exception ignored) {} + } + threadManager.markAsSeen(directItem); + } } diff --git a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java index 30f89a74..5e4afea5 100644 --- a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java @@ -29,6 +29,8 @@ import awais.instagrabber.repositories.requests.directmessages.VideoBroadcastOpt import awais.instagrabber.repositories.requests.directmessages.VoiceBroadcastOptions; import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; +import awais.instagrabber.repositories.responses.directmessages.DirectItem; +import awais.instagrabber.repositories.responses.directmessages.DirectItemSeenResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; @@ -448,4 +450,17 @@ public class DirectMessagesService extends BaseService { ); return repository.declineRequest(threadId, form); } + + public Call markAsSeen(@NonNull final String threadId, + @NonNull final DirectItem directItem) { + final ImmutableMap form = ImmutableMap.builder() + .put("_csrftoken", csrfToken) + .put("_uuid", deviceUuid) + .put("use_unified_inbox", "true") + .put("action", "mark_seen") + .put("thread_id", threadId) + .put("item_id", directItem.getItemId()) + .build(); + return repository.markItemSeen(threadId, directItem.getItemId(), form); + } }