Browse Source

Add pending inbox

renovate/org.robolectric-robolectric-4.x
Ammar Githam 4 years ago
parent
commit
5e3aed38b9
  1. 68
      app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java
  2. 53
      app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java
  3. 123
      app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java
  4. 152
      app/src/main/java/awais/instagrabber/fragments/directmessages/DirectPendingInboxFragment.java
  5. 13
      app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java
  6. 7
      app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectInboxResponse.java
  7. 15
      app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java
  8. 21
      app/src/main/java/awais/instagrabber/repositories/responses/directmessages/ThreadContext.java
  9. 2
      app/src/main/java/awais/instagrabber/utils/DirectItemFactory.java
  10. 15
      app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java
  11. 141
      app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java
  12. 6
      app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java
  13. 77
      app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java
  14. 32
      app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java
  15. 11
      app/src/main/res/drawable/ic_account_clock_24.xml
  16. 78
      app/src/main/res/layout/fragment_direct_messages_thread.xml
  17. 23
      app/src/main/res/layout/fragment_direct_pending_inbox.xml
  18. 43
      app/src/main/res/navigation/direct_messages_nav_graph.xml
  19. 1
      app/src/main/res/values/ids.xml
  20. 4
      app/src/main/res/values/strings.xml

68
app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java

@ -1,11 +1,15 @@
package awais.instagrabber.fragments.directmessages; package awais.instagrabber.fragments.directmessages;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -17,11 +21,13 @@ import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner; import androidx.lifecycle.ViewModelStoreOwner;
import androidx.navigation.NavController; import androidx.navigation.NavController;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.badge.BadgeDrawable;
import com.google.android.material.badge.BadgeUtils;
import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.List; import java.util.List;
@ -41,7 +47,6 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
private CoordinatorLayout root; private CoordinatorLayout root;
private RecyclerLazyLoaderAtEdge lazyLoader; private RecyclerLazyLoaderAtEdge lazyLoader;
private DirectInboxViewModel viewModel; private DirectInboxViewModel viewModel;
// private boolean refreshInbox = false;
private boolean shouldRefresh = true; private boolean shouldRefresh = true;
private FragmentDirectMessagesInboxBinding binding; private FragmentDirectMessagesInboxBinding binding;
private DMRefreshBroadcastReceiver receiver; private DMRefreshBroadcastReceiver receiver;
@ -50,6 +55,9 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
private boolean scrollToTop = false; private boolean scrollToTop = false;
private boolean navigating; private boolean navigating;
private Observer<List<DirectThread>> threadsObserver; private Observer<List<DirectThread>> threadsObserver;
private MenuItem pendingRequestsMenuItem;
private BadgeDrawable pendingRequestTotalBadgeDrawable;
private boolean isPendingRequestTotalBadgeAttached;
@Override @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
@ -60,6 +68,7 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph);
viewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class); viewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class);
} }
setHasOptionsMenu(true);
} }
@Override @Override
@ -99,7 +108,7 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
observeViewModel();
setupObservers();
final Context context = getContext(); final Context context = getContext();
if (context == null) return; if (context == null) return;
receiver = new DMRefreshBroadcastReceiver(() -> { receiver = new DMRefreshBroadcastReceiver(() -> {
@ -109,10 +118,36 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
context.registerReceiver(receiver, new IntentFilter(DMRefreshBroadcastReceiver.ACTION_REFRESH_DM)); context.registerReceiver(receiver, new IntentFilter(DMRefreshBroadcastReceiver.ACTION_REFRESH_DM));
} }
@SuppressLint("UnsafeExperimentalUsageError")
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
unregisterReceiver(); unregisterReceiver();
isPendingRequestTotalBadgeAttached = false;
if (pendingRequestTotalBadgeDrawable != null) {
BadgeUtils.detachBadgeDrawable(pendingRequestTotalBadgeDrawable, fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId());
pendingRequestTotalBadgeDrawable = null;
}
}
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
pendingRequestsMenuItem = menu.add(Menu.NONE, R.id.pending_requests, Menu.NONE, "Pending requests");
pendingRequestsMenuItem.setIcon(R.drawable.ic_account_clock_24)
.setVisible(false)
.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS);
attachPendingRequestsBadge(viewModel.getPendingRequestsTotal().getValue());
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() == R.id.pending_requests) {
final NavDirections directions = DirectMessageInboxFragmentDirections.actionInboxToPendingInbox();
NavHostFragment.findNavController(this).navigate(directions);
return true;
}
return super.onOptionsItemSelected(item);
} }
private void unregisterReceiver() { private void unregisterReceiver() {
@ -136,7 +171,7 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
viewModel.onDestroy(); viewModel.onDestroy();
} }
private void observeViewModel() {
private void setupObservers() {
threadsObserver = list -> { threadsObserver = list -> {
if (inboxAdapter == null) return; if (inboxAdapter == null) return;
inboxAdapter.submitList(list, () -> { inboxAdapter.submitList(list, () -> {
@ -148,6 +183,31 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
viewModel.getThreads().observe(fragmentActivity, threadsObserver); viewModel.getThreads().observe(fragmentActivity, threadsObserver);
viewModel.getFetchingInbox().observe(getViewLifecycleOwner(), fetching -> binding.swipeRefreshLayout.setRefreshing(fetching)); viewModel.getFetchingInbox().observe(getViewLifecycleOwner(), fetching -> binding.swipeRefreshLayout.setRefreshing(fetching));
viewModel.getUnseenCount().observe(getViewLifecycleOwner(), this::setBottomNavBarBadge); viewModel.getUnseenCount().observe(getViewLifecycleOwner(), this::setBottomNavBarBadge);
viewModel.getPendingRequestsTotal().observe(getViewLifecycleOwner(), this::attachPendingRequestsBadge);
}
@SuppressLint("UnsafeExperimentalUsageError")
private void attachPendingRequestsBadge(@Nullable final Integer count) {
if (pendingRequestsMenuItem == null) return;
if (pendingRequestTotalBadgeDrawable == null) {
final Context context = getContext();
if (context == null) return;
pendingRequestTotalBadgeDrawable = BadgeDrawable.create(context);
}
if (count == null || count == 0) {
BadgeUtils.detachBadgeDrawable(pendingRequestTotalBadgeDrawable, fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId());
isPendingRequestTotalBadgeAttached = false;
pendingRequestTotalBadgeDrawable.setNumber(0);
pendingRequestsMenuItem.setVisible(false);
return;
}
pendingRequestsMenuItem.setVisible(true);
if (pendingRequestTotalBadgeDrawable.getNumber() == count) return;
pendingRequestTotalBadgeDrawable.setNumber(count);
if (!isPendingRequestTotalBadgeAttached) {
BadgeUtils.attachBadgeDrawable(pendingRequestTotalBadgeDrawable, fragmentActivity.getToolbar(), pendingRequestsMenuItem.getItemId());
isPendingRequestTotalBadgeAttached = true;
}
} }
private void removeViewModelObservers() { private void removeViewModelObservers() {
@ -161,7 +221,7 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
private void init() { private void init() {
final Context context = getContext(); final Context context = getContext();
if (context == null) return; if (context == null) return;
observeViewModel();
setupObservers();
binding.swipeRefreshLayout.setOnRefreshListener(this); binding.swipeRefreshLayout.setOnRefreshListener(this);
binding.inboxList.setHasFixedSize(true); binding.inboxList.setHasFixedSize(true);
binding.inboxList.setItemViewCacheSize(20); binding.inboxList.setItemViewCacheSize(20);

53
app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java

@ -56,6 +56,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
import awais.instagrabber.viewmodels.DirectInboxViewModel; import awais.instagrabber.viewmodels.DirectInboxViewModel;
import awais.instagrabber.viewmodels.DirectPendingInboxViewModel;
import awais.instagrabber.viewmodels.DirectSettingsViewModel; import awais.instagrabber.viewmodels.DirectSettingsViewModel;
public class DirectMessageSettingsFragment extends Fragment implements ConfirmDialogFragmentCallback { public class DirectMessageSettingsFragment extends Fragment implements ConfirmDialogFragmentCallback {
@ -70,22 +71,28 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi
private boolean isPendingRequestsSetupDone = false; private boolean isPendingRequestsSetupDone = false;
private DirectPendingUsersAdapter pendingUsersAdapter; private DirectPendingUsersAdapter pendingUsersAdapter;
private Set<User> approvalRequiredUsers; private Set<User> approvalRequiredUsers;
// private List<Option<String>> options;
@Override @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final Bundle arguments = getArguments();
if (arguments == null) return;
final NavController navController = NavHostFragment.findNavController(this); final NavController navController = NavHostFragment.findNavController(this);
final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph);
final DirectMessageSettingsFragmentArgs args = DirectMessageSettingsFragmentArgs.fromBundle(arguments);
final boolean pending = args.getPending();
final List<DirectThread> threads;
final User viewer;
if (pending) {
final DirectPendingInboxViewModel inboxViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectPendingInboxViewModel.class);
threads = inboxViewModel.getThreads().getValue();
viewer = inboxViewModel.getViewer();
} else {
final DirectInboxViewModel inboxViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class); final DirectInboxViewModel inboxViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class);
final List<DirectThread> threads = inboxViewModel.getThreads().getValue();
final Bundle arguments = getArguments();
if (arguments == null) {
navController.navigateUp();
return;
threads = inboxViewModel.getThreads().getValue();
viewer = inboxViewModel.getViewer();
} }
final DirectMessageSettingsFragmentArgs fragmentArgs = DirectMessageSettingsFragmentArgs.fromBundle(arguments);
final String threadId = fragmentArgs.getThreadId();
final String threadId = args.getThreadId();
final Optional<DirectThread> first = threads != null ? threads.stream() final Optional<DirectThread> first = threads != null ? threads.stream()
.filter(thread -> thread.getThreadId().equals(threadId)) .filter(thread -> thread.getThreadId().equals(threadId))
.findFirst() .findFirst()
@ -95,7 +102,7 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi
return; return;
} }
viewModel = new ViewModelProvider(this).get(DirectSettingsViewModel.class); viewModel = new ViewModelProvider(this).get(DirectSettingsViewModel.class);
viewModel.setViewer(inboxViewModel.getViewer());
viewModel.setViewer(viewer);
viewModel.setThread(first.get()); viewModel.setThread(first.get());
} }
@ -105,37 +112,14 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi
final ViewGroup container, final ViewGroup container,
final Bundle savedInstanceState) { final Bundle savedInstanceState) {
binding = FragmentDirectMessagesSettingsBinding.inflate(inflater, container, false); binding = FragmentDirectMessagesSettingsBinding.inflate(inflater, container, false);
// final String threadId = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getThreadId();
// threadTitle = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getTitle();
// binding.swipeRefreshLayout.setEnabled(false);
// final ActionBar actionBar = fragmentActivity.getSupportActionBar();
// if (actionBar != null) {
// actionBar.setTitle(threadTitle);
// }
// titleSend.setOnClickListener(v -> new ChangeSettings(titleText.getText().toString()).execute("update_title"));
// binding.titleText.addTextChangedListener(new TextWatcherAdapter() {
// @Override
// public void onTextChanged(CharSequence s, int start, int before, int count) {
// binding.titleSend.setVisibility(s.toString().equals(threadTitle) ? View.GONE : View.VISIBLE);
// }
// });
// final AppCompatButton btnLeave = binding.btnLeave;
// btnLeave.setOnClickListener(v -> new AlertDialog.Builder(context)
// .setTitle(R.string.dms_action_leave_question)
// .setPositiveButton(R.string.yes, (x, y) -> new ChangeSettings(titleText.getText().toString()).execute("leave"))
// .setNegativeButton(R.string.no, null)
// .show());
// currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute(); // currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute();
return binding.getRoot(); return binding.getRoot();
} }
@Override @Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
final Bundle arguments = getArguments();
if (arguments == null) return;
init(); init();
setupObservers(); setupObservers();
} }
@ -169,6 +153,7 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi
usersAdapter.setAdminUserIds(adminUserIds); usersAdapter.setAdminUserIds(adminUserIds);
}); });
viewModel.getMuted().observe(getViewLifecycleOwner(), muted -> binding.muteMessages.setChecked(muted)); viewModel.getMuted().observe(getViewLifecycleOwner(), muted -> binding.muteMessages.setChecked(muted));
viewModel.isPending().observe(getViewLifecycleOwner(), pending -> binding.muteMessages.setVisibility(pending ? View.GONE : View.VISIBLE));
if (viewModel.isViewerAdmin()) { if (viewModel.isViewerAdmin()) {
viewModel.getApprovalRequiredToJoin().observe(getViewLifecycleOwner(), required -> binding.approvalRequired.setChecked(required)); viewModel.getApprovalRequiredToJoin().observe(getViewLifecycleOwner(), required -> binding.approvalRequired.setChecked(required));
viewModel.getPendingRequests().observe(getViewLifecycleOwner(), this::setPendingRequests); viewModel.getPendingRequests().observe(getViewLifecycleOwner(), this::setPendingRequests);

123
app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java

@ -105,6 +105,7 @@ import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.viewmodels.AppStateViewModel; import awais.instagrabber.viewmodels.AppStateViewModel;
import awais.instagrabber.viewmodels.DirectInboxViewModel; import awais.instagrabber.viewmodels.DirectInboxViewModel;
import awais.instagrabber.viewmodels.DirectPendingInboxViewModel;
import awais.instagrabber.viewmodels.DirectThreadViewModel; import awais.instagrabber.viewmodels.DirectThreadViewModel;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
@ -347,9 +348,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
public boolean onOptionsItemSelected(@NonNull final MenuItem item) { public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
final int itemId = item.getItemId(); final int itemId = item.getItemId();
if (itemId == R.id.info) { if (itemId == R.id.info) {
final NavDirections action = DirectMessageThreadFragmentDirections
.actionDMThreadFragmentToDMSettingsFragment(viewModel.getThreadId(), null);
NavHostFragment.findNavController(this).navigate(action);
final DirectMessageThreadFragmentDirections.ActionThreadToSettings directions = DirectMessageThreadFragmentDirections
.actionThreadToSettings(viewModel.getThreadId(), null);
final Boolean pending = viewModel.isPending().getValue();
directions.setPending(pending == null ? false : pending);
NavHostFragment.findNavController(this).navigate(directions);
return true; return true;
} }
if (itemId == R.id.mark_as_seen) { if (itemId == R.id.mark_as_seen) {
@ -470,10 +473,20 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
} }
private void getInitialData() { private void getInitialData() {
final Bundle arguments = getArguments();
if (arguments == null) return;
final DirectMessageThreadFragmentArgs args = DirectMessageThreadFragmentArgs.fromBundle(arguments);
final boolean pending = args.getPending();
final NavController navController = NavHostFragment.findNavController(this); final NavController navController = NavHostFragment.findNavController(this);
final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph);
final List<DirectThread> threads;
if (!pending) {
final DirectInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class); final DirectInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class);
final List<DirectThread> threads = threadListViewModel.getThreads().getValue();
threads = threadListViewModel.getThreads().getValue();
} else {
final DirectPendingInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectPendingInboxViewModel.class);
threads = threadListViewModel.getThreads().getValue();
}
final Optional<DirectThread> first = threads != null final Optional<DirectThread> first = threads != null
? threads.stream() ? threads.stream()
.filter(thread -> thread.getThreadId().equals(viewModel.getThreadId())) .filter(thread -> thread.getThreadId().equals(viewModel.getThreadId()))
@ -529,19 +542,26 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
} }
private void setObservers() { private void setObservers() {
viewModel.isPending().observe(getViewLifecycleOwner(), isPending -> {
if (isPending == null) {
hideInput();
return;
}
if (isPending) {
showPendingOptions();
return;
}
hidePendingOptions();
final Integer inputMode = viewModel.getInputMode().getValue();
if (inputMode != null && inputMode == 1) return;
showInput();
});
viewModel.getInputMode().observe(getViewLifecycleOwner(), inputMode -> { viewModel.getInputMode().observe(getViewLifecycleOwner(), inputMode -> {
final Boolean isPending = viewModel.isPending().getValue();
if (isPending != null && isPending) return;
if (inputMode == null || inputMode == 0) return; if (inputMode == null || inputMode == 0) return;
if (inputMode == 1) { if (inputMode == 1) {
binding.emojiToggle.setVisibility(View.GONE);
binding.camera.setVisibility(View.GONE);
binding.gallery.setVisibility(View.GONE);
binding.input.setVisibility(View.GONE);
binding.inputBg.setVisibility(View.GONE);
binding.recordView.setVisibility(View.GONE);
binding.send.setVisibility(View.GONE);
if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null);
}
hideInput();
} }
}); });
viewModel.getThreadTitle().observe(getViewLifecycleOwner(), this::setTitle); viewModel.getThreadTitle().observe(getViewLifecycleOwner(), this::setTitle);
@ -614,6 +634,81 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
prevLength = length; prevLength = length;
}); });
viewModel.getPendingRequestsCount().observe(getViewLifecycleOwner(), this::attachPendingRequestsBadge); viewModel.getPendingRequestsCount().observe(getViewLifecycleOwner(), this::attachPendingRequestsBadge);
viewModel.getUsers().observe(getViewLifecycleOwner(), users -> {
if (users == null || users.isEmpty()) return;
final User user = users.get(0);
binding.acceptPendingRequestQuestion.setText(getString(R.string.accept_request_from_user, user.getUsername(), user.getFullName()));
});
}
private void hidePendingOptions() {
binding.acceptPendingRequestQuestion.setVisibility(View.GONE);
binding.decline.setVisibility(View.GONE);
binding.accept.setVisibility(View.GONE);
}
private void showPendingOptions() {
binding.acceptPendingRequestQuestion.setVisibility(View.VISIBLE);
binding.decline.setVisibility(View.VISIBLE);
binding.accept.setVisibility(View.VISIBLE);
binding.accept.setOnClickListener(v -> {
final LiveData<Resource<Object>> resourceLiveData = viewModel.acceptRequest();
handlePendingChangeResource(resourceLiveData, false);
});
binding.decline.setOnClickListener(v -> {
final LiveData<Resource<Object>> resourceLiveData = viewModel.declineRequest();
handlePendingChangeResource(resourceLiveData, true);
});
}
private void handlePendingChangeResource(final LiveData<Resource<Object>> resourceLiveData, final boolean isDecline) {
resourceLiveData.observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
final Resource.Status status = resource.status;
switch (status) {
case SUCCESS:
resourceLiveData.removeObservers(getViewLifecycleOwner());
if (isDecline) {
final NavController navController = NavHostFragment.findNavController(this);
navController.navigateUp();
}
break;
case LOADING:
break;
case ERROR:
if (resource.message != null) {
Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
}
resourceLiveData.removeObservers(getViewLifecycleOwner());
break;
}
});
}
private void hideInput() {
binding.emojiToggle.setVisibility(View.GONE);
binding.camera.setVisibility(View.GONE);
binding.gallery.setVisibility(View.GONE);
binding.input.setVisibility(View.GONE);
binding.inputBg.setVisibility(View.GONE);
binding.recordView.setVisibility(View.GONE);
binding.send.setVisibility(View.GONE);
if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null);
}
}
private void showInput() {
binding.emojiToggle.setVisibility(View.VISIBLE);
binding.camera.setVisibility(View.VISIBLE);
binding.gallery.setVisibility(View.VISIBLE);
binding.input.setVisibility(View.VISIBLE);
binding.inputBg.setVisibility(View.VISIBLE);
binding.recordView.setVisibility(View.VISIBLE);
binding.send.setVisibility(View.VISIBLE);
if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(binding.chats);
}
} }
@SuppressLint("UnsafeExperimentalUsageError") @SuppressLint("UnsafeExperimentalUsageError")

