diff --git a/app/build.gradle b/app/build.gradle index 8083b4e2..58e2788a 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,8 +39,8 @@ android { buildTypes { debug { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' +// minifyEnabled true +// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } release { @@ -62,7 +62,7 @@ dependencies { def nav_version = '2.3.2' def exoplayer_version = '2.12.0' - implementation 'com.google.android.material:material:1.3.0-beta01' + implementation 'com.google.android.material:material:1.3.0-rc01' implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version" implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version" diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index 117ded8b..341fe21c 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -38,6 +38,7 @@ import androidx.emoji.text.FontRequestEmojiCompatConfig; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavBackStackEntry; import androidx.navigation.NavController; import androidx.navigation.NavDestination; @@ -75,6 +76,7 @@ import awais.instagrabber.utils.IntentUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.emoji.EmojiParser; +import awais.instagrabber.viewmodels.AppStateViewModel; import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -102,6 +104,7 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage private int firstFragmentGraphIndex; private boolean isActivityCheckerServiceBound = false; private boolean isBackStackEmpty = false; + private boolean isLoggedIn; private final ServiceConnection serviceConnection = new ServiceConnection() { @Override @@ -131,6 +134,7 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage binding = ActivityMainBinding.inflate(getLayoutInflater()); final String cookie = settingsHelper.getString(Constants.COOKIE); CookieUtils.setupCookies(cookie); + isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0; setContentView(binding.getRoot()); final Toolbar toolbar = binding.toolbar; setSupportActionBar(toolbar); @@ -142,6 +146,7 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage final boolean checkUpdates = settingsHelper.getBoolean(Constants.CHECK_UPDATES); if (checkUpdates) FlavorTown.updateCheck(this); FlavorTown.changelogCheck(this); + new ViewModelProvider(this).get(AppStateViewModel.class); // Just initiate the App state here final Intent intent = getIntent(); handleIntent(intent); if (!TextUtils.isEmpty(cookie) && settingsHelper.getBoolean(Constants.CHECK_ACTIVITY)) { @@ -387,8 +392,6 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage private void setupBottomNavigationBar(final boolean setDefaultFromSettings) { int main_nav_ids = R.array.main_nav_ids; - final String cookie = settingsHelper.getString(Constants.COOKIE); - final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0; if (!isLoggedIn) { main_nav_ids = R.array.logged_out_main_nav_ids; final int selectedItemId = binding.bottomNavView.getSelectedItemId(); diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java index ae72002c..9fa309f2 100644 --- a/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java @@ -258,7 +258,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter list) { diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectMessageInboxAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectMessageInboxAdapter.java index ba3a7a17..2ccb5df3 100644 --- a/app/src/main/java/awais/instagrabber/adapters/DirectMessageInboxAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/DirectMessageInboxAdapter.java @@ -36,7 +36,9 @@ public final class DirectMessageInboxAdapter extends ListAdapter lastSeenAtMap = thread.getLastSeenAt(); - final boolean read = ResponseBodyUtils.isRead(item, lastSeenAtMap, Collections.singletonList(thread.getViewerId()), thread.getDirectStory()); + final boolean read; + if (thread.getDirectStory() != null) { + read = false; + } else { + final DirectItem item = thread.getFirstDirectItem(); + if (item.getUserId() == thread.getViewerId()) { + // if last item was sent by user, then it is read (even though we have auto read unchecked?) + read = true; + } else { + final Map lastSeenAtMap = thread.getLastSeenAt(); + read = ResponseBodyUtils.isRead(item, lastSeenAtMap, Collections.singletonList(thread.getViewerId())); + } + } binding.unread.setVisibility(read ? View.GONE : View.VISIBLE); binding.threadTitle.setTypeface(binding.threadTitle.getTypeface(), read ? Typeface.NORMAL : Typeface.BOLD); binding.subtitle.setTypeface(binding.subtitle.getTypeface(), read ? Typeface.NORMAL : Typeface.BOLD); diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java index 739a4a66..c1bec7c0 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java @@ -196,7 +196,10 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple if (item.isPending()) { binding.deliveryStatus.setImageResource(R.drawable.ic_check_24); } else { - final boolean read = ResponseBodyUtils.isRead(item, thread.getLastSeenAt(), userIds, null); + final boolean read = ResponseBodyUtils.isRead(item, + thread.getLastSeenAt(), + userIds + ); binding.deliveryStatus.setImageResource(R.drawable.ic_check_all_24); ImageViewCompat.setImageTintList( binding.deliveryStatus, @@ -358,7 +361,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple final DirectItemReactions reactions = item.getReactions(); final List emojis = reactions != null ? reactions.getEmojis() : null; if (emojis == null || emojis.isEmpty()) { - binding.container.setPadding(messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall); + binding.container.setPadding(messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall, 0); binding.reactionsWrapper.setVisibility(View.GONE); return; } diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java index 5d086d53..d2db9f8f 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java @@ -29,6 +29,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 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.snackbar.Snackbar; import java.util.List; @@ -172,6 +173,7 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh } private void setupObservers() { + removeViewModelObservers(); threadsObserver = list -> { if (inboxAdapter == null) return; inboxAdapter.submitList(list, () -> { @@ -181,8 +183,28 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh }); }; viewModel.getThreads().observe(fragmentActivity, threadsObserver); - viewModel.getFetchingInbox().observe(getViewLifecycleOwner(), fetching -> binding.swipeRefreshLayout.setRefreshing(fetching)); - viewModel.getUnseenCount().observe(getViewLifecycleOwner(), this::setBottomNavBarBadge); + viewModel.getInbox().observe(getViewLifecycleOwner(), inboxResource -> { + if (inboxResource == null) return; + switch (inboxResource.status) { + case SUCCESS: + binding.swipeRefreshLayout.setRefreshing(false); + break; + case ERROR: + if (inboxResource.message != null) { + Snackbar.make(binding.getRoot(), inboxResource.message, Snackbar.LENGTH_LONG).show(); + } + binding.swipeRefreshLayout.setRefreshing(false); + break; + case LOADING: + binding.swipeRefreshLayout.setRefreshing(true); + break; + } + }); + viewModel.getUnseenCount().observe(getViewLifecycleOwner(), unseenCountResource -> { + if (unseenCountResource == null) return; + final Integer unseenCount = unseenCountResource.data; + setBottomNavBarBadge(unseenCount == null ? 0 : unseenCount); + }); viewModel.getPendingRequestsTotal().observe(getViewLifecycleOwner(), this::attachPendingRequestsBadge); } @@ -230,11 +252,10 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh inboxAdapter = new DirectMessageInboxAdapter(thread -> { if (navigating) return; navigating = true; - final Bundle bundle = new Bundle(); - bundle.putString("threadId", thread.getThreadId()); - bundle.putString("title", thread.getThreadTitle()); if (isAdded()) { - NavHostFragment.findNavController(this).navigate(R.id.action_inbox_to_thread, bundle); + final DirectMessageInboxFragmentDirections.ActionInboxToThread directions = DirectMessageInboxFragmentDirections + .actionInboxToThread(thread.getThreadId(), thread.getThreadTitle()); + NavHostFragment.findNavController(this).navigate(directions); } navigating = false; }); diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java index f29dd393..d35ecd09 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java @@ -11,13 +11,11 @@ import android.widget.CompoundButton; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.util.Pair; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProvider; -import androidx.lifecycle.ViewModelStoreOwner; import androidx.navigation.NavBackStackEntry; import androidx.navigation.NavController; import androidx.navigation.NavDestination; @@ -31,13 +29,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import awais.instagrabber.ProfileNavGraphDirections; import awais.instagrabber.R; import awais.instagrabber.UserSearchNavGraphDirections; +import awais.instagrabber.activities.MainActivity; import awais.instagrabber.adapters.DirectPendingUsersAdapter; import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUser; import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUserCallback; @@ -52,12 +50,11 @@ import awais.instagrabber.fragments.UserSearchFragment; import awais.instagrabber.fragments.UserSearchFragmentDirections; import awais.instagrabber.models.Resource; import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; -import awais.instagrabber.viewmodels.DirectInboxViewModel; -import awais.instagrabber.viewmodels.DirectPendingInboxViewModel; +import awais.instagrabber.viewmodels.AppStateViewModel; import awais.instagrabber.viewmodels.DirectSettingsViewModel; +import awais.instagrabber.viewmodels.factories.DirectSettingsViewModelFactory; public class DirectMessageSettingsFragment extends Fragment implements ConfirmDialogFragmentCallback { private static final String TAG = DirectMessageSettingsFragment.class.getSimpleName(); @@ -77,33 +74,14 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi super.onCreate(savedInstanceState); final Bundle arguments = getArguments(); if (arguments == null) return; - final NavController navController = NavHostFragment.findNavController(this); - final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); final DirectMessageSettingsFragmentArgs args = DirectMessageSettingsFragmentArgs.fromBundle(arguments); - final boolean pending = args.getPending(); - final List 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); - threads = inboxViewModel.getThreads().getValue(); - viewer = inboxViewModel.getViewer(); - } - final String threadId = args.getThreadId(); - final Optional first = threads != null ? threads.stream() - .filter(thread -> thread.getThreadId().equals(threadId)) - .findFirst() - : Optional.empty(); - if (!first.isPresent()) { - navController.navigateUp(); - return; - } - viewModel = new ViewModelProvider(this).get(DirectSettingsViewModel.class); - viewModel.setViewer(viewer); - viewModel.setThread(first.get()); + final MainActivity fragmentActivity = (MainActivity) requireActivity(); + final AppStateViewModel appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class); + viewModel = new ViewModelProvider(this, new DirectSettingsViewModelFactory(fragmentActivity.getApplication(), + args.getThreadId(), + args.getPending(), + appStateViewModel.getCurrentUser())) + .get(DirectSettingsViewModel.class); } @NonNull @@ -143,21 +121,23 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi binding.muteMessages.setVisibility(View.GONE); } }); - viewModel.getUsers().observe(getViewLifecycleOwner(), users -> { + // Need to observe, so that getValue is correct + viewModel.getUsers().observe(getViewLifecycleOwner(), users -> {}); + viewModel.getLeftUsers().observe(getViewLifecycleOwner(), users -> {}); + viewModel.getUsersAndLeftUsers().observe(getViewLifecycleOwner(), usersPair -> { if (usersAdapter == null) return; - usersAdapter.submitUsers(users.first, users.second); + usersAdapter.submitUsers(usersPair.first, usersPair.second); }); viewModel.getTitle().observe(getViewLifecycleOwner(), title -> binding.titleEdit.setText(title)); viewModel.getAdminUserIds().observe(getViewLifecycleOwner(), adminUserIds -> { if (usersAdapter == null) return; usersAdapter.setAdminUserIds(adminUserIds); }); - viewModel.getMuted().observe(getViewLifecycleOwner(), muted -> binding.muteMessages.setChecked(muted)); + viewModel.isMuted().observe(getViewLifecycleOwner(), muted -> binding.muteMessages.setChecked(muted)); viewModel.isPending().observe(getViewLifecycleOwner(), pending -> binding.muteMessages.setVisibility(pending ? View.GONE : View.VISIBLE)); - if (viewModel.isViewerAdmin()) { - viewModel.getApprovalRequiredToJoin().observe(getViewLifecycleOwner(), required -> binding.approvalRequired.setChecked(required)); - viewModel.getPendingRequests().observe(getViewLifecycleOwner(), this::setPendingRequests); - } + viewModel.isViewerAdmin().observe(getViewLifecycleOwner(), this::setApprovalRelatedUI); + viewModel.getApprovalRequiredToJoin().observe(getViewLifecycleOwner(), required -> binding.approvalRequired.setChecked(required)); + viewModel.getPendingRequests().observe(getViewLifecycleOwner(), this::setPendingRequests); final NavController navController = NavHostFragment.findNavController(this); final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry(); if (backStackEntry != null) { @@ -192,7 +172,11 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi private void addMembers(final Set users) { final Boolean approvalRequired = viewModel.getApprovalRequiredToJoin().getValue(); - if (!viewModel.isViewerAdmin() && approvalRequired != null && approvalRequired) { + Boolean isViewerAdmin = viewModel.isViewerAdmin().getValue(); + if (isViewerAdmin == null) { + isViewerAdmin = false; + } + if (!isViewerAdmin && approvalRequired != null && approvalRequired) { approvalRequiredUsers = users; final ConfirmDialogFragment confirmDialogFragment = ConfirmDialogFragment.newInstance( APPROVAL_REQUIRED_REQUEST_CODE, @@ -226,13 +210,15 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi } private void setupSettings() { - binding.groupSettings.setVisibility(viewModel.isGroup() ? View.VISIBLE : View.GONE); + Boolean isGroup = viewModel.isGroup().getValue(); + if (isGroup == null) isGroup = false; + binding.groupSettings.setVisibility(isGroup ? View.VISIBLE : View.GONE); binding.muteMessagesLabel.setOnClickListener(v -> binding.muteMessages.toggle()); binding.muteMessages.setOnCheckedChangeListener((buttonView, isChecked) -> { final LiveData> resourceLiveData = isChecked ? viewModel.mute() : viewModel.unmute(); handleSwitchChangeResource(resourceLiveData, buttonView); }); - if (!viewModel.isGroup()) return; + if (!isGroup) return; binding.titleEdit.addTextChangedListener(new TextWatcherAdapter() { @Override public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { @@ -256,14 +242,13 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi final NavDestination currentDestination = navController.getCurrentDestination(); if (currentDestination == null) return; if (currentDestination.getId() != R.id.directMessagesSettingsFragment) return; - final Pair, List> users = viewModel.getUsers().getValue(); + final List users = viewModel.getUsers().getValue(); final long[] currentUserIds; - if (users != null && users.first != null) { - final List currentMembers = users.first; - currentUserIds = currentMembers.stream() - .mapToLong(User::getPk) - .sorted() - .toArray(); + if (users != null) { + currentUserIds = users.stream() + .mapToLong(User::getPk) + .sorted() + .toArray(); } else { currentUserIds = new long[0]; } @@ -281,7 +266,6 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi final LiveData> resourceLiveData = isChecked ? viewModel.muteMentions() : viewModel.unmuteMentions(); handleSwitchChangeResource(resourceLiveData, buttonView); }); - setApprovalRelatedUI(); binding.leave.setOnClickListener(v -> { final ConfirmDialogFragment confirmDialogFragment = ConfirmDialogFragment.newInstance( LEAVE_THREAD_REQUEST_CODE, @@ -293,7 +277,9 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi ); confirmDialogFragment.show(getChildFragmentManager(), "leave_thread_confirmation_dialog"); }); - if (viewModel.isViewerAdmin()) { + Boolean isViewerAdmin = viewModel.isViewerAdmin().getValue(); + if (isViewerAdmin == null) isViewerAdmin = false; + if (isViewerAdmin) { binding.end.setVisibility(View.VISIBLE); binding.end.setOnClickListener(v -> { final ConfirmDialogFragment confirmDialogFragment = ConfirmDialogFragment.newInstance( @@ -311,8 +297,8 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi } } - private void setApprovalRelatedUI() { - if (!viewModel.isViewerAdmin()) { + private void setApprovalRelatedUI(final boolean isViewerAdmin) { + if (!isViewerAdmin) { binding.pendingMembersGroup.setVisibility(View.GONE); binding.approvalRequired.setVisibility(View.GONE); binding.approvalRequiredLabel.setVisibility(View.GONE); @@ -352,7 +338,7 @@ public class DirectMessageSettingsFragment extends Fragment implements ConfirmDi final Context context = getContext(); if (context == null) return; binding.users.setLayoutManager(new LinearLayoutManager(context)); - final User inviter = viewModel.getThread().getInviter(); + final User inviter = viewModel.getInviter().getValue(); usersAdapter = new DirectUsersAdapter( inviter != null ? inviter.getPk() : -1, (position, user, selected) -> { 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 0eb2fc61..f6b4ab67 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -37,7 +37,6 @@ import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; -import androidx.lifecycle.ViewModelStoreOwner; import androidx.navigation.NavBackStackEntry; import androidx.navigation.NavController; import androidx.navigation.NavDirections; @@ -58,7 +57,6 @@ import com.google.common.collect.ImmutableList; import java.io.File; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.Set; import awais.instagrabber.ProfileNavGraphDirections; @@ -104,9 +102,8 @@ import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.AppStateViewModel; -import awais.instagrabber.viewmodels.DirectInboxViewModel; -import awais.instagrabber.viewmodels.DirectPendingInboxViewModel; import awais.instagrabber.viewmodels.DirectThreadViewModel; +import awais.instagrabber.viewmodels.factories.DirectThreadViewModelFactory; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; @@ -235,7 +232,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact @Override public void onReaction(final DirectItem item, final Emoji emoji) { if (item == null) return; - final LiveData> resourceLiveData = viewModel.sendReaction(item, emoji); + final LiveData> resourceLiveData = viewModel.sendReaction(item, emoji); if (resourceLiveData != null) { resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData)); } @@ -295,13 +292,29 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact }; 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); - viewModel = new ViewModelProvider(this).get(DirectThreadViewModel.class); + final Bundle arguments = getArguments(); + if (arguments == null) return; + final DirectMessageThreadFragmentArgs fragmentArgs = DirectMessageThreadFragmentArgs.fromBundle(arguments); + viewModel = new ViewModelProvider(this, new DirectThreadViewModelFactory(fragmentActivity.getApplication(), + fragmentArgs.getThreadId(), + fragmentArgs.getPending(), + appStateViewModel.getCurrentUser())) + .get(DirectThreadViewModel.class); setHasOptionsMenu(true); } @@ -332,7 +345,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact init(); binding.send.post(() -> initialSendX = binding.send.getX()); shouldRefresh = false; - setObservers(); } @Override @@ -406,6 +418,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact wasKbShowing = true; binding.emojiPicker.setAlpha(0); } + removeObservers(); super.onPause(); } @@ -423,7 +436,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } binding.send.stopScale(); setupBackStackResultObserver(); - attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue()); + setObservers(); + // attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue()); } @Override @@ -463,42 +477,38 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (context == null) return; if (getArguments() == null) return; actionBar = fragmentActivity.getSupportActionBar(); - final DirectMessageThreadFragmentArgs fragmentArgs = DirectMessageThreadFragmentArgs.fromBundle(getArguments()); - viewModel.getThreadTitle().postValue(fragmentArgs.getTitle()); - final String threadId = fragmentArgs.getThreadId(); - viewModel.setThreadId(threadId); setupList(); root.post(this::setupInput); - root.post(this::getInitialData); + // root.post(this::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 ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); - final List threads; - if (!pending) { - final DirectInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class); - threads = threadListViewModel.getThreads().getValue(); - } else { - final DirectPendingInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectPendingInboxViewModel.class); - threads = threadListViewModel.getThreads().getValue(); - } - final Optional first = threads != null - ? threads.stream() - .filter(thread -> thread.getThreadId().equals(viewModel.getThreadId())) - .findFirst() - : Optional.empty(); - if (first.isPresent()) { - final DirectThread thread = first.get(); - viewModel.setThread(thread); - return; - } - viewModel.fetchChats(); - } + // 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 ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); + // final List threads; + // if (!pending) { + // final DirectInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class); + // threads = threadListViewModel.getThreads().getValue(); + // } else { + // final DirectPendingInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectPendingInboxViewModel.class); + // threads = threadListViewModel.getThreads().getValue(); + // } + // final Optional first = threads != null + // ? threads.stream() + // .filter(thread -> thread.getThreadId().equals(viewModel.getThreadId())) + // .findFirst() + // : Optional.empty(); + // if (first.isPresent()) { + // final DirectThread thread = first.get(); + // viewModel.setThread(thread); + // return; + // } + // viewModel.fetchChats(); + // } private void setupList() { final Context context = getContext(); @@ -542,7 +552,14 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } private void setObservers() { - viewModel.isPending().observe(getViewLifecycleOwner(), isPending -> { + threadLiveData = viewModel.getThread(); + if (threadLiveData == null) { + final NavController navController = NavHostFragment.findNavController(this); + navController.navigateUp(); + return; + } + pendingLiveData = viewModel.isPending(); + pendingLiveData.observe(getViewLifecycleOwner(), isPending -> { if (isPending == null) { hideInput(); return; @@ -556,7 +573,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (inputMode != null && inputMode == 1) return; showInput(); }); - viewModel.getInputMode().observe(getViewLifecycleOwner(), inputMode -> { + inputModeLiveData = viewModel.getInputMode(); + inputModeLiveData.observe(getViewLifecycleOwner(), inputMode -> { final Boolean isPending = viewModel.isPending().getValue(); if (isPending != null && isPending) return; if (inputMode == null || inputMode == 0) return; @@ -564,21 +582,34 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact hideInput(); } }); - viewModel.getThreadTitle().observe(getViewLifecycleOwner(), this::setTitle); - viewModel.getFetching().observe(getViewLifecycleOwner(), fetching -> { - if (fetching) { - setTitle(UPDATING_TITLE); - return; + threadTitleLiveData = viewModel.getThreadTitle(); + threadTitleLiveData.observe(getViewLifecycleOwner(), this::setTitle); + fetchingLiveData = viewModel.isFetching(); + fetchingLiveData.observe(getViewLifecycleOwner(), fetchingResource -> { + if (fetchingResource == null) return; + switch (fetchingResource.status) { + case SUCCESS: + case ERROR: + setTitle(viewModel.getThreadTitle().getValue()); + if (fetchingResource.message != null) { + Snackbar.make(binding.getRoot(), fetchingResource.message, Snackbar.LENGTH_LONG).show(); + } + break; + case LOADING: + setTitle(UPDATING_TITLE); + break; } - setTitle(viewModel.getThreadTitle().getValue()); }); - final ItemsAdapterDataMerger itemsAdapterDataMerger = new ItemsAdapterDataMerger(appStateViewModel.getCurrentUser(), viewModel.getThread()); - itemsAdapterDataMerger.observe(getViewLifecycleOwner(), userThreadPair -> { - viewModel.setCurrentUser(userThreadPair.first); - setupItemsAdapter(userThreadPair.first, userThreadPair.second); - }); - viewModel.getItems().observe(getViewLifecycleOwner(), this::submitItemsToAdapter); - viewModel.getReplyToItem().observe(getViewLifecycleOwner(), item -> { + // final ItemsAdapterDataMerger itemsAdapterDataMerger = new ItemsAdapterDataMerger(appStateViewModel.getCurrentUser(), viewModel.getThread()); + // itemsAdapterDataMerger.observe(getViewLifecycleOwner(), userThreadPair -> { + // viewModel.setCurrentUser(userThreadPair.first); + // setupItemsAdapter(userThreadPair.first, userThreadPair.second); + // }); + threadLiveData.observe(getViewLifecycleOwner(), this::setupItemsAdapter); + itemsLiveData = viewModel.getItems(); + itemsLiveData.observe(getViewLifecycleOwner(), this::submitItemsToAdapter); + replyToItemLiveData = viewModel.getReplyToItem(); + replyToItemLiveData.observe(getViewLifecycleOwner(), item -> { if (item == null) { if (binding.input.length() == 0) { showExtraInputOption(true); @@ -633,14 +664,30 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } prevLength = length; }); - viewModel.getPendingRequestsCount().observe(getViewLifecycleOwner(), this::attachPendingRequestsBadge); - viewModel.getUsers().observe(getViewLifecycleOwner(), users -> { + pendingRequestsCountLiveData = viewModel.getPendingRequestsCount(); + pendingRequestsCountLiveData.observe(getViewLifecycleOwner(), this::attachPendingRequestsBadge); + usersLiveData = viewModel.getUsers(); + usersLiveData.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 removeObservers() { + pendingLiveData.removeObservers(getViewLifecycleOwner()); + inputModeLiveData.removeObservers(getViewLifecycleOwner()); + threadTitleLiveData.removeObservers(getViewLifecycleOwner()); + fetchingLiveData.removeObservers(getViewLifecycleOwner()); + threadLiveData.removeObservers(getViewLifecycleOwner()); + itemsLiveData.removeObservers(getViewLifecycleOwner()); + replyToItemLiveData.removeObservers(getViewLifecycleOwner()); + inputLength.removeObservers(getViewLifecycleOwner()); + pendingRequestsCountLiveData.removeObservers(getViewLifecycleOwner()); + usersLiveData.removeObservers(getViewLifecycleOwner()); + + } + private void hidePendingOptions() { binding.acceptPendingRequestQuestion.setVisibility(View.GONE); binding.decline.setVisibility(View.GONE); @@ -669,9 +716,15 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact case SUCCESS: resourceLiveData.removeObservers(getViewLifecycleOwner()); if (isDecline) { + removeObservers(); + viewModel.removeThread(); final NavController navController = NavHostFragment.findNavController(this); navController.navigateUp(); + return; } + removeObservers(); + viewModel.moveFromPending(); + setObservers(); break; case LOADING: break; @@ -838,12 +891,15 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact }); } - private void setupItemsAdapter(final User currentUser, final DirectThread thread) { + private void setupItemsAdapter(final DirectThread thread) { + if (thread == null) return; if (itemsAdapter != null) { if (itemsAdapter.getThread() == thread) return; itemsAdapter.setThread(thread); return; } + final User currentUser = appStateViewModel.getCurrentUser(); + if (currentUser == null) return; itemsAdapter = new DirectItemsAdapter(currentUser, thread, directItemCallback, directItemLongClickListener); itemsAdapter.setHasStableIds(true); itemsAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); @@ -958,7 +1014,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact binding.send.setOnRecordClickListener(v -> { final Editable text = binding.input.getText(); if (TextUtils.isEmpty(text)) return; - final LiveData> resourceLiveData = viewModel.sendText(text.toString()); + final LiveData> resourceLiveData = viewModel.sendText(text.toString()); resourceLiveData.observe(getViewLifecycleOwner(), resource -> handleSentMessage(resourceLiveData)); binding.input.setText(""); viewModel.setReplyToItem(null); @@ -1031,8 +1087,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact navController.navigate(navDirections); } - private void handleSentMessage(final LiveData> resourceLiveData) { - final Resource resource = resourceLiveData.getValue(); + private void handleSentMessage(final LiveData> resourceLiveData) { + final Resource resource = resourceLiveData.getValue(); if (resource == null) return; final Resource.Status status = resource.status; switch (status) { @@ -1380,10 +1436,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact animatorSet.start(); } - private void showLongClickOptions(final View itemView) { - - } - private void showReactionsDialog(final DirectItem item) { final LiveData> users = viewModel.getUsers(); final LiveData> leftUsers = viewModel.getLeftUsers(); @@ -1410,7 +1462,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } if (reaction == null) return; if (reaction.getSenderId() == viewModel.getViewerId()) { - final LiveData> resourceLiveData = viewModel.sendDeleteReaction(itemId); + final LiveData> resourceLiveData = viewModel.sendDeleteReaction(itemId); if (resourceLiveData != null) { resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData)); } diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectPendingInboxFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectPendingInboxFragment.java index fc702ab3..8ee0d1c4 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectPendingInboxFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectPendingInboxFragment.java @@ -19,6 +19,9 @@ import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import com.google.android.material.snackbar.Snackbar; + +import java.util.Collections; import java.util.List; import awais.instagrabber.R; @@ -102,16 +105,41 @@ public class DirectPendingInboxFragment extends Fragment implements SwipeRefresh } private void setupObservers() { + removeViewModelObservers(); threadsObserver = list -> { if (inboxAdapter == null) return; - inboxAdapter.submitList(list, () -> { + if (binding.swipeRefreshLayout.getVisibility() == View.GONE) { + binding.swipeRefreshLayout.setVisibility(View.VISIBLE); + binding.empty.setVisibility(View.GONE); + } + inboxAdapter.submitList(list == null ? Collections.emptyList() : list, () -> { if (!scrollToTop) return; binding.pendingInboxList.smoothScrollToPosition(0); scrollToTop = false; }); + if (list == null || list.isEmpty()) { + binding.swipeRefreshLayout.setVisibility(View.GONE); + binding.empty.setVisibility(View.VISIBLE); + } }; viewModel.getThreads().observe(fragmentActivity, threadsObserver); - viewModel.getFetchingInbox().observe(getViewLifecycleOwner(), fetching -> binding.swipeRefreshLayout.setRefreshing(fetching)); + viewModel.getInbox().observe(getViewLifecycleOwner(), inboxResource -> { + if (inboxResource == null) return; + switch (inboxResource.status) { + case SUCCESS: + binding.swipeRefreshLayout.setRefreshing(false); + break; + case ERROR: + if (inboxResource.message != null) { + Snackbar.make(binding.getRoot(), inboxResource.message, Snackbar.LENGTH_LONG).show(); + } + binding.swipeRefreshLayout.setRefreshing(false); + break; + case LOADING: + binding.swipeRefreshLayout.setRefreshing(true); + break; + } + }); } private void removeViewModelObservers() { diff --git a/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.java b/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.java new file mode 100644 index 00000000..6bc7f4da --- /dev/null +++ b/app/src/main/java/awais/instagrabber/managers/DirectMessagesManager.java @@ -0,0 +1,81 @@ +package awais.instagrabber.managers; + +import android.content.ContentResolver; + +import androidx.annotation.NonNull; + +import com.google.common.collect.Iterables; + +import java.util.List; + +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.repositories.responses.directmessages.DirectItem; +import awais.instagrabber.repositories.responses.directmessages.DirectThread; + +public final class DirectMessagesManager { + private static final String TAG = DirectMessagesManager.class.getSimpleName(); + private static final Object LOCK = new Object(); + + private static DirectMessagesManager instance; + + private final InboxManager inboxManager; + private final InboxManager pendingInboxManager; + + public static DirectMessagesManager getInstance() { + if (instance == null) { + synchronized (LOCK) { + if (instance == null) { + instance = new DirectMessagesManager(); + } + } + } + return instance; + } + + private DirectMessagesManager() { + inboxManager = InboxManager.getInstance(false); + pendingInboxManager = InboxManager.getInstance(true); + } + + public void moveThreadFromPending(@NonNull final String threadId) { + final List pendingThreads = pendingInboxManager.getThreads().getValue(); + if (pendingThreads == null) return; + final int index = Iterables.indexOf(pendingThreads, t -> t.getThreadId().equals(threadId)); + if (index < 0) return; + final DirectThread thread = pendingThreads.get(index); + final DirectItem threadFirstDirectItem = thread.getFirstDirectItem(); + if (threadFirstDirectItem == null) return; + final List threads = inboxManager.getThreads().getValue(); + int insertIndex = 0; + for (final DirectThread tempThread : threads) { + final DirectItem firstDirectItem = tempThread.getFirstDirectItem(); + if (firstDirectItem == null) continue; + final long timestamp = firstDirectItem.getTimestamp(); + if (timestamp < threadFirstDirectItem.getTimestamp()) { + break; + } + insertIndex++; + } + thread.setPending(false); + inboxManager.addThread(thread, insertIndex); + pendingInboxManager.removeThread(threadId); + final Integer currentTotal = inboxManager.getPendingRequestsTotal().getValue(); + if (currentTotal == null) return; + inboxManager.setPendingRequestsTotal(currentTotal - 1); + } + + public InboxManager getInboxManager() { + return inboxManager; + } + + public InboxManager getPendingInboxManager() { + return pendingInboxManager; + } + + public ThreadManager getThreadManager(@NonNull final String threadId, + final boolean pending, + @NonNull final User currentUser, + @NonNull final ContentResolver contentResolver) { + return ThreadManager.getInstance(threadId, pending, currentUser, contentResolver); + } +} diff --git a/app/src/main/java/awais/instagrabber/managers/InboxManager.java b/app/src/main/java/awais/instagrabber/managers/InboxManager.java new file mode 100644 index 00000000..81ddd195 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/managers/InboxManager.java @@ -0,0 +1,358 @@ +package awais.instagrabber.managers; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Transformations; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import awais.instagrabber.models.Resource; +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.DirectItem; +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 androidx.lifecycle.Transformations.distinctUntilChanged; +import static awais.instagrabber.utils.Utils.settingsHelper; + +public final class InboxManager { + private static final String TAG = InboxManager.class.getSimpleName(); + private static final LoadingCache THREAD_LOCKS = CacheBuilder + .newBuilder() + .expireAfterAccess(1, TimeUnit.MINUTES) // max lock time ever expected + .build(CacheLoader.from(Object::new)); + private static final Comparator THREAD_COMPARATOR = (t1, t2) -> { + final DirectItem t1FirstDirectItem = t1.getFirstDirectItem(); + final DirectItem t2FirstDirectItem = t2.getFirstDirectItem(); + if (t1FirstDirectItem == null && t2FirstDirectItem == null) return 0; + if (t1FirstDirectItem == null) return 1; + if (t2FirstDirectItem == null) return -1; + return Long.compare(t2FirstDirectItem.getTimestamp(), t1FirstDirectItem.getTimestamp()); + }; + + private final MutableLiveData> inbox = new MutableLiveData<>(); + private final MutableLiveData> unseenCount = new MutableLiveData<>(); + private final MutableLiveData pendingRequestsTotal = new MutableLiveData<>(0); + + private final LiveData> threads; + private final DirectMessagesService service; + private final boolean pending; + + private Call inboxRequest; + private Call unseenCountRequest; + private long seqId; + private String cursor; + private boolean hasOlder = true; + private User viewer; + + @NonNull + public static InboxManager getInstance(final boolean pending) { + return new InboxManager(pending); + } + + private InboxManager(final boolean pending) { + this.pending = pending; + 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); + + // Transformations + threads = distinctUntilChanged(Transformations.map(inbox, inboxResource -> { + if (inboxResource == null) { + return Collections.emptyList(); + } + final DirectInbox inbox = inboxResource.data; + if (inbox == null) { + return Collections.emptyList(); + } + return ImmutableList.sortedCopyOf(THREAD_COMPARATOR, inbox.getThreads()); + })); + + fetchInbox(); + if (!pending) { + fetchUnseenCount(); + } + } + + public LiveData> getInbox() { + return distinctUntilChanged(inbox); + } + + public LiveData> getThreads() { + return threads; + } + + public LiveData> getUnseenCount() { + return distinctUntilChanged(unseenCount); + } + + public LiveData getPendingRequestsTotal() { + return distinctUntilChanged(pendingRequestsTotal); + } + + public User getViewer() { + return viewer; + } + + public void fetchInbox() { + final Resource inboxResource = inbox.getValue(); + if ((inboxResource != null && inboxResource.status == Resource.Status.LOADING) || !hasOlder) return; + stopCurrentInboxRequest(); + inbox.postValue(Resource.loading(getCurrentDirectInbox())); + inboxRequest = pending ? service.fetchPendingInbox(cursor, seqId) : service.fetchInbox(cursor, seqId); + inboxRequest.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + parseInboxResponse(response.body()); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "Failed fetching dm inbox", t); + inbox.postValue(Resource.error(t.getMessage(), getCurrentDirectInbox())); + hasOlder = false; + } + }); + } + + public void fetchUnseenCount() { + final Resource unseenCountResource = unseenCount.getValue(); + if ((unseenCountResource != null && unseenCountResource.status == Resource.Status.LOADING)) return; + stopCurrentUnseenCountRequest(); + unseenCount.postValue(Resource.loading(getCurrentUnseenCount())); + unseenCountRequest = service.fetchUnseenCount(); + unseenCountRequest.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final DirectBadgeCount directBadgeCount = response.body(); + if (directBadgeCount == null) { + Log.e(TAG, "onResponse: directBadgeCount Response is null"); + unseenCount.postValue(Resource.error("Unseen count response is null", getCurrentUnseenCount())); + return; + } + unseenCount.postValue(Resource.success(directBadgeCount.getBadgeCount())); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "Failed fetching unseen count", t); + unseenCount.postValue(Resource.error(t.getMessage(), getCurrentUnseenCount())); + } + }); + } + + public void refresh() { + cursor = null; + seqId = 0; + hasOlder = true; + fetchInbox(); + if (!pending) { + fetchUnseenCount(); + } + } + + private DirectInbox getCurrentDirectInbox() { + final Resource inboxResource = inbox.getValue(); + return inboxResource != null ? inboxResource.data : null; + } + + private void parseInboxResponse(final DirectInboxResponse response) { + if (response == null) { + Log.e(TAG, "parseInboxResponse: Response is null"); + inbox.postValue(Resource.error("Response is null", getCurrentDirectInbox())); + hasOlder = false; + return; + } + if (!response.getStatus().equals("ok")) { + final String msg = "DM inbox fetch response: status not ok"; + Log.e(TAG, msg); + inbox.postValue(Resource.error(msg, getCurrentDirectInbox())); + hasOlder = false; + return; + } + seqId = response.getSeqId(); + if (viewer == null) { + viewer = response.getViewer(); + } + final DirectInbox inbox = response.getInbox(); + if (!TextUtils.isEmpty(cursor)) { + final DirectInbox currentDirectInbox = getCurrentDirectInbox(); + if (currentDirectInbox != null) { + List threads = currentDirectInbox.getThreads(); + threads = threads == null ? new LinkedList<>() : new LinkedList<>(threads); + threads.addAll(inbox.getThreads()); + inbox.setThreads(threads); + } + } + this.inbox.postValue(Resource.success(inbox)); + cursor = inbox.getOldestCursor(); + hasOlder = inbox.hasOlder(); + pendingRequestsTotal.postValue(response.getPendingRequestsTotal()); + } + + public void setThread(@NonNull final String threadId, + @NonNull final DirectThread thread) { + final DirectInbox inbox = getCurrentDirectInbox(); + if (inbox == null) return; + final int index = getThreadIndex(threadId, inbox); + setThread(inbox, index, thread); + } + + private void setThread(@NonNull final DirectInbox inbox, + final int index, + @NonNull final DirectThread thread) { + if (index < 0) return; + synchronized (this.inbox) { + final List threadsCopy = new LinkedList<>(inbox.getThreads()); + threadsCopy.set(index, thread); + try { + final DirectInbox clone = (DirectInbox) inbox.clone(); + clone.setThreads(threadsCopy); + this.inbox.postValue(Resource.success(clone)); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "setThread: ", e); + } + } + } + + public void addItemsToThread(@NonNull final String threadId, + final int insertIndex, + @NonNull final Collection items) { + final DirectInbox inbox = getCurrentDirectInbox(); + if (inbox == null) return; + synchronized (THREAD_LOCKS.getUnchecked(threadId)) { + final int index = getThreadIndex(threadId, inbox); + if (index < 0) return; + final List threads = inbox.getThreads(); + final DirectThread thread = threads.get(index); + List list = thread.getItems(); + list = list == null ? new LinkedList<>() : new LinkedList<>(list); + if (insertIndex >= 0) { + list.addAll(insertIndex, items); + } else { + list.addAll(items); + } + try { + final DirectThread threadClone = (DirectThread) thread.clone(); + threadClone.setItems(list); + setThread(inbox, index, threadClone); + } catch (Exception e) { + Log.e(TAG, "addItemsToThread: ", e); + } + } + } + + public void setItemsToThread(@NonNull final String threadId, + @NonNull final List updatedItems) { + final DirectInbox inbox = getCurrentDirectInbox(); + if (inbox == null) return; + synchronized (THREAD_LOCKS.getUnchecked(threadId)) { + final int index = getThreadIndex(threadId, inbox); + if (index < 0) return; + final List threads = inbox.getThreads(); + final DirectThread thread = threads.get(index); + thread.setItems(updatedItems); + setThread(inbox, index, thread); + } + } + + private int getThreadIndex(@NonNull final String threadId, + @NonNull final DirectInbox inbox) { + final List threads = inbox.getThreads(); + if (threads == null || threads.isEmpty()) { + return -1; + } + return Iterables.indexOf(threads, t -> { + if (t == null) return false; + return t.getThreadId().equals(threadId); + }); + } + + private Integer getCurrentUnseenCount() { + final Resource unseenCountResource = unseenCount.getValue(); + return unseenCountResource != null ? unseenCountResource.data : null; + } + + private void stopCurrentInboxRequest() { + if (inboxRequest == null || inboxRequest.isCanceled() || inboxRequest.isExecuted()) return; + inboxRequest.cancel(); + inboxRequest = null; + } + + private void stopCurrentUnseenCountRequest() { + if (unseenCountRequest == null || unseenCountRequest.isCanceled() || unseenCountRequest.isExecuted()) return; + unseenCountRequest.cancel(); + unseenCountRequest = null; + } + + public void onDestroy() { + stopCurrentInboxRequest(); + stopCurrentUnseenCountRequest(); + } + + public void addThread(@NonNull final DirectThread thread, final int insertIndex) { + if (insertIndex < 0) return; + synchronized (this.inbox) { + final DirectInbox currentDirectInbox = getCurrentDirectInbox(); + final List threadsCopy = new LinkedList<>(currentDirectInbox.getThreads()); + threadsCopy.add(insertIndex, thread); + try { + final DirectInbox clone = (DirectInbox) currentDirectInbox.clone(); + clone.setThreads(threadsCopy); + this.inbox.setValue(Resource.success(clone)); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "setThread: ", e); + } + } + } + + public void removeThread(@NonNull final String threadId) { + synchronized (this.inbox) { + final DirectInbox currentDirectInbox = getCurrentDirectInbox(); + final List threadsCopy = currentDirectInbox.getThreads() + .stream() + .filter(t -> !t.getThreadId().equals(threadId)) + .collect(Collectors.toList()); + try { + final DirectInbox clone = (DirectInbox) currentDirectInbox.clone(); + clone.setThreads(threadsCopy); + this.inbox.postValue(Resource.success(clone)); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "setThread: ", e); + } + } + } + + public void setPendingRequestsTotal(final int total) { + pendingRequestsTotal.postValue(total); + } +} diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java new file mode 100644 index 00000000..bf23a8bc --- /dev/null +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java @@ -0,0 +1,1780 @@ +package awais.instagrabber.managers; + +import android.content.ContentResolver; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import org.json.JSONObject; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import awais.instagrabber.customviews.emoji.Emoji; +import awais.instagrabber.models.Resource; +import awais.instagrabber.models.Resource.Status; +import awais.instagrabber.models.UploadVideoOptions; +import awais.instagrabber.models.enums.DirectItemType; +import awais.instagrabber.repositories.requests.UploadFinishOptions; +import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds; +import awais.instagrabber.repositories.responses.FriendshipChangeResponse; +import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; +import awais.instagrabber.repositories.responses.User; +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.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.DirectThreadParticipantRequestsResponse; +import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; +import awais.instagrabber.utils.BitmapUtils; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.DirectItemFactory; +import awais.instagrabber.utils.MediaController; +import awais.instagrabber.utils.MediaUploadHelper; +import awais.instagrabber.utils.MediaUploader; +import awais.instagrabber.utils.MediaUtils; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; +import awais.instagrabber.webservices.DirectMessagesService; +import awais.instagrabber.webservices.FriendshipService; +import awais.instagrabber.webservices.MediaService; +import awais.instagrabber.webservices.ServiceCallback; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static androidx.lifecycle.Transformations.distinctUntilChanged; +import static androidx.lifecycle.Transformations.map; +import static awais.instagrabber.utils.Utils.settingsHelper; + +public final class ThreadManager { + private static final String TAG = ThreadManager.class.getSimpleName(); + private static final Object LOCK = new Object(); + private static final String ERROR_INVALID_USER = "Invalid user"; + private static final String ERROR_RESPONSE_NOT_OK = "Response status from server was not ok"; + private static final String ERROR_VIDEO_TOO_LONG = "Instagram does not allow uploading videos longer than 60 secs for Direct messages"; + private static final String ERROR_AUDIO_TOO_LONG = "Instagram does not allow uploading audio longer than 60 secs"; + private static final Map INSTANCE_MAP = new ConcurrentHashMap<>(); + + private final MutableLiveData> fetching = new MutableLiveData<>(); + private final MutableLiveData replyToItem = new MutableLiveData<>(); + private final MutableLiveData pendingRequests = new MutableLiveData<>(null); + + private final String threadId; + private final DirectMessagesService service; + private final long viewerId; + private final ThreadIdOrUserIds threadIdOrUserIds; + private final User currentUser; + private final ContentResolver contentResolver; + private final MediaService mediaService; + private final FriendshipService friendshipService; + + private InboxManager inboxManager; + private LiveData thread; + private LiveData inputMode; + private LiveData threadTitle; + private LiveData> users; + private LiveData> usersWithCurrent; + private LiveData> leftUsers; + private LiveData, List>> usersAndLeftUsers; + private LiveData pending; + private LiveData> adminUserIds; + private LiveData> items; + private LiveData isViewerAdmin; + private LiveData isGroup; + private LiveData isMuted; + private LiveData isApprovalRequiredToJoin; + private LiveData isMentionsMuted; + private LiveData pendingRequestsCount; + private LiveData inviter; + private boolean hasOlder = true; + private String cursor; + private Call chatsRequest; + + public static ThreadManager getInstance(@NonNull final String threadId, + final boolean pending, + @NonNull final User currentUser, + @NonNull final ContentResolver contentResolver) { + ThreadManager instance = INSTANCE_MAP.get(threadId); + if (instance == null) { + synchronized (LOCK) { + instance = INSTANCE_MAP.get(threadId); + if (instance == null) { + instance = new ThreadManager(threadId, pending, currentUser, contentResolver); + INSTANCE_MAP.put(threadId, instance); + } + } + } + return instance; + } + + private String getThreadId() { + return threadId; + } + + private ThreadManager(@NonNull final String threadId, + final boolean pending, + @NonNull final User currentUser, + @NonNull final ContentResolver contentResolver) { + final DirectMessagesManager messagesManager = DirectMessagesManager.getInstance(); + this.inboxManager = pending ? messagesManager.getPendingInboxManager() : messagesManager.getInboxManager(); + this.threadId = threadId; + this.threadIdOrUserIds = ThreadIdOrUserIds.of(threadId); + this.currentUser = currentUser; + this.contentResolver = contentResolver; + final String cookie = settingsHelper.getString(Constants.COOKIE); + viewerId = CookieUtils.getUserIdFromCookie(cookie); + final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); + final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) { + throw new IllegalArgumentException("User is not logged in!"); + } + service = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid); + mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId); + friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, viewerId); + setupTransformations(); + // fetchChats(); + } + + public void moveFromPending() { + final DirectMessagesManager messagesManager = DirectMessagesManager.getInstance(); + this.inboxManager = messagesManager.getInboxManager(); + setupTransformations(); + } + + private void setupTransformations() { + // Transformations + thread = distinctUntilChanged(map(inboxManager.getInbox(), inboxResource -> { + if (inboxResource == null) { + return null; + } + final DirectInbox inbox = inboxResource.data; + final List threads = inbox.getThreads(); + if (threads == null || threads.isEmpty()) { + return null; + } + final DirectThread thread = threads.stream() + .filter(t -> t.getThreadId().equals(threadId)) + .findFirst() + .orElse(null); + if (thread != null) { + cursor = thread.getOldestCursor(); + hasOlder = thread.hasOlder(); + } + return thread; + })); + inputMode = distinctUntilChanged(map(thread, t -> { + if (t == null) return 1; + return t.getInputMode(); + })); + threadTitle = distinctUntilChanged(map(thread, t -> { + if (t == null) return null; + return t.getThreadTitle(); + })); + users = distinctUntilChanged(map(thread, t -> { + if (t == null) return Collections.emptyList(); + return t.getUsers(); + })); + usersWithCurrent = distinctUntilChanged(map(thread, t -> { + if (t == null) return Collections.emptyList(); + return getUsersWithCurrentUser(t); + })); + leftUsers = distinctUntilChanged(map(thread, t -> { + if (t == null) return Collections.emptyList(); + return t.getLeftUsers(); + })); + usersAndLeftUsers = distinctUntilChanged(map(thread, t -> { + if (t == null) { + return new Pair<>(Collections.emptyList(), Collections.emptyList()); + } + final List users = getUsersWithCurrentUser(t); + final List leftUsers = t.getLeftUsers(); + return new Pair<>(users, leftUsers); + })); + pending = distinctUntilChanged(map(thread, t -> { + if (t == null) return true; + return t.isPending(); + })); + adminUserIds = distinctUntilChanged(map(thread, t -> { + if (t == null) return Collections.emptyList(); + return t.getAdminUserIds(); + })); + items = distinctUntilChanged(map(thread, t -> { + if (t == null) return Collections.emptyList(); + return t.getItems(); + })); + isViewerAdmin = distinctUntilChanged(map(thread, t -> { + if (t == null) return false; + return t.getAdminUserIds().contains(viewerId); + })); + isGroup = distinctUntilChanged(map(thread, t -> { + if (t == null) return false; + return t.isGroup(); + })); + isMuted = distinctUntilChanged(map(thread, t -> { + if (t == null) return false; + return t.isMuted(); + })); + isApprovalRequiredToJoin = distinctUntilChanged(map(thread, t -> { + if (t == null) return false; + return t.isApprovalRequiredForNewMembers(); + })); + isMentionsMuted = distinctUntilChanged(map(thread, t -> { + if (t == null) return false; + return t.isMentionsMuted(); + })); + pendingRequestsCount = distinctUntilChanged(map(pendingRequests, p -> { + if (p == null) return 0; + return p.getTotalParticipantRequests(); + })); + inviter = distinctUntilChanged(map(thread, t -> { + if (t == null) return null; + return t.getInviter(); + })); + } + + private List getUsersWithCurrentUser(final DirectThread t) { + final ImmutableList.Builder builder = ImmutableList.builder().add(currentUser); + final List users = t.getUsers(); + if (users != null) { + builder.addAll(users); + } + return builder.build(); + } + + public LiveData getThread() { + return thread; + } + + public LiveData getInputMode() { + return inputMode; + } + + public LiveData getThreadTitle() { + return threadTitle; + } + + public LiveData> getUsers() { + return users; + } + + public LiveData> getUsersWithCurrent() { + return usersWithCurrent; + } + + public LiveData> getLeftUsers() { + return leftUsers; + } + + public LiveData, List>> getUsersAndLeftUsers() { + return usersAndLeftUsers; + } + + public LiveData isPending() { + return pending; + } + + public LiveData> getAdminUserIds() { + return adminUserIds; + } + + public LiveData> getItems() { + return items; + } + + public LiveData> isFetching() { + return fetching; + } + + public LiveData getReplyToItem() { + return replyToItem; + } + + public LiveData getPendingRequestsCount() { + return pendingRequestsCount; + } + + public LiveData getPendingRequests() { + return pendingRequests; + } + + public LiveData isGroup() { + return isGroup; + } + + public LiveData isMuted() { + return isMuted; + } + + public LiveData isApprovalRequiredToJoin() { + return isApprovalRequiredToJoin; + } + + public LiveData isViewerAdmin() { + return isViewerAdmin; + } + + public LiveData isMentionsMuted() { + return isMentionsMuted; + } + + public void fetchChats() { + final Resource fetchingValue = fetching.getValue(); + if ((fetchingValue != null && fetchingValue.status == Status.LOADING) || !hasOlder) return; + fetching.postValue(Resource.loading(null)); + chatsRequest = service.fetchThread(threadId, cursor); + chatsRequest.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final DirectThreadFeedResponse feedResponse = response.body(); + if (feedResponse == null) { + fetching.postValue(Resource.error("response was null!", null)); + Log.e(TAG, "onResponse: response was null!"); + return; + } + if (!feedResponse.getStatus().equals("ok")) { + fetching.postValue(Resource.error("response was not ok", null)); + return; + } + final DirectThread thread = feedResponse.getThread(); + setThread(thread); + fetching.postValue(Resource.success(new Object())); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "Failed fetching dm chats", t); + fetching.postValue(Resource.error(t.getMessage(), null)); + hasOlder = false; + } + }); + if (cursor == null) { + fetchPendingRequests(); + } + } + + public void fetchPendingRequests() { + final Call request = service.participantRequests(threadId, 1, null); + request.enqueue(new Callback() { + + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (!response.isSuccessful()) { + if (response.errorBody() != null) { + try { + final String string = 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); + } catch (IOException e) { + Log.e(TAG, "onResponse: ", e); + } + return; + } + Log.e(TAG, "onResponse: request was not successful and response error body was null"); + return; + } + final DirectThreadParticipantRequestsResponse body = response.body(); + if (body == null) { + Log.e(TAG, "onResponse: response body was null"); + return; + } + pendingRequests.postValue(body); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + } + }); + } + + private void setThread(@NonNull final DirectThread thread, final boolean skipItems) { + // if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) { + // fetchPendingRequests(); + // } + final List items = thread.getItems(); + if (skipItems) { + final DirectThread currentThread = this.thread.getValue(); + if (currentThread != null) { + thread.setItems(currentThread.getItems()); + } + } + if (!skipItems && !TextUtils.isEmpty(cursor)) { + final DirectThread currentThread = this.thread.getValue(); + if (currentThread != null) { + List list = currentThread.getItems(); + list = list == null ? new LinkedList<>() : new LinkedList<>(list); + list.addAll(items); + thread.setItems(list); + } + } + inboxManager.setThread(threadId, thread); + } + + private void setThread(@NonNull final DirectThread thread) { + setThread(thread, false); + } + + private void setThreadUsers(final List users, final List leftUsers) { + final DirectThread currentThread = this.thread.getValue(); + if (currentThread == null) return; + final DirectThread thread; + try { + thread = (DirectThread) currentThread.clone(); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "setThreadUsers: ", e); + return; + } + if (users != null) { + thread.setUsers(users); + } + if (leftUsers != null) { + thread.setLeftUsers(leftUsers); + } + inboxManager.setThread(threadId, thread); + } + + private void addItems(final int index, final Collection items) { + if (items == null) return; + inboxManager.addItemsToThread(threadId, index, items); + } + + private void addReaction(final DirectItem item, final Emoji emoji) { + if (item == null || emoji == null || currentUser == null) return; + final boolean isLike = emoji.getUnicode().equals("❤️"); + DirectItemReactions reactions = item.getReactions(); + if (reactions == null) { + reactions = new DirectItemReactions(null, null); + } else { + try { + reactions = (DirectItemReactions) reactions.clone(); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "addReaction: ", e); + return; + } + } + if (isLike) { + final List likes = addEmoji(reactions.getLikes(), null, false); + reactions.setLikes(likes); + } + final List emojis = addEmoji(reactions.getEmojis(), emoji.getUnicode(), true); + reactions.setEmojis(emojis); + List list = this.items.getValue(); + list = list == null ? new LinkedList<>() : new LinkedList<>(list); + int index = getItemIndex(item, list); + if (index >= 0) { + try { + final DirectItem clone = (DirectItem) list.get(index).clone(); + clone.setReactions(reactions); + list.set(index, clone); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "addReaction: error cloning", e); + } + } + inboxManager.setItemsToThread(threadId, list); + } + + private void removeReaction(final DirectItem item) { + try { + final DirectItem itemClone = (DirectItem) item.clone(); + final DirectItemReactions reactions = itemClone.getReactions(); + final DirectItemReactions reactionsClone = (DirectItemReactions) reactions.clone(); + final List likes = reactionsClone.getLikes(); + if (likes != null) { + final List updatedLikes = likes.stream() + .filter(like -> like.getSenderId() != viewerId) + .collect(Collectors.toList()); + reactionsClone.setLikes(updatedLikes); + } + final List emojis = reactionsClone.getEmojis(); + if (emojis != null) { + final List updatedEmojis = emojis.stream() + .filter(emoji -> emoji.getSenderId() != viewerId) + .collect(Collectors.toList()); + reactionsClone.setEmojis(updatedEmojis); + } + itemClone.setReactions(reactionsClone); + List list = this.items.getValue(); + list = list == null ? new LinkedList<>() : new LinkedList<>(list); + int index = getItemIndex(item, list); + if (index >= 0) { + list.set(index, itemClone); + } + inboxManager.setItemsToThread(threadId, list); + } catch (Exception e) { + Log.e(TAG, "removeReaction: ", e); + } + } + + private int removeItem(final DirectItem item) { + if (item == null) return 0; + List list = this.items.getValue(); + list = list == null ? new LinkedList<>() : new LinkedList<>(list); + int index = getItemIndex(item, list); + if (index >= 0) { + list.remove(index); + inboxManager.setItemsToThread(threadId, list); + } + return index; + } + + private List addEmoji(final List reactionList, + final String emoji, + final boolean shouldReplaceIfAlreadyReacted) { + final List temp = reactionList == null ? new ArrayList<>() : new ArrayList<>(reactionList); + int index = -1; + for (int i = 0; i < temp.size(); i++) { + final DirectItemEmojiReaction directItemEmojiReaction = temp.get(i); + if (directItemEmojiReaction.getSenderId() == currentUser.getPk()) { + index = i; + break; + } + } + final DirectItemEmojiReaction reaction = new DirectItemEmojiReaction( + currentUser.getPk(), + System.currentTimeMillis() * 1000, + emoji, + "none" + ); + if (index < 0) { + temp.add(0, reaction); + } else if (shouldReplaceIfAlreadyReacted) { + temp.add(0, reaction); + temp.remove(index); + } + return temp; + } + + public LiveData> sendText(final String text) { + final MutableLiveData> data = new MutableLiveData<>(); + final Long userId = getCurrentUserId(data); + if (userId == null) return data; + final String clientContext = UUID.randomUUID().toString(); + final DirectItem replyToItemValue = replyToItem.getValue(); + final DirectItem directItem = DirectItemFactory.createText(userId, clientContext, text, replyToItemValue); + // Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId()); + directItem.setPending(true); + addItems(0, Collections.singletonList(directItem)); + data.postValue(Resource.loading(directItem)); + final String repliedToItemId = replyToItemValue != null ? replyToItemValue.getItemId() : null; + final String repliedToClientContext = replyToItemValue != null ? replyToItemValue.getClientContext() : null; + final Call request = service.broadcastText( + clientContext, + threadIdOrUserIds, + text, + repliedToItemId, + repliedToClientContext + ); + enqueueRequest(request, data, directItem); + return data; + } + + public LiveData> sendUri(final MediaController.MediaEntry entry) { + final MutableLiveData> data = new MutableLiveData<>(); + if (entry == null) { + data.postValue(Resource.error("Entry is null", null)); + return data; + } + final Uri uri = Uri.fromFile(new File(entry.path)); + if (!entry.isVideo) { + sendPhoto(data, uri, entry.width, entry.height); + return data; + } + sendVideo(data, uri, entry.size, entry.duration, entry.width, entry.height); + return data; + } + + public LiveData> sendUri(final Uri uri) { + final MutableLiveData> data = new MutableLiveData<>(); + if (uri == null) { + data.postValue(Resource.error("Uri is null", null)); + return data; + } + final String mimeType = Utils.getMimeType(uri, contentResolver); + if (TextUtils.isEmpty(mimeType)) { + data.postValue(Resource.error("Unknown MediaType", null)); + return data; + } + final boolean isPhoto = mimeType.startsWith("image"); + if (isPhoto) { + sendPhoto(data, uri); + return data; + } + if (mimeType.startsWith("video")) { + sendVideo(data, uri); + } + return data; + } + + public void sendVoice(@NonNull final MutableLiveData> data, + @NonNull final Uri uri, + @NonNull final List waveform, + final int samplingFreq, + final long duration, + final long byteLength) { + if (duration > 60000) { + // instagram does not allow uploading audio longer than 60 secs for Direct messages + data.postValue(Resource.error(ERROR_AUDIO_TOO_LONG, null)); + return; + } + final Long userId = getCurrentUserId(data); + if (userId == null) return; + final String clientContext = UUID.randomUUID().toString(); + final DirectItem directItem = DirectItemFactory.createVoice(userId, clientContext, uri, duration, waveform, samplingFreq); + directItem.setPending(true); + addItems(0, Collections.singletonList(directItem)); + data.postValue(Resource.loading(directItem)); + final UploadVideoOptions uploadDmVoiceOptions = MediaUploadHelper.createUploadDmVoiceOptions(byteLength, duration); + MediaUploader.uploadVideo(uri, contentResolver, uploadDmVoiceOptions, new MediaUploader.OnMediaUploadCompleteListener() { + @Override + public void onUploadComplete(final MediaUploader.MediaUploadResponse response) { + // Log.d(TAG, "onUploadComplete: " + response); + if (handleInvalidResponse(data, response)) return; + final UploadFinishOptions uploadFinishOptions = new UploadFinishOptions() + .setUploadId(uploadDmVoiceOptions.getUploadId()) + .setSourceType("4"); + final Call uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions); + uploadFinishRequest.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (response.isSuccessful()) { + final Call request = service.broadcastVoice( + clientContext, + threadIdOrUserIds, + uploadDmVoiceOptions.getUploadId(), + waveform, + samplingFreq + ); + enqueueRequest(request, data, directItem); + return; + } + if (response.errorBody() != null) { + handleErrorBody(call, response, data); + return; + } + data.postValue(Resource.error("uploadFinishRequest was not successful and response error body was null", directItem)); + Log.e(TAG, "uploadFinishRequest was not successful and response error body was null"); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + data.postValue(Resource.error(t.getMessage(), directItem)); + Log.e(TAG, "onFailure: ", t); + } + }); + } + + @Override + public void onFailure(final Throwable t) { + data.postValue(Resource.error(t.getMessage(), directItem)); + Log.e(TAG, "onFailure: ", t); + } + }); + } + + public LiveData> sendReaction(final DirectItem item, final Emoji emoji) { + final MutableLiveData> data = new MutableLiveData<>(); + final Long userId = getCurrentUserId(data); + if (userId == null) { + data.postValue(Resource.error("userId is null", null)); + return data; + } + final String clientContext = UUID.randomUUID().toString(); + // Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId()); + data.postValue(Resource.loading(item)); + addReaction(item, emoji); + String emojiUnicode = null; + if (!emoji.getUnicode().equals("❤️")) { + emojiUnicode = emoji.getUnicode(); + } + final Call request = service.broadcastReaction( + clientContext, threadIdOrUserIds, item.getItemId(), emojiUnicode, false); + handleBroadcastReactionRequest(data, item, request); + return data; + } + + public LiveData> sendDeleteReaction(final String itemId) { + final MutableLiveData> data = new MutableLiveData<>(); + final DirectItem item = getItem(itemId); + if (item == null) { + data.postValue(Resource.error("Invalid item", null)); + return data; + } + final DirectItemReactions reactions = item.getReactions(); + if (reactions == null) { + // already removed? + data.postValue(Resource.success(item)); + return data; + } + removeReaction(item); + final String clientContext = UUID.randomUUID().toString(); + final Call request = service.broadcastReaction(clientContext, threadIdOrUserIds, item.getItemId(), null, true); + handleBroadcastReactionRequest(data, item, request); + return data; + } + + public LiveData> unsend(final DirectItem item) { + final MutableLiveData> data = new MutableLiveData<>(); + if (item == null) { + data.postValue(Resource.error("item is null", null)); + return data; + } + final int index = removeItem(item); + final Call request = service.deleteItem(threadId, item.getItemId()); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (response.isSuccessful()) { + // Log.d(TAG, "onResponse: " + response.body()); + return; + } + // add the item back if unsuccessful + addItems(index, Collections.singletonList(item)); + if (response.errorBody() != null) { + handleErrorBody(call, response, data); + return; + } + data.postValue(Resource.error("request was not successful and response error body was null", item)); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + data.postValue(Resource.error(t.getMessage(), item)); + Log.e(TAG, "enqueueRequest: onFailure: ", t); + } + }); + return data; + } + + public void forward(final Set recipients, final DirectItem itemToForward) { + if (recipients == null || itemToForward == null) return; + for (final RankedRecipient recipient : recipients) { + forward(recipient, itemToForward); + } + } + + public void forward(final RankedRecipient recipient, final DirectItem itemToForward) { + if (recipient == null || itemToForward == null) return; + if (recipient.getThread() == null && recipient.getUser() != null) { + // create thread and forward + final Call createThreadRequest = service.createThread(Collections.singletonList(recipient.getUser().getPk()), null); + createThreadRequest.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (!response.isSuccessful()) { + if (response.errorBody() != null) { + try { + final String string = 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); + } catch (IOException e) { + Log.e(TAG, "onResponse: ", e); + } + return; + } + Log.e(TAG, "onResponse: request was not successful and response error body was null"); + return; + } + final DirectThread thread = response.body(); + if (thread == null) { + Log.e(TAG, "onResponse: thread is null"); + return; + } + forward(thread, itemToForward); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + + } + }); + return; + } + if (recipient.getThread() != null) { + // just forward + final DirectThread thread = recipient.getThread(); + forward(thread, itemToForward); + } + } + + public void setReplyToItem(final DirectItem item) { + // Log.d(TAG, "setReplyToItem: " + item); + replyToItem.postValue(item); + } + + private void forward(@NonNull final DirectThread thread, @NonNull final DirectItem itemToForward) { + final DirectItemType itemType = itemToForward.getItemType(); + final String itemTypeName = itemType.getName(); + if (itemTypeName == null) { + Log.e(TAG, "forward: itemTypeName was null!"); + return; + } + final Call request = service.forward(thread.getThreadId(), + itemTypeName, + threadId, + itemToForward.getItemId()); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (response.isSuccessful()) return; + if (response.errorBody() != null) { + try { + final String string = 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); + } catch (IOException e) { + Log.e(TAG, "onResponse: ", e); + } + return; + } + Log.e(TAG, "onResponse: request was not successful and response error body was null"); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + } + }); + } + + public LiveData> acceptRequest() { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.success(new Object())); + // final Call request = service.approveRequest(threadId); + // request.enqueue(new Callback() { + // @Override + // public void onResponse(@NonNull final Call call, + // @NonNull final Response 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())); + // // refreshChats(); + // } + // + // @Override + // public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + // Log.e(TAG, "onFailure: ", t); + // data.postValue(Resource.error(t.getMessage(), null)); + // } + // }); + return data; + } + + public LiveData> declineRequest() { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.success(new Object())); + // final Call request = service.declineRequest(threadId); + // request.enqueue(new Callback() { + // @Override + // public void onResponse(@NonNull final Call call, + // @NonNull final Response 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 call, @NonNull final Throwable t) { + // Log.e(TAG, "onFailure: ", t); + // data.postValue(Resource.error(t.getMessage(), null)); + // } + // }); + return data; + } + + public void refreshChats() { + final Resource isFetching = fetching.getValue(); + if (isFetching != null && isFetching.status == Status.LOADING) { + stopCurrentRequest(); + } + cursor = null; + hasOlder = true; + fetchChats(); + } + + private void sendPhoto(@NonNull final MutableLiveData> data, + @NonNull final Uri uri) { + try { + final Pair dimensions = BitmapUtils.decodeDimensions(contentResolver, uri); + if (dimensions == null) { + data.postValue(Resource.error("Decoding dimensions failed", null)); + return; + } + sendPhoto(data, uri, dimensions.first, dimensions.second); + } catch (FileNotFoundException e) { + data.postValue(Resource.error(e.getMessage(), null)); + Log.e(TAG, "sendPhoto: ", e); + } + } + + private void sendPhoto(@NonNull final MutableLiveData> data, + @NonNull final Uri uri, + final int width, + final int height) { + final Long userId = getCurrentUserId(data); + if (userId == null) return; + final String clientContext = UUID.randomUUID().toString(); + final DirectItem directItem = DirectItemFactory.createImageOrVideo(userId, clientContext, uri, width, height, false); + directItem.setPending(true); + addItems(0, Collections.singletonList(directItem)); + data.postValue(Resource.loading(directItem)); + MediaUploader.uploadPhoto(uri, contentResolver, new MediaUploader.OnMediaUploadCompleteListener() { + @Override + public void onUploadComplete(final MediaUploader.MediaUploadResponse response) { + if (handleInvalidResponse(data, response)) return; + final String uploadId = response.getResponse().optString("upload_id"); + final Call request = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId); + enqueueRequest(request, data, directItem); + } + + @Override + public void onFailure(final Throwable t) { + data.postValue(Resource.error(t.getMessage(), directItem)); + Log.e(TAG, "onFailure: ", t); + } + }); + } + + private void sendVideo(@NonNull final MutableLiveData> data, + @NonNull final Uri uri) { + MediaUtils.getVideoInfo(contentResolver, uri, new MediaUtils.OnInfoLoadListener() { + @Override + public void onLoad(@Nullable final MediaUtils.VideoInfo info) { + if (info == null) { + data.postValue(Resource.error("Could not get the video info", null)); + return; + } + sendVideo(data, uri, info.size, info.duration, info.width, info.height); + } + + @Override + public void onFailure(final Throwable t) { + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + } + + private void sendVideo(@NonNull final MutableLiveData> data, + @NonNull final Uri uri, + final long byteLength, + final long duration, + final int width, + final int height) { + if (duration > 60000) { + // instagram does not allow uploading videos longer than 60 secs for Direct messages + data.postValue(Resource.error(ERROR_VIDEO_TOO_LONG, null)); + return; + } + final Long userId = getCurrentUserId(data); + if (userId == null) return; + final String clientContext = UUID.randomUUID().toString(); + final DirectItem directItem = DirectItemFactory.createImageOrVideo(userId, clientContext, uri, width, height, true); + directItem.setPending(true); + addItems(0, Collections.singletonList(directItem)); + data.postValue(Resource.loading(directItem)); + final UploadVideoOptions uploadDmVideoOptions = MediaUploadHelper.createUploadDmVideoOptions(byteLength, duration, width, height); + MediaUploader.uploadVideo(uri, contentResolver, uploadDmVideoOptions, new MediaUploader.OnMediaUploadCompleteListener() { + @Override + public void onUploadComplete(final MediaUploader.MediaUploadResponse response) { + // Log.d(TAG, "onUploadComplete: " + response); + if (handleInvalidResponse(data, response)) return; + final UploadFinishOptions uploadFinishOptions = new UploadFinishOptions() + .setUploadId(uploadDmVideoOptions.getUploadId()) + .setSourceType("2") + .setVideoOptions(new UploadFinishOptions.VideoOptions().setLength(duration / 1000f)); + final Call uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions); + uploadFinishRequest.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (response.isSuccessful()) { + final Call request = service.broadcastVideo( + clientContext, + threadIdOrUserIds, + uploadDmVideoOptions.getUploadId(), + "", + true + ); + enqueueRequest(request, data, directItem); + return; + } + if (response.errorBody() != null) { + handleErrorBody(call, response, data); + return; + } + data.postValue(Resource.error("uploadFinishRequest was not successful and response error body was null", directItem)); + Log.e(TAG, "uploadFinishRequest was not successful and response error body was null"); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + data.postValue(Resource.error(t.getMessage(), directItem)); + Log.e(TAG, "onFailure: ", t); + } + }); + } + + @Override + public void onFailure(final Throwable t) { + data.postValue(Resource.error(t.getMessage(), directItem)); + Log.e(TAG, "onFailure: ", t); + } + }); + } + + private void enqueueRequest(@NonNull final Call request, + @NonNull final MutableLiveData> data, + @NonNull final DirectItem directItem) { + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (response.isSuccessful()) { + final DirectThreadBroadcastResponse broadcastResponse = response.body(); + if (broadcastResponse == null) { + data.postValue(Resource.error("Response was null from server", directItem)); + Log.e(TAG, "enqueueRequest: onResponse: response body is null"); + return; + } + final String payloadClientContext; + final long timestamp; + final String itemId; + final DirectThreadBroadcastResponsePayload payload = broadcastResponse.getPayload(); + if (payload == null) { + final List messageMetadata = broadcastResponse.getMessageMetadata(); + if (messageMetadata == null || messageMetadata.isEmpty()) { + data.postValue(Resource.success(directItem)); + return; + } + final DirectThreadBroadcastResponseMessageMetadata metadata = messageMetadata.get(0); + payloadClientContext = metadata.getClientContext(); + itemId = metadata.getItemId(); + timestamp = metadata.getTimestamp(); + } else { + payloadClientContext = payload.getClientContext(); + timestamp = payload.getTimestamp(); + itemId = payload.getItemId(); + } + updateItemSent(payloadClientContext, timestamp, itemId); + data.postValue(Resource.success(directItem)); + return; + } + if (response.errorBody() != null) { + handleErrorBody(call, response, data); + } + data.postValue(Resource.error("request was not successful and response error body was null", directItem)); + } + + @Override + public void onFailure(@NonNull final Call call, + @NonNull final Throwable t) { + data.postValue(Resource.error(t.getMessage(), directItem)); + Log.e(TAG, "enqueueRequest: onFailure: ", t); + } + }); + } + + private void updateItemSent(final String clientContext, final long timestamp, final String itemId) { + if (clientContext == null) return; + List list = this.items.getValue(); + list = list == null ? new LinkedList<>() : new LinkedList<>(list); + final int index = Iterables.indexOf(list, item -> { + if (item == null) return false; + return item.getClientContext().equals(clientContext); + }); + if (index < 0) return; + final DirectItem directItem = list.get(index); + try { + final DirectItem itemClone = (DirectItem) directItem.clone(); + itemClone.setItemId(itemId); + itemClone.setPending(false); + itemClone.setTimestamp(timestamp); + list.set(index, itemClone); + inboxManager.setItemsToThread(threadId, list); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "updateItemSent: ", e); + } + } + + private void handleErrorBody(@NonNull final Call call, + @NonNull final Response response, + @NonNull final MutableLiveData> data) { + 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); + data.postValue(Resource.error(msg, null)); + Log.e(TAG, msg); + } catch (IOException e) { + data.postValue(Resource.error(e.getMessage(), null)); + Log.e(TAG, "onResponse: ", e); + } + } + + private boolean handleInvalidResponse(final MutableLiveData> data, + @NonNull final MediaUploader.MediaUploadResponse response) { + final JSONObject responseJson = response.getResponse(); + if (responseJson == null || response.getResponseCode() != HttpURLConnection.HTTP_OK) { + data.postValue(Resource.error(ERROR_RESPONSE_NOT_OK, null)); + return true; + } + final String status = responseJson.optString("status"); + if (TextUtils.isEmpty(status) || !status.equals("ok")) { + data.postValue(Resource.error(ERROR_RESPONSE_NOT_OK, null)); + return true; + } + return false; + } + + private int getItemIndex(final DirectItem item, final List list) { + return Iterables.indexOf(list, i -> i != null && i.getItemId().equals(item.getItemId())); + } + + @Nullable + private DirectItem getItem(final String itemId) { + if (itemId == null) return null; + final List items = this.items.getValue(); + if (items == null) return null; + return items.stream() + .filter(directItem -> directItem.getItemId().equals(itemId)) + .findFirst() + .orElse(null); + } + + private void handleBroadcastReactionRequest(final MutableLiveData> data, + final DirectItem item, + @NonNull final Call request) { + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (!response.isSuccessful()) { + if (response.errorBody() != null) { + handleErrorBody(call, response, data); + return; + } + data.postValue(Resource.error("request was not successful and response error body was null", item)); + return; + } + final DirectThreadBroadcastResponse body = response.body(); + if (body == null) { + data.postValue(Resource.error("Response is null!", item)); + } + // otherwise nothing to do? maybe update the timestamp in the emoji? + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + data.postValue(Resource.error(t.getMessage(), item)); + Log.e(TAG, "enqueueRequest: onFailure: ", t); + } + }); + } + + private void stopCurrentRequest() { + if (chatsRequest == null || chatsRequest.isExecuted() || chatsRequest.isCanceled()) { + return; + } + chatsRequest.cancel(); + fetching.postValue(Resource.success(new Object())); + } + + @Nullable + private Long getCurrentUserId(final MutableLiveData> data) { + if (currentUser == null || currentUser.getPk() <= 0) { + data.postValue(Resource.error(ERROR_INVALID_USER, null)); + return null; + } + return currentUser.getPk(); + } + + public void removeThread() { + final Boolean pendingValue = pending.getValue(); + final boolean threadInPending = pendingValue != null && pendingValue; + inboxManager.removeThread(threadId); + if (threadInPending) { + final Integer totalValue = inboxManager.getPendingRequestsTotal().getValue(); + if (totalValue == null) return; + inboxManager.setPendingRequestsTotal(totalValue - 1); + } + } + + public LiveData> updateTitle(final String newTitle) { + final MutableLiveData> data = new MutableLiveData<>(); + final Call addUsersRequest = service.updateTitle(threadId, newTitle.trim()); + handleDetailsChangeRequest(data, addUsersRequest); + return data; + } + + public LiveData> addMembers(final Set users) { + final MutableLiveData> data = new MutableLiveData<>(); + final Call addUsersRequest = service.addUsers(threadId, + users.stream() + .map(User::getPk) + .collect(Collectors.toList())); + handleDetailsChangeRequest(data, addUsersRequest); + return data; + } + + public LiveData> removeMember(final User user) { + final MutableLiveData> data = new MutableLiveData<>(); + final Call request = service.removeUsers(threadId, Collections.singleton(user.getPk())); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (!response.isSuccessful()) { + handleErrorBody(call, response, data); + return; + } + data.postValue(Resource.success(new Object())); + List activeUsers = users.getValue(); + List leftUsersValue = leftUsers.getValue(); + if (activeUsers == null) { + activeUsers = Collections.emptyList(); + } + if (leftUsersValue == null) { + leftUsersValue = Collections.emptyList(); + } + final List updatedActiveUsers = activeUsers.stream() + .filter(u -> u.getPk() != user.getPk()) + .collect(Collectors.toList()); + final ImmutableList.Builder updatedLeftUsersBuilder = ImmutableList.builder().addAll(leftUsersValue); + if (!leftUsersValue.contains(user)) { + updatedLeftUsersBuilder.add(user); + } + final ImmutableList updatedLeftUsers = updatedLeftUsersBuilder.build(); + setThreadUsers(updatedActiveUsers, updatedLeftUsers); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public boolean isAdmin(final User user) { + final List adminUserIdsValue = adminUserIds.getValue(); + return adminUserIdsValue != null && adminUserIdsValue.contains(user.getPk()); + } + + public LiveData> makeAdmin(final User user) { + final MutableLiveData> data = new MutableLiveData<>(); + if (isAdmin(user)) return data; + final Call request = service.addAdmins(threadId, Collections.singleton(user.getPk())); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (!response.isSuccessful()) { + handleErrorBody(call, response, data); + return; + } + final List currentAdminIds = adminUserIds.getValue(); + final ImmutableList updatedAdminIds = ImmutableList.builder() + .addAll(currentAdminIds != null ? currentAdminIds : Collections.emptyList()) + .add(user.getPk()) + .build(); + final DirectThread currentThread = ThreadManager.this.thread.getValue(); + if (currentThread == null) return; + try { + final DirectThread thread = (DirectThread) currentThread.clone(); + thread.setAdminUserIds(updatedAdminIds); + inboxManager.setThread(threadId, thread); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "onResponse: ", e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public LiveData> removeAdmin(final User user) { + final MutableLiveData> data = new MutableLiveData<>(); + if (!isAdmin(user)) return data; + final Call request = service.removeAdmins(threadId, Collections.singleton(user.getPk())); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (!response.isSuccessful()) { + handleErrorBody(call, response, data); + return; + } + final List currentAdmins = adminUserIds.getValue(); + if (currentAdmins == null) return; + final List updatedAdminUserIds = currentAdmins.stream() + .filter(userId1 -> userId1 != user.getPk()) + .collect(Collectors.toList()); + final DirectThread currentThread = ThreadManager.this.thread.getValue(); + if (currentThread == null) return; + try { + final DirectThread thread = (DirectThread) currentThread.clone(); + thread.setAdminUserIds(updatedAdminUserIds); + inboxManager.setThread(threadId, thread); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "onResponse: ", e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public LiveData> mute() { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Boolean muted = isMuted.getValue(); + if (muted != null && muted) { + data.postValue(Resource.success(new Object())); + return data; + } + final Call request = service.mute(threadId); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (!response.isSuccessful()) { + handleErrorBody(call, response, data); + return; + } + data.postValue(Resource.success(new Object())); + final DirectThread currentThread = ThreadManager.this.thread.getValue(); + if (currentThread == null) return; + try { + final DirectThread thread = (DirectThread) currentThread.clone(); + thread.setMuted(true); + inboxManager.setThread(threadId, thread); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "onResponse: ", e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public LiveData> unmute() { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Boolean muted = isMuted.getValue(); + if (muted != null && !muted) { + data.postValue(Resource.success(new Object())); + return data; + } + final Call request = service.unmute(threadId); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (!response.isSuccessful()) { + handleErrorBody(call, response, data); + return; + } + data.postValue(Resource.success(new Object())); + final DirectThread currentThread = ThreadManager.this.thread.getValue(); + if (currentThread == null) return; + try { + final DirectThread thread = (DirectThread) currentThread.clone(); + thread.setMuted(false); + inboxManager.setThread(threadId, thread); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "onResponse: ", e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public LiveData> muteMentions() { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Boolean mentionsMuted = isMentionsMuted.getValue(); + if (mentionsMuted != null && mentionsMuted) { + data.postValue(Resource.success(new Object())); + return data; + } + final Call request = service.muteMentions(threadId); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (!response.isSuccessful()) { + handleErrorBody(call, response, data); + return; + } + data.postValue(Resource.success(new Object())); + final DirectThread currentThread = ThreadManager.this.thread.getValue(); + if (currentThread == null) return; + try { + final DirectThread thread = (DirectThread) currentThread.clone(); + thread.setMentionsMuted(true); + inboxManager.setThread(threadId, thread); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "onResponse: ", e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public LiveData> unmuteMentions() { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Boolean mentionsMuted = isMentionsMuted.getValue(); + if (mentionsMuted != null && !mentionsMuted) { + data.postValue(Resource.success(new Object())); + return data; + } + final Call request = service.unmuteMentions(threadId); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + if (!response.isSuccessful()) { + handleErrorBody(call, response, data); + return; + } + data.postValue(Resource.success(new Object())); + final DirectThread currentThread = ThreadManager.this.thread.getValue(); + if (currentThread == null) return; + try { + final DirectThread thread = (DirectThread) currentThread.clone(); + thread.setMentionsMuted(false); + inboxManager.setThread(threadId, thread); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "onResponse: ", e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public LiveData> blockUser(final User user) { + final MutableLiveData> data = new MutableLiveData<>(); + friendshipService.block(user.getPk(), new ServiceCallback() { + @Override + public void onSuccess(final FriendshipChangeResponse result) { + refreshChats(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public LiveData> unblockUser(final User user) { + final MutableLiveData> data = new MutableLiveData<>(); + friendshipService.unblock(user.getPk(), new ServiceCallback() { + @Override + public void onSuccess(final FriendshipChangeResponse result) { + refreshChats(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public LiveData> restrictUser(final User user) { + final MutableLiveData> data = new MutableLiveData<>(); + friendshipService.toggleRestrict(user.getPk(), true, new ServiceCallback() { + @Override + public void onSuccess(final FriendshipRestrictResponse result) { + refreshChats(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public LiveData> unRestrictUser(final User user) { + final MutableLiveData> data = new MutableLiveData<>(); + friendshipService.toggleRestrict(user.getPk(), false, new ServiceCallback() { + @Override + public void onSuccess(final FriendshipRestrictResponse result) { + refreshChats(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + public LiveData> approveUsers(final List users) { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Call approveUsersRequest = service + .approveParticipantRequests(threadId, + users.stream().map(User::getPk).collect(Collectors.toList())); + handleDetailsChangeRequest(data, approveUsersRequest, () -> pendingUserApproveDenySuccessAction(users)); + return data; + } + + public LiveData> denyUsers(final List users) { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Call approveUsersRequest = service + .declineParticipantRequests(threadId, + users.stream().map(User::getPk).collect(Collectors.toList())); + handleDetailsChangeRequest(data, approveUsersRequest, () -> pendingUserApproveDenySuccessAction(users)); + return data; + } + + private void pendingUserApproveDenySuccessAction(final List users) { + final DirectThreadParticipantRequestsResponse pendingRequestsValue = pendingRequests.getValue(); + if (pendingRequestsValue == null) return; + final List pendingUsers = pendingRequestsValue.getUsers(); + if (pendingUsers == null || pendingUsers.isEmpty()) return; + final List filtered = pendingUsers.stream() + .filter(o -> !users.contains(o)) + .collect(Collectors.toList()); + try { + final DirectThreadParticipantRequestsResponse clone = (DirectThreadParticipantRequestsResponse) pendingRequestsValue.clone(); + clone.setUsers(filtered); + final int totalParticipantRequests = clone.getTotalParticipantRequests(); + clone.setTotalParticipantRequests(totalParticipantRequests > 0 ? totalParticipantRequests - 1 : 0); + pendingRequests.postValue(clone); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "pendingUserApproveDenySuccessAction: ", e); + } + } + + public LiveData> approvalRequired() { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Boolean approvalRequiredToJoin = isApprovalRequiredToJoin.getValue(); + if (approvalRequiredToJoin != null && approvalRequiredToJoin) { + data.postValue(Resource.success(new Object())); + return data; + } + final Call request = service.approvalRequired(threadId); + handleDetailsChangeRequest(data, request, () -> { + final DirectThread currentThread = ThreadManager.this.thread.getValue(); + if (currentThread == null) return; + try { + final DirectThread thread = (DirectThread) currentThread.clone(); + thread.setApprovalRequiredForNewMembers(true); + inboxManager.setThread(threadId, thread); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "onResponse: ", e); + } + }); + return data; + } + + public LiveData> approvalNotRequired() { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Boolean approvalRequiredToJoin = isApprovalRequiredToJoin.getValue(); + if (approvalRequiredToJoin != null && !approvalRequiredToJoin) { + data.postValue(Resource.success(new Object())); + return data; + } + final Call request = service.approvalNotRequired(threadId); + handleDetailsChangeRequest(data, request, () -> { + final DirectThread currentThread = ThreadManager.this.thread.getValue(); + if (currentThread == null) return; + try { + final DirectThread thread = (DirectThread) currentThread.clone(); + thread.setApprovalRequiredForNewMembers(false); + inboxManager.setThread(threadId, thread); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "onResponse: ", e); + } + }); + return data; + } + + public LiveData> leave() { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Call request = service.leave(threadId); + handleDetailsChangeRequest(data, request); + return data; + } + + public LiveData> end() { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Call request = service.end(threadId); + handleDetailsChangeRequest(data, request, () -> { + final DirectThread currentThread = ThreadManager.this.thread.getValue(); + if (currentThread == null) return; + try { + final DirectThread thread = (DirectThread) currentThread.clone(); + thread.setInputMode(1); + inboxManager.setThread(threadId, thread); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "onResponse: ", e); + } + }); + return data; + } + + private void handleDetailsChangeRequest(final MutableLiveData> data, + final Call request) { + handleDetailsChangeRequest(data, request, null); + } + + private void handleDetailsChangeRequest(final MutableLiveData> data, + final Call request, + @Nullable final OnSuccessAction action) { + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (!response.isSuccessful()) { + handleErrorBody(call, response, data); + return; + } + final DirectThreadDetailsChangeResponse changeResponse = response.body(); + if (changeResponse == null) { + data.postValue(Resource.error("Response is null", null)); + return; + } + data.postValue(Resource.success(new Object())); + final DirectThread thread = changeResponse.getThread(); + if (thread != null) { + setThread(thread, true); + } + if (action != null) { + action.onSuccess(); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + } + + public LiveData getInviter() { + return inviter; + } + + private interface OnSuccessAction { + void onSuccess(); + } +} diff --git a/app/src/main/java/awais/instagrabber/models/Resource.java b/app/src/main/java/awais/instagrabber/models/Resource.java index bbbaa390..f5dc5e7b 100644 --- a/app/src/main/java/awais/instagrabber/models/Resource.java +++ b/app/src/main/java/awais/instagrabber/models/Resource.java @@ -3,6 +3,8 @@ package awais.instagrabber.models; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Objects; + public class Resource { public final Status status; public final T data; @@ -31,6 +33,21 @@ public class Resource { return new Resource<>(Status.LOADING, data, null); } + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Resource resource = (Resource) o; + return status == resource.status && + Objects.equals(data, resource.data) && + Objects.equals(message, resource.message); + } + + @Override + public int hashCode() { + return Objects.hash(status, data, message); + } + public enum Status { SUCCESS, ERROR, diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/AnimatedMediaFixedHeight.java b/app/src/main/java/awais/instagrabber/repositories/responses/AnimatedMediaFixedHeight.java index e3ccd208..c5ffa272 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/AnimatedMediaFixedHeight.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/AnimatedMediaFixedHeight.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses; +import java.util.Objects; + public class AnimatedMediaFixedHeight { private final int height; private final int width; @@ -34,4 +36,21 @@ public class AnimatedMediaFixedHeight { public String getWebp() { return webp; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final AnimatedMediaFixedHeight that = (AnimatedMediaFixedHeight) o; + return height == that.height && + width == that.width && + Objects.equals(mp4, that.mp4) && + Objects.equals(url, that.url) && + Objects.equals(webp, that.webp); + } + + @Override + public int hashCode() { + return Objects.hash(height, width, mp4, url, webp); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/AnimatedMediaImages.java b/app/src/main/java/awais/instagrabber/repositories/responses/AnimatedMediaImages.java index ce02d9bb..d679fc92 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/AnimatedMediaImages.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/AnimatedMediaImages.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses; +import java.util.Objects; + public class AnimatedMediaImages { private final AnimatedMediaFixedHeight fixedHeight; @@ -10,4 +12,17 @@ public class AnimatedMediaImages { public AnimatedMediaFixedHeight getFixedHeight() { return fixedHeight; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final AnimatedMediaImages that = (AnimatedMediaImages) o; + return Objects.equals(fixedHeight, that.fixedHeight); + } + + @Override + public int hashCode() { + return Objects.hash(fixedHeight); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/Audio.java b/app/src/main/java/awais/instagrabber/repositories/responses/Audio.java index 0e408442..90412e44 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/Audio.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/Audio.java @@ -2,6 +2,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; import java.util.List; +import java.util.Objects; public class Audio implements Serializable { private final String audioSrc; @@ -41,4 +42,21 @@ public class Audio implements Serializable { public long getAudioSrcExpirationTimestampUs() { return audioSrcExpirationTimestampUs; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Audio audio = (Audio) o; + return duration == audio.duration && + waveformSamplingFrequencyHz == audio.waveformSamplingFrequencyHz && + audioSrcExpirationTimestampUs == audio.audioSrcExpirationTimestampUs && + Objects.equals(audioSrc, audio.audioSrc) && + Objects.equals(waveformData, audio.waveformData); + } + + @Override + public int hashCode() { + return Objects.hash(audioSrc, duration, waveformData, waveformSamplingFrequencyHz, audioSrcExpirationTimestampUs); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/Caption.java b/app/src/main/java/awais/instagrabber/repositories/responses/Caption.java index fe0b4c8b..cef3e5fb 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/Caption.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/Caption.java @@ -11,6 +11,7 @@ import com.google.gson.JsonParseException; import java.io.Serializable; import java.lang.reflect.Type; +import java.util.Objects; public class Caption implements Serializable { private long mPk; @@ -42,6 +43,21 @@ public class Caption implements Serializable { this.text = text; } + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Caption caption = (Caption) o; + return mPk == caption.mPk && + userId == caption.userId && + Objects.equals(text, caption.text); + } + + @Override + public int hashCode() { + return Objects.hash(mPk, userId, text); + } + public static class CaptionDeserializer implements JsonDeserializer { private static final String TAG = CaptionDeserializer.class.getSimpleName(); diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedDemarcator.java b/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedDemarcator.java index e50db021..44b9936d 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedDemarcator.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedDemarcator.java @@ -1,6 +1,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; +import java.util.Objects; public class EndOfFeedDemarcator implements Serializable { private final long id; @@ -18,4 +19,18 @@ public class EndOfFeedDemarcator implements Serializable { public EndOfFeedGroupSet getGroupSet() { return groupSet; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final EndOfFeedDemarcator that = (EndOfFeedDemarcator) o; + return id == that.id && + Objects.equals(groupSet, that.groupSet); + } + + @Override + public int hashCode() { + return Objects.hash(id, groupSet); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedGroup.java b/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedGroup.java index 6ff815d0..43384a41 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedGroup.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedGroup.java @@ -2,6 +2,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; import java.util.List; +import java.util.Objects; public class EndOfFeedGroup implements Serializable { private final String id; @@ -31,4 +32,20 @@ public class EndOfFeedGroup implements Serializable { public List getFeedItems() { return feedItems; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final EndOfFeedGroup that = (EndOfFeedGroup) o; + return Objects.equals(id, that.id) && + Objects.equals(title, that.title) && + Objects.equals(nextMaxId, that.nextMaxId) && + Objects.equals(feedItems, that.feedItems); + } + + @Override + public int hashCode() { + return Objects.hash(id, title, nextMaxId, feedItems); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedGroupSet.java b/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedGroupSet.java index f213eb86..3da29e03 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedGroupSet.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/EndOfFeedGroupSet.java @@ -2,6 +2,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; import java.util.List; +import java.util.Objects; public class EndOfFeedGroupSet implements Serializable { private final long id; @@ -48,4 +49,22 @@ public class EndOfFeedGroupSet implements Serializable { public List getGroups() { return groups; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final EndOfFeedGroupSet that = (EndOfFeedGroupSet) o; + return id == that.id && + Objects.equals(activeGroupId, that.activeGroupId) && + Objects.equals(connectedGroupId, that.connectedGroupId) && + Objects.equals(nextMaxId, that.nextMaxId) && + Objects.equals(paginationSource, that.paginationSource) && + Objects.equals(groups, that.groups); + } + + @Override + public int hashCode() { + return Objects.hash(id, activeGroupId, connectedGroupId, nextMaxId, paginationSource, groups); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/FriendshipStatus.java b/app/src/main/java/awais/instagrabber/repositories/responses/FriendshipStatus.java index 22833bfa..4e7b1bd0 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/FriendshipStatus.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/FriendshipStatus.java @@ -3,6 +3,7 @@ package awais.instagrabber.repositories.responses; import androidx.annotation.NonNull; import java.io.Serializable; +import java.util.Objects; public class FriendshipStatus implements Serializable { private final boolean following; @@ -78,6 +79,29 @@ public class FriendshipStatus implements Serializable { return isMutingReel; } + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final FriendshipStatus that = (FriendshipStatus) o; + return following == that.following && + followedBy == that.followedBy && + blocking == that.blocking && + muting == that.muting && + isPrivate == that.isPrivate && + incomingRequest == that.incomingRequest && + outgoingRequest == that.outgoingRequest && + isBestie == that.isBestie && + isRestricted == that.isRestricted && + isMutingReel == that.isMutingReel; + } + + @Override + public int hashCode() { + return Objects.hash(following, followedBy, blocking, muting, isPrivate, incomingRequest, outgoingRequest, isBestie, isRestricted, + isMutingReel); + } + @NonNull @Override public String toString() { diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/ImageVersions2.java b/app/src/main/java/awais/instagrabber/repositories/responses/ImageVersions2.java index 1dd3d743..d71d8a00 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/ImageVersions2.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/ImageVersions2.java @@ -2,6 +2,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; import java.util.List; +import java.util.Objects; public class ImageVersions2 implements Serializable { private final List candidates; @@ -13,4 +14,17 @@ public class ImageVersions2 implements Serializable { public List getCandidates() { return candidates; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final ImageVersions2 that = (ImageVersions2) o; + return Objects.equals(candidates, that.candidates); + } + + @Override + public int hashCode() { + return Objects.hash(candidates); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/Location.java b/app/src/main/java/awais/instagrabber/repositories/responses/Location.java index e7a13362..ea1e66fe 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/Location.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/Location.java @@ -1,6 +1,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; +import java.util.Objects; public class Location implements Serializable { private final long pk; @@ -54,4 +55,23 @@ public class Location implements Serializable { public float getLat() { return lat; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Location location = (Location) o; + return pk == location.pk && + Float.compare(location.lng, lng) == 0 && + Float.compare(location.lat, lat) == 0 && + Objects.equals(shortName, location.shortName) && + Objects.equals(name, location.name) && + Objects.equals(address, location.address) && + Objects.equals(city, location.city); + } + + @Override + public int hashCode() { + return Objects.hash(pk, shortName, name, address, city, lng, lat); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/Media.java b/app/src/main/java/awais/instagrabber/repositories/responses/Media.java index f864a434..1a149a66 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/Media.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/Media.java @@ -6,6 +6,7 @@ import java.io.Serializable; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Objects; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.utils.Utils; @@ -272,4 +273,52 @@ public class Media implements Serializable { final Caption caption1 = getCaption(); caption1.setText(caption); } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Media media = (Media) o; + return takenAt == media.takenAt && + canViewerReshare == media.canViewerReshare && + commentLikesEnabled == media.commentLikesEnabled && + commentsDisabled == media.commentsDisabled && + nextMaxId == media.nextMaxId && + commentCount == media.commentCount && + originalWidth == media.originalWidth && + originalHeight == media.originalHeight && + likeCount == media.likeCount && + hasLiked == media.hasLiked && + isReelMedia == media.isReelMedia && + hasAudio == media.hasAudio && + Double.compare(media.videoDuration, videoDuration) == 0 && + viewCount == media.viewCount && + canViewerSave == media.canViewerSave && + isSidecarChild == media.isSidecarChild && + hasViewerSaved == media.hasViewerSaved && + Objects.equals(pk, media.pk) && + Objects.equals(id, media.id) && + Objects.equals(code, media.code) && + Objects.equals(user, media.user) && + mediaType == media.mediaType && + Objects.equals(imageVersions2, media.imageVersions2) && + Objects.equals(videoVersions, media.videoVersions) && + Objects.equals(caption, media.caption) && + Objects.equals(audio, media.audio) && + Objects.equals(title, media.title) && + Objects.equals(location, media.location) && + Objects.equals(usertags, media.usertags) && + Objects.equals(carouselMedia, media.carouselMedia) && + Objects.equals(injected, media.injected) && + Objects.equals(endOfFeedDemarcator, media.endOfFeedDemarcator) && + Objects.equals(dateString, media.dateString); + } + + @Override + public int hashCode() { + return Objects.hash(pk, id, code, takenAt, user, mediaType, canViewerReshare, commentLikesEnabled, commentsDisabled, nextMaxId, commentCount, + imageVersions2, originalWidth, originalHeight, likeCount, hasLiked, isReelMedia, videoVersions, hasAudio, videoDuration, + viewCount, caption, canViewerSave, audio, title, location, usertags, carouselMedia, isSidecarChild, hasViewerSaved, + injected, endOfFeedDemarcator, dateString); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/MediaCandidate.java b/app/src/main/java/awais/instagrabber/repositories/responses/MediaCandidate.java index 41d05a10..3a243497 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/MediaCandidate.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/MediaCandidate.java @@ -1,6 +1,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; +import java.util.Objects; public class MediaCandidate implements Serializable { private final int width; @@ -24,4 +25,19 @@ public class MediaCandidate implements Serializable { public String getUrl() { return url; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final MediaCandidate that = (MediaCandidate) o; + return width == that.width && + height == that.height && + Objects.equals(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(width, height, url); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/User.java b/app/src/main/java/awais/instagrabber/repositories/responses/User.java index 29660685..1897b8ca 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/User.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/User.java @@ -207,17 +207,40 @@ public class User implements Serializable { // null); // } + @Override public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - final User that = (User) o; - return pk == that.pk && - Objects.equals(username, that.username); + final User user = (User) o; + return pk == user.pk && + isPrivate == user.isPrivate && + isVerified == user.isVerified && + hasAnonymousProfilePicture == user.hasAnonymousProfilePicture && + isUnpublished == user.isUnpublished && + isFavorite == user.isFavorite && + isDirectappInstalled == user.isDirectappInstalled && + mediaCount == user.mediaCount && + followerCount == user.followerCount && + followingCount == user.followingCount && + followingTagCount == user.followingTagCount && + usertagsCount == user.usertagsCount && + Objects.equals(username, user.username) && + Objects.equals(fullName, user.fullName) && + Objects.equals(profilePicUrl, user.profilePicUrl) && + Objects.equals(profilePicId, user.profilePicId) && + Objects.equals(friendshipStatus, user.friendshipStatus) && + Objects.equals(reelAutoArchive, user.reelAutoArchive) && + Objects.equals(allowedCommenterType, user.allowedCommenterType) && + Objects.equals(biography, user.biography) && + Objects.equals(externalUrl, user.externalUrl) && + Objects.equals(publicEmail, user.publicEmail); } @Override public int hashCode() { - return Objects.hash(pk, username); + return Objects.hash(pk, username, fullName, isPrivate, profilePicUrl, profilePicId, friendshipStatus, isVerified, hasAnonymousProfilePicture, + isUnpublished, isFavorite, isDirectappInstalled, reelAutoArchive, allowedCommenterType, mediaCount, followerCount, + followingCount, followingTagCount, biography, externalUrl, usertagsCount, publicEmail); } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/UsertagIn.java b/app/src/main/java/awais/instagrabber/repositories/responses/UsertagIn.java index f634475d..9a5456e1 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/UsertagIn.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/UsertagIn.java @@ -2,6 +2,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; import java.util.List; +import java.util.Objects; public class UsertagIn implements Serializable { private final User user; @@ -19,4 +20,18 @@ public class UsertagIn implements Serializable { public List getPosition() { return position; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final UsertagIn usertagIn = (UsertagIn) o; + return Objects.equals(user, usertagIn.user) && + Objects.equals(position, usertagIn.position); + } + + @Override + public int hashCode() { + return Objects.hash(user, position); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/Usertags.java b/app/src/main/java/awais/instagrabber/repositories/responses/Usertags.java index 17e60419..08bf7b15 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/Usertags.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/Usertags.java @@ -2,6 +2,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; import java.util.List; +import java.util.Objects; public class Usertags implements Serializable { private final List in; @@ -13,4 +14,17 @@ public class Usertags implements Serializable { public List getIn() { return in; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Usertags usertags = (Usertags) o; + return Objects.equals(in, usertags.in); + } + + @Override + public int hashCode() { + return Objects.hash(in); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/VideoVersion.java b/app/src/main/java/awais/instagrabber/repositories/responses/VideoVersion.java index 52d2f53a..b635cd93 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/VideoVersion.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/VideoVersion.java @@ -1,6 +1,7 @@ package awais.instagrabber.repositories.responses; import java.io.Serializable; +import java.util.Objects; public class VideoVersion implements Serializable { private final String id; @@ -36,4 +37,21 @@ public class VideoVersion implements Serializable { public String getUrl() { return url; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final VideoVersion that = (VideoVersion) o; + return width == that.width && + height == that.height && + Objects.equals(id, that.id) && + Objects.equals(type, that.type) && + Objects.equals(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(id, type, width, height, url); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectInbox.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectInbox.java index ea18e8b1..bf6db660 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectInbox.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectInbox.java @@ -1,9 +1,11 @@ package awais.instagrabber.repositories.responses.directmessages; +import androidx.annotation.NonNull; + import java.util.List; -public class DirectInbox { - private final List threads; +public class DirectInbox implements Cloneable { + private List threads; private final boolean hasOlder; private final int unseenCount; private final String unseenCountTs; @@ -28,6 +30,10 @@ public class DirectInbox { return threads; } + public void setThreads(final List threads) { + this.threads = threads; + } + public boolean hasOlder() { return hasOlder; } @@ -47,4 +53,10 @@ public class DirectInbox { public boolean isBlendedInboxEnabled() { return blendedInboxEnabled; } + + @NonNull + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java index a687be3e..0bda29c7 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import java.util.Date; import java.util.List; +import java.util.Objects; import awais.instagrabber.models.enums.DirectItemType; import awais.instagrabber.repositories.responses.Location; @@ -239,21 +240,47 @@ public class DirectItem implements Cloneable { return super.clone(); } - // @Override - // public boolean equals(final Object o) { - // if (this == o) return true; - // if (o == null || getClass() != o.getClass()) return false; - // final DirectItem that = (DirectItem) o; - // return userId == that.userId && - // timestamp == that.timestamp && - // isPending == that.isPending && - // Objects.equals(itemId, that.itemId) && - // itemType == that.itemType && - // Objects.equals(clientContext, that.clientContext); - // } - // - // @Override - // public int hashCode() { - // return Objects.hash(itemId, userId, timestamp, itemType, clientContext, isPending); - // } + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItem that = (DirectItem) o; + return userId == that.userId && + timestamp == that.timestamp && + hideInThread == that.hideInThread && + isPending == that.isPending && + showForwardAttribution == that.showForwardAttribution && + Objects.equals(itemId, that.itemId) && + itemType == that.itemType && + Objects.equals(text, that.text) && + Objects.equals(like, that.like) && + Objects.equals(link, that.link) && + Objects.equals(clientContext, that.clientContext) && + Objects.equals(reelShare, that.reelShare) && + Objects.equals(storyShare, that.storyShare) && + Objects.equals(mediaShare, that.mediaShare) && + Objects.equals(profile, that.profile) && + Objects.equals(placeholder, that.placeholder) && + Objects.equals(media, that.media) && + Objects.equals(previewMedias, that.previewMedias) && + Objects.equals(actionLog, that.actionLog) && + Objects.equals(videoCallEvent, that.videoCallEvent) && + Objects.equals(clip, that.clip) && + Objects.equals(felixShare, that.felixShare) && + Objects.equals(visualMedia, that.visualMedia) && + Objects.equals(animatedMedia, that.animatedMedia) && + Objects.equals(reactions, that.reactions) && + Objects.equals(repliedToMessage, that.repliedToMessage) && + Objects.equals(voiceMedia, that.voiceMedia) && + Objects.equals(location, that.location) && + Objects.equals(date, that.date); + } + + @Override + public int hashCode() { + return Objects + .hash(itemId, userId, timestamp, itemType, text, like, link, clientContext, reelShare, storyShare, mediaShare, profile, placeholder, + media, previewMedias, actionLog, videoCallEvent, clip, felixShare, visualMedia, animatedMedia, reactions, repliedToMessage, + voiceMedia, location, hideInThread, date, isPending, showForwardAttribution); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemActionLog.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemActionLog.java index e430c022..3e31cb23 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemActionLog.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemActionLog.java @@ -1,6 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; import java.util.List; +import java.util.Objects; public class DirectItemActionLog { private final String description; @@ -27,6 +28,21 @@ public class DirectItemActionLog { return textAttributes; } + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemActionLog that = (DirectItemActionLog) o; + return Objects.equals(description, that.description) && + Objects.equals(bold, that.bold) && + Objects.equals(textAttributes, that.textAttributes); + } + + @Override + public int hashCode() { + return Objects.hash(description, bold, textAttributes); + } + public static class TextRange { private final int start; private final int end; @@ -55,5 +71,21 @@ public class DirectItemActionLog { public String getIntent() { return intent; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final TextRange textRange = (TextRange) o; + return start == textRange.start && + end == textRange.end && + Objects.equals(color, textRange.color) && + Objects.equals(intent, textRange.intent); + } + + @Override + public int hashCode() { + return Objects.hash(start, end, color, intent); + } } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemAnimatedMedia.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemAnimatedMedia.java index 462052d7..98158862 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemAnimatedMedia.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemAnimatedMedia.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + import awais.instagrabber.repositories.responses.AnimatedMediaImages; public final class DirectItemAnimatedMedia { @@ -31,4 +33,20 @@ public final class DirectItemAnimatedMedia { public boolean isSticker() { return isSticker; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemAnimatedMedia that = (DirectItemAnimatedMedia) o; + return isRandom == that.isRandom && + isSticker == that.isSticker && + Objects.equals(id, that.id) && + Objects.equals(images, that.images); + } + + @Override + public int hashCode() { + return Objects.hash(id, images, isRandom, isSticker); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemClip.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemClip.java index 2bb23479..22660187 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemClip.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemClip.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + import awais.instagrabber.repositories.responses.Media; public class DirectItemClip { @@ -12,4 +14,17 @@ public class DirectItemClip { public Media getClip() { return clip; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemClip that = (DirectItemClip) o; + return Objects.equals(clip, that.clip); + } + + @Override + public int hashCode() { + return Objects.hash(clip); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemFelixShare.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemFelixShare.java index 078a3810..fee2ef59 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemFelixShare.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemFelixShare.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + import awais.instagrabber.repositories.responses.Media; public class DirectItemFelixShare { @@ -12,4 +14,17 @@ public class DirectItemFelixShare { public Media getVideo() { return video; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemFelixShare that = (DirectItemFelixShare) o; + return Objects.equals(video, that.video); + } + + @Override + public int hashCode() { + return Objects.hash(video); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemLink.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemLink.java index d730b50c..2fe3f49c 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemLink.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemLink.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + public class DirectItemLink { private final String text; private final DirectItemLinkContext linkContext; @@ -31,4 +33,20 @@ public class DirectItemLink { public String getMutationToken() { return mutationToken; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemLink that = (DirectItemLink) o; + return Objects.equals(text, that.text) && + Objects.equals(linkContext, that.linkContext) && + Objects.equals(clientContext, that.clientContext) && + Objects.equals(mutationToken, that.mutationToken); + } + + @Override + public int hashCode() { + return Objects.hash(text, linkContext, clientContext, mutationToken); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemLinkContext.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemLinkContext.java index 142bb16b..79beb422 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemLinkContext.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemLinkContext.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + public class DirectItemLinkContext { private final String linkUrl; private final String linkTitle; @@ -31,4 +33,20 @@ public class DirectItemLinkContext { public String getLinkImageUrl() { return linkImageUrl; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemLinkContext that = (DirectItemLinkContext) o; + return Objects.equals(linkUrl, that.linkUrl) && + Objects.equals(linkTitle, that.linkTitle) && + Objects.equals(linkSummary, that.linkSummary) && + Objects.equals(linkImageUrl, that.linkImageUrl); + } + + @Override + public int hashCode() { + return Objects.hash(linkUrl, linkTitle, linkSummary, linkImageUrl); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemPlaceholder.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemPlaceholder.java index ae37da53..0c6002ae 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemPlaceholder.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemPlaceholder.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + public class DirectItemPlaceholder { private final boolean isLinked; private final String title; @@ -24,4 +26,19 @@ public class DirectItemPlaceholder { public String getMessage() { return message; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemPlaceholder that = (DirectItemPlaceholder) o; + return isLinked == that.isLinked && + Objects.equals(title, that.title) && + Objects.equals(message, that.message); + } + + @Override + public int hashCode() { + return Objects.hash(isLinked, title, message); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReelShare.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReelShare.java index e1142e55..65fe4fbf 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReelShare.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReelShare.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + import awais.instagrabber.repositories.responses.Media; public class DirectItemReelShare { @@ -61,4 +63,24 @@ public class DirectItemReelShare { public long getMentionedUserId() { return mentionedUserId; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemReelShare that = (DirectItemReelShare) o; + return reelOwnerId == that.reelOwnerId && + mentionedUserId == that.mentionedUserId && + isReelPersisted == that.isReelPersisted && + Objects.equals(text, that.text) && + Objects.equals(type, that.type) && + Objects.equals(reelType, that.reelType) && + Objects.equals(media, that.media) && + Objects.equals(reactionInfo, that.reactionInfo); + } + + @Override + public int hashCode() { + return Objects.hash(text, type, reelOwnerId, mentionedUserId, isReelPersisted, reelType, media, reactionInfo); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReelShareReactionInfo.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReelShareReactionInfo.java index 451cfee4..4b951a88 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReelShareReactionInfo.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReelShareReactionInfo.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + public class DirectItemReelShareReactionInfo { private final String emoji; private final String intensity; @@ -16,4 +18,18 @@ public class DirectItemReelShareReactionInfo { public String getIntensity() { return intensity; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemReelShareReactionInfo that = (DirectItemReelShareReactionInfo) o; + return Objects.equals(emoji, that.emoji) && + Objects.equals(intensity, that.intensity); + } + + @Override + public int hashCode() { + return Objects.hash(emoji, intensity); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemStoryShare.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemStoryShare.java index 8a8ea535..4fc2c2cc 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemStoryShare.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemStoryShare.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + import awais.instagrabber.repositories.responses.Media; public class DirectItemStoryShare { @@ -54,4 +56,23 @@ public class DirectItemStoryShare { public String getMessage() { return message; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemStoryShare that = (DirectItemStoryShare) o; + return isReelPersisted == that.isReelPersisted && + Objects.equals(reelId, that.reelId) && + Objects.equals(reelType, that.reelType) && + Objects.equals(text, that.text) && + Objects.equals(media, that.media) && + Objects.equals(title, that.title) && + Objects.equals(message, that.message); + } + + @Override + public int hashCode() { + return Objects.hash(reelId, reelType, text, isReelPersisted, media, title, message); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVideoCallEvent.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVideoCallEvent.java index ac7b48bf..6bc3911d 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVideoCallEvent.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVideoCallEvent.java @@ -1,6 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; import java.util.List; +import java.util.Objects; public final class DirectItemVideoCallEvent { private final String action; @@ -40,4 +41,21 @@ public final class DirectItemVideoCallEvent { public List getTextAttributes() { return textAttributes; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemVideoCallEvent that = (DirectItemVideoCallEvent) o; + return threadHasAudioOnlyCall == that.threadHasAudioOnlyCall && + Objects.equals(action, that.action) && + Objects.equals(encodedServerDataInfo, that.encodedServerDataInfo) && + Objects.equals(description, that.description) && + Objects.equals(textAttributes, that.textAttributes); + } + + @Override + public int hashCode() { + return Objects.hash(action, encodedServerDataInfo, description, threadHasAudioOnlyCall, textAttributes); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVisualMedia.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVisualMedia.java index fed2184b..660f74b3 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVisualMedia.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVisualMedia.java @@ -1,6 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; import java.util.List; +import java.util.Objects; import awais.instagrabber.models.enums.RavenMediaViewMode; import awais.instagrabber.repositories.responses.Media; @@ -64,4 +65,25 @@ public class DirectItemVisualMedia { public Media getMedia() { return media; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemVisualMedia media1 = (DirectItemVisualMedia) o; + return urlExpireAtSecs == media1.urlExpireAtSecs && + playbackDurationSecs == media1.playbackDurationSecs && + seenCount == media1.seenCount && + replayExpiringAtUs == media1.replayExpiringAtUs && + Objects.equals(seenUserIds, media1.seenUserIds) && + viewMode == media1.viewMode && + Objects.equals(expiringMediaActionSummary, media1.expiringMediaActionSummary) && + Objects.equals(media, media1.media); + } + + @Override + public int hashCode() { + return Objects + .hash(urlExpireAtSecs, playbackDurationSecs, seenUserIds, viewMode, seenCount, replayExpiringAtUs, expiringMediaActionSummary, media); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVoiceMedia.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVoiceMedia.java index c5618018..2ffa86af 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVoiceMedia.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemVoiceMedia.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + import awais.instagrabber.repositories.responses.Media; public class DirectItemVoiceMedia { @@ -24,4 +26,19 @@ public class DirectItemVoiceMedia { public String getViewMode() { return viewMode; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemVoiceMedia that = (DirectItemVoiceMedia) o; + return seenCount == that.seenCount && + Objects.equals(media, that.media) && + Objects.equals(viewMode, that.viewMode); + } + + @Override + public int hashCode() { + return Objects.hash(media, seenCount, viewMode); + } } 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 0e58f08e..5be30e3a 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 @@ -1,5 +1,6 @@ package awais.instagrabber.repositories.responses.directmessages; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.Serializable; @@ -9,13 +10,13 @@ import java.util.Objects; import awais.instagrabber.repositories.responses.User; -public class DirectThread implements Serializable { +public class DirectThread implements Serializable, Cloneable { private final String threadId; private final String threadV2Id; - private final List users; - private final List leftUsers; - private final List adminUserIds; - private final List items; + private List users; + private List leftUsers; + private List adminUserIds; + private List items; private final long lastActivityAt; private boolean muted; private final boolean isPin; @@ -127,18 +128,34 @@ public class DirectThread implements Serializable { return users; } + public void setUsers(final List users) { + this.users = users; + } + public List getLeftUsers() { return leftUsers; } + public void setLeftUsers(final List leftUsers) { + this.leftUsers = leftUsers; + } + public List getAdminUserIds() { return adminUserIds; } + public void setAdminUserIds(final List adminUserIds) { + this.adminUserIds = adminUserIds; + } + public List getItems() { return items; } + public void setItems(final List items) { + this.items = items; + } + public long getLastActivityAt() { return lastActivityAt; } @@ -284,17 +301,59 @@ public class DirectThread implements Serializable { return firstItem; } + @NonNull + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + @Override public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final DirectThread that = (DirectThread) o; - return Objects.equals(threadId, that.threadId) && - Objects.equals(threadV2Id, that.threadV2Id); + return lastActivityAt == that.lastActivityAt && + muted == that.muted && + isPin == that.isPin && + named == that.named && + canonical == that.canonical && + pending == that.pending && + archived == that.archived && + valuedRequest == that.valuedRequest && + viewerId == that.viewerId && + folder == that.folder && + vcMuted == that.vcMuted && + isGroup == that.isGroup && + mentionsMuted == that.mentionsMuted && + hasOlder == that.hasOlder && + hasNewer == that.hasNewer && + isSpam == that.isSpam && + approvalRequiredForNewMembers == that.approvalRequiredForNewMembers && + inputMode == that.inputMode && + Objects.equals(threadId, that.threadId) && + Objects.equals(threadV2Id, that.threadV2Id) && + Objects.equals(users, that.users) && + Objects.equals(leftUsers, that.leftUsers) && + Objects.equals(adminUserIds, that.adminUserIds) && + Objects.equals(items, that.items) && + Objects.equals(threadType, that.threadType) && + Objects.equals(threadTitle, that.threadTitle) && + Objects.equals(pendingScore, that.pendingScore) && + Objects.equals(inviter, that.inviter) && + Objects.equals(lastSeenAt, that.lastSeenAt) && + Objects.equals(newestCursor, that.newestCursor) && + Objects.equals(oldestCursor, that.oldestCursor) && + Objects.equals(lastPermanentItem, that.lastPermanentItem) && + Objects.equals(directStory, that.directStory) && + Objects.equals(threadContextItems, that.threadContextItems); } @Override public int hashCode() { - return Objects.hash(threadId, threadV2Id); + return Objects + .hash(threadId, threadV2Id, users, leftUsers, adminUserIds, items, lastActivityAt, muted, isPin, named, canonical, pending, archived, + valuedRequest, threadType, viewerId, threadTitle, pendingScore, folder, vcMuted, isGroup, mentionsMuted, inviter, hasOlder, + hasNewer, lastSeenAt, newestCursor, oldestCursor, isSpam, lastPermanentItem, directStory, approvalRequiredForNewMembers, + inputMode, threadContextItems); } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadDirectStory.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadDirectStory.java index 5077cf87..2c74598e 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadDirectStory.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadDirectStory.java @@ -1,6 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; import java.util.List; +import java.util.Objects; public class DirectThreadDirectStory { private final List items; @@ -18,4 +19,18 @@ public class DirectThreadDirectStory { public int getUnseenCount() { return unseenCount; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectThreadDirectStory that = (DirectThreadDirectStory) o; + return unseenCount == that.unseenCount && + Objects.equals(items, that.items); + } + + @Override + public int hashCode() { + return Objects.hash(items, unseenCount); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadLastSeenAt.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadLastSeenAt.java index 3ec4a804..7fedda5f 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadLastSeenAt.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadLastSeenAt.java @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + public class DirectThreadLastSeenAt { private final String timestamp; private final String itemId; @@ -16,4 +18,18 @@ public class DirectThreadLastSeenAt { public String getItemId() { return itemId; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectThreadLastSeenAt that = (DirectThreadLastSeenAt) o; + return Objects.equals(timestamp, that.timestamp) && + Objects.equals(itemId, that.itemId); + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, itemId); + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadParticipantRequestsResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadParticipantRequestsResponse.java index adb6780c..d02bae90 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadParticipantRequestsResponse.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadParticipantRequestsResponse.java @@ -13,7 +13,7 @@ public class DirectThreadParticipantRequestsResponse implements Serializable, Cl private final Map requesterUsernames; private final String cursor; private final int totalThreadParticipants; - private final int totalParticipantRequests; + private int totalParticipantRequests; private final String status; public DirectThreadParticipantRequestsResponse(final List users, @@ -54,6 +54,10 @@ public class DirectThreadParticipantRequestsResponse implements Serializable, Cl return totalParticipantRequests; } + public void setTotalParticipantRequests(final int totalParticipantRequests) { + this.totalParticipantRequests = totalParticipantRequests; + } + public String getStatus() { return status; } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/RavenExpiringMediaActionSummary.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/RavenExpiringMediaActionSummary.java index 70be4091..17f5c151 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/RavenExpiringMediaActionSummary.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/RavenExpiringMediaActionSummary.java @@ -2,6 +2,8 @@ package awais.instagrabber.repositories.responses.directmessages; import com.google.gson.annotations.SerializedName; +import java.util.Objects; + public final class RavenExpiringMediaActionSummary { private final ActionType type; private final long timestamp; @@ -25,6 +27,21 @@ public final class RavenExpiringMediaActionSummary { return type; } + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final RavenExpiringMediaActionSummary that = (RavenExpiringMediaActionSummary) o; + return timestamp == that.timestamp && + count == that.count && + type == that.type; + } + + @Override + public int hashCode() { + return Objects.hash(type, timestamp, count); + } + // thanks to http://github.com/warifp/InstagramAutoPostImageUrl/blob/master/vendor/mgp25/instagram-php/src/Response/Model/ActionBadge.php public enum ActionType { @SerializedName("raven_delivered") diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/ThreadContext.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/ThreadContext.java index d48c2b74..c44c0e29 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/ThreadContext.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/ThreadContext.java @@ -1,6 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; import java.io.Serializable; +import java.util.Objects; public class ThreadContext implements Serializable { private final int type; @@ -18,4 +19,18 @@ public class ThreadContext implements Serializable { public String getText() { return text; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final ThreadContext that = (ThreadContext) o; + return type == that.type && + Objects.equals(text, that.text); + } + + @Override + public int hashCode() { + return Objects.hash(type, text); + } } diff --git a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java index cd9d18d5..c1241ebd 100644 --- a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java @@ -7,10 +7,10 @@ import android.graphics.BitmapFactory; import android.net.Uri; import android.util.Log; import android.util.LruCache; -import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Pair; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; diff --git a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java index 853f262c..a23186e7 100644 --- a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java @@ -32,7 +32,6 @@ import awais.instagrabber.repositories.responses.MediaCandidate; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.VideoVersion; import awais.instagrabber.repositories.responses.directmessages.DirectItem; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadDirectStory; import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; import awaisomereport.LogCollector; @@ -1126,34 +1125,32 @@ public final class ResponseBodyUtils { public static boolean isRead(final DirectItem item, final Map lastSeenAt, - final List userIdsToCheck, - final DirectThreadDirectStory directStory) { - boolean read = lastSeenAt.entrySet() - .stream() - .filter(entry -> userIdsToCheck.contains(entry.getKey())) - .anyMatch(entry -> { - final String userLastSeenTsString = entry.getValue().getTimestamp(); - if (userLastSeenTsString == null) return false; - final long userTs = Long.parseLong(userLastSeenTsString); - final long itemTs = item.getTimestamp(); - return userTs >= itemTs; - }); + final List userIdsToCheck) { // Further check if directStory exists - if (read && directStory != null) { - read = false; - } - return read; + // if (read && directStory != null) { + // read = false; + // } + return lastSeenAt.entrySet() + .stream() + .filter(entry -> userIdsToCheck.contains(entry.getKey())) + .anyMatch(entry -> { + final String userLastSeenTsString = entry.getValue().getTimestamp(); + if (userLastSeenTsString == null) return false; + final long userTs = Long.parseLong(userLastSeenTsString); + final long itemTs = item.getTimestamp(); + return userTs >= itemTs; + }); } - + public static StoryModel parseBroadcastItem(final JSONObject data) throws JSONException { final StoryModel model = new StoryModel(data.getString("id"), - data.getString("cover_frame_url"), - data.getString("cover_frame_url"), - MediaItemType.MEDIA_TYPE_LIVE, - data.optLong("published_time", 0), - data.getJSONObject("broadcast_owner").getString("username"), - data.getJSONObject("broadcast_owner").getLong("pk"), - false); + data.getString("cover_frame_url"), + data.getString("cover_frame_url"), + MediaItemType.MEDIA_TYPE_LIVE, + data.optLong("published_time", 0), + data.getJSONObject("broadcast_owner").getString("username"), + data.getJSONObject("broadcast_owner").getLong("pk"), + false); model.setVideoUrl(data.getString("dash_playback_url")); return model; } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java index ce0335eb..2e1f01f2 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java @@ -5,8 +5,6 @@ import android.os.AsyncTask; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import awais.instagrabber.asyncs.ProfileFetcher; import awais.instagrabber.asyncs.UsernameFetcher; @@ -25,10 +23,10 @@ import static awais.instagrabber.utils.Utils.settingsHelper; public class AppStateViewModel extends AndroidViewModel { private static final String TAG = AppStateViewModel.class.getSimpleName(); - private final MutableLiveData currentUser = new MutableLiveData<>(); private final String cookie; private final boolean isLoggedIn; + private User currentUser; private AccountRepository accountRepository; private String username; @@ -52,7 +50,7 @@ public class AppStateViewModel extends AndroidViewModel { fetchUsername(usernameListener); } - public LiveData getCurrentUser() { + public User getCurrentUser() { return currentUser; } @@ -84,7 +82,7 @@ public class AppStateViewModel extends AndroidViewModel { new ProfileFetcher( username.trim().substring(1), true, - currentUser::postValue + user -> this.currentUser = user ).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java index e012c040..f003f4b0 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java @@ -1,188 +1,56 @@ 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.managers.DirectMessagesManager; +import awais.instagrabber.managers.InboxManager; +import awais.instagrabber.models.Resource; 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 DirectInboxViewModel extends ViewModel { private static final String TAG = DirectInboxViewModel.class.getSimpleName(); - private final DirectMessagesService service; - private final MutableLiveData fetchingInbox = new MutableLiveData<>(false); - private final MutableLiveData> threads = new MutableLiveData<>(); - private final MutableLiveData fetchingUnseenCount = new MutableLiveData<>(false); - private final MutableLiveData unseenCount = new MutableLiveData<>(0); - private final MutableLiveData pendingRequestsTotal = new MutableLiveData<>(0); - - private Call inboxRequest; - private Call unseenCountRequest; - private long seqId; - private String cursor; - private boolean hasOlder = true; - private User viewer; + private final InboxManager inboxManager; public DirectInboxViewModel() { - 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(); - fetchUnseenCount(); + final DirectMessagesManager messagesManager = DirectMessagesManager.getInstance(); + inboxManager = messagesManager.getInboxManager(); + } + + public LiveData> getInbox() { + return inboxManager.getInbox(); } public LiveData> getThreads() { - return threads; + return inboxManager.getThreads(); } - public void setThreads(final List threads) { - this.threads.postValue(threads); - } - - public void addThreads(final Collection threads) { - if (threads == null) return; - List list = getThreads().getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - list.addAll(threads); - this.threads.postValue(list); - } - - public LiveData getUnseenCount() { - return unseenCount; - } - - public LiveData getFetchingInbox() { - return fetchingInbox; + public LiveData> getUnseenCount() { + return inboxManager.getUnseenCount(); } public LiveData getPendingRequestsTotal() { - return pendingRequestsTotal; + return inboxManager.getPendingRequestsTotal(); } public User getViewer() { - return viewer; + return inboxManager.getViewer(); } public void fetchInbox() { - if ((fetchingInbox.getValue() != null && fetchingInbox.getValue()) || !hasOlder) return; - stopCurrentInboxRequest(); - fetchingInbox.postValue(true); - inboxRequest = service.fetchInbox(cursor, seqId); - inboxRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - parseInboxResponse(response.body()); - fetchingInbox.postValue(false); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Failed fetching dm 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 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 threads = inbox.getThreads(); - if (!TextUtils.isEmpty(cursor)) { - addThreads(threads); - } else { - setThreads(threads); - } - cursor = inbox.getOldestCursor(); - hasOlder = inbox.hasOlder(); - pendingRequestsTotal.postValue(response.getPendingRequestsTotal()); - // unseenCount.postValue(inbox.getUnseenCount()); - } - - private void stopCurrentInboxRequest() { - if (inboxRequest == null || inboxRequest.isCanceled() || inboxRequest.isExecuted()) return; - inboxRequest.cancel(); - inboxRequest = null; - } - - public void fetchUnseenCount() { - if ((fetchingUnseenCount.getValue() != null && fetchingUnseenCount.getValue())) return; - stopCurrentUnseenCountRequest(); - fetchingUnseenCount.postValue(true); - unseenCountRequest = service.fetchUnseenCount(); - unseenCountRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - parseUnseenCountResponse(response.body()); - fetchingUnseenCount.postValue(false); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Failed fetching unseen count", t); - fetchingUnseenCount.postValue(false); - } - }); - } - - private void parseUnseenCountResponse(final DirectBadgeCount directBadgeCount) { - if (directBadgeCount == null) return; - unseenCount.postValue(directBadgeCount.getBadgeCount()); - } - - private void stopCurrentUnseenCountRequest() { - if (unseenCountRequest == null || unseenCountRequest.isCanceled() || unseenCountRequest.isExecuted()) return; - unseenCountRequest.cancel(); - unseenCountRequest = null; + inboxManager.fetchInbox(); } public void refresh() { - cursor = null; - seqId = 0; - hasOlder = true; - fetchInbox(); - fetchUnseenCount(); + inboxManager.refresh(); } public void onDestroy() { - stopCurrentInboxRequest(); - // getThreads().postValue(Collections.emptyList()); + inboxManager.onDestroy(); } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java index f6972e9a..85d28e7d 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectPendingInboxViewModel.java @@ -1,141 +1,48 @@ 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.managers.DirectMessagesManager; +import awais.instagrabber.managers.InboxManager; +import awais.instagrabber.models.Resource; 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 fetchingInbox = new MutableLiveData<>(false); - private final MutableLiveData> threads = new MutableLiveData<>(); - - private Call inboxRequest; - private Call unseenCountRequest; - private long seqId; - private String cursor; - private boolean hasOlder = true; - private User viewer; + private final InboxManager inboxManager; 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(); + inboxManager = DirectMessagesManager.getInstance().getPendingInboxManager(); + inboxManager.fetchInbox(); } public LiveData> getThreads() { - return threads; + return inboxManager.getThreads(); } - public void setThreads(final List threads) { - this.threads.postValue(threads); - } - - public void addThreads(final Collection threads) { - if (threads == null) return; - List list = getThreads().getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - list.addAll(threads); - this.threads.postValue(list); - } - - public LiveData getFetchingInbox() { - return fetchingInbox; + public LiveData> getInbox() { + return inboxManager.getInbox(); } public User getViewer() { - return viewer; + return inboxManager.getViewer(); } public void fetchInbox() { - if ((fetchingInbox.getValue() != null && fetchingInbox.getValue()) || !hasOlder) return; - stopCurrentInboxRequest(); - fetchingInbox.postValue(true); - inboxRequest = service.fetchPendingInbox(cursor, seqId); - inboxRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - parseInboxResponse(response.body()); - fetchingInbox.postValue(false); - } - - @Override - public void onFailure(@NonNull final Call 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 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; + inboxManager.fetchInbox(); } public void refresh() { - cursor = null; - seqId = 0; - hasOlder = true; - fetchInbox(); + inboxManager.refresh(); } public void onDestroy() { - stopCurrentInboxRequest(); + inboxManager.onDestroy(); } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java index c65b6c80..e9d33085 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java @@ -1,49 +1,30 @@ package awais.instagrabber.viewmodels; import android.app.Application; +import android.content.ContentResolver; import android.content.res.Resources; -import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.util.Pair; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import com.google.common.collect.ImmutableList; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Locale; import java.util.Set; -import java.util.stream.Collectors; import awais.instagrabber.R; import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option; +import awais.instagrabber.managers.DirectMessagesManager; +import awais.instagrabber.managers.ThreadManager; import awais.instagrabber.models.Resource; -import awais.instagrabber.repositories.responses.FriendshipChangeResponse; -import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.directmessages.DirectThread; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.webservices.DirectMessagesService; -import awais.instagrabber.webservices.FriendshipService; -import awais.instagrabber.webservices.ServiceCallback; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -58,573 +39,186 @@ public class DirectSettingsViewModel extends AndroidViewModel { private static final String ACTION_RESTRICT = "restrict"; private static final String ACTION_UNRESTRICT = "unrestrict"; - private final MutableLiveData, List>> users = new MutableLiveData<>( - new Pair<>(Collections.emptyList(), Collections.emptyList())); - private final MutableLiveData title = new MutableLiveData<>(""); - private final MutableLiveData> adminUserIds = new MutableLiveData<>(Collections.emptyList()); - private final MutableLiveData muted = new MutableLiveData<>(false); - private final MutableLiveData mentionsMuted = new MutableLiveData<>(false); - private final MutableLiveData approvalRequiredToJoin = new MutableLiveData<>(false); - private final MutableLiveData pendingRequests = new MutableLiveData<>(null); - private final MutableLiveData inputMode = new MutableLiveData<>(null); - private final MutableLiveData isPending = new MutableLiveData<>(false); - private final DirectMessagesService directMessagesService; - private final long userId; + private final long viewerId; private final Resources resources; - private final FriendshipService friendshipService; - private final String csrfToken; + private final ThreadManager threadManager; - private DirectThread thread; - private boolean viewerIsAdmin; - private User viewer; - - public DirectSettingsViewModel(final Application application) { + public DirectSettingsViewModel(final Application application, + @NonNull final String threadId, + final boolean pending, + @NonNull final User currentUser) { super(application); final String cookie = settingsHelper.getString(Constants.COOKIE); - userId = CookieUtils.getUserIdFromCookie(cookie); + viewerId = CookieUtils.getUserIdFromCookie(cookie); final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); - csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); - if (TextUtils.isEmpty(csrfToken) || userId <= 0 || TextUtils.isEmpty(deviceUuid)) { + final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) { throw new IllegalArgumentException("User is not logged in!"); } - directMessagesService = DirectMessagesService.getInstance(csrfToken, userId, deviceUuid); - friendshipService = FriendshipService.getInstance(deviceUuid, csrfToken, userId); + final ContentResolver contentResolver = application.getContentResolver(); resources = getApplication().getResources(); + final DirectMessagesManager messagesManager = DirectMessagesManager.getInstance(); + threadManager = messagesManager.getThreadManager(threadId, pending, currentUser, contentResolver); } @NonNull - public DirectThread getThread() { - return thread; + public LiveData getThread() { + return threadManager.getThread(); } - public void setThread(@NonNull final DirectThread thread) { - this.thread = thread; - inputMode.postValue(thread.getInputMode()); - List users = thread.getUsers(); - if (viewer != null) { - final ImmutableList.Builder builder = ImmutableList.builder().add(viewer); - if (users != null) { - builder.addAll(users); - } - users = builder.build(); - } - this.users.postValue(new Pair<>(users, thread.getLeftUsers())); - setTitle(thread.getThreadTitle()); - final List adminUserIds = thread.getAdminUserIds(); - this.adminUserIds.postValue(adminUserIds); - viewerIsAdmin = adminUserIds.contains(userId); - muted.postValue(thread.isMuted()); - mentionsMuted.postValue(thread.isMentionsMuted()); - approvalRequiredToJoin.postValue(thread.isApprovalRequiredForNewMembers()); - isPending.postValue(thread.isPending()); - if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) { - fetchPendingRequests(); - } - } + // public void setThread(@NonNull final DirectThread thread) { + // this.thread = thread; + // inputMode.postValue(thread.getInputMode()); + // List users = thread.getUsers(); + // final ImmutableList.Builder builder = ImmutableList.builder().add(currentUser); + // if (users != null) { + // builder.addAll(users); + // } + // users = builder.build(); + // this.users.postValue(new Pair<>(users, thread.getLeftUsers())); + // // setTitle(thread.getThreadTitle()); + // final List adminUserIds = thread.getAdminUserIds(); + // this.adminUserIds.postValue(adminUserIds); + // viewerIsAdmin = adminUserIds.contains(viewerId); + // muted.postValue(thread.isMuted()); + // mentionsMuted.postValue(thread.isMentionsMuted()); + // approvalRequiredToJoin.postValue(thread.isApprovalRequiredForNewMembers()); + // isPending.postValue(thread.isPending()); + // if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) { + // fetchPendingRequests(); + // } + // } public LiveData getInputMode() { - return inputMode; + return threadManager.getInputMode(); } - public boolean isGroup() { - if (thread != null) { - return thread.isGroup(); - } - return false; + public LiveData isGroup() { + return threadManager.isGroup(); } - public LiveData, List>> getUsers() { - return users; + public LiveData> getUsers() { + return threadManager.getUsersWithCurrent(); + } + + public LiveData> getLeftUsers() { + return threadManager.getLeftUsers(); + } + + public LiveData, List>> getUsersAndLeftUsers() { + return threadManager.getUsersAndLeftUsers(); } public LiveData getTitle() { - return title; + return threadManager.getThreadTitle(); } - public void setTitle(final String title) { - if (title == null) { - this.title.postValue(""); - return; - } - this.title.postValue(title.trim()); - } + // public void setTitle(final String title) { + // if (title == null) { + // this.title.postValue(""); + // return; + // } + // this.title.postValue(title.trim()); + // } public LiveData> getAdminUserIds() { - return adminUserIds; + return threadManager.getAdminUserIds(); } - public LiveData getMuted() { - return muted; + public LiveData isMuted() { + return threadManager.isMuted(); } public LiveData getApprovalRequiredToJoin() { - return approvalRequiredToJoin; + return threadManager.isApprovalRequiredToJoin(); } public LiveData getPendingRequests() { - return pendingRequests; + return threadManager.getPendingRequests(); } public LiveData isPending() { - return isPending; + return threadManager.isPending(); } - public boolean isViewerAdmin() { - return viewerIsAdmin; + public LiveData isViewerAdmin() { + return threadManager.isViewerAdmin(); } public LiveData> updateTitle(final String newTitle) { - final MutableLiveData> data = new MutableLiveData<>(); - final Call addUsersRequest = directMessagesService - .updateTitle(thread.getThreadId(), newTitle.trim()); - handleDetailsChangeRequest(data, addUsersRequest); - return data; + return threadManager.updateTitle(newTitle); } public LiveData> addMembers(final Set users) { - final MutableLiveData> data = new MutableLiveData<>(); - final Call addUsersRequest = directMessagesService - .addUsers(thread.getThreadId(), users.stream().map(User::getPk).collect(Collectors.toList())); - handleDetailsChangeRequest(data, addUsersRequest); - return data; + return threadManager.addMembers(users); } public LiveData> removeMember(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - final Call request = directMessagesService - .removeUsers(thread.getThreadId(), Collections.singleton(user.getPk())); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleSettingChangeResponseError(response, data); - return; - } - Pair, List> usersValue = users.getValue(); - if (usersValue == null) { - usersValue = new Pair<>(Collections.emptyList(), Collections.emptyList()); - } - List activeUsers = usersValue.first; - if (activeUsers == null) { - activeUsers = Collections.emptyList(); - } - final List updatedActiveUsers = activeUsers.stream() - .filter(user1 -> user1.getPk() != user.getPk()) - .collect(Collectors.toList()); - List leftUsers = usersValue.second; - if (leftUsers == null) { - leftUsers = Collections.emptyList(); - } - final ImmutableList updateLeftUsers = ImmutableList.builder() - .addAll(leftUsers) - .add(user) - .build(); - users.postValue(new Pair<>(updatedActiveUsers, updateLeftUsers)); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.removeMember(user); } private LiveData> makeAdmin(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - if (isAdmin(user)) return data; - final Call request = directMessagesService.addAdmins(thread.getThreadId(), Collections.singleton(user.getPk())); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleSettingChangeResponseError(response, data); - return; - } - final List currentAdmins = adminUserIds.getValue(); - adminUserIds.postValue(ImmutableList.builder() - .addAll(currentAdmins != null ? currentAdmins : Collections.emptyList()) - .add(user.getPk()) - .build()); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.makeAdmin(user); } private LiveData> removeAdmin(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - if (!isAdmin(user)) return data; - final Call request = directMessagesService.removeAdmins(thread.getThreadId(), Collections.singleton(user.getPk())); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleSettingChangeResponseError(response, data); - return; - } - final List currentAdmins = adminUserIds.getValue(); - if (currentAdmins == null) return; - adminUserIds.postValue(currentAdmins.stream() - .filter(userId1 -> userId1 != user.getPk()) - .collect(Collectors.toList())); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.removeAdmin(user); } public LiveData> mute() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - if (thread.isMuted()) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = directMessagesService.mute(thread.getThreadId()); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleSettingChangeResponseError(response, data); - return; - } - thread.setMuted(true); - muted.postValue(true); - data.postValue(Resource.success(new Object())); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.mute(); } public LiveData> unmute() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - if (!thread.isMuted()) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = directMessagesService.unmute(thread.getThreadId()); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleSettingChangeResponseError(response, data); - return; - } - thread.setMuted(false); - muted.postValue(false); - data.postValue(Resource.success(new Object())); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.unmute(); } public LiveData> muteMentions() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - if (thread.isMentionsMuted()) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = directMessagesService.muteMentions(thread.getThreadId()); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleSettingChangeResponseError(response, data); - return; - } - thread.setMentionsMuted(true); - mentionsMuted.postValue(true); - data.postValue(Resource.success(new Object())); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.muteMentions(); } public LiveData> unmuteMentions() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - if (!thread.isMentionsMuted()) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = directMessagesService.unmuteMentions(thread.getThreadId()); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - handleSettingChangeResponseError(response, data); - return; - } - thread.setMentionsMuted(false); - mentionsMuted.postValue(false); - data.postValue(Resource.success(new Object())); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; - } - - private void handleSettingChangeResponseError(@NonNull final Response response, - final MutableLiveData> data) { - final ResponseBody errorBody = response.errorBody(); - if (errorBody == null) { - handleErrorResponse(response, data); - return; - } - try { - final JSONObject json = new JSONObject(errorBody.string()); - if (json.has("message")) { - data.postValue(Resource.error(json.getString("message"), null)); - } - } catch (IOException | JSONException e) { - Log.e(TAG, "onResponse: ", e); - data.postValue(Resource.error(e.getMessage(), null)); - } + return threadManager.unmuteMentions(); } private LiveData> blockUser(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - friendshipService.block(user.getPk(), new ServiceCallback() { - @Override - public void onSuccess(final FriendshipChangeResponse result) { - // refresh thread - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.blockUser(user); } private LiveData> unblockUser(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - friendshipService.unblock(user.getPk(), new ServiceCallback() { - @Override - public void onSuccess(final FriendshipChangeResponse result) { - // refresh thread - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.unblockUser(user); } private LiveData> restrictUser(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - friendshipService.toggleRestrict(user.getPk(), true, new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRestrictResponse result) { - // refresh thread - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.restrictUser(user); } private LiveData> unRestrictUser(final User user) { - final MutableLiveData> data = new MutableLiveData<>(); - friendshipService.toggleRestrict(user.getPk(), false, new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRestrictResponse result) { - // refresh thread - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.unRestrictUser(user); } public LiveData> approveUsers(final List users) { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Call approveUsersRequest = directMessagesService - .approveParticipantRequests(thread.getThreadId(), users.stream().map(User::getPk).collect(Collectors.toList())); - handleDetailsChangeRequest(data, approveUsersRequest); - return data; + return threadManager.approveUsers(users); } public LiveData> denyUsers(final List users) { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Call approveUsersRequest = directMessagesService - .declineParticipantRequests(thread.getThreadId(), users.stream().map(User::getPk).collect(Collectors.toList())); - handleDetailsChangeRequest(data, approveUsersRequest, () -> { - final DirectThreadParticipantRequestsResponse pendingRequestsValue = pendingRequests.getValue(); - if (pendingRequestsValue == null) return; - final List pendingUsers = pendingRequestsValue.getUsers(); - if (pendingUsers == null || pendingUsers.isEmpty()) return; - final List filtered = pendingUsers.stream() - .filter(o -> !users.contains(o)) - .collect(Collectors.toList()); - try { - final DirectThreadParticipantRequestsResponse clone = (DirectThreadParticipantRequestsResponse) pendingRequestsValue.clone(); - clone.setUsers(filtered); - pendingRequests.postValue(clone); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "denyUsers: ", e); - } - }); - return data; + return threadManager.denyUsers(users); } public LiveData> approvalRequired() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - if (thread.isApprovalRequiredForNewMembers()) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = directMessagesService.approvalRequired(thread.getThreadId()); - handleDetailsChangeRequest(data, request, () -> { - thread.setApprovalRequiredForNewMembers(true); - approvalRequiredToJoin.postValue(true); - }); - return data; + return threadManager.approvalRequired(); } public LiveData> approvalNotRequired() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - if (!thread.isApprovalRequiredForNewMembers()) { - data.postValue(Resource.success(new Object())); - return data; - } - final Call request = directMessagesService.approvalNotRequired(thread.getThreadId()); - handleDetailsChangeRequest(data, request, () -> { - thread.setApprovalRequiredForNewMembers(false); - approvalRequiredToJoin.postValue(false); - }); - return data; + return threadManager.approvalNotRequired(); } public LiveData> leave() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Call request = directMessagesService.leave(thread.getThreadId()); - handleDetailsChangeRequest(data, request); - return data; + return threadManager.leave(); } public LiveData> end() { - final MutableLiveData> data = new MutableLiveData<>(); - data.postValue(Resource.loading(null)); - final Call request = directMessagesService.end(thread.getThreadId()); - handleDetailsChangeRequest(data, request, () -> { - thread.setInputMode(1); - inputMode.postValue(1); - }); - return data; - } - - private interface OnSuccessAction { - void onSuccess(); - } - - private void handleDetailsChangeRequest(final MutableLiveData> data, - final Call request) { - handleDetailsChangeRequest(data, request, null); - } - - private void handleDetailsChangeRequest(final MutableLiveData> data, - final Call request, - @Nullable final OnSuccessAction action) { - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (!response.isSuccessful()) { - handleErrorResponse(response, data); - return; - } - final DirectThreadDetailsChangeResponse changeResponse = response.body(); - if (changeResponse == null) { - data.postValue(Resource.error("Response is null", null)); - return; - } - data.postValue(Resource.success(new Object())); - final DirectThread thread = changeResponse.getThread(); - if (thread != null) { - setThread(thread); - } - if (action != null) { - action.onSuccess(); - } - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - } - - private void handleErrorResponse(@NonNull final Response response, - final MutableLiveData> data) { - final ResponseBody errorBody = response.errorBody(); - if (errorBody == null) { - data.postValue(Resource.error("Request failed!", null)); - return; - } - try { - data.postValue(Resource.error(errorBody.string(), null)); - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); - data.postValue(Resource.error(e.getMessage(), null)); - } + return threadManager.end(); } public ArrayList> createUserOptions(final User user) { @@ -632,10 +226,11 @@ public class DirectSettingsViewModel extends AndroidViewModel { if (user == null || isSelf(user) || hasLeft(user)) { return options; } - if (viewerIsAdmin) { + final Boolean viewerIsAdmin = threadManager.isViewerAdmin().getValue(); + if (viewerIsAdmin != null && viewerIsAdmin) { options.add(new Option<>(getString(R.string.dms_action_kick), ACTION_KICK)); - final boolean isAdmin = isAdmin(user); + final boolean isAdmin = threadManager.isAdmin(user); options.add(new Option<>( isAdmin ? getString(R.string.dms_action_remove_admin) : getString(R.string.dms_action_make_admin), isAdmin ? ACTION_REMOVE_ADMIN : ACTION_MAKE_ADMIN @@ -649,8 +244,8 @@ public class DirectSettingsViewModel extends AndroidViewModel { )); // options.add(new Option<>(getString(R.string.report), ACTION_REPORT)); - - if (!isGroup()) { + final Boolean isGroup = threadManager.isGroup().getValue(); + if (isGroup != null && isGroup) { final boolean restricted = user.getFriendshipStatus().isRestricted(); options.add(new Option<>( restricted ? getString(R.string.unrestrict) : getString(R.string.restrict), @@ -661,18 +256,13 @@ public class DirectSettingsViewModel extends AndroidViewModel { } private boolean hasLeft(final User user) { - final Pair, List> users = this.users.getValue(); - if (users == null || users.second == null) return false; - return users.second.contains(user); - } - - private boolean isAdmin(final User user) { - final List adminUserIdsValue = adminUserIds.getValue(); - return adminUserIdsValue != null && adminUserIdsValue.contains(user.getPk()); + final List leftUsers = getLeftUsers().getValue(); + if (leftUsers == null) return false; + return leftUsers.contains(user); } private boolean isSelf(final User user) { - return user.getPk() == userId; + return user.getPk() == viewerId; } private String getString(@StringRes final int resId) { @@ -703,47 +293,7 @@ public class DirectSettingsViewModel extends AndroidViewModel { } } - public void setViewer(final User viewer) { - this.viewer = viewer; - } - - private void fetchPendingRequests() { - final Call request = directMessagesService.participantRequests(thread.getThreadId(), 5, null); - request.enqueue(new Callback() { - - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (!response.isSuccessful()) { - if (response.errorBody() != null) { - try { - final String string = 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); - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); - } - return; - } - Log.e(TAG, "onResponse: request was not successful and response error body was null"); - return; - } - final DirectThreadParticipantRequestsResponse body = response.body(); - if (body == null) { - Log.e(TAG, "onResponse: response body was null"); - return; - } - pendingRequests.postValue(body); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - } - }); + public LiveData getInviter() { + return threadManager.getInviter(); } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java index a12e3dbe..860728c2 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java @@ -5,610 +5,154 @@ import android.content.ContentResolver; import android.media.MediaScannerConnection; import android.net.Uri; import android.util.Log; -import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; - -import com.google.common.collect.Iterables; - -import org.json.JSONObject; +import androidx.lifecycle.Transformations; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; import java.util.List; -import java.util.Locale; import java.util.Set; -import java.util.UUID; import java.util.stream.Collectors; import awais.instagrabber.customviews.emoji.Emoji; +import awais.instagrabber.managers.DirectMessagesManager; +import awais.instagrabber.managers.ThreadManager; import awais.instagrabber.models.Resource; -import awais.instagrabber.models.UploadVideoOptions; -import awais.instagrabber.models.enums.DirectItemType; -import awais.instagrabber.repositories.requests.UploadFinishOptions; -import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds; import awais.instagrabber.repositories.responses.User; 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.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.DirectThreadFeedResponse; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; -import awais.instagrabber.utils.BitmapUtils; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DirectItemFactory; import awais.instagrabber.utils.DirectoryUtils; import awais.instagrabber.utils.MediaController; -import awais.instagrabber.utils.MediaUploadHelper; -import awais.instagrabber.utils.MediaUploader; import awais.instagrabber.utils.MediaUtils; import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.VoiceRecorder; -import awais.instagrabber.webservices.DirectMessagesService; -import awais.instagrabber.webservices.MediaService; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; import static awais.instagrabber.utils.Utils.settingsHelper; public class DirectThreadViewModel extends AndroidViewModel { private static final String TAG = DirectThreadViewModel.class.getSimpleName(); - private static final String ERROR_INVALID_USER = "Invalid user"; - private static final String ERROR_INVALID_THREAD = "Invalid thread"; - private static final String ERROR_RESPONSE_NOT_OK = "Response status from server was not ok"; - private static final String ERROR_VIDEO_TOO_LONG = "Instagram does not allow uploading videos longer than 60 secs for Direct messages"; - private static final String ERROR_AUDIO_TOO_LONG = "Instagram does not allow uploading audio longer than 60 secs"; + // private static final String ERROR_INVALID_THREAD = "Invalid thread"; - private final MutableLiveData thread = new MutableLiveData<>(); - private final MutableLiveData> items = new MutableLiveData<>(new LinkedList<>()); - private final MutableLiveData threadTitle = new MutableLiveData<>(""); - private final MutableLiveData fetching = new MutableLiveData<>(false); - private final MutableLiveData> users = new MutableLiveData<>(new ArrayList<>()); - private final MutableLiveData> leftUsers = new MutableLiveData<>(new ArrayList<>()); - private final MutableLiveData replyToItem = new MutableLiveData<>(); - private final MutableLiveData pendingRequestsCount = new MutableLiveData<>(null); - private final MutableLiveData inputMode = new MutableLiveData<>(0); - private final MutableLiveData isPending = new MutableLiveData<>(null); - - private final DirectMessagesService service; private final ContentResolver contentResolver; - private final MediaService mediaService; - private final String csrfToken; private final File recordingsDir; private final Application application; private final long viewerId; + private final String threadId; + private final User currentUser; + private final ThreadManager threadManager; - private String cursor; - private String threadId; - private boolean hasOlder = true; - private ThreadIdOrUserIds threadIdOrUserIds; - private User currentUser; - private Call chatsRequest; private VoiceRecorder voiceRecorder; - private boolean viewerIsAdmin; - public DirectThreadViewModel(@NonNull final Application application) { + public DirectThreadViewModel(@NonNull final Application application, + @NonNull final String threadId, + final boolean pending, + @NonNull final User currentUser) { super(application); + this.application = application; + this.threadId = threadId; + this.currentUser = currentUser; final String cookie = settingsHelper.getString(Constants.COOKIE); viewerId = CookieUtils.getUserIdFromCookie(cookie); final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); - csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) { throw new IllegalArgumentException("User is not logged in!"); } - service = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid); - mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId); contentResolver = application.getContentResolver(); recordingsDir = DirectoryUtils.getOutputMediaDirectory(application, "Recordings"); - this.application = application; + final DirectMessagesManager messagesManager = DirectMessagesManager.getInstance(); + threadManager = messagesManager.getThreadManager(threadId, pending, currentUser, contentResolver); + threadManager.fetchPendingRequests(); } - public MutableLiveData getThreadTitle() { - return threadTitle; + public void moveFromPending() { + DirectMessagesManager.getInstance().moveThreadFromPending(threadId); + threadManager.moveFromPending(); + } + + public void removeThread() { + threadManager.removeThread(); } public String getThreadId() { return threadId; } - public void setThreadId(final String threadId) { - this.threadId = threadId; - this.threadIdOrUserIds = ThreadIdOrUserIds.of(threadId); + public LiveData getThreadTitle() { + return threadManager.getThreadTitle(); } public LiveData getThread() { - return thread; - } - - public void setThread(final DirectThread thread) { - if (thread == null) return; - this.thread.postValue(thread); - setThreadId(thread.getThreadId()); - fetching.postValue(true); - setupThreadInfo(thread); + return threadManager.getThread(); } public LiveData> getItems() { - return items; + return Transformations.map(threadManager.getItems(), items -> items.stream() + .filter(directItem -> directItem.getHideInThread() == 0) + .collect(Collectors.toList())); + } + + public LiveData> isFetching() { + return threadManager.isFetching(); + } + + public LiveData> getUsers() { + return threadManager.getUsers(); + } + + public LiveData> getLeftUsers() { + return threadManager.getLeftUsers(); + } + + public LiveData getPendingRequestsCount() { + return threadManager.getPendingRequestsCount(); + } + + public LiveData getInputMode() { + return threadManager.getInputMode(); + } + + public LiveData isPending() { + return threadManager.isPending(); } public long getViewerId() { return viewerId; } - public void setItems(final List items) { - this.items.postValue(items); - } - - public void addItems(final Collection items) { - addItems(-1, items); - } - - public void addItems(final int index, final Collection items) { - if (items == null) return; - List list = this.items.getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - if (index >= 0) { - list.addAll(index, items); - } else { - list.addAll(items); - } - this.items.postValue(list); - } - - private void addReaction(final DirectItem item, final Emoji emoji) { - if (item == null || emoji == null || currentUser == null) return; - final boolean isLike = emoji.getUnicode().equals("❤️"); - DirectItemReactions reactions = item.getReactions(); - if (reactions == null) { - reactions = new DirectItemReactions(null, null); - } else { - try { - reactions = (DirectItemReactions) reactions.clone(); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "addReaction: ", e); - return; - } - } - if (isLike) { - final List likes = addEmoji(reactions.getLikes(), null, false); - reactions.setLikes(likes); - } - final List emojis = addEmoji(reactions.getEmojis(), emoji.getUnicode(), true); - reactions.setEmojis(emojis); - List list = this.items.getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - int index = getItemIndex(item, list); - if (index >= 0) { - try { - final DirectItem clone = (DirectItem) list.get(index).clone(); - clone.setReactions(reactions); - list.set(index, clone); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "addReaction: error cloning", e); - } - } - this.items.postValue(list); - } - - private List addEmoji(final List reactionList, - final String emoji, - final boolean shouldReplaceIfAlreadyReacted) { - final List temp = reactionList == null ? new ArrayList<>() : new ArrayList<>(reactionList); - int index = -1; - for (int i = 0; i < temp.size(); i++) { - final DirectItemEmojiReaction directItemEmojiReaction = temp.get(i); - if (directItemEmojiReaction.getSenderId() == currentUser.getPk()) { - index = i; - break; - } - } - final DirectItemEmojiReaction reaction = new DirectItemEmojiReaction( - currentUser.getPk(), - System.currentTimeMillis() * 1000, - emoji, - "none" - ); - if (index < 0) { - temp.add(0, reaction); - } else if (shouldReplaceIfAlreadyReacted) { - temp.add(0, reaction); - temp.remove(index); - } - return temp; - } - - private void removeReaction(final DirectItem item) { - try { - final DirectItem itemClone = (DirectItem) item.clone(); - final DirectItemReactions reactions = itemClone.getReactions(); - final DirectItemReactions reactionsClone = (DirectItemReactions) reactions.clone(); - final List likes = reactionsClone.getLikes(); - if (likes != null) { - final List updatedLikes = likes.stream() - .filter(like -> like.getSenderId() != viewerId) - .collect(Collectors.toList()); - reactionsClone.setLikes(updatedLikes); - } - final List emojis = reactionsClone.getEmojis(); - if (emojis != null) { - final List updatedEmojis = emojis.stream() - .filter(emoji -> emoji.getSenderId() != viewerId) - .collect(Collectors.toList()); - reactionsClone.setEmojis(updatedEmojis); - } - itemClone.setReactions(reactionsClone); - List list = this.items.getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - int index = getItemIndex(item, list); - if (index >= 0) { - list.set(index, itemClone); - } - this.items.postValue(list); - } catch (Exception e) { - Log.e(TAG, "removeReaction: ", e); - } - } - - private int removeItem(final DirectItem item) { - if (item == null) return 0; - List list = this.items.getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - int index = getItemIndex(item, list); - if (index >= 0) { - list.remove(index); - this.items.postValue(list); - } - return index; - } - - private int getItemIndex(final DirectItem item, final List list) { - int index = -1; - for (int i = 0; i < list.size(); i++) { - final DirectItem directItem = list.get(i); - if (directItem.getItemId().equals(item.getItemId())) { - index = i; - break; - } - } - return index; - } - - private void updateItemSent(final String clientContext, final long timestamp, final String itemId) { - if (clientContext == null) return; - List list = this.items.getValue(); - list = list == null ? new LinkedList<>() : new LinkedList<>(list); - final int index = Iterables.indexOf(list, item -> { - if (item == null) return false; - return item.getClientContext().equals(clientContext); - }); - if (index < 0) return; - final DirectItem directItem = list.get(index); - try { - final DirectItem itemClone = (DirectItem) directItem.clone(); - itemClone.setItemId(itemId); - itemClone.setPending(false); - itemClone.setTimestamp(timestamp); - list.set(index, itemClone); - this.items.postValue(list); - } catch (CloneNotSupportedException e) { - Log.e(TAG, "updateItemSent: ", e); - } - } - - public void removeAllItems() { - items.setValue(Collections.emptyList()); - } - - public LiveData getFetching() { - return fetching; - } - - public LiveData> getUsers() { - return users; - } - - public LiveData> getLeftUsers() { - return leftUsers; - } - public LiveData getReplyToItem() { - return replyToItem; - } - - public LiveData getPendingRequestsCount() { - return pendingRequestsCount; - } - - public LiveData getInputMode() { - return inputMode; - } - - public LiveData isPending() { - return isPending; + return threadManager.getReplyToItem(); } public void fetchChats() { - final Boolean isFetching = fetching.getValue(); - if ((isFetching != null && isFetching) || !hasOlder) return; - fetching.postValue(true); - chatsRequest = service.fetchThread(threadId, cursor); - chatsRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - final DirectThreadFeedResponse feedResponse = response.body(); - if (feedResponse == null) { - Log.e(TAG, "onResponse: response was null!"); - return; - } - if (!feedResponse.getStatus().equals("ok")) return; - final DirectThread thread = feedResponse.getThread(); - setThread(thread); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "Failed fetching dm chats", t); - fetching.postValue(false); - hasOlder = false; - } - }); + threadManager.fetchChats(); } public void refreshChats() { - final Boolean isFetching = fetching.getValue(); - if (isFetching != null && isFetching) { - stopCurrentRequest(); - } - cursor = null; - hasOlder = true; - fetchChats(); + threadManager.refreshChats(); } - private void stopCurrentRequest() { - if (chatsRequest == null || chatsRequest.isExecuted() || chatsRequest.isCanceled()) { - return; - } - chatsRequest.cancel(); - fetching.postValue(false); + public LiveData> sendText(final String text) { + return threadManager.sendText(text); } - private void setupThreadInfo(final DirectThread thread) { - if (thread == null) return; - inputMode.postValue(thread.getInputMode()); - final List items = thread.getItems() - .stream() - .filter(directItem -> directItem.getHideInThread() == 0) - .collect(Collectors.toList()); - if (!TextUtils.isEmpty(cursor)) { - addItems(items); - } else { - setItems(items); - } - setThreadId(thread.getThreadId()); - threadTitle.postValue(thread.getThreadTitle()); - cursor = thread.getOldestCursor(); - hasOlder = thread.hasOlder(); - users.postValue(thread.getUsers()); - leftUsers.postValue(thread.getLeftUsers()); - fetching.postValue(false); - isPending.postValue(thread.isPending()); - final List adminUserIds = thread.getAdminUserIds(); - viewerIsAdmin = adminUserIds.contains(viewerId); - if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) { - fetchPendingRequests(); - } + public LiveData> sendUri(final MediaController.MediaEntry entry) { + return threadManager.sendUri(entry); } - public LiveData> sendText(final String text) { - final MutableLiveData> data = new MutableLiveData<>(); - final Long userId = handleCurrentUser(data); - if (userId == null) return data; - final String clientContext = UUID.randomUUID().toString(); - final DirectItem replyToItemValue = replyToItem.getValue(); - final DirectItem directItem = DirectItemFactory.createText(userId, clientContext, text, replyToItemValue); - // Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId()); - directItem.setPending(true); - addItems(0, Collections.singletonList(directItem)); - data.postValue(Resource.loading(directItem)); - final String repliedToItemId = replyToItemValue != null ? replyToItemValue.getItemId() : null; - final String repliedToClientContext = replyToItemValue != null ? replyToItemValue.getClientContext() : null; - final Call request = service.broadcastText( - clientContext, - threadIdOrUserIds, - text, - repliedToItemId, - repliedToClientContext - ); - enqueueRequest(request, data, directItem); - return data; + public LiveData> sendUri(final Uri uri) { + return threadManager.sendUri(uri); } - public LiveData> sendUri(final MediaController.MediaEntry entry) { - final MutableLiveData> data = new MutableLiveData<>(); - if (entry == null) { - data.postValue(Resource.error("Entry is null", null)); - return data; - } - final Uri uri = Uri.fromFile(new File(entry.path)); - if (!entry.isVideo) { - sendPhoto(data, uri, entry.width, entry.height); - return data; - } - sendVideo(data, uri, entry.size, entry.duration, entry.width, entry.height); - return data; - } - - public LiveData> sendUri(final Uri uri) { - final MutableLiveData> data = new MutableLiveData<>(); - if (uri == null) { - data.postValue(Resource.error("Uri is null", null)); - return data; - } - final String mimeType = Utils.getMimeType(uri, contentResolver); - if (TextUtils.isEmpty(mimeType)) { - data.postValue(Resource.error("Unknown MediaType", null)); - return data; - } - final boolean isPhoto = mimeType.startsWith("image"); - if (isPhoto) { - sendPhoto(data, uri); - return data; - } - if (mimeType.startsWith("video")) { - sendVideo(data, uri); - } - return data; - } - - private void sendPhoto(final MutableLiveData> data, - @NonNull final Uri uri) { - try { - final Pair dimensions = BitmapUtils.decodeDimensions(contentResolver, uri); - if (dimensions == null) { - data.postValue(Resource.error("Decoding dimensions failed", null)); - return; - } - sendPhoto(data, uri, dimensions.first, dimensions.second); - } catch (FileNotFoundException e) { - data.postValue(Resource.error(e.getMessage(), null)); - Log.e(TAG, "sendPhoto: ", e); - } - } - - private void sendPhoto(final MutableLiveData> data, - @NonNull final Uri uri, - final int width, - final int height) { - final Long userId = handleCurrentUser(data); - if (userId == null) return; - final String clientContext = UUID.randomUUID().toString(); - final DirectItem directItem = DirectItemFactory.createImageOrVideo(userId, clientContext, uri, width, height, false); - directItem.setPending(true); - addItems(0, Collections.singletonList(directItem)); - data.postValue(Resource.loading(directItem)); - MediaUploader.uploadPhoto(uri, contentResolver, new MediaUploader.OnMediaUploadCompleteListener() { - @Override - public void onUploadComplete(final MediaUploader.MediaUploadResponse response) { - if (handleInvalidResponse(data, response, directItem)) return; - final String uploadId = response.getResponse().optString("upload_id"); - final Call request = service.broadcastPhoto(clientContext, threadIdOrUserIds, uploadId); - enqueueRequest(request, data, directItem); - } - - @Override - public void onFailure(final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "onFailure: ", t); - } - }); - } - - private void sendVideo(@NonNull final MutableLiveData> data, - @NonNull final Uri uri) { - MediaUtils.getVideoInfo(contentResolver, uri, new MediaUtils.OnInfoLoadListener() { - @Override - public void onLoad(@Nullable final MediaUtils.VideoInfo info) { - if (info == null) { - data.postValue(Resource.error("Could not get the video info", null)); - return; - } - sendVideo(data, uri, info.size, info.duration, info.width, info.height); - } - - @Override - public void onFailure(final Throwable t) { - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - } - - private void sendVideo(@NonNull final MutableLiveData> data, - @NonNull final Uri uri, - final long byteLength, - final long duration, - final int width, - final int height) { - if (duration > 60000) { - // instagram does not allow uploading videos longer than 60 secs for Direct messages - data.postValue(Resource.error(ERROR_VIDEO_TOO_LONG, null)); - return; - } - final Long userId = handleCurrentUser(data); - if (userId == null) return; - final String clientContext = UUID.randomUUID().toString(); - final DirectItem directItem = DirectItemFactory.createImageOrVideo(userId, clientContext, uri, width, height, true); - directItem.setPending(true); - addItems(0, Collections.singletonList(directItem)); - data.postValue(Resource.loading(directItem)); - final UploadVideoOptions uploadDmVideoOptions = MediaUploadHelper.createUploadDmVideoOptions(byteLength, duration, width, height); - MediaUploader.uploadVideo(uri, contentResolver, uploadDmVideoOptions, new MediaUploader.OnMediaUploadCompleteListener() { - @Override - public void onUploadComplete(final MediaUploader.MediaUploadResponse response) { - // Log.d(TAG, "onUploadComplete: " + response); - if (handleInvalidResponse(data, response, directItem)) return; - final UploadFinishOptions uploadFinishOptions = new UploadFinishOptions() - .setUploadId(uploadDmVideoOptions.getUploadId()) - .setSourceType("2") - .setVideoOptions(new UploadFinishOptions.VideoOptions().setLength(duration / 1000f)); - final Call uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions); - uploadFinishRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (response.isSuccessful()) { - final Call request = service.broadcastVideo( - clientContext, - threadIdOrUserIds, - uploadDmVideoOptions.getUploadId(), - "", - true - ); - enqueueRequest(request, data, directItem); - return; - } - if (response.errorBody() != null) { - handleErrorBody(call, response, data, directItem); - return; - } - data.postValue(Resource.error("uploadFinishRequest was not successful and response error body was null", directItem)); - Log.e(TAG, "uploadFinishRequest was not successful and response error body was null"); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "onFailure: ", t); - } - }); - } - - @Override - public void onFailure(final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "onFailure: ", t); - } - }); - } - - public LiveData> startRecording() { - final MutableLiveData> data = new MutableLiveData<>(); + public LiveData> startRecording() { + final MutableLiveData> data = new MutableLiveData<>(); voiceRecorder = new VoiceRecorder(recordingsDir, new VoiceRecorder.VoiceRecorderCallback() { @Override public void onStart() {} @@ -631,7 +175,12 @@ public class DirectThreadViewModel extends AndroidViewModel { MediaUtils.getVoiceInfo(contentResolver, uri, new MediaUtils.OnInfoLoadListener() { @Override public void onLoad(@Nullable final MediaUtils.VideoInfo videoInfo) { - sendVoice(data, uri, result.getWaveform(), result.getSamplingFreq(), videoInfo.duration, videoInfo.size); + threadManager.sendVoice(data, + uri, + result.getWaveform(), + result.getSamplingFreq(), + videoInfo == null ? 0 : videoInfo.duration, + videoInfo == null ? 0 : videoInfo.size); } @Override @@ -658,195 +207,22 @@ public class DirectThreadViewModel extends AndroidViewModel { voiceRecorder = null; } - private void sendVoice(@NonNull final MutableLiveData> data, - @NonNull final Uri uri, - @NonNull final List waveform, - final int samplingFreq, - final long duration, - final long byteLength) { - if (duration > 60000) { - // instagram does not allow uploading audio longer than 60 secs for Direct messages - data.postValue(Resource.error(ERROR_AUDIO_TOO_LONG, null)); - return; - } - final Long userId = handleCurrentUser(data); - if (userId == null) return; - final String clientContext = UUID.randomUUID().toString(); - final DirectItem directItem = DirectItemFactory.createVoice(userId, clientContext, uri, duration, waveform, samplingFreq); - directItem.setPending(true); - addItems(0, Collections.singletonList(directItem)); - data.postValue(Resource.loading(directItem)); - final UploadVideoOptions uploadDmVoiceOptions = MediaUploadHelper.createUploadDmVoiceOptions(byteLength, duration); - MediaUploader.uploadVideo(uri, contentResolver, uploadDmVoiceOptions, new MediaUploader.OnMediaUploadCompleteListener() { - @Override - public void onUploadComplete(final MediaUploader.MediaUploadResponse response) { - // Log.d(TAG, "onUploadComplete: " + response); - if (handleInvalidResponse(data, response, directItem)) return; - final UploadFinishOptions uploadFinishOptions = new UploadFinishOptions() - .setUploadId(uploadDmVoiceOptions.getUploadId()) - .setSourceType("4"); - final Call uploadFinishRequest = mediaService.uploadFinish(uploadFinishOptions); - uploadFinishRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (response.isSuccessful()) { - final Call request = service.broadcastVoice( - clientContext, - threadIdOrUserIds, - uploadDmVoiceOptions.getUploadId(), - waveform, - samplingFreq - ); - enqueueRequest(request, data, directItem); - return; - } - if (response.errorBody() != null) { - handleErrorBody(call, response, data, directItem); - return; - } - data.postValue(Resource.error("uploadFinishRequest was not successful and response error body was null", directItem)); - Log.e(TAG, "uploadFinishRequest was not successful and response error body was null"); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "onFailure: ", t); - } - }); - } - - @Override - public void onFailure(final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "onFailure: ", t); - } - }); + public LiveData> sendReaction(final DirectItem item, final Emoji emoji) { + return threadManager.sendReaction(item, emoji); } - public LiveData> sendReaction(final DirectItem item, final Emoji emoji) { - final MutableLiveData> data = new MutableLiveData<>(); - final Long userId = handleCurrentUser(data); - if (userId == null) { - data.postValue(Resource.error("userId is null", null)); - return data; - } - final String clientContext = UUID.randomUUID().toString(); - // Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId()); - data.postValue(Resource.loading(item)); - addReaction(item, emoji); - String emojiUnicode = null; - if (!emoji.getUnicode().equals("❤️")) { - emojiUnicode = emoji.getUnicode(); - } - final Call request = service.broadcastReaction( - clientContext, threadIdOrUserIds, item.getItemId(), emojiUnicode, false); - handleBroadcastReactionRequest(data, item, request); - return data; + public LiveData> sendDeleteReaction(final String itemId) { + return threadManager.sendDeleteReaction(itemId); } - public LiveData> sendDeleteReaction(final String itemId) { - final MutableLiveData> data = new MutableLiveData<>(); - final DirectItem item = getItem(itemId); - if (item == null) { - data.postValue(Resource.error("Invalid item", null)); - return data; - } - final DirectItemReactions reactions = item.getReactions(); - if (reactions == null) { - // already removed? - data.postValue(Resource.success(item)); - return data; - } - removeReaction(item); - final String clientContext = UUID.randomUUID().toString(); - final Call request = service.broadcastReaction(clientContext, threadIdOrUserIds, item.getItemId(), null, true); - handleBroadcastReactionRequest(data, item, request); - return data; - } - - public LiveData> unsend(final DirectItem item) { - final MutableLiveData> data = new MutableLiveData<>(); - if (item == null) { - data.postValue(Resource.error("item is null", null)); - return data; - } - final int index = removeItem(item); - final Call request = service.deleteItem(threadId, item.getItemId()); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (response.isSuccessful()) { - // Log.d(TAG, "onResponse: " + response.body()); - return; - } - // add the item back if unsuccessful - addItems(index, Collections.singletonList(item)); - if (response.errorBody() != null) { - handleErrorBody(call, response, data, item); - return; - } - data.postValue(Resource.error("request was not successful and response error body was null", item)); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - data.postValue(Resource.error(t.getMessage(), item)); - Log.e(TAG, "enqueueRequest: onFailure: ", t); - } - }); - return data; - } - - private void handleBroadcastReactionRequest(final MutableLiveData> data, - final DirectItem item, - @NonNull final Call request) { - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (!response.isSuccessful()) { - if (response.errorBody() != null) { - handleErrorBody(call, response, data, item); - return; - } - data.postValue(Resource.error("request was not successful and response error body was null", item)); - return; - } - final DirectThreadBroadcastResponse body = response.body(); - if (body == null) { - data.postValue(Resource.error("Response is null!", item)); - } - // otherwise nothing to do? maybe update the timestamp in the emoji? - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - data.postValue(Resource.error(t.getMessage(), item)); - Log.e(TAG, "enqueueRequest: onFailure: ", t); - } - }); - } - - @Nullable - private DirectItem getItem(final String itemId) { - if (itemId == null) return null; - final List items = this.items.getValue(); - if (items == null) return null; - return items.stream() - .filter(directItem -> directItem.getItemId().equals(itemId)) - .findFirst() - .orElse(null); + public LiveData> unsend(final DirectItem item) { + return threadManager.unsend(item); } public User getCurrentUser() { return currentUser; } - public void setCurrentUser(final User currentUser) { - this.currentUser = currentUser; - } - @Nullable public User getUser(final long userId) { final LiveData> users = getUsers(); @@ -871,315 +247,24 @@ public class DirectThreadViewModel extends AndroidViewModel { return match; } - private void enqueueRequest(@NonNull final Call request, - @NonNull final MutableLiveData> data, - @NonNull final DirectItem directItem) { - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (response.isSuccessful()) { - final DirectThreadBroadcastResponse broadcastResponse = response.body(); - if (broadcastResponse == null) { - data.postValue(Resource.error("Response was null from server", directItem)); - Log.e(TAG, "enqueueRequest: onResponse: response body is null"); - return; - } - final String payloadClientContext; - final long timestamp; - final String itemId; - final DirectThreadBroadcastResponsePayload payload = broadcastResponse.getPayload(); - if (payload == null) { - final List messageMetadata = broadcastResponse.getMessageMetadata(); - if (messageMetadata == null || messageMetadata.isEmpty()) { - data.postValue(Resource.success(directItem)); - return; - } - final DirectThreadBroadcastResponseMessageMetadata metadata = messageMetadata.get(0); - payloadClientContext = metadata.getClientContext(); - itemId = metadata.getItemId(); - timestamp = metadata.getTimestamp(); - } else { - payloadClientContext = payload.getClientContext(); - timestamp = payload.getTimestamp(); - itemId = payload.getItemId(); - } - updateItemSent(payloadClientContext, timestamp, itemId); - data.postValue(Resource.success(directItem)); - return; - } - if (response.errorBody() != null) { - handleErrorBody(call, response, data, directItem); - } - data.postValue(Resource.error("request was not successful and response error body was null", directItem)); - } - - @Override - public void onFailure(@NonNull final Call call, - @NonNull final Throwable t) { - data.postValue(Resource.error(t.getMessage(), directItem)); - Log.e(TAG, "enqueueRequest: onFailure: ", t); - } - }); - } - - @Nullable - private Long handleCurrentUser(final MutableLiveData> data) { - if (currentUser == null || currentUser.getPk() <= 0) { - data.postValue(Resource.error(ERROR_INVALID_USER, null)); - return null; - } - final long userId = currentUser.getPk(); - if (threadIdOrUserIds == null) { - data.postValue(Resource.error(ERROR_INVALID_THREAD, null)); - return null; - } - return userId; - } - - private boolean handleInvalidResponse(final MutableLiveData> data, - final MediaUploader.MediaUploadResponse response, - final DirectItem directItem) { - final JSONObject responseJson = response.getResponse(); - if (responseJson == null || response.getResponseCode() != HttpURLConnection.HTTP_OK) { - data.postValue(Resource.error(ERROR_RESPONSE_NOT_OK, directItem)); - return true; - } - final String status = responseJson.optString("status"); - if (TextUtils.isEmpty(status) || !status.equals("ok")) { - data.postValue(Resource.error(ERROR_RESPONSE_NOT_OK, directItem)); - return true; - } - return false; - } - - private void handleErrorBody(@NonNull final Call call, - @NonNull final Response response, - @NonNull final MutableLiveData> data, - @NonNull final DirectItem directItem) { - 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); - data.postValue(Resource.error(msg, directItem)); - Log.e(TAG, msg); - } catch (IOException e) { - data.postValue(Resource.error(e.getMessage(), directItem)); - Log.e(TAG, "onResponse: ", e); - } - } - public void forward(final Set recipients, final DirectItem itemToForward) { - if (recipients == null || itemToForward == null) return; - for (final RankedRecipient recipient : recipients) { - forward(recipient, itemToForward); - } + threadManager.forward(recipients, itemToForward); } public void forward(final RankedRecipient recipient, final DirectItem itemToForward) { - if (recipient == null || itemToForward == null) return; - if (recipient.getThread() == null && recipient.getUser() != null) { - // create thread and forward - final Call createThreadRequest = service.createThread(Collections.singletonList(recipient.getUser().getPk()), null); - createThreadRequest.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, @NonNull final Response response) { - if (!response.isSuccessful()) { - if (response.errorBody() != null) { - try { - final String string = 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); - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); - } - return; - } - Log.e(TAG, "onResponse: request was not successful and response error body was null"); - return; - } - final DirectThread thread = response.body(); - if (thread == null) { - Log.e(TAG, "onResponse: thread is null"); - return; - } - forward(thread, itemToForward); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - - } - }); - return; - } - if (recipient.getThread() != null) { - // just forward - final DirectThread thread = recipient.getThread(); - forward(thread, itemToForward); - } - } - - private void forward(@NonNull final DirectThread thread, @NonNull final DirectItem itemToForward) { - final DirectItemType itemType = itemToForward.getItemType(); - final String itemTypeName = itemType.getName(); - if (itemTypeName == null) { - Log.e(TAG, "forward: itemTypeName was null!"); - return; - } - final Call request = service.forward(thread.getThreadId(), - itemTypeName, - threadId, - itemToForward.getItemId()); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (response.isSuccessful()) return; - if (response.errorBody() != null) { - try { - final String string = 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); - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); - } - return; - } - Log.e(TAG, "onResponse: request was not successful and response error body was null"); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - } - }); + threadManager.forward(recipient, itemToForward); } public void setReplyToItem(final DirectItem item) { // Log.d(TAG, "setReplyToItem: " + item); - replyToItem.postValue(item); - } - - private void fetchPendingRequests() { - final Call request = service.participantRequests(threadId, 1, null); - request.enqueue(new Callback() { - - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response response) { - if (!response.isSuccessful()) { - if (response.errorBody() != null) { - try { - final String string = 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); - } catch (IOException e) { - Log.e(TAG, "onResponse: ", e); - } - return; - } - Log.e(TAG, "onResponse: request was not successful and response error body was null"); - return; - } - final DirectThreadParticipantRequestsResponse body = response.body(); - if (body == null) { - Log.e(TAG, "onResponse: response body was null"); - return; - } - pendingRequestsCount.postValue(body.getTotalParticipantRequests()); - } - - @Override - public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - } - }); + threadManager.setReplyToItem(item); } public LiveData> acceptRequest() { - final MutableLiveData> data = new MutableLiveData<>(); - final Call request = service.approveRequest(threadId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response 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 call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.acceptRequest(); } public LiveData> declineRequest() { - final MutableLiveData> data = new MutableLiveData<>(); - final Call request = service.declineRequest(threadId); - request.enqueue(new Callback() { - @Override - public void onResponse(@NonNull final Call call, - @NonNull final Response 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 call, @NonNull final Throwable t) { - Log.e(TAG, "onFailure: ", t); - data.postValue(Resource.error(t.getMessage(), null)); - } - }); - return data; + return threadManager.declineRequest(); } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/factories/DirectSettingsViewModelFactory.java b/app/src/main/java/awais/instagrabber/viewmodels/factories/DirectSettingsViewModelFactory.java new file mode 100644 index 00000000..2d3503bf --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/factories/DirectSettingsViewModelFactory.java @@ -0,0 +1,35 @@ +package awais.instagrabber.viewmodels.factories; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.viewmodels.DirectSettingsViewModel; + +public class DirectSettingsViewModelFactory implements ViewModelProvider.Factory { + + private final Application application; + private final String threadId; + private final boolean pending; + private final User currentUser; + + public DirectSettingsViewModelFactory(@NonNull final Application application, + @NonNull final String threadId, + final boolean pending, + @NonNull final User currentUser) { + this.application = application; + this.threadId = threadId; + this.pending = pending; + this.currentUser = currentUser; + } + + @NonNull + @Override + public T create(@NonNull final Class modelClass) { + //noinspection unchecked + return (T) new DirectSettingsViewModel(application, threadId, pending, currentUser); + } +} diff --git a/app/src/main/java/awais/instagrabber/viewmodels/factories/DirectThreadViewModelFactory.java b/app/src/main/java/awais/instagrabber/viewmodels/factories/DirectThreadViewModelFactory.java new file mode 100644 index 00000000..586e1cc0 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/factories/DirectThreadViewModelFactory.java @@ -0,0 +1,35 @@ +package awais.instagrabber.viewmodels.factories; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.viewmodels.DirectThreadViewModel; + +public class DirectThreadViewModelFactory implements ViewModelProvider.Factory { + + private final Application application; + private final String threadId; + private final boolean pending; + private final User currentUser; + + public DirectThreadViewModelFactory(@NonNull final Application application, + @NonNull final String threadId, + final boolean pending, + @NonNull final User currentUser) { + this.application = application; + this.threadId = threadId; + this.pending = pending; + this.currentUser = currentUser; + } + + @NonNull + @Override + public T create(@NonNull final Class modelClass) { + //noinspection unchecked + return (T) new DirectThreadViewModel(application, threadId, pending, currentUser); + } +} diff --git a/app/src/main/res/layout/fragment_direct_pending_inbox.xml b/app/src/main/res/layout/fragment_direct_pending_inbox.xml index dfb19d63..aba9eb83 100644 --- a/app/src/main/res/layout/fragment_direct_pending_inbox.xml +++ b/app/src/main/res/layout/fragment_direct_pending_inbox.xml @@ -20,4 +20,14 @@ android:paddingBottom="?attr/actionBarSize" tools:listitem="@layout/layout_dm_inbox_item" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4f007c79..f93975cb 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -428,4 +428,5 @@ Accept request from %1s (%2s)? Decline Accept + No pending requests diff --git a/build.gradle b/build.gradle index cdffced1..1c106f22 100755 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:4.1.2' def nav_version = "2.3.2" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" }