152
app/src/main/java/awais/instagrabber/fragments/directmessages/DirectPendingInboxFragment.java

@ -0,0 +1,152 @@
package awais.instagrabber.fragments.directmessages;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.activities.MainActivity;
import awais.instagrabber.adapters.DirectMessageInboxAdapter;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge;
import awais.instagrabber.databinding.FragmentDirectPendingInboxBinding;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.viewmodels.DirectPendingInboxViewModel;
public class DirectPendingInboxFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = DirectPendingInboxFragment.class.getSimpleName();
private CoordinatorLayout root;
private RecyclerLazyLoaderAtEdge lazyLoader;
private DirectPendingInboxViewModel viewModel;
private boolean shouldRefresh = true;
private FragmentDirectPendingInboxBinding binding;
private DirectMessageInboxAdapter inboxAdapter;
private MainActivity fragmentActivity;
private boolean scrollToTop = false;
private boolean navigating;
private Observer<List<DirectThread>> threadsObserver;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
fragmentActivity = (MainActivity) getActivity();
if (fragmentActivity != null) {
final NavController navController = NavHostFragment.findNavController(this);
final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph);
viewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectPendingInboxViewModel.class);
}
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState) {
if (root != null) {
shouldRefresh = false;
return root;
}
binding = FragmentDirectPendingInboxBinding.inflate(inflater, container, false);
root = binding.getRoot();
return root;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
if (!shouldRefresh) return;
init();
}
@Override
public void onRefresh() {
lazyLoader.resetState();
scrollToTop = true;
if (viewModel != null) {
viewModel.refresh();
}
}
@Override
public void onResume() {
super.onResume();
setupObservers();
}
@Override
public void onConfigurationChanged(@NonNull final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
init();
}
@Override
public void onDestroy() {
super.onDestroy();
removeViewModelObservers();
viewModel.onDestroy();
}
private void setupObservers() {
threadsObserver = list -> {
if (inboxAdapter == null) return;
inboxAdapter.submitList(list, () -> {
if (!scrollToTop) return;
binding.pendingInboxList.smoothScrollToPosition(0);
scrollToTop = false;
});
};
viewModel.getThreads().observe(fragmentActivity, threadsObserver);
viewModel.getFetchingInbox().observe(getViewLifecycleOwner(), fetching -> binding.swipeRefreshLayout.setRefreshing(fetching));
}
private void removeViewModelObservers() {
if (viewModel == null) return;
if (threadsObserver != null) {
viewModel.getThreads().removeObserver(threadsObserver);
}
}
private void init() {
final Context context = getContext();
if (context == null) return;
setupObservers();
binding.swipeRefreshLayout.setOnRefreshListener(this);
binding.pendingInboxList.setHasFixedSize(true);
binding.pendingInboxList.setItemViewCacheSize(20);
final LinearLayoutManager layoutManager = new LinearLayoutManager(context);
binding.pendingInboxList.setLayoutManager(layoutManager);
inboxAdapter = new DirectMessageInboxAdapter(thread -> {
if (navigating) return;
navigating = true;
if (isAdded()) {
final DirectPendingInboxFragmentDirections.ActionPendingInboxToThread directions = DirectPendingInboxFragmentDirections
.actionPendingInboxToThread(thread.getThreadId(), thread.getThreadTitle());
directions.setPending(true);
NavHostFragment.findNavController(this).navigate(directions);
}
navigating = false;
});
inboxAdapter.setHasStableIds(true);
binding.pendingInboxList.setAdapter(inboxAdapter);
lazyLoader = new RecyclerLazyLoaderAtEdge(layoutManager, page -> {
if (viewModel == null) return;
viewModel.fetchInbox();
});
binding.pendingInboxList.addOnScrollListener(lazyLoader);
}
}

13
app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java

@ -24,6 +24,9 @@ public interface DirectMessagesRepository {
@GET("/api/v1/direct_v2/inbox/") @GET("/api/v1/direct_v2/inbox/")
Call<DirectInboxResponse> fetchInbox(@QueryMap Map<String, Object> queryMap); Call<DirectInboxResponse> fetchInbox(@QueryMap Map<String, Object> queryMap);
@GET("/api/v1/direct_v2/pending_inbox/")
Call<DirectInboxResponse> fetchPendingInbox(@QueryMap Map<String, Object> queryMap);
@GET("/api/v1/direct_v2/threads/{threadId}/") @GET("/api/v1/direct_v2/threads/{threadId}/")
Call<DirectThreadFeedResponse> fetchThread(@Path("threadId") String threadId, Call<DirectThreadFeedResponse> fetchThread(@Path("threadId") String threadId,
@QueryMap Map<String, Object> queryMap); @QueryMap Map<String, Object> queryMap);
@ -132,4 +135,14 @@ public interface DirectMessagesRepository {
@POST("/api/v1/direct_v2/threads/{threadId}/remove_all_users/") @POST("/api/v1/direct_v2/threads/{threadId}/remove_all_users/")
Call<DirectThreadDetailsChangeResponse> end(@Path("threadId") String threadId, Call<DirectThreadDetailsChangeResponse> end(@Path("threadId") String threadId,
@FieldMap final Map<String, String> form); @FieldMap final Map<String, String> form);
@FormUrlEncoded
@POST("/api/v1/direct_v2/threads/{threadId}/approve/")
Call<String> approveRequest(@Path("threadId") String threadId,
@FieldMap final Map<String, String> form);
@FormUrlEncoded
@POST("/api/v1/direct_v2/threads/{threadId}/decline/")
Call<String> declineRequest(@Path("threadId") String threadId,
@FieldMap final Map<String, String> form);
} }

7
app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectInboxResponse.java

@ -8,6 +8,7 @@ public class DirectInboxResponse {
private final long seqId; private final long seqId;
private final long snapshotAtMs; private final long snapshotAtMs;
private final int pendingRequestsTotal; private final int pendingRequestsTotal;
private final boolean hasPendingTopRequests;
private final User mostRecentInviter; private final User mostRecentInviter;
private final String status; private final String status;
@ -16,6 +17,7 @@ public class DirectInboxResponse {
final long seqId, final long seqId,
final long snapshotAtMs, final long snapshotAtMs,
final int pendingRequestsTotal, final int pendingRequestsTotal,
final boolean hasPendingTopRequests,
final User mostRecentInviter, final User mostRecentInviter,
final String status) { final String status) {
this.viewer = viewer; this.viewer = viewer;
@ -23,6 +25,7 @@ public class DirectInboxResponse {
this.seqId = seqId; this.seqId = seqId;
this.snapshotAtMs = snapshotAtMs; this.snapshotAtMs = snapshotAtMs;
this.pendingRequestsTotal = pendingRequestsTotal; this.pendingRequestsTotal = pendingRequestsTotal;
this.hasPendingTopRequests = hasPendingTopRequests;
this.mostRecentInviter = mostRecentInviter; this.mostRecentInviter = mostRecentInviter;
this.status = status; this.status = status;
} }
@ -47,6 +50,10 @@ public class DirectInboxResponse {
return pendingRequestsTotal; return pendingRequestsTotal;
} }
public boolean hasPendingTopRequests() {
return hasPendingTopRequests;
}
public User getMostRecentInviter() { public User getMostRecentInviter() {
return mostRecentInviter; return mostRecentInviter;
} }

15
app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java

@ -21,7 +21,7 @@ public class DirectThread implements Serializable {
private final boolean isPin; private final boolean isPin;
private final boolean named; private final boolean named;
private final boolean canonical; private final boolean canonical;
private final boolean pending;
private boolean pending;
private final boolean archived; private final boolean archived;
private final boolean valuedRequest; private final boolean valuedRequest;
private final String threadType; private final String threadType;
@ -43,6 +43,7 @@ public class DirectThread implements Serializable {
private final DirectThreadDirectStory directStory; private final DirectThreadDirectStory directStory;
private boolean approvalRequiredForNewMembers; private boolean approvalRequiredForNewMembers;
private int inputMode; private int inputMode;
private final List<ThreadContext> threadContextItems;
public DirectThread(final String threadId, public DirectThread(final String threadId,
final String threadV2Id, final String threadV2Id,
@ -76,7 +77,8 @@ public class DirectThread implements Serializable {
final DirectItem lastPermanentItem, final DirectItem lastPermanentItem,
final DirectThreadDirectStory directStory, final DirectThreadDirectStory directStory,
final boolean approvalRequiredForNewMembers, final boolean approvalRequiredForNewMembers,
final int inputMode) {
final int inputMode,
final List<ThreadContext> threadContextItems) {
this.threadId = threadId; this.threadId = threadId;
this.threadV2Id = threadV2Id; this.threadV2Id = threadV2Id;
this.users = users; this.users = users;
@ -110,6 +112,7 @@ public class DirectThread implements Serializable {
this.directStory = directStory; this.directStory = directStory;
this.approvalRequiredForNewMembers = approvalRequiredForNewMembers; this.approvalRequiredForNewMembers = approvalRequiredForNewMembers;
this.inputMode = inputMode; this.inputMode = inputMode;
this.threadContextItems = threadContextItems;
} }
public String getThreadId() { public String getThreadId() {
@ -164,6 +167,10 @@ public class DirectThread implements Serializable {
return pending; return pending;
} }
public void setPending(final boolean pending) {
this.pending = pending;
}
public boolean isArchived() { public boolean isArchived() {
return archived; return archived;
} }
@ -260,6 +267,10 @@ public class DirectThread implements Serializable {
this.inputMode = inputMode; this.inputMode = inputMode;
} }
public List<ThreadContext> getThreadContextItems() {
return threadContextItems;
}
@Nullable @Nullable
public DirectItem getFirstDirectItem() { public DirectItem getFirstDirectItem() {
DirectItem firstItem = null; DirectItem firstItem = null;

21
app/src/main/java/awais/instagrabber/repositories/responses/directmessages/ThreadContext.java

@ -0,0 +1,21 @@
package awais.instagrabber.repositories.responses.directmessages;
import java.io.Serializable;
public class ThreadContext implements Serializable {
private final int type;
private final String text;
public ThreadContext(final int type, final String text) {
this.type = type;
this.text = text;
}
public int getType() {
return type;
}
public String getText() {
return text;
}
}

2
app/src/main/java/awais/instagrabber/utils/DirectItemFactory.java

@ -16,7 +16,7 @@ import awais.instagrabber.repositories.responses.VideoVersion;
import awais.instagrabber.repositories.responses.directmessages.DirectItem; import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVoiceMedia; import awais.instagrabber.repositories.responses.directmessages.DirectItemVoiceMedia;
public class DirectItemFactory {
public final class DirectItemFactory {
public static DirectItem createText(final long userId, public static DirectItem createText(final long userId,
final String clientContext, final String clientContext,

15
app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java

@ -3,6 +3,7 @@ package awais.instagrabber.viewmodels;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
@ -33,6 +34,7 @@ public class DirectInboxViewModel extends ViewModel {
private final MutableLiveData<List<DirectThread>> threads = new MutableLiveData<>(); private final MutableLiveData<List<DirectThread>> threads = new MutableLiveData<>();
private final MutableLiveData<Boolean> fetchingUnseenCount = new MutableLiveData<>(false); private final MutableLiveData<Boolean> fetchingUnseenCount = new MutableLiveData<>(false);
private final MutableLiveData<Integer> unseenCount = new MutableLiveData<>(0); private final MutableLiveData<Integer> unseenCount = new MutableLiveData<>(0);
private final MutableLiveData<Integer> pendingRequestsTotal = new MutableLiveData<>(0);
private Call<DirectInboxResponse> inboxRequest; private Call<DirectInboxResponse> inboxRequest;
private Call<DirectBadgeCount> unseenCountRequest; private Call<DirectBadgeCount> unseenCountRequest;
@ -54,12 +56,12 @@ public class DirectInboxViewModel extends ViewModel {
fetchUnseenCount(); fetchUnseenCount();
} }
public MutableLiveData<List<DirectThread>> getThreads() {
public LiveData<List<DirectThread>> getThreads() {
return threads; return threads;
} }
public void setThreads(final List<DirectThread> threads) { public void setThreads(final List<DirectThread> threads) {
getThreads().postValue(threads);
this.threads.postValue(threads);
} }
public void addThreads(final Collection<DirectThread> threads) { public void addThreads(final Collection<DirectThread> threads) {
@ -70,14 +72,18 @@ public class DirectInboxViewModel extends ViewModel {
this.threads.postValue(list); this.threads.postValue(list);
} }
public MutableLiveData<Integer> getUnseenCount() {
public LiveData<Integer> getUnseenCount() {
return unseenCount; return unseenCount;
} }
public MutableLiveData<Boolean> getFetchingInbox() {
public LiveData<Boolean> getFetchingInbox() {
return fetchingInbox; return fetchingInbox;
} }
public LiveData<Integer> getPendingRequestsTotal() {
return pendingRequestsTotal;
}
public User getViewer() { public User getViewer() {
return viewer; return viewer;
} }
@ -126,6 +132,7 @@ public class DirectInboxViewModel extends ViewModel {
} }
cursor = inbox.getOldestCursor(); cursor = inbox.getOldestCursor();
hasOlder = inbox.hasOlder(); hasOlder = inbox.hasOlder();
pendingRequestsTotal.postValue(response.getPendingRequestsTotal());
// unseenCount.postValue(inbox.getUnseenCount()); // unseenCount.postValue(inbox.getUnseenCount());
} }

141
app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java

@ -0,0 +1,141 @@
package awais.instagrabber.viewmodels;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount;
import awais.instagrabber.repositories.responses.directmessages.DirectInbox;
import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.DirectMessagesService;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class DirectPendingInboxViewModel extends ViewModel {
private static final String TAG = DirectPendingInboxViewModel.class.getSimpleName();
private final DirectMessagesService service;
private final MutableLiveData<Boolean> fetchingInbox = new MutableLiveData<>(false);
private final MutableLiveData<List<DirectThread>> threads = new MutableLiveData<>();
private Call<DirectInboxResponse> inboxRequest;
private Call<DirectBadgeCount> unseenCountRequest;
private long seqId;
private String cursor;
private boolean hasOlder = true;
private User viewer;
public DirectPendingInboxViewModel() {
final String cookie = settingsHelper.getString(Constants.COOKIE);
final long userId = CookieUtils.getUserIdFromCookie(cookie);
final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID);
final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
if (TextUtils.isEmpty(csrfToken) || userId <= 0 || TextUtils.isEmpty(deviceUuid)) {
throw new IllegalArgumentException("User is not logged in!");
}
service = DirectMessagesService.getInstance(csrfToken, userId, deviceUuid);
fetchInbox();
}
public LiveData<List<DirectThread>> getThreads() {
return threads;
}
public void setThreads(final List<DirectThread> threads) {
this.threads.postValue(threads);
}
public void addThreads(final Collection<DirectThread> threads) {
if (threads == null) return;
List<DirectThread> list = getThreads().getValue();
list = list == null ? new LinkedList<>() : new LinkedList<>(list);
list.addAll(threads);
this.threads.postValue(list);
}
public LiveData<Boolean> getFetchingInbox() {
return fetchingInbox;
}
public User getViewer() {
return viewer;
}
public void fetchInbox() {
if ((fetchingInbox.getValue() != null && fetchingInbox.getValue()) || !hasOlder) return;
stopCurrentInboxRequest();
fetchingInbox.postValue(true);
inboxRequest = service.fetchPendingInbox(cursor, seqId);
inboxRequest.enqueue(new Callback<DirectInboxResponse>() {
@Override
public void onResponse(@NonNull final Call<DirectInboxResponse> call, @NonNull final Response<DirectInboxResponse> response) {
parseInboxResponse(response.body());
fetchingInbox.postValue(false);
}
@Override
public void onFailure(@NonNull final Call<DirectInboxResponse> call, @NonNull final Throwable t) {
Log.e(TAG, "Failed fetching pending inbox", t);
fetchingInbox.postValue(false);
hasOlder = false;
}
});
}
private void parseInboxResponse(final DirectInboxResponse response) {
if (response == null) {
hasOlder = false;
return;
}
if (!response.getStatus().equals("ok")) {
Log.e(TAG, "DM pending inbox fetch response: status not ok");
hasOlder = false;
return;
}
seqId = response.getSeqId();
if (viewer == null) {
viewer = response.getViewer();
}
final DirectInbox inbox = response.getInbox();
final List<DirectThread> threads = inbox.getThreads();
if (!TextUtils.isEmpty(cursor)) {
addThreads(threads);
} else {
setThreads(threads);
}
cursor = inbox.getOldestCursor();
hasOlder = inbox.hasOlder();
}
private void stopCurrentInboxRequest() {
if (inboxRequest == null || inboxRequest.isCanceled() || inboxRequest.isExecuted()) return;
inboxRequest.cancel();
inboxRequest = null;
}
public void refresh() {
cursor = null;
seqId = 0;
hasOlder = true;
fetchInbox();
}
public void onDestroy() {
stopCurrentInboxRequest();
}
}

6
app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java

@ -67,6 +67,7 @@ public class DirectSettingsViewModel extends AndroidViewModel {
private final MutableLiveData<Boolean> approvalRequiredToJoin = new MutableLiveData<>(false); private final MutableLiveData<Boolean> approvalRequiredToJoin = new MutableLiveData<>(false);
private final MutableLiveData<DirectThreadParticipantRequestsResponse> pendingRequests = new MutableLiveData<>(null); private final MutableLiveData<DirectThreadParticipantRequestsResponse> pendingRequests = new MutableLiveData<>(null);
private final MutableLiveData<Integer> inputMode = new MutableLiveData<>(null); private final MutableLiveData<Integer> inputMode = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> isPending = new MutableLiveData<>(false);
private final DirectMessagesService directMessagesService; private final DirectMessagesService directMessagesService;
private final long userId; private final long userId;
private final Resources resources; private final Resources resources;
@ -115,6 +116,7 @@ public class DirectSettingsViewModel extends AndroidViewModel {
muted.postValue(thread.isMuted()); muted.postValue(thread.isMuted());
mentionsMuted.postValue(thread.isMentionsMuted()); mentionsMuted.postValue(thread.isMentionsMuted());
approvalRequiredToJoin.postValue(thread.isApprovalRequiredForNewMembers()); approvalRequiredToJoin.postValue(thread.isApprovalRequiredForNewMembers());
isPending.postValue(thread.isPending());
if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) { if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) {
fetchPendingRequests(); fetchPendingRequests();
} }
@ -163,6 +165,10 @@ public class DirectSettingsViewModel extends AndroidViewModel {
return pendingRequests; return pendingRequests;
} }
public LiveData<Boolean> isPending() {
return isPending;
}
public boolean isViewerAdmin() { public boolean isViewerAdmin() {
return viewerIsAdmin; return viewerIsAdmin;
} }

77
app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java

@ -85,6 +85,7 @@ public class DirectThreadViewModel extends AndroidViewModel {
private final MutableLiveData<DirectItem> replyToItem = new MutableLiveData<>(); private final MutableLiveData<DirectItem> replyToItem = new MutableLiveData<>();
private final MutableLiveData<Integer> pendingRequestsCount = new MutableLiveData<>(null); private final MutableLiveData<Integer> pendingRequestsCount = new MutableLiveData<>(null);
private final MutableLiveData<Integer> inputMode = new MutableLiveData<>(0); private final MutableLiveData<Integer> inputMode = new MutableLiveData<>(0);
private final MutableLiveData<Boolean> isPending = new MutableLiveData<>(null);
private final DirectMessagesService service; private final DirectMessagesService service;
private final ContentResolver contentResolver; private final ContentResolver contentResolver;
@ -340,6 +341,10 @@ public class DirectThreadViewModel extends AndroidViewModel {
return inputMode; return inputMode;
} }
public LiveData<Boolean> isPending() {
return isPending;
}
public void fetchChats() { public void fetchChats() {
final Boolean isFetching = fetching.getValue(); final Boolean isFetching = fetching.getValue();
if ((isFetching != null && isFetching) || !hasOlder) return; if ((isFetching != null && isFetching) || !hasOlder) return;
@ -404,6 +409,7 @@ public class DirectThreadViewModel extends AndroidViewModel {
users.postValue(thread.getUsers()); users.postValue(thread.getUsers());
leftUsers.postValue(thread.getLeftUsers()); leftUsers.postValue(thread.getLeftUsers());
fetching.postValue(false); fetching.postValue(false);
isPending.postValue(thread.isPending());
final List<Long> adminUserIds = thread.getAdminUserIds(); final List<Long> adminUserIds = thread.getAdminUserIds();
viewerIsAdmin = adminUserIds.contains(viewerId); viewerIsAdmin = adminUserIds.contains(viewerId);
if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) { if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) {
@ -1105,4 +1111,75 @@ public class DirectThreadViewModel extends AndroidViewModel {
} }
}); });
} }
public LiveData<Resource<Object>> acceptRequest() {
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
final Call<String> request = service.approveRequest(threadId);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call,
@NonNull final Response<String> response) {
if (!response.isSuccessful()) {
try {
final String string = response.errorBody() != null ? response.errorBody().string() : "";
final String msg = String.format(Locale.US,
"onResponse: url: %s, responseCode: %d, errorBody: %s",
call.request().url().toString(),
response.code(),
string);
Log.e(TAG, msg);
data.postValue(Resource.error(msg, null));
return;
} catch (IOException e) {
Log.e(TAG, "onResponse: ", e);
}
return;
}
isPending.postValue(false);
data.postValue(Resource.success(new Object()));
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
Log.e(TAG, "onFailure: ", t);
data.postValue(Resource.error(t.getMessage(), null));
}
});
return data;
}
public LiveData<Resource<Object>> declineRequest() {
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>();
final Call<String> request = service.declineRequest(threadId);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call,
@NonNull final Response<String> response) {
if (!response.isSuccessful()) {
try {
final String string = response.errorBody() != null ? response.errorBody().string() : "";
final String msg = String.format(Locale.US,
"onResponse: url: %s, responseCode: %d, errorBody: %s",
call.request().url().toString(),
response.code(),
string);
Log.e(TAG, msg);
data.postValue(Resource.error(msg, null));
return;
} catch (IOException e) {
Log.e(TAG, "onResponse: ", e);
}
return;
}
data.postValue(Resource.success(new Object()));
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
Log.e(TAG, "onFailure: ", t);
data.postValue(Resource.error(t.getMessage(), null));
}
});
return data;
}
} }

32
app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java

@ -409,4 +409,36 @@ public class DirectMessagesService extends BaseService {
); );
return repository.end(threadId, form); return repository.end(threadId, form);
} }
public Call<DirectInboxResponse> fetchPendingInbox(final String cursor, final long seqId) {
final ImmutableMap.Builder<String, Object> queryMapBuilder = ImmutableMap.<String, Object>builder()
.put("visual_message_return_type", "unseen")
.put("thread_message_limit", 10)
.put("persistentBadging", true)
.put("limit", 10);
if (!TextUtils.isEmpty(cursor)) {
queryMapBuilder.put("cursor", cursor);
queryMapBuilder.put("direction", "older");
}
if (seqId != 0) {
queryMapBuilder.put("seq_id", seqId);
}
return repository.fetchPendingInbox(queryMapBuilder.build());
}
public Call<String> approveRequest(@NonNull final String threadId) {
final ImmutableMap<String, String> form = ImmutableMap.of(
"_csrftoken", csrfToken,
"_uuid", deviceUuid
);
return repository.approveRequest(threadId, form);
}
public Call<String> declineRequest(@NonNull final String threadId) {
final ImmutableMap<String, String> form = ImmutableMap.of(
"_csrftoken", csrfToken,
"_uuid", deviceUuid
);
return repository.declineRequest(threadId, form);
}
} }

11
app/src/main/res/drawable/ic_account_clock_24.xml

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M10.63,14.1C12.23,10.58 16.38,9.03 19.9,10.63C23.42,12.23 24.97,16.38 23.37,19.9C22.24,22.4 19.75,24 17,24C14.3,24 11.83,22.44 10.67,20H1V18C1.06,16.86 1.84,15.93 3.34,15.18C4.84,14.43 6.72,14.04 9,14C9.57,14 10.11,14.05 10.63,14.1V14.1M9,4C10.12,4.03 11.06,4.42 11.81,5.17C12.56,5.92 12.93,6.86 12.93,8C12.93,9.14 12.56,10.08 11.81,10.83C11.06,11.58 10.12,11.95 9,11.95C7.88,11.95 6.94,11.58 6.19,10.83C5.44,10.08 5.07,9.14 5.07,8C5.07,6.86 5.44,5.92 6.19,5.17C6.94,4.42 7.88,4.03 9,4M17,22A5,5 0 0,0 22,17A5,5 0 0,0 17,12A5,5 0 0,0 12,17A5,5 0 0,0 17,22M16,14H17.5V16.82L19.94,18.23L19.19,19.53L16,17.69V14Z" />
</vector>

78
app/src/main/res/layout/fragment_direct_messages_thread.xml

@ -11,12 +11,18 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:scrollbars="none" android:scrollbars="none"
app:layout_constraintBottom_toTopOf="@id/reply_info"
app:layout_constraintBottom_toTopOf="@id/chats_barrier"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/layout_dm_base" /> tools:listitem="@layout/layout_dm_base" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/chats_barrier"
android:layout_width="0dp"
android:layout_height="0dp"
app:barrierDirection="bottom" />
<View <View
android:id="@+id/reply_bg" android:id="@+id/reply_bg"
android:layout_width="0dp" android:layout_width="0dp"
@ -44,7 +50,7 @@
app:layout_constraintBottom_toTopOf="@id/reply_preview_text" app:layout_constraintBottom_toTopOf="@id/reply_preview_text"
app:layout_constraintEnd_toStartOf="@id/reply_preview_image" app:layout_constraintEnd_toStartOf="@id/reply_preview_image"
app:layout_constraintStart_toStartOf="@id/input_bg" app:layout_constraintStart_toStartOf="@id/input_bg"
app:layout_constraintTop_toBottomOf="@id/chats"
app:layout_constraintTop_toBottomOf="@id/chats_barrier"
tools:text="Replying to yourself" tools:text="Replying to yourself"
tools:visibility="gone" /> tools:visibility="gone" />
@ -110,6 +116,7 @@
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:background="@drawable/bg_input" android:background="@drawable/bg_input"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/input" app:layout_constraintBottom_toBottomOf="@id/input"
app:layout_constraintEnd_toStartOf="@id/send" app:layout_constraintEnd_toStartOf="@id/send"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -124,6 +131,7 @@
android:layout_marginEnd="2dp" android:layout_marginEnd="2dp"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:scrollbars="none" android:scrollbars="none"
android:visibility="gone"
app:icon="@drawable/ic_face_24" app:icon="@drawable/ic_face_24"
app:iconGravity="textStart" app:iconGravity="textStart"
app:iconSize="24dp" app:iconSize="24dp"
@ -148,6 +156,7 @@
android:paddingBottom="12dp" android:paddingBottom="12dp"
android:textColor="@color/white" android:textColor="@color/white"
android:textColorHint="@color/grey_500" android:textColorHint="@color/grey_500"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/camera" app:layout_constraintEnd_toStartOf="@id/camera"
app:layout_constraintStart_toEndOf="@id/emoji_toggle" app:layout_constraintStart_toEndOf="@id/emoji_toggle"
@ -163,12 +172,13 @@
android:paddingStart="4dp" android:paddingStart="4dp"
android:paddingEnd="4dp" android:paddingEnd="4dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/input_bg" app:layout_constraintBottom_toBottomOf="@id/input_bg"
app:layout_constraintEnd_toStartOf="@id/gallery" app:layout_constraintEnd_toStartOf="@id/gallery"
app:layout_constraintStart_toEndOf="@id/input" app:layout_constraintStart_toEndOf="@id/input"
app:layout_constraintTop_toTopOf="@id/input" app:layout_constraintTop_toTopOf="@id/input"
app:srcCompat="@drawable/ic_camera_24" app:srcCompat="@drawable/ic_camera_24"
tools:visibility="visible" />
tools:visibility="gone" />
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/gallery" android:id="@+id/gallery"
@ -181,22 +191,23 @@
android:paddingEnd="4dp" android:paddingEnd="4dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_image_24" android:src="@drawable/ic_image_24"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/input_bg" app:layout_constraintBottom_toBottomOf="@id/input_bg"
app:layout_constraintEnd_toStartOf="@id/send" app:layout_constraintEnd_toStartOf="@id/send"
app:layout_constraintStart_toEndOf="@id/camera" app:layout_constraintStart_toEndOf="@id/camera"
app:layout_constraintTop_toTopOf="@id/input" app:layout_constraintTop_toTopOf="@id/input"
tools:visibility="visible" />
tools:visibility="gone" />
<awais.instagrabber.customviews.RecordView <awais.instagrabber.customviews.RecordView
android:id="@+id/record_view" android:id="@+id/record_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:visibility="visible"
android:visibility="gone"
app:counter_time_color="@color/white" app:counter_time_color="@color/white"
app:layout_constraintBottom_toBottomOf="@id/input_bg" app:layout_constraintBottom_toBottomOf="@id/input_bg"
app:layout_constraintEnd_toEndOf="@id/input_bg" app:layout_constraintEnd_toEndOf="@id/input_bg"
app:layout_constraintStart_toStartOf="@id/input" app:layout_constraintStart_toStartOf="@id/input"
app:layout_constraintTop_toBottomOf="@id/chats"
app:layout_constraintTop_toBottomOf="@id/chats_barrier"
app:slide_to_cancel_arrow="@drawable/recv_ic_arrow" app:slide_to_cancel_arrow="@drawable/recv_ic_arrow"
app:slide_to_cancel_arrow_color="@color/white" app:slide_to_cancel_arrow_color="@color/white"
app:slide_to_cancel_bounds="0dp" app:slide_to_cancel_bounds="0dp"
@ -210,6 +221,7 @@
style="@style/Widget.MaterialComponents.Button.Icon.NoInsets" style="@style/Widget.MaterialComponents.Button.Icon.NoInsets"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:visibility="gone"
app:backgroundTint="@color/blue_900" app:backgroundTint="@color/blue_900"
app:elevation="4dp" app:elevation="4dp"
app:icon="@drawable/avd_mic_to_send_anim" app:icon="@drawable/avd_mic_to_send_anim"
@ -226,17 +238,55 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="250dp" android:layout_height="250dp"
android:translationY="250dp" android:translationY="250dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<View
android:id="@+id/long_click_backdrop"
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/accept_pending_request_question"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:elevation="5dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:visibility="gone" />
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:text="@string/accept_request_from_user"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/decline"
app:layout_constraintTop_toBottomOf="@id/chats_barrier"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/decline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:padding="16dp"
android:text="@string/decline"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/red_500"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/accept"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accept_pending_request_question"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/accept"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:padding="16dp"
android:text="@string/accept"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/decline"
app:layout_constraintTop_toBottomOf="@id/accept_pending_request_question"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

23
app/src/main/res/layout/fragment_direct_pending_inbox.xml

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/pending_inbox_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="?attr/actionBarSize"
tools:listitem="@layout/layout_dm_inbox_item" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

43
app/src/main/res/navigation/direct_messages_nav_graph.xml

@ -80,25 +80,7 @@
<action <action
android:id="@+id/action_global_user_search" android:id="@+id/action_global_user_search"
app:destination="@id/user_search_nav_graph">
<!--<argument-->
<!-- android:name="multiple"-->
<!-- app:argType="boolean" />-->
<!--<argument-->
<!-- android:name="title"-->
<!-- app:argType="string"-->
<!-- app:nullable="true" />-->
<!--<argument-->
<!-- android:name="action_label"-->
<!-- app:argType="string"-->
<!-- app:nullable="true" />-->
<!--<argument-->
<!-- android:name="hideUserIds"-->
<!-- app:argType="long[]" />-->
</action>
app:destination="@id/user_search_nav_graph" />
<fragment <fragment
android:id="@+id/directMessagesInboxFragment" android:id="@+id/directMessagesInboxFragment"
@ -108,6 +90,9 @@
<action <action
android:id="@+id/action_inbox_to_thread" android:id="@+id/action_inbox_to_thread"
app:destination="@id/directMessagesThreadFragment" /> app:destination="@id/directMessagesThreadFragment" />
<action
android:id="@+id/action_inbox_to_pending_inbox"
app:destination="@id/directPendingInboxFragment" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/directMessagesThreadFragment" android:id="@+id/directMessagesThreadFragment"
@ -119,8 +104,12 @@
<argument <argument
android:name="title" android:name="title"
app:argType="string" /> app:argType="string" />
<argument
android:name="pending"
android:defaultValue="false"
app:argType="boolean" />
<action <action
android:id="@+id/action_dMThreadFragment_to_dMSettingsFragment"
android:id="@+id/action_thread_to_settings"
app:destination="@id/directMessagesSettingsFragment" /> app:destination="@id/directMessagesSettingsFragment" />
<action <action
android:id="@+id/action_thread_to_image_edit" android:id="@+id/action_thread_to_image_edit"
@ -144,6 +133,11 @@
app:argType="string" app:argType="string"
app:nullable="true" /> app:nullable="true" />
<argument
android:name="pending"
android:defaultValue="false"
app:argType="boolean" />
<action <action
android:id="@+id/action_settings_to_inbox" android:id="@+id/action_settings_to_inbox"
app:destination="@id/directMessagesInboxFragment" app:destination="@id/directMessagesInboxFragment"
@ -170,4 +164,13 @@
android:name="options" android:name="options"
app:argType="awais.instagrabber.repositories.requests.StoryViewerOptions" /> app:argType="awais.instagrabber.repositories.requests.StoryViewerOptions" />
</fragment> </fragment>
<fragment
android:id="@+id/directPendingInboxFragment"
android:name="awais.instagrabber.fragments.directmessages.DirectPendingInboxFragment"
android:label="@string/pending_requests"
tools:layout="@layout/fragment_direct_pending_inbox">
<action
android:id="@+id/action_pending_inbox_to_thread"
app:destination="@id/directMessagesThreadFragment" />
</fragment>
</navigation> </navigation>

1
app/src/main/res/values/ids.xml

@ -3,4 +3,5 @@
<item name="reply" type="id" /> <item name="reply" type="id" />
<item name="unsend" type="id" /> <item name="unsend" type="id" />
<item name="forward" type="id" /> <item name="forward" type="id" />
<item name="pending_requests" type="id" />
</resources> </resources>

4
app/src/main/res/values/strings.xml

@ -418,4 +418,8 @@
<string name="dms_action_end">End chat</string> <string name="dms_action_end">End chat</string>
<string name="dms_action_end_question">End chat?</string> <string name="dms_action_end_question">End chat?</string>
<string name="dms_action_end_description">All members will be removed from the group. They will still be able to view the chat history.</string> <string name="dms_action_end_description">All members will be removed from the group. They will still be able to view the chat history.</string>
<string name="pending_requests">Pending Requests</string>
<string name="accept_request_from_user">Accept request from %1s (%2s)?</string>
<string name="decline">Decline</string>
<string name="accept">Accept</string>
</resources> </resources>
Loading…
Cancel
Save