diff --git a/app/build.gradle b/app/build.gradle index e738ac73..53cfe918 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,13 +56,13 @@ configurations.all { } dependencies { - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' def appcompat_version = "1.2.0" - def nav_version = '2.3.2' + def nav_version = '2.3.3' def exoplayer_version = '2.12.0' - implementation 'com.google.android.material:material:1.3.0' + implementation 'com.google.android.material:material:1.4.0-alpha01' implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version" implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version" @@ -70,15 +70,16 @@ dependencies { implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" - implementation "androidx.recyclerview:recyclerview:1.2.0-beta01" + implementation "androidx.recyclerview:recyclerview:1.2.0-beta02" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.preference:preference:1.1.1" - implementation "androidx.work:work-runtime:2.4.0" + implementation "androidx.work:work-runtime:2.5.0" implementation 'androidx.palette:palette:1.0.0' + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation 'com.google.guava:guava:27.0.1-android' @@ -89,10 +90,10 @@ dependencies { annotationProcessor "androidx.room:room-compiler:$room_version" // CameraX - def camerax_version = "1.0.0-rc01" + def camerax_version = "1.1.0-alpha02" implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-lifecycle:$camerax_version" - implementation "androidx.camera:camera-view:1.0.0-alpha20" + implementation "androidx.camera:camera-view:1.0.0-alpha22" // EmojiCompat def emoji_compat_version = "1.1.0" diff --git a/app/schemas/awais.instagrabber.db.AppDatabase/5.json b/app/schemas/awais.instagrabber.db.AppDatabase/5.json new file mode 100644 index 00000000..a60a7ea8 --- /dev/null +++ b/app/schemas/awais.instagrabber.db.AppDatabase/5.json @@ -0,0 +1,161 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "0b38e12b76bb081ec837191c5ef5b54e", + "entities": [ + { + "tableName": "accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `username` TEXT, `cookie` TEXT, `full_name` TEXT, `profile_pic` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "profilePic", + "columnName": "profile_pic", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_text` TEXT, `type` TEXT, `display_name` TEXT, `pic_url` TEXT, `date_added` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query_text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "picUrl", + "columnName": "pic_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateAdded", + "columnName": "date_added", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dm_last_notified", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thread_id` TEXT, `last_notified_msg_ts` INTEGER, `last_notified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "threadId", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastNotifiedMsgTs", + "columnName": "last_notified_msg_ts", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastNotifiedAt", + "columnName": "last_notified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_dm_last_notified_thread_id", + "unique": true, + "columnNames": [ + "thread_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_last_notified_thread_id` ON `${TABLE_NAME}` (`thread_id`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0b38e12b76bb081ec837191c5ef5b54e')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ba525c14..fd517a33 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,12 +8,12 @@ + - - - + + @@ -147,6 +147,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index 117ded8b..a9eb9ec0 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -38,6 +38,8 @@ import androidx.emoji.text.FontRequestEmojiCompatConfig; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavBackStackEntry; import androidx.navigation.NavController; import androidx.navigation.NavDestination; @@ -45,6 +47,7 @@ import androidx.navigation.ui.NavigationUI; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.CollapsingToolbarLayout; +import com.google.android.material.behavior.HideBottomViewOnScrollBehavior; import com.google.android.material.bottomnavigation.BottomNavigationView; import java.util.ArrayList; @@ -61,12 +64,15 @@ import awais.instagrabber.asyncs.SuggestionsFetcher; import awais.instagrabber.customviews.emoji.EmojiVariantManager; import awais.instagrabber.databinding.ActivityMainBinding; import awais.instagrabber.fragments.PostViewV2Fragment; +import awais.instagrabber.fragments.directmessages.DirectMessageInboxFragmentDirections; import awais.instagrabber.fragments.main.FeedFragment; +import awais.instagrabber.fragments.settings.PreferenceKeys; import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.IntentModel; import awais.instagrabber.models.SuggestionModel; import awais.instagrabber.models.enums.SuggestionType; import awais.instagrabber.services.ActivityCheckerService; +import awais.instagrabber.services.DMSyncAlarmReceiver; import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; @@ -75,6 +81,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 +109,8 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage private int firstFragmentGraphIndex; private boolean isActivityCheckerServiceBound = false; private boolean isBackStackEmpty = false; + private boolean isLoggedIn; + private HideBottomViewOnScrollBehavior behavior; private final ServiceConnection serviceConnection = new ServiceConnection() { @Override @@ -131,10 +140,18 @@ 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); createNotificationChannels(); + try { + final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.bottomNavView.getLayoutParams(); + //noinspection unchecked + behavior = (HideBottomViewOnScrollBehavior) layoutParams.getBehavior(); + } catch (Exception e) { + Log.e(TAG, "onCreate: ", e); + } if (savedInstanceState == null) { setupBottomNavigationBar(true); } @@ -142,6 +159,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)) { @@ -154,6 +172,14 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage EmojiVariantManager.getInstance(); }); initEmojiCompat(); + // initDmService(); + } + + private void initDmService() { + if (!isLoggedIn) return; + final boolean enabled = settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH); + if (!enabled) return; + DMSyncAlarmReceiver.setAlarm(this); } @Override @@ -242,15 +268,22 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage } private void createNotificationChannels() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); - notificationManager.createNotificationChannel(new NotificationChannel(Constants.DOWNLOAD_CHANNEL_ID, - Constants.DOWNLOAD_CHANNEL_NAME, - NotificationManager.IMPORTANCE_DEFAULT)); - notificationManager.createNotificationChannel(new NotificationChannel(Constants.ACTIVITY_CHANNEL_ID, - Constants.ACTIVITY_CHANNEL_NAME, - NotificationManager.IMPORTANCE_DEFAULT)); - } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; + final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); + notificationManager.createNotificationChannel(new NotificationChannel(Constants.DOWNLOAD_CHANNEL_ID, + Constants.DOWNLOAD_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT)); + notificationManager.createNotificationChannel(new NotificationChannel(Constants.ACTIVITY_CHANNEL_ID, + Constants.ACTIVITY_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT)); + notificationManager.createNotificationChannel(new NotificationChannel(Constants.DM_UNREAD_CHANNEL_ID, + Constants.DM_UNREAD_CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT)); + final NotificationChannel silentNotificationChannel = new NotificationChannel(Constants.SILENT_NOTIFICATIONS_CHANNEL_ID, + Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME, + NotificationManager.IMPORTANCE_LOW); + silentNotificationChannel.setSound(null, null); + notificationManager.createNotificationChannel(silentNotificationChannel); } private void setupSuggestions() { @@ -387,8 +420,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(); @@ -477,7 +508,11 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage final int destinationId = destination.getId(); @SuppressLint("RestrictedApi") final Deque backStack = navController.getBackStack(); setupMenu(backStack.size(), destinationId); - binding.bottomNavView.setVisibility(SHOW_BOTTOM_VIEW_DESTINATIONS.contains(destinationId) ? View.VISIBLE : View.GONE); + final boolean contains = SHOW_BOTTOM_VIEW_DESTINATIONS.contains(destinationId); + binding.bottomNavView.setVisibility(contains ? View.VISIBLE : View.GONE); + if (contains && behavior != null) { + behavior.slideUp(binding.bottomNavView); + } // explicitly hide keyboard when we navigate final View view = getCurrentFocus(); @@ -518,6 +553,10 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage showActivityView(); return; } + if (Constants.ACTION_SHOW_DM_THREAD.equals(action)) { + showThread(intent); + return; + } if (Intent.ACTION_SEND.equals(action) && type != null) { if (type.equals("text/plain")) { handleUrl(intent.getStringExtra(Intent.EXTRA_TEXT)); @@ -531,6 +570,58 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage } } + private void showThread(@NonNull final Intent intent) { + final String threadId = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_ID); + final String threadTitle = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_TITLE); + navigateToThread(threadId, threadTitle); + } + + public void navigateToThread(final String threadId, final String threadTitle) { + if (threadId == null || threadTitle == null) return; + currentNavControllerLiveData.observe(this, new Observer() { + @Override + public void onChanged(final NavController navController) { + if (navController == null) return; + if (navController.getGraph().getId() != R.id.direct_messages_nav_graph) return; + try { + final NavDestination currentDestination = navController.getCurrentDestination(); + if (currentDestination != null && currentDestination.getId() == R.id.directMessagesInboxFragment) { + // if we are already on the inbox page, navigate to the thread + // need handler.post() to wait for the fragment manager to be ready to navigate + new Handler().post(() -> { + final DirectMessageInboxFragmentDirections.ActionInboxToThread action = DirectMessageInboxFragmentDirections + .actionInboxToThread(threadId, threadTitle); + navController.navigate(action); + }); + return; + } + // add a destination change listener to navigate to thread once we are on the inbox page + navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() { + @Override + public void onDestinationChanged(@NonNull final NavController controller, + @NonNull final NavDestination destination, + @Nullable final Bundle arguments) { + if (destination.getId() == R.id.directMessagesInboxFragment) { + final DirectMessageInboxFragmentDirections.ActionInboxToThread action = DirectMessageInboxFragmentDirections + .actionInboxToThread(threadId, threadTitle); + controller.navigate(action); + controller.removeOnDestinationChangedListener(this); + } + } + }); + // pop back stack until we reach the inbox page + navController.popBackStack(R.id.directMessagesInboxFragment, false); + } finally { + currentNavControllerLiveData.removeObserver(this); + } + } + }); + final int selectedItemId = binding.bottomNavView.getSelectedItemId(); + if (selectedItemId != R.navigation.direct_messages_nav_graph) { + setBottomNavSelectedItem(R.navigation.direct_messages_nav_graph); + } + } + private void handleUrl(final String url) { if (url == null) return; // Log.d(TAG, url); 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 DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(@NonNull final Media oldItem, @NonNull final Media newItem) { - return oldItem.getPk().equals(newItem.getPk()); + return Objects.equals(oldItem.getPk(), newItem.getPk()); } @Override public boolean areContentsTheSame(@NonNull final Media oldItem, @NonNull final Media newItem) { final Caption oldItemCaption = oldItem.getCaption(); final Caption newItemCaption = newItem.getCaption(); - return oldItem.getPk().equals(newItem.getPk()) && Objects.equals(getCaptionText(oldItemCaption), getCaptionText(newItemCaption)); + return Objects.equals(oldItem.getPk(), newItem.getPk()) + && Objects.equals(getCaptionText(oldItemCaption), getCaptionText(newItemCaption)); } private String getCaptionText(final Caption caption) { diff --git a/app/src/main/java/awais/instagrabber/adapters/GifItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/GifItemsAdapter.java new file mode 100644 index 00000000..83a6f62c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/GifItemsAdapter.java @@ -0,0 +1,107 @@ +package awais.instagrabber.adapters; + +import android.net.Uri; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder; +import com.facebook.drawee.controller.BaseControllerListener; +import com.facebook.drawee.drawable.ScalingUtils; +import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; +import com.facebook.imagepipeline.common.ResizeOptions; +import com.facebook.imagepipeline.image.ImageInfo; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; + +import java.util.Objects; + +import awais.instagrabber.databinding.ItemMediaBinding; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; +import awais.instagrabber.utils.Utils; + +public class GifItemsAdapter extends ListAdapter { + + private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull final GiphyGif oldItem, @NonNull final GiphyGif newItem) { + return Objects.equals(oldItem.getId(), newItem.getId()); + } + + @Override + public boolean areContentsTheSame(@NonNull final GiphyGif oldItem, @NonNull final GiphyGif newItem) { + return Objects.equals(oldItem.getId(), newItem.getId()); + } + }; + + private final OnItemClickListener onItemClickListener; + + public GifItemsAdapter(final OnItemClickListener onItemClickListener) { + super(diffCallback); + this.onItemClickListener = onItemClickListener; + } + + @NonNull + @Override + public GifViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + final ItemMediaBinding binding = ItemMediaBinding.inflate(layoutInflater, parent, false); + return new GifViewHolder(binding, onItemClickListener); + } + + @Override + public void onBindViewHolder(@NonNull final GifViewHolder holder, final int position) { + holder.bind(getItem(position)); + } + + public static class GifViewHolder extends RecyclerView.ViewHolder { + private static final String TAG = GifViewHolder.class.getSimpleName(); + private static final int size = Utils.displayMetrics.widthPixels / 3; + + private final ItemMediaBinding binding; + private final OnItemClickListener onItemClickListener; + + public GifViewHolder(@NonNull final ItemMediaBinding binding, + final OnItemClickListener onItemClickListener) { + super(binding.getRoot()); + this.binding = binding; + this.onItemClickListener = onItemClickListener; + binding.duration.setVisibility(View.GONE); + final GenericDraweeHierarchyBuilder builder = new GenericDraweeHierarchyBuilder(itemView.getResources()); + builder.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER); + binding.item.setHierarchy(builder.build()); + } + + public void bind(final GiphyGif item) { + if (onItemClickListener != null) { + itemView.setOnClickListener(v -> onItemClickListener.onItemClick(item)); + } + final BaseControllerListener controllerListener = new BaseControllerListener() { + @Override + public void onFailure(final String id, final Throwable throwable) { + Log.e(TAG, "onFailure: ", throwable); + } + }; + final ImageRequest request = ImageRequestBuilder + .newBuilderWithSource(Uri.parse(item.getImages().getFixedHeight().getWebp())) + .setResizeOptions(ResizeOptions.forDimensions(size, size)) + .build(); + final PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder() + .setImageRequest(request) + .setAutoPlayAnimations(true) + .setControllerListener(controllerListener); + binding.item.setController(builder.build()); + } + } + + public interface OnItemClickListener { + void onItemClick(GiphyGif giphyGif); + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java index e3e7f281..86b0d35b 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/TopicClusterViewHolder.java @@ -81,16 +81,18 @@ public class TopicClusterViewHolder extends RecyclerView.ViewHolder { } if (bitmap != null) { Palette.from(bitmap).generate(p -> { - final Palette.Swatch swatch = p.getDominantSwatch(); final Resources resources = itemView.getResources(); int titleTextColor = resources.getColor(R.color.white); - if (swatch != null) { - backgroundColor.set(swatch.getRgb()); - GradientDrawable gd = new GradientDrawable( - GradientDrawable.Orientation.TOP_BOTTOM, - new int[]{Color.TRANSPARENT, backgroundColor.get()}); - titleTextColor = swatch.getTitleTextColor(); - binding.background.setBackground(gd); + if (p != null) { + final Palette.Swatch swatch = p.getDominantSwatch(); + if (swatch != null) { + backgroundColor.set(swatch.getRgb()); + GradientDrawable gd = new GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, + new int[]{Color.TRANSPARENT, backgroundColor.get()}); + titleTextColor = swatch.getTitleTextColor(); + binding.background.setBackground(gd); + } } titleColor.set(titleTextColor); binding.title.setTextColor(titleTextColor); @@ -127,8 +129,8 @@ public class TopicClusterViewHolder extends RecyclerView.ViewHolder { // binding.title.setTransitionName("title-" + topicCluster.getId()); binding.cover.setTransitionName("cover-" + topicCluster.getId()); final String thumbUrl = ResponseBodyUtils.getThumbUrl(topicCluster.getCoverMedias() == null - ? topicCluster.getCoverMedia() - : topicCluster.getCoverMedias().get(0)); + ? topicCluster.getCoverMedia() + : topicCluster.getCoverMedias().get(0)); if (thumbUrl == null) { binding.cover.setImageURI((String) null); } else { diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java index dfecaa0c..f6b0f679 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java @@ -12,26 +12,17 @@ import androidx.recyclerview.widget.RecyclerView; import com.facebook.drawee.view.SimpleDraweeView; import com.google.common.collect.ImmutableList; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import awais.instagrabber.R; import awais.instagrabber.adapters.DirectMessageInboxAdapter.OnItemClickListener; import awais.instagrabber.databinding.LayoutDmInboxItemBinding; -import awais.instagrabber.models.enums.DirectItemType; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.directmessages.DirectItem; -import awais.instagrabber.repositories.responses.directmessages.DirectItemReelShare; -import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThreadDirectStory; -import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; -import awais.instagrabber.repositories.responses.directmessages.RavenExpiringMediaActionSummary; -import awais.instagrabber.utils.ResponseBodyUtils; +import awais.instagrabber.utils.DMUtils; import awais.instagrabber.utils.TextUtils; public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder { @@ -133,218 +124,17 @@ public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder { if (directStory != null && !directStory.getItems().isEmpty()) { final DirectItem item = directStory.getItems().get(0); final MediaItemType mediaType = item.getVisualMedia().getMedia().getMediaType(); - final String username = getUsername(thread.getUsers(), item.getUserId(), viewerId, resources); - final String subtitle = getMediaSpecificSubtitle(username, resources, mediaType); + final String username = DMUtils.getUsername(thread.getUsers(), item.getUserId(), viewerId, resources); + final String subtitle = DMUtils.getMediaSpecificSubtitle(username, resources, mediaType); binding.subtitle.setText(subtitle); return; } final DirectItem item = thread.getFirstDirectItem(); if (item == null) return; - final long senderId = item.getUserId(); - final DirectItemType itemType = item.getItemType(); - String subtitle = null; - final String username = getUsername(thread.getUsers(), senderId, viewerId, resources); - String message = ""; - if (itemType == null) { - message = resources.getString(R.string.dms_inbox_raven_message_unknown); - } else { - switch (itemType) { - case TEXT: - message = item.getText(); - break; - case LIKE: - message = item.getLike(); - break; - case LINK: - message = item.getLink().getText(); - break; - case PLACEHOLDER: - message = item.getPlaceholder().getMessage(); - break; - case MEDIA_SHARE: - subtitle = resources.getString(R.string.dms_inbox_shared_post, username != null ? username : "", item.getMediaShare().getUser().getUsername()); - break; - case ANIMATED_MEDIA: - subtitle = resources.getString(R.string.dms_inbox_shared_gif, username != null ? username : ""); - break; - case PROFILE: - subtitle = resources.getString(R.string.dms_inbox_shared_profile, username != null ? username : "", item.getProfile().getUsername()); - break; - case LOCATION: - subtitle = resources.getString(R.string.dms_inbox_shared_location, username != null ? username : "", item.getLocation().getName()); - break; - case MEDIA: { - final MediaItemType mediaType = item.getMedia().getMediaType(); - subtitle = getMediaSpecificSubtitle(username, resources, mediaType); - break; - } - case STORY_SHARE: { - final String reelType = item.getStoryShare().getReelType(); - if (reelType == null) { - subtitle = item.getStoryShare().getTitle(); - } else { - final int format = reelType.equals("highlight_reel") - ? R.string.dms_inbox_shared_highlight - : R.string.dms_inbox_shared_story; - subtitle = resources.getString(format, username != null ? username : "", - item.getStoryShare().getMedia().getUser().getUsername()); - } - break; - } - case VOICE_MEDIA: - subtitle = resources.getString(R.string.dms_inbox_shared_voice, username != null ? username : ""); - break; - case ACTION_LOG: - subtitle = item.getActionLog().getDescription(); - break; - case VIDEO_CALL_EVENT: - subtitle = item.getVideoCallEvent().getDescription(); - break; - case CLIP: - subtitle = resources.getString(R.string.dms_inbox_shared_clip, username != null ? username : "", - item.getClip().getClip().getUser().getUsername()); - break; - case FELIX_SHARE: - subtitle = resources.getString(R.string.dms_inbox_shared_igtv, username != null ? username : "", - item.getFelixShare().getVideo().getUser().getUsername()); - break; - case RAVEN_MEDIA: - subtitle = getRavenMediaSubtitle(item, resources, username); - break; - case REEL_SHARE: - final DirectItemReelShare reelShare = item.getReelShare(); - if (reelShare == null) { - subtitle = ""; - break; - } - final String reelType = reelShare.getType(); - switch (reelType) { - case "reply": - if (viewerId == item.getUserId()) { - subtitle = resources.getString(R.string.dms_inbox_replied_story_outgoing, reelShare.getText()); - } else { - subtitle = resources.getString(R.string.dms_inbox_replied_story_incoming, username != null ? username : "", reelShare.getText()); - } - break; - case "mention": - if (viewerId == item.getUserId()) { - // You mentioned the other person - final long mentionedUserId = item.getReelShare().getMentionedUserId(); - final String otherUsername = getUsername(thread.getUsers(), mentionedUserId, viewerId, resources); - subtitle = resources.getString(R.string.dms_inbox_mentioned_story_outgoing, otherUsername); - } else { - // They mentioned you - subtitle = resources.getString(R.string.dms_inbox_mentioned_story_incoming, username != null ? username : ""); - } - break; - case "reaction": - if (viewerId == item.getUserId()) { - subtitle = resources.getString(R.string.dms_inbox_reacted_story_outgoing, reelShare.getText()); - } else { - subtitle = resources.getString(R.string.dms_inbox_reacted_story_incoming, username != null ? username : "", reelShare.getText()); - } - break; - default: - subtitle = ""; - break; - } - break; - default: - message = resources.getString(R.string.dms_inbox_raven_message_unknown); - } - } - if (subtitle == null) { - if (thread.getUsers().size() > 1 - || (thread.getUsers().size() == 1 && senderId == viewerId)) { - subtitle = String.format("%s: %s", username != null ? username : "", message); - } else { - subtitle = message; - } - } + final String subtitle = DMUtils.getMessageString(thread, resources, viewerId, item); binding.subtitle.setText(subtitle != null ? subtitle : ""); } - private String getMediaSpecificSubtitle(final String username, final Resources resources, final MediaItemType mediaType) { - final String userSharedAnImage = resources.getString(R.string.dms_inbox_shared_image, username != null ? username : ""); - final String userSharedAVideo = resources.getString(R.string.dms_inbox_shared_video, username != null ? username : ""); - final String userSentAMessage = resources.getString(R.string.dms_inbox_shared_message, username != null ? username : ""); - String subtitle; - switch (mediaType) { - case MEDIA_TYPE_IMAGE: - subtitle = userSharedAnImage; - break; - case MEDIA_TYPE_VIDEO: - subtitle = userSharedAVideo; - break; - default: - subtitle = userSentAMessage; - break; - } - return subtitle; - } - - private String getRavenMediaSubtitle(final DirectItem item, - final Resources resources, - final String username) { - String subtitle = "↗ "; - final DirectItemVisualMedia visualMedia = item.getVisualMedia(); - final RavenExpiringMediaActionSummary summary = visualMedia.getExpiringMediaActionSummary(); - if (summary != null) { - final RavenExpiringMediaActionSummary.ActionType expiringMediaType = summary.getType(); - int textRes = 0; - switch (expiringMediaType) { - case DELIVERED: - textRes = R.string.dms_inbox_raven_media_delivered; - break; - case SENT: - textRes = R.string.dms_inbox_raven_media_sent; - break; - case OPENED: - textRes = R.string.dms_inbox_raven_media_opened; - break; - case REPLAYED: - textRes = R.string.dms_inbox_raven_media_replayed; - break; - case SENDING: - textRes = R.string.dms_inbox_raven_media_sending; - break; - case BLOCKED: - textRes = R.string.dms_inbox_raven_media_blocked; - break; - case SUGGESTED: - textRes = R.string.dms_inbox_raven_media_suggested; - break; - case SCREENSHOT: - textRes = R.string.dms_inbox_raven_media_screenshot; - break; - case CANNOT_DELIVER: - textRes = R.string.dms_inbox_raven_media_cant_deliver; - break; - } - if (textRes > 0) { - subtitle += itemView.getContext().getString(textRes); - } - return subtitle; - } - final MediaItemType mediaType = visualMedia.getMedia().getMediaType(); - subtitle = getMediaSpecificSubtitle(username, resources, mediaType); - return subtitle; - } - - private String getUsername(final List users, - final long userId, - final long viewerId, - final Resources resources) { - if (userId == viewerId) { - return resources.getString(R.string.you); - } - final Optional senderOptional = users.stream() - .filter(Objects::nonNull) - .filter(user -> user.getPk() == userId) - .findFirst(); - return senderOptional.map(User::getUsername).orElse(null); - } - private void setDateTime(@NonNull final DirectItem item) { final long timestamp = item.getTimestamp() / 1000; final String dateTimeString = TextUtils.getRelativeDateTimeString(itemView.getContext(), timestamp); @@ -352,9 +142,7 @@ public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder { } private void setReadState(@NonNull final DirectThread thread) { - final DirectItem item = thread.getItems().get(0); - final Map lastSeenAtMap = thread.getLastSeenAt(); - final boolean read = ResponseBodyUtils.isRead(item, lastSeenAtMap, Collections.singletonList(thread.getViewerId()), thread.getDirectStory()); + final boolean read = DMUtils.isRead(thread); binding.unread.setVisibility(read ? View.GONE : View.VISIBLE); binding.threadTitle.setTypeface(null, read ? Typeface.NORMAL : Typeface.BOLD); binding.subtitle.setTypeface(null, 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 571568bb..8271c38c 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 @@ -45,6 +45,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiR import awais.instagrabber.repositories.responses.directmessages.DirectItemReactions; import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare; import awais.instagrabber.repositories.responses.directmessages.DirectThread; +import awais.instagrabber.utils.DMUtils; import awais.instagrabber.utils.DeepLinkParser; import awais.instagrabber.utils.ResponseBodyUtils; @@ -196,7 +197,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 = DMUtils.isRead(item, + thread.getLastSeenAt(), + userIds + ); binding.deliveryStatus.setImageResource(R.drawable.ic_check_all_24); ImageViewCompat.setImageTintList( binding.deliveryStatus, @@ -321,8 +325,8 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple final String repliedToUsername = user != null ? user.getUsername() : ""; if (item.getUserId() == currentUser.getPk()) { return thread.isGroup() - ? resources.getString(R.string.replied_you_group, repliedToUsername) - : resources.getString(R.string.replied_you); + ? resources.getString(R.string.replied_you_group, repliedToUsername) + : resources.getString(R.string.replied_you); } if (repliedToUserId == currentUser.getPk()) { return resources.getString(R.string.replied_to_you); @@ -361,7 +365,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/asyncs/CreateThreadAction.java b/app/src/main/java/awais/instagrabber/asyncs/CreateThreadAction.java index ba35ef7e..384b3bbd 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/CreateThreadAction.java +++ b/app/src/main/java/awais/instagrabber/asyncs/CreateThreadAction.java @@ -5,7 +5,6 @@ import android.util.Log; import androidx.annotation.NonNull; -import java.io.DataOutputStream; import java.io.IOException; import java.util.Collections; import java.util.Locale; @@ -13,15 +12,12 @@ import java.util.Locale; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.NetworkUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.webservices.DirectMessagesService; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; -import static awais.instagrabber.utils.Utils.settingsHelper; - public class CreateThreadAction extends AsyncTask { private static final String TAG = "CommentAction"; diff --git a/app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java index 5da3b5aa..612259bd 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/ProfileFetcher.java @@ -5,9 +5,6 @@ import android.util.Log; import androidx.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; - import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.repositories.responses.FriendshipStatus; import awais.instagrabber.repositories.responses.User; diff --git a/app/src/main/java/awais/instagrabber/db/AppDatabase.java b/app/src/main/java/awais/instagrabber/db/AppDatabase.java index 11547601..63e2ce43 100644 --- a/app/src/main/java/awais/instagrabber/db/AppDatabase.java +++ b/app/src/main/java/awais/instagrabber/db/AppDatabase.java @@ -20,14 +20,16 @@ import java.util.Date; import java.util.List; import awais.instagrabber.db.dao.AccountDao; +import awais.instagrabber.db.dao.DMLastNotifiedDao; import awais.instagrabber.db.dao.FavoriteDao; import awais.instagrabber.db.entities.Account; +import awais.instagrabber.db.entities.DMLastNotified; import awais.instagrabber.db.entities.Favorite; import awais.instagrabber.models.enums.FavoriteType; import awais.instagrabber.utils.Utils; -@Database(entities = {Account.class, Favorite.class}, - version = 4) +@Database(entities = {Account.class, Favorite.class, DMLastNotified.class}, + version = 5) @TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { private static final String TAG = AppDatabase.class.getSimpleName(); @@ -38,12 +40,14 @@ public abstract class AppDatabase extends RoomDatabase { public abstract FavoriteDao favoriteDao(); + public abstract DMLastNotifiedDao dmLastNotifiedDao(); + public static AppDatabase getDatabase(final Context context) { if (INSTANCE == null) { synchronized (AppDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "cookiebox.db") - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) .build(); } } @@ -140,6 +144,18 @@ public abstract class AppDatabase extends RoomDatabase { } }; + static final Migration MIGRATION_4_5 = new Migration(4, 5) { + @Override + public void migrate(@NonNull final SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `dm_last_notified` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`thread_id` TEXT, " + + "`last_notified_msg_ts` INTEGER, " + + "`last_notified_at` INTEGER)"); + database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_last_notified_thread_id` ON `dm_last_notified` (`thread_id`)"); + } + }; + @NonNull private static List backupOldFavorites(@NonNull final SupportSQLiteDatabase db) { // check if old favorites table had the column query_display diff --git a/app/src/main/java/awais/instagrabber/db/Converters.java b/app/src/main/java/awais/instagrabber/db/Converters.java index 50252755..de10e3fc 100644 --- a/app/src/main/java/awais/instagrabber/db/Converters.java +++ b/app/src/main/java/awais/instagrabber/db/Converters.java @@ -2,6 +2,10 @@ package awais.instagrabber.db; import androidx.room.TypeConverter; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.Date; import awais.instagrabber.models.enums.FavoriteType; @@ -30,4 +34,16 @@ public class Converters { public static String favoriteTypeToString(FavoriteType favoriteType) { return favoriteType == null ? null : favoriteType.toString(); } + + @TypeConverter + public static LocalDateTime fromTimestampToLocalDateTime(Long value) { + if (value == null) return null; + return LocalDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneOffset.systemDefault()); + } + + @TypeConverter + public static Long localDateTimeToTimestamp(LocalDateTime localDateTime) { + if (localDateTime == null) return null; + return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } } diff --git a/app/src/main/java/awais/instagrabber/db/dao/DMLastNotifiedDao.java b/app/src/main/java/awais/instagrabber/db/dao/DMLastNotifiedDao.java new file mode 100644 index 00000000..ddd6968c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/dao/DMLastNotifiedDao.java @@ -0,0 +1,34 @@ +package awais.instagrabber.db.dao; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; +import androidx.room.Update; + +import java.util.List; + +import awais.instagrabber.db.entities.DMLastNotified; + +@Dao +public interface DMLastNotifiedDao { + + @Query("SELECT * FROM dm_last_notified") + List getAllDMDmLastNotified(); + + @Query("SELECT * FROM dm_last_notified WHERE thread_id = :threadId") + DMLastNotified findDMLastNotifiedByThreadId(String threadId); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + List insertDMLastNotified(DMLastNotified... dmLastNotified); + + @Update + void updateDMLastNotified(DMLastNotified... dmLastNotified); + + @Delete + void deleteDMLastNotified(DMLastNotified... dmLastNotified); + + @Query("DELETE from dm_last_notified") + void deleteAllDMLastNotified(); +} diff --git a/app/src/main/java/awais/instagrabber/db/datasources/DMLastNotifiedDataSource.java b/app/src/main/java/awais/instagrabber/db/datasources/DMLastNotifiedDataSource.java new file mode 100644 index 00000000..70a9a171 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/datasources/DMLastNotifiedDataSource.java @@ -0,0 +1,70 @@ +package awais.instagrabber.db.datasources; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.time.LocalDateTime; +import java.util.List; + +import awais.instagrabber.db.AppDatabase; +import awais.instagrabber.db.dao.DMLastNotifiedDao; +import awais.instagrabber.db.entities.DMLastNotified; + +public class DMLastNotifiedDataSource { + private static final String TAG = DMLastNotifiedDataSource.class.getSimpleName(); + + private static DMLastNotifiedDataSource INSTANCE; + + private final DMLastNotifiedDao dmLastNotifiedDao; + + private DMLastNotifiedDataSource(final DMLastNotifiedDao dmLastNotifiedDao) { + this.dmLastNotifiedDao = dmLastNotifiedDao; + } + + public static DMLastNotifiedDataSource getInstance(@NonNull Context context) { + if (INSTANCE == null) { + synchronized (DMLastNotifiedDataSource.class) { + if (INSTANCE == null) { + final AppDatabase database = AppDatabase.getDatabase(context); + INSTANCE = new DMLastNotifiedDataSource(database.dmLastNotifiedDao()); + } + } + } + return INSTANCE; + } + + @Nullable + public final DMLastNotified getDMLastNotified(final String threadId) { + return dmLastNotifiedDao.findDMLastNotifiedByThreadId(threadId); + } + + @NonNull + public final List getAllDMDmLastNotified() { + return dmLastNotifiedDao.getAllDMDmLastNotified(); + } + + public final void insertOrUpdateDMLastNotified(final String threadId, + final LocalDateTime lastNotifiedMsgTs, + final LocalDateTime lastNotifiedAt) { + final DMLastNotified dmLastNotified = getDMLastNotified(threadId); + final DMLastNotified toUpdate = new DMLastNotified(dmLastNotified == null ? 0 : dmLastNotified.getId(), + threadId, + lastNotifiedMsgTs, + lastNotifiedAt); + if (dmLastNotified != null) { + dmLastNotifiedDao.updateDMLastNotified(toUpdate); + return; + } + dmLastNotifiedDao.insertDMLastNotified(toUpdate); + } + + public final void deleteDMLastNotified(@NonNull final DMLastNotified dmLastNotified) { + dmLastNotifiedDao.deleteDMLastNotified(dmLastNotified); + } + + public final void deleteAllDMLastNotified() { + dmLastNotifiedDao.deleteAllDMLastNotified(); + } +} diff --git a/app/src/main/java/awais/instagrabber/db/entities/DMLastNotified.java b/app/src/main/java/awais/instagrabber/db/entities/DMLastNotified.java new file mode 100644 index 00000000..c0ae776f --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/entities/DMLastNotified.java @@ -0,0 +1,85 @@ +package awais.instagrabber.db.entities; + +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity(tableName = DMLastNotified.TABLE_NAME, indices = {@Index(value = DMLastNotified.COL_THREAD_ID, unique = true)}) +public class DMLastNotified { + public final static String TABLE_NAME = "dm_last_notified"; + public final static String COL_ID = "id"; + public final static String COL_THREAD_ID = "thread_id"; + public final static String COL_LAST_NOTIFIED_MSG_TS = "last_notified_msg_ts"; + public final static String COL_LAST_NOTIFIED_AT = "last_notified_at"; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = COL_ID) + private final int id; + + @ColumnInfo(name = COL_THREAD_ID) + private final String threadId; + + @ColumnInfo(name = COL_LAST_NOTIFIED_MSG_TS) + private final LocalDateTime lastNotifiedMsgTs; + + @ColumnInfo(name = COL_LAST_NOTIFIED_AT) + private final LocalDateTime lastNotifiedAt; + + public DMLastNotified(final int id, + final String threadId, + final LocalDateTime lastNotifiedMsgTs, + final LocalDateTime lastNotifiedAt) { + this.id = id; + this.threadId = threadId; + this.lastNotifiedMsgTs = lastNotifiedMsgTs; + this.lastNotifiedAt = lastNotifiedAt; + } + + public int getId() { + return id; + } + + public String getThreadId() { + return threadId; + } + + public LocalDateTime getLastNotifiedMsgTs() { + return lastNotifiedMsgTs; + } + + public LocalDateTime getLastNotifiedAt() { + return lastNotifiedAt; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DMLastNotified that = (DMLastNotified) o; + return id == that.id && + Objects.equals(threadId, that.threadId) && + Objects.equals(lastNotifiedMsgTs, that.lastNotifiedMsgTs) && + Objects.equals(lastNotifiedAt, that.lastNotifiedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, threadId, lastNotifiedMsgTs, lastNotifiedAt); + } + + @NonNull + @Override + public String toString() { + return "DMLastNotified{" + + "id=" + id + + ", threadId='" + threadId + '\'' + + ", lastNotifiedMsgTs='" + lastNotifiedMsgTs + '\'' + + ", lastNotifiedAt='" + lastNotifiedAt + '\'' + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/db/repositories/DMLastNotifiedRepository.java b/app/src/main/java/awais/instagrabber/db/repositories/DMLastNotifiedRepository.java new file mode 100644 index 00000000..9cb15291 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/db/repositories/DMLastNotifiedRepository.java @@ -0,0 +1,126 @@ +package awais.instagrabber.db.repositories; + +import java.time.LocalDateTime; +import java.util.List; + +import awais.instagrabber.db.datasources.DMLastNotifiedDataSource; +import awais.instagrabber.db.entities.DMLastNotified; +import awais.instagrabber.utils.AppExecutors; + +public class DMLastNotifiedRepository { + private static final String TAG = DMLastNotifiedRepository.class.getSimpleName(); + + private static DMLastNotifiedRepository instance; + + private final AppExecutors appExecutors; + private final DMLastNotifiedDataSource dmLastNotifiedDataSource; + + private DMLastNotifiedRepository(final AppExecutors appExecutors, final DMLastNotifiedDataSource dmLastNotifiedDataSource) { + this.appExecutors = appExecutors; + this.dmLastNotifiedDataSource = dmLastNotifiedDataSource; + } + + public static DMLastNotifiedRepository getInstance(final DMLastNotifiedDataSource dmLastNotifiedDataSource) { + if (instance == null) { + instance = new DMLastNotifiedRepository(AppExecutors.getInstance(), dmLastNotifiedDataSource); + } + return instance; + } + + public void getDMLastNotified(final String threadId, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final DMLastNotified dmLastNotified = dmLastNotifiedDataSource.getDMLastNotified(threadId); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (dmLastNotified == null) { + callback.onDataNotAvailable(); + return; + } + callback.onSuccess(dmLastNotified); + }); + }); + } + + public void getAllDMDmLastNotified(final RepositoryCallback> callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + final List allDMDmLastNotified = dmLastNotifiedDataSource.getAllDMDmLastNotified(); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (allDMDmLastNotified == null) { + callback.onDataNotAvailable(); + return; + } + // cachedAccounts = accounts; + callback.onSuccess(allDMDmLastNotified); + }); + }); + } + + public void insertOrUpdateDMLastNotified(final List dmLastNotifiedList, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + for (final DMLastNotified dmLastNotified : dmLastNotifiedList) { + dmLastNotifiedDataSource.insertOrUpdateDMLastNotified(dmLastNotified.getThreadId(), + dmLastNotified.getLastNotifiedMsgTs(), + dmLastNotified.getLastNotifiedAt()); + } + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + + public void insertOrUpdateDMLastNotified(final String threadId, + final LocalDateTime lastNotifiedMsgTs, + final LocalDateTime lastNotifiedAt, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + dmLastNotifiedDataSource.insertOrUpdateDMLastNotified(threadId, lastNotifiedMsgTs, lastNotifiedAt); + final DMLastNotified updated = dmLastNotifiedDataSource.getDMLastNotified(threadId); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + if (updated == null) { + callback.onDataNotAvailable(); + return; + } + callback.onSuccess(updated); + }); + }); + } + + public void deleteDMLastNotified(final DMLastNotified dmLastNotified, + final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + dmLastNotifiedDataSource.deleteDMLastNotified(dmLastNotified); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + + public void deleteAllDMLastNotified(final RepositoryCallback callback) { + // request on the I/O thread + appExecutors.diskIO().execute(() -> { + dmLastNotifiedDataSource.deleteAllDMLastNotified(); + // notify on the main thread + appExecutors.mainThread().execute(() -> { + if (callback == null) return; + callback.onSuccess(null); + }); + }); + } + +} diff --git a/app/src/main/java/awais/instagrabber/dialogs/GifPickerBottomDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/GifPickerBottomDialogFragment.java new file mode 100644 index 00000000..a7ab61e4 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/dialogs/GifPickerBottomDialogFragment.java @@ -0,0 +1,150 @@ +package awais.instagrabber.dialogs; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; + +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.snackbar.Snackbar; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.GifItemsAdapter; +import awais.instagrabber.customviews.helpers.TextWatcherAdapter; +import awais.instagrabber.databinding.LayoutGifPickerBinding; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; +import awais.instagrabber.utils.Debouncer; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.viewmodels.GifPickerViewModel; + +public class GifPickerBottomDialogFragment extends BottomSheetDialogFragment { + private static final String TAG = GifPickerBottomDialogFragment.class.getSimpleName(); + private static final int INPUT_DEBOUNCE_INTERVAL = 500; + private static final String INPUT_KEY = "gif_search_input"; + + private LayoutGifPickerBinding binding; + private GifPickerViewModel viewModel; + private GifItemsAdapter gifItemsAdapter; + private OnSelectListener onSelectListener; + private Debouncer inputDebouncer; + + public static GifPickerBottomDialogFragment newInstance() { + final Bundle args = new Bundle(); + final GifPickerBottomDialogFragment fragment = new GifPickerBottomDialogFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(DialogFragment.STYLE_NORMAL, R.style.ThemeOverlay_Rounded_BottomSheetDialog); + final Debouncer.Callback callback = new Debouncer.Callback() { + @Override + public void call(final String key) { + final Editable text = binding.input.getText(); + if (TextUtils.isEmpty(text)) { + viewModel.search(null); + return; + } + viewModel.search(text.toString().trim()); + } + + @Override + public void onError(final Throwable t) { + Log.e(TAG, "onError: ", t); + } + }; + inputDebouncer = new Debouncer<>(callback, INPUT_DEBOUNCE_INTERVAL); + } + + @Nullable + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + binding = LayoutGifPickerBinding.inflate(inflater, container, false); + viewModel = new ViewModelProvider(this).get(GifPickerViewModel.class); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + init(); + } + + @Override + public void onStart() { + super.onStart(); + final Dialog dialog = getDialog(); + if (dialog == null) return; + final BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialog; + final View bottomSheetInternal = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet); + if (bottomSheetInternal == null) return; + bottomSheetInternal.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + bottomSheetInternal.requestLayout(); + } + + private void init() { + setupList(); + setupInput(); + setupObservers(); + } + + private void setupList() { + final Context context = getContext(); + if (context == null) return; + binding.gifList.setLayoutManager(new GridLayoutManager(context, 3)); + binding.gifList.setHasFixedSize(true); + gifItemsAdapter = new GifItemsAdapter(entry -> { + if (onSelectListener == null) return; + onSelectListener.onSelect(entry); + }); + binding.gifList.setAdapter(gifItemsAdapter); + } + + private void setupInput() { + binding.input.addTextChangedListener(new TextWatcherAdapter() { + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + inputDebouncer.call(INPUT_KEY); + } + }); + } + + private void setupObservers() { + viewModel.getImages().observe(getViewLifecycleOwner(), imagesResource -> { + if (imagesResource == null) return; + switch (imagesResource.status) { + case SUCCESS: + gifItemsAdapter.submitList(imagesResource.data); + break; + case ERROR: + final Context context = getContext(); + if (context != null && imagesResource.message != null) { + Snackbar.make(context, binding.getRoot(), imagesResource.message, Snackbar.LENGTH_LONG); + } + break; + case LOADING: + break; + } + }); + } + + public void setOnSelectListener(final OnSelectListener onSelectListener) { + this.onSelectListener = onSelectListener; + } + + public interface OnSelectListener { + void onSelect(GiphyGif giphyGif); + } +} diff --git a/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java index 09862f49..affe5cba 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java @@ -41,20 +41,26 @@ import static awais.instagrabber.utils.Utils.settingsHelper; public class ProfilePicDialogFragment extends DialogFragment { private static final String TAG = "ProfilePicDlgFragment"; - private final long id; - private final String name; - private final String fallbackUrl; + private long id; + private String name; + private String fallbackUrl; private boolean isLoggedIn; private DialogProfilepicBinding binding; private String url; - public ProfilePicDialogFragment(final long id, final String name, final String fallbackUrl) { - this.id = id; - this.name = name; - this.fallbackUrl = fallbackUrl; + public static ProfilePicDialogFragment getInstance(final long id, final String name, final String fallbackUrl) { + final Bundle args = new Bundle(); + args.putLong("id", id); + args.putString("name", name); + args.putString("fallbackUrl", fallbackUrl); + final ProfilePicDialogFragment fragment = new ProfilePicDialogFragment(); + fragment.setArguments(args); + return fragment; } + public ProfilePicDialogFragment() {} + @Override public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, @@ -94,6 +100,14 @@ public class ProfilePicDialogFragment extends DialogFragment { } private void init() { + final Bundle arguments = getArguments(); + if (arguments == null) { + dismiss(); + return; + } + id = arguments.getLong("id"); + name = arguments.getString("name"); + fallbackUrl = arguments.getString("fallbackUrl"); binding.download.setOnClickListener(v -> { final Context context = getContext(); if (context == null) return; @@ -127,11 +141,12 @@ public class ProfilePicDialogFragment extends DialogFragment { @Override public void onFailure(final Throwable t) { final Context context = getContext(); - try { - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + if (context == null) { + dismiss(); + return; } - catch(final Throwable e) {} - getDialog().dismiss(); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + dismiss(); } }); } else setupPhoto(fallbackUrl); diff --git a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java index ee2852c8..d21e750e 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java @@ -416,6 +416,8 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR // binding.locationBiography.setCaptionIsExpandable(true); // binding.locationBiography.setCaptionIsExpanded(true); + final Context context = getContext(); + if (context == null) return; if (TextUtils.isEmpty(biography)) { locationDetailsBinding.locationBiography.setVisibility(View.GONE); } else { @@ -432,13 +434,13 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR final String originalText = autoLinkItem.getOriginalText().trim(); navigateToProfile(originalText); }); - locationDetailsBinding.locationBiography.addOnEmailClickListener(autoLinkItem -> Utils.openEmailAddress(getContext(), + locationDetailsBinding.locationBiography.addOnEmailClickListener(autoLinkItem -> Utils.openEmailAddress(context, autoLinkItem.getOriginalText() .trim())); locationDetailsBinding.locationBiography - .addOnURLClickListener(autoLinkItem -> Utils.openURL(getContext(), autoLinkItem.getOriginalText().trim())); + .addOnURLClickListener(autoLinkItem -> Utils.openURL(context, autoLinkItem.getOriginalText().trim())); locationDetailsBinding.locationBiography.setOnLongClickListener(v -> { - Utils.copyText(getContext(), biography); + Utils.copyText(context, biography); return true; }); } @@ -465,7 +467,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR locationDetailsBinding.locationUrl.setVisibility(View.VISIBLE); locationDetailsBinding.locationUrl.setText(TextUtils.getSpannableUrl(url)); } - final FavoriteDataSource dataSource = FavoriteDataSource.getInstance(getContext()); + final FavoriteDataSource dataSource = FavoriteDataSource.getInstance(context); final FavoriteRepository favoriteRepository = FavoriteRepository.getInstance(dataSource); locationDetailsBinding.favChip.setVisibility(View.VISIBLE); favoriteRepository.getFavorite(String.valueOf(locationId), FavoriteType.LOCATION, new RepositoryCallback() { 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 7fa55fea..4ce2ef40 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -30,14 +30,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.LiveData; 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 +56,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; @@ -83,6 +80,7 @@ import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCall import awais.instagrabber.customviews.helpers.TextWatcherAdapter; import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding; import awais.instagrabber.dialogs.DirectItemReactionDialogFragment; +import awais.instagrabber.dialogs.GifPickerBottomDialogFragment; import awais.instagrabber.dialogs.MediaPickerBottomDialogFragment; import awais.instagrabber.fragments.PostViewV2Fragment; import awais.instagrabber.fragments.UserSearchFragment; @@ -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; @@ -232,7 +229,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)); } @@ -292,13 +289,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); } @@ -329,7 +342,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact init(); binding.send.post(() -> initialSendX = binding.send.getX()); shouldRefresh = false; - setObservers(); } @Override @@ -403,6 +415,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact wasKbShowing = true; binding.emojiPicker.setAlpha(0); } + removeObservers(); super.onPause(); } @@ -420,7 +433,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } binding.send.stopScale(); setupBackStackResultObserver(); - attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue()); + setObservers(); + // attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue()); } @Override @@ -460,42 +474,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(); @@ -539,7 +549,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; @@ -553,7 +570,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; @@ -561,21 +579,34 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact hideInput(); } }); - viewModel.getThreadTitle().observe(getViewLifecycleOwner(), this::setTitle); - viewModel.getFetching().observe(getViewLifecycleOwner(), fetching -> { - if (fetching) { - setTitle(getString(R.string.dms_thread_updating)); - 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(getString(R.string.dms_thread_updating)); + 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); @@ -630,14 +661,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); @@ -666,9 +713,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; @@ -684,6 +737,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private void hideInput() { binding.emojiToggle.setVisibility(View.GONE); + binding.gif.setVisibility(View.GONE); binding.camera.setVisibility(View.GONE); binding.gallery.setVisibility(View.GONE); binding.input.setVisibility(View.GONE); @@ -697,6 +751,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private void showInput() { binding.emojiToggle.setVisibility(View.VISIBLE); + binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); binding.gallery.setVisibility(View.VISIBLE); binding.input.setVisibility(View.VISIBLE); @@ -735,16 +790,18 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact binding.send.setListenForRecord(true); startIconAnimation(); } - binding.gallery.setVisibility(View.VISIBLE); + binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); + binding.gallery.setVisibility(View.VISIBLE); return; } if (binding.send.isListenForRecord()) { binding.send.setListenForRecord(false); startIconAnimation(); } - binding.gallery.setVisibility(View.GONE); + binding.gif.setVisibility(View.GONE); binding.camera.setVisibility(View.GONE); + binding.gallery.setVisibility(View.GONE); } private String getDirectItemPreviewText(final DirectItem item) { @@ -835,12 +892,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); @@ -881,8 +941,9 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact public void onStart() { isRecording = true; binding.input.setHint(null); - binding.gallery.setVisibility(View.GONE); + binding.gif.setVisibility(View.GONE); binding.camera.setVisibility(View.GONE); + binding.gallery.setVisibility(View.GONE); if (PermissionUtils.hasAudioRecordPerms(context)) { viewModel.startRecording(); return; @@ -902,8 +963,9 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact public void onFinish(final long recordTime) { Log.d(TAG, "onFinish"); binding.input.setHint("Message"); - binding.gallery.setVisibility(View.VISIBLE); + binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); + binding.gallery.setVisibility(View.VISIBLE); viewModel.stopRecording(false); isRecording = false; } @@ -915,16 +977,18 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (PermissionUtils.hasAudioRecordPerms(context)) { tooltip.show(binding.send); } - binding.gallery.setVisibility(View.VISIBLE); + binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); + binding.gallery.setVisibility(View.VISIBLE); viewModel.stopRecording(true); isRecording = false; } }); binding.recordView.setOnBasketAnimationEndListener(() -> { binding.input.setHint(R.string.dms_thread_message_hint); - binding.gallery.setVisibility(View.VISIBLE); + binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); + binding.gallery.setVisibility(View.VISIBLE); }); binding.input.addTextChangedListener(new TextWatcherAdapter() { // int prevLength = 0; @@ -955,7 +1019,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); @@ -1001,6 +1065,16 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact mediaPicker.show(getChildFragmentManager(), "MediaPicker"); hideKeyboard(true); }); + binding.gif.setOnClickListener(v -> { + final GifPickerBottomDialogFragment gifPicker = GifPickerBottomDialogFragment.newInstance(); + gifPicker.setOnSelectListener(giphyGif -> { + gifPicker.dismiss(); + if (giphyGif == null) return; + handleSentMessage(viewModel.sendAnimatedMedia(giphyGif)); + }); + gifPicker.show(getChildFragmentManager(), "GifPicker"); + hideKeyboard(true); + }); binding.camera.setOnClickListener(v -> { final Intent intent = new Intent(context, CameraActivity.class); startActivityForResult(intent, CAMERA_REQUEST_CODE); @@ -1028,8 +1102,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) { @@ -1129,7 +1203,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private void setSendToMicIcon() { final Context context = getContext(); if (context == null) return; - final Drawable sendToMicDrawable = ContextCompat.getDrawable(context, R.drawable.avd_send_to_mic_anim); + final Drawable sendToMicDrawable = Utils.getAnimatableDrawable(context, R.drawable.avd_send_to_mic_anim); if (sendToMicDrawable instanceof Animatable) { AnimatedVectorDrawableCompat.registerAnimationCallback(sendToMicDrawable, sendToMicAnimationCallback); } @@ -1139,7 +1213,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private void setMicToSendIcon() { final Context context = getContext(); if (context == null) return; - final Drawable micToSendDrawable = ContextCompat.getDrawable(context, R.drawable.avd_mic_to_send_anim); + final Drawable micToSendDrawable = Utils.getAnimatableDrawable(context, R.drawable.avd_mic_to_send_anim); if (micToSendDrawable instanceof Animatable) { AnimatedVectorDrawableCompat.registerAnimationCallback(micToSendDrawable, micToSendAnimationCallback); } @@ -1350,6 +1424,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact ObjectAnimator.ofFloat(binding.inputBg, TRANSLATION_Y, -height), ObjectAnimator.ofFloat(binding.recordView, TRANSLATION_Y, -height), ObjectAnimator.ofFloat(binding.emojiToggle, TRANSLATION_Y, -height), + ObjectAnimator.ofFloat(binding.gif, TRANSLATION_Y, -height), ObjectAnimator.ofFloat(binding.gallery, TRANSLATION_Y, -height), ObjectAnimator.ofFloat(binding.camera, TRANSLATION_Y, -height), ObjectAnimator.ofFloat(binding.send, TRANSLATION_Y, -height), @@ -1377,10 +1452,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(); @@ -1407,7 +1478,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/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 7b0a1c35..e7e5c085 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -375,8 +375,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe if (profileModel != null) { restrictMenuItem.setVisible(!Objects.equals(profileModel.getPk(), CookieUtils.getUserIdFromCookie(cookie))); restrictMenuItem.setTitle(profileModel.getFriendshipStatus().isRestricted() ? R.string.unrestrict : R.string.restrict); - } - else { + } else { restrictMenuItem.setVisible(false); } } @@ -394,8 +393,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe if (profileModel != null) { mutePostsMenuItem.setVisible(!Objects.equals(profileModel.getPk(), CookieUtils.getUserIdFromCookie(cookie))); mutePostsMenuItem.setTitle(profileModel.getFriendshipStatus().isMuting() ? R.string.mute_posts : R.string.unmute_posts); - } - else { + } else { mutePostsMenuItem.setVisible(false); } } @@ -403,8 +401,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe if (chainingMenuItem != null) { if (profileModel != null) { chainingMenuItem.setVisible(!Objects.equals(profileModel.getPk(), CookieUtils.getUserIdFromCookie(cookie))); - } - else { + } else { chainingMenuItem.setVisible(false); } } @@ -524,7 +521,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onRefresh() { - profileDetailsBinding.countsBarrier.setVisibility(View.GONE); + profileDetailsBinding.countsBarrier.getRoot().setVisibility(View.GONE); profileDetailsBinding.mainProfileImage.setVisibility(View.INVISIBLE); fetchProfileDetails(); } @@ -641,11 +638,10 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe final Context context = getContext(); try { if (t == null) Toast.makeText(context, - isLoggedIn ? R.string.error_loading_profile_loggedin : R.string.error_loading_profile, - Toast.LENGTH_LONG).show(); + isLoggedIn ? R.string.error_loading_profile_loggedin : R.string.error_loading_profile, + Toast.LENGTH_LONG).show(); else Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } - catch(final Throwable e) {} + } catch (final Throwable e) {} } }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -742,7 +738,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe profileDetailsBinding.mainProfileImage.setImageURI(profileModel.getProfilePicUrl()); profileDetailsBinding.mainProfileImage.setVisibility(View.VISIBLE); - profileDetailsBinding.countsBarrier.setVisibility(View.VISIBLE); + profileDetailsBinding.countsBarrier.getRoot().setVisibility(View.VISIBLE); final long followersCount = profileModel.getFollowerCount(); final long followingCount = profileModel.getFollowingCount(); @@ -784,8 +780,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe final String biography = profileModel.getBiography(); if (TextUtils.isEmpty(biography)) { profileDetailsBinding.mainBiography.setVisibility(View.GONE); - } - else { + } else { profileDetailsBinding.mainBiography.setVisibility(View.VISIBLE); profileDetailsBinding.mainBiography.setText(biography); profileDetailsBinding.mainBiography.addOnHashtagListener(autoLinkItem -> { @@ -855,15 +850,14 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe String profileContext = profileModel.getProfileContext(); if (TextUtils.isEmpty(profileContext)) { profileDetailsBinding.profileContext.setVisibility(View.GONE); - } - else { + } else { profileDetailsBinding.profileContext.setVisibility(View.VISIBLE); final List userProfileContextLinks = profileModel.getProfileContextLinks(); for (int i = 0; i < userProfileContextLinks.size(); i++) { final UserProfileContextLink link = userProfileContextLinks.get(i); if (link.getUsername() != null) profileContext = profileContext.substring(0, link.getStart() + i) - + "@" + profileContext.substring(link.getStart() + i); + + "@" + profileContext.substring(link.getStart() + i); } profileDetailsBinding.profileContext.setText(profileContext); profileDetailsBinding.profileContext.addOnMentionClickListener(autoLinkItem -> { @@ -921,13 +915,13 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe profileDetailsBinding.btnTagged.setVisibility(View.VISIBLE); profileDetailsBinding.btnSaved.setVisibility(View.VISIBLE); profileDetailsBinding.btnLiked.setVisibility(View.VISIBLE); -// profileDetailsBinding.btnDM.setVisibility(View.GONE); + // profileDetailsBinding.btnDM.setVisibility(View.GONE); profileDetailsBinding.btnSaved.setText(R.string.saved); return; } profileDetailsBinding.btnSaved.setVisibility(View.GONE); profileDetailsBinding.btnLiked.setVisibility(View.GONE); -// profileDetailsBinding.btnDM.setVisibility(View.VISIBLE); + // profileDetailsBinding.btnDM.setVisibility(View.VISIBLE); profileDetailsBinding.btnFollow.setVisibility(View.VISIBLE); final Context context = getContext(); if (context == null) return; @@ -1037,8 +1031,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe })) .setNegativeButton(R.string.cancel, null) .show(); - } - else if (profileModel.getFriendshipStatus().isFollowing() || profileModel.getFriendshipStatus().isOutgoingRequest()) { + } else if (profileModel.getFriendshipStatus().isFollowing() || profileModel.getFriendshipStatus().isOutgoingRequest()) { friendshipService.unfollow( profileModel.getPk(), new ServiceCallback() { @@ -1086,27 +1079,27 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe PostItemType.TAGGED); NavHostFragment.findNavController(this).navigate(action); }); -// profileDetailsBinding.btnDM.setOnClickListener(v -> { -// profileDetailsBinding.btnDM.setEnabled(false); -// new CreateThreadAction(cookie, profileModel.getPk(), thread -> { -// if (thread == null) { -// Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); -// profileDetailsBinding.btnDM.setEnabled(true); -// return; -// } -// if (isAdded()) { -// final Bundle bundle = new Bundle(); -// bundle.putString("threadId", thread.getThreadId()); -// bundle.putString("title", thread.getThreadTitle()); -// if (isAdded()) { -// final NavDirections action = ProfileFragmentDirections -// .actionProfileFragmentToDMThreadFragment(thread.getThreadId(), profileModel.getUsername()); -// NavHostFragment.findNavController(this).navigate(action); -// } -// } -// profileDetailsBinding.btnDM.setEnabled(true); -// }).execute(); -// }); + // profileDetailsBinding.btnDM.setOnClickListener(v -> { + // profileDetailsBinding.btnDM.setEnabled(false); + // new CreateThreadAction(cookie, profileModel.getPk(), thread -> { + // if (thread == null) { + // Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + // profileDetailsBinding.btnDM.setEnabled(true); + // return; + // } + // if (isAdded()) { + // final Bundle bundle = new Bundle(); + // bundle.putString("threadId", thread.getThreadId()); + // bundle.putString("title", thread.getThreadTitle()); + // if (isAdded()) { + // final NavDirections action = ProfileFragmentDirections + // .actionProfileFragmentToDMThreadFragment(thread.getThreadId(), profileModel.getUsername()); + // NavHostFragment.findNavController(this).navigate(action); + // } + // } + // profileDetailsBinding.btnDM.setEnabled(true); + // }).execute(); + // }); profileDetailsBinding.mainProfileImage.setOnClickListener(v -> { if (!hasStories) { // show profile pic @@ -1149,7 +1142,9 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe private void showProfilePicDialog() { if (profileModel != null) { final FragmentManager fragmentManager = getParentFragmentManager(); - final ProfilePicDialogFragment fragment = new ProfilePicDialogFragment(profileModel.getPk(), username, profileModel.getProfilePicUrl()); + final ProfilePicDialogFragment fragment = ProfilePicDialogFragment.getInstance(profileModel.getPk(), + username, + profileModel.getProfilePicUrl()); final FragmentTransaction ft = fragmentManager.beginTransaction(); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) .add(fragment, "profilePicDialog") diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/DMPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/DMPreferencesFragment.java new file mode 100644 index 00000000..466e2490 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/DMPreferencesFragment.java @@ -0,0 +1,201 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; +import android.content.Intent; +import android.text.Editable; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; + +import java.util.Objects; + +import awais.instagrabber.R; +import awais.instagrabber.customviews.helpers.TextWatcherAdapter; +import awais.instagrabber.databinding.PrefAutoRefreshDmFreqBinding; +import awais.instagrabber.services.DMSyncAlarmReceiver; +import awais.instagrabber.services.DMSyncService; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.Debouncer; +import awais.instagrabber.utils.TextUtils; + +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT; +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class DMPreferencesFragment extends BasePreferencesFragment { + private static final String TAG = DMPreferencesFragment.class.getSimpleName(); + + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + screen.addPreference(getMarkDMSeenPreference(context)); + // screen.addPreference(getAutoRefreshDMPreference(context)); + // screen.addPreference(getAutoRefreshDMFreqPreference(context)); + } + + private Preference getMarkDMSeenPreference(@NonNull final Context context) { + return PreferenceHelper.getSwitchPreference( + context, + Constants.DM_MARK_AS_SEEN, + R.string.dm_mark_as_seen_setting, + R.string.dm_mark_as_seen_setting_summary, + false, + null + ); + } + + private Preference getAutoRefreshDMPreference(@NonNull final Context context) { + return PreferenceHelper.getSwitchPreference( + context, + PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH, + R.string.enable_dm_auto_refesh, + -1, + false, + (preference, newValue) -> { + if (!(newValue instanceof Boolean)) return false; + final boolean enabled = (Boolean) newValue; + if (enabled) { + DMSyncAlarmReceiver.setAlarm(context); + return true; + } + DMSyncAlarmReceiver.cancelAlarm(context); + try { + final Context applicationContext = context.getApplicationContext(); + applicationContext.stopService(new Intent(applicationContext, DMSyncService.class)); + } catch (Exception e) { + Log.e(TAG, "getAutoRefreshDMPreference: ", e); + } + return true; + } + ); + } + + private Preference getAutoRefreshDMFreqPreference(@NonNull final Context context) { + return new AutoRefreshDMFrePreference(context); + } + + public static class AutoRefreshDMFrePreference extends Preference { + private static final String TAG = AutoRefreshDMFrePreference.class.getSimpleName(); + private static final String DEBOUNCE_KEY = "dm_sync_service_update"; + public static final int INTERVAL = 2000; + + private final Debouncer.Callback changeCallback; + + private Debouncer serviceUpdateDebouncer; + private PrefAutoRefreshDmFreqBinding binding; + + public AutoRefreshDMFrePreference(final Context context) { + super(context); + setLayoutResource(R.layout.pref_auto_refresh_dm_freq); + // setKey(key); + setIconSpaceReserved(false); + changeCallback = new Debouncer.Callback() { + @Override + public void call(final String key) { + DMSyncAlarmReceiver.setAlarm(context); + } + + @Override + public void onError(final Throwable t) { + Log.e(TAG, "onError: ", t); + } + }; + serviceUpdateDebouncer = new Debouncer<>(changeCallback, INTERVAL); + } + + @Override + public void onDependencyChanged(final Preference dependency, final boolean disableDependent) { + // super.onDependencyChanged(dependency, disableDependent); + if (binding == null) return; + binding.startText.setEnabled(!disableDependent); + binding.freqNum.setEnabled(!disableDependent); + binding.freqUnit.setEnabled(!disableDependent); + if (disableDependent) { + serviceUpdateDebouncer.terminate(); + return; + } + serviceUpdateDebouncer = new Debouncer<>(changeCallback, INTERVAL); + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + setDependency(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH); + binding = PrefAutoRefreshDmFreqBinding.bind(holder.itemView); + final Context context = getContext(); + if (context == null) return; + setupUnitSpinner(context); + setupNumberEditText(context); + } + + private void setupUnitSpinner(final Context context) { + final ArrayAdapter adapter = ArrayAdapter.createFromResource(context, + R.array.dm_auto_refresh_freq_unit_labels, + android.R.layout.simple_spinner_item); + final String[] values = context.getResources().getStringArray(R.array.dm_auto_refresh_freq_units); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + binding.freqUnit.setAdapter(adapter); + + String unit = settingsHelper.getString(PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT); + if (TextUtils.isEmpty(unit)) { + unit = "secs"; + } + int position = 0; + for (int i = 0; i < values.length; i++) { + if (Objects.equals(unit, values[i])) { + position = i; + break; + } + } + binding.freqUnit.setSelection(position); + binding.freqUnit.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(final AdapterView parent, final View view, final int position, final long id) { + settingsHelper.putString(PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT, values[position]); + if (!isEnabled()) { + serviceUpdateDebouncer.terminate(); + return; + } + serviceUpdateDebouncer.call(DEBOUNCE_KEY); + } + + @Override + public void onNothingSelected(final AdapterView parent) {} + }); + } + + private void setupNumberEditText(final Context context) { + int currentValue = settingsHelper.getInteger(PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER); + if (currentValue <= 0) { + currentValue = 30; + } + binding.freqNum.setText(String.valueOf(currentValue)); + binding.freqNum.addTextChangedListener(new TextWatcherAdapter() { + + @Override + public void afterTextChanged(final Editable s) { + if (TextUtils.isEmpty(s)) return; + try { + final int value = Integer.parseInt(s.toString()); + if (value <= 0) return; + settingsHelper.putInteger(PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER, value); + if (!isEnabled()) { + serviceUpdateDebouncer.terminate(); + return; + } + serviceUpdateDebouncer.call(DEBOUNCE_KEY); + } catch (Exception e) { + Log.e(TAG, "afterTextChanged: ", e); + } + } + }); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java new file mode 100644 index 00000000..c2637b78 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java @@ -0,0 +1,101 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatButton; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; +import androidx.preference.SwitchPreferenceCompat; + +import com.google.android.material.switchmaterial.SwitchMaterial; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.DirectoryChooser; +import awais.instagrabber.utils.TextUtils; + +import static awais.instagrabber.utils.Constants.FOLDER_PATH; +import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class DownloadsPreferencesFragment extends BasePreferencesFragment { + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + screen.addPreference(getDownloadUserFolderPreference(context)); + screen.addPreference(getSaveToCustomFolderPreference(context)); + } + + private Preference getDownloadUserFolderPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.DOWNLOAD_USER_FOLDER); + preference.setTitle(R.string.download_user_folder); + preference.setIconSpaceReserved(false); + return preference; + } + + private Preference getSaveToCustomFolderPreference(@NonNull final Context context) { + return new SaveToCustomFolderPreference(context, (resultCallback) -> new DirectoryChooser() + .setInitialDirectory(settingsHelper.getString(FOLDER_PATH)) + .setInteractionListener(file -> { + settingsHelper.putString(FOLDER_PATH, file.getAbsolutePath()); + resultCallback.onResult(file.getAbsolutePath()); + }) + .show(getParentFragmentManager(), null)); + } + + public static class SaveToCustomFolderPreference extends Preference { + private AppCompatTextView customPathTextView; + private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener; + private final String key; + + public SaveToCustomFolderPreference(final Context context, final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) { + super(context); + this.onSelectFolderButtonClickListener = onSelectFolderButtonClickListener; + key = Constants.FOLDER_SAVE_TO; + setLayoutResource(R.layout.pref_custom_folder); + setKey(key); + setTitle(R.string.save_to_folder); + setIconSpaceReserved(false); + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + final SwitchMaterial cbSaveTo = (SwitchMaterial) holder.findViewById(R.id.cbSaveTo); + final View buttonContainer = holder.findViewById(R.id.button_container); + customPathTextView = (AppCompatTextView) holder.findViewById(R.id.custom_path); + cbSaveTo.setOnCheckedChangeListener((buttonView, isChecked) -> { + settingsHelper.putBoolean(FOLDER_SAVE_TO, isChecked); + buttonContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE); + final String customPath = settingsHelper.getString(FOLDER_PATH); + customPathTextView.setText(customPath); + }); + final boolean savedToEnabled = settingsHelper.getBoolean(key); + holder.itemView.setOnClickListener(v -> cbSaveTo.toggle()); + cbSaveTo.setChecked(savedToEnabled); + buttonContainer.setVisibility(savedToEnabled ? View.VISIBLE : View.GONE); + final AppCompatButton btnSaveTo = (AppCompatButton) holder.findViewById(R.id.btnSaveTo); + btnSaveTo.setOnClickListener(v -> { + if (onSelectFolderButtonClickListener == null) return; + onSelectFolderButtonClickListener.onClick(result -> { + if (TextUtils.isEmpty(result)) return; + customPathTextView.setText(result); + }); + }); + } + + public interface ResultCallback { + void onResult(String result); + } + + public interface OnSelectFolderButtonClickListener { + void onClick(ResultCallback resultCallback); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java new file mode 100644 index 00000000..deb7c277 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java @@ -0,0 +1,61 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; +import android.content.res.TypedArray; + +import androidx.annotation.NonNull; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreferenceCompat; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.TextUtils; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class GeneralPreferencesFragment extends BasePreferencesFragment { + + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + final String cookie = settingsHelper.getString(Constants.COOKIE); + final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; + if (isLoggedIn) { + screen.addPreference(getDefaultTabPreference(context)); + } + screen.addPreference(getUpdateCheckPreference(context)); + } + + private Preference getDefaultTabPreference(@NonNull final Context context) { + final ListPreference preference = new ListPreference(context); + preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); + final TypedArray mainNavIds = getResources().obtainTypedArray(R.array.main_nav_ids); + final int length = mainNavIds.length(); + final String[] values = new String[length]; + for (int i = 0; i < length; i++) { + final int resourceId = mainNavIds.getResourceId(i, -1); + if (resourceId < 0) continue; + values[i] = getResources().getResourceEntryName(resourceId); + } + mainNavIds.recycle(); + preference.setKey(Constants.DEFAULT_TAB); + preference.setTitle(R.string.pref_start_screen); + preference.setDialogTitle(R.string.pref_start_screen); + preference.setEntries(R.array.main_nav_ids_values); + preference.setEntryValues(values); + preference.setIconSpaceReserved(false); + return preference; + } + + private Preference getUpdateCheckPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.CHECK_UPDATES); + preference.setTitle(R.string.update_check); + preference.setIconSpaceReserved(false); + return preference; + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/LocalePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/LocalePreferencesFragment.java new file mode 100644 index 00000000..a02a8764 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/LocalePreferencesFragment.java @@ -0,0 +1,93 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import awais.instagrabber.R; +import awais.instagrabber.dialogs.TimeSettingsDialog; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.LocaleUtils; +import awais.instagrabber.utils.UserAgentUtils; +import awais.instagrabber.utils.Utils; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class LocalePreferencesFragment extends BasePreferencesFragment { + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + screen.addPreference(getLanguagePreference(context)); + screen.addPreference(getPostTimeFormatPreference(context)); + } + + private Preference getLanguagePreference(@NonNull final Context context) { + final ListPreference preference = new ListPreference(context); + preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); + final int length = getResources().getStringArray(R.array.languages).length; + final String[] values = new String[length]; + for (int i = 0; i < length; i++) { + values[i] = String.valueOf(i); + } + preference.setKey(Constants.APP_LANGUAGE); + preference.setTitle(R.string.select_language); + preference.setDialogTitle(R.string.select_language); + preference.setEntries(R.array.languages); + preference.setIconSpaceReserved(false); + preference.setEntryValues(values); + preference.setOnPreferenceChangeListener((preference1, newValue) -> { + shouldRecreate(); + final int appUaCode = settingsHelper.getInteger(Constants.APP_UA_CODE); + final String appUa = UserAgentUtils.generateAppUA(appUaCode, LocaleUtils.getCurrentLocale().getLanguage()); + settingsHelper.putString(Constants.APP_UA, appUa); + return true; + }); + return preference; + } + + private Preference getPostTimeFormatPreference(@NonNull final Context context) { + final Preference preference = new Preference(context); + preference.setTitle(R.string.time_settings); + preference.setSummary(Utils.datetimeParser.format(new Date())); + preference.setIconSpaceReserved(false); + preference.setOnPreferenceClickListener(preference1 -> { + new TimeSettingsDialog( + settingsHelper.getBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED), + settingsHelper.getString(Constants.CUSTOM_DATE_TIME_FORMAT), + settingsHelper.getString(Constants.DATE_TIME_SELECTION), + settingsHelper.getBoolean(Constants.SWAP_DATE_TIME_FORMAT_ENABLED), + (isCustomFormat, + formatSelection, + spTimeFormatSelectedItemPosition, + spSeparatorSelectedItemPosition, + spDateFormatSelectedItemPosition, + selectedFormat, + currentFormat, + swapDateTime) -> { + if (isCustomFormat) { + settingsHelper.putString(Constants.CUSTOM_DATE_TIME_FORMAT, formatSelection); + } else { + final String formatSelectionUpdated = spTimeFormatSelectedItemPosition + ";" + + spSeparatorSelectedItemPosition + ';' + + spDateFormatSelectedItemPosition; // time;separator;date + settingsHelper.putString(Constants.DATE_TIME_FORMAT, selectedFormat); + settingsHelper.putString(Constants.DATE_TIME_SELECTION, formatSelectionUpdated); + } + settingsHelper.putBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED, isCustomFormat); + settingsHelper.putBoolean(Constants.SWAP_DATE_TIME_FORMAT_ENABLED, swapDateTime); + Utils.datetimeParser = (SimpleDateFormat) currentFormat.clone(); + preference.setSummary(Utils.datetimeParser.format(new Date())); + } + ).show(getParentFragmentManager(), null); + return true; + }); + return preference; + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java index 2b39f02b..f9947ecc 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java @@ -14,6 +14,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; +import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; import androidx.preference.Preference; @@ -132,43 +133,58 @@ public class MorePreferencesFragment extends BasePreferencesFragment { // generalCategory.setIconSpaceReserved(false); // screen.addPreference(generalCategory); screen.addPreference(getDivider(context)); + final NavController navController = NavHostFragment.findNavController(this); if (isLoggedIn) { screen.addPreference(getPreference(R.string.action_notif, R.drawable.ic_not_liked, preference -> { - final NavDirections navDirections = MorePreferencesFragmentDirections.actionGlobalNotificationsViewerFragment("notif", 0l); - NavHostFragment.findNavController(this).navigate(navDirections); + if (isSafeToNavigate(navController)) { + final NavDirections navDirections = MorePreferencesFragmentDirections.actionGlobalNotificationsViewerFragment("notif", 0L); + navController.navigate(navDirections); + } return true; })); screen.addPreference(getPreference(R.string.action_ayml, R.drawable.ic_suggested_users, preference -> { - final NavDirections navDirections = MorePreferencesFragmentDirections.actionGlobalNotificationsViewerFragment("ayml", 0l); - NavHostFragment.findNavController(this).navigate(navDirections); + if (isSafeToNavigate(navController)) { + final NavDirections navDirections = MorePreferencesFragmentDirections.actionGlobalNotificationsViewerFragment("ayml", 0L); + navController.navigate(navDirections); + } return true; })); screen.addPreference(getPreference(R.string.action_archive, R.drawable.ic_archive, preference -> { - final NavDirections navDirections = MorePreferencesFragmentDirections.actionGlobalStoryListViewerFragment("archive"); - NavHostFragment.findNavController(this).navigate(navDirections); + if (isSafeToNavigate(navController)) { + final NavDirections navDirections = MorePreferencesFragmentDirections.actionGlobalStoryListViewerFragment("archive"); + navController.navigate(navDirections); + } return true; })); } screen.addPreference(getPreference(R.string.title_favorites, R.drawable.ic_star_24, preference -> { - final NavDirections navDirections = MorePreferencesFragmentDirections.actionMorePreferencesFragmentToFavoritesFragment(); - NavHostFragment.findNavController(this).navigate(navDirections); + if (isSafeToNavigate(navController)) { + final NavDirections navDirections = MorePreferencesFragmentDirections.actionMorePreferencesFragmentToFavoritesFragment(); + navController.navigate(navDirections); + } return true; })); screen.addPreference(getDivider(context)); screen.addPreference(getPreference(R.string.action_settings, R.drawable.ic_outline_settings_24, preference -> { - final NavDirections navDirections = MorePreferencesFragmentDirections.actionMorePreferencesFragmentToSettingsPreferencesFragment(); - NavHostFragment.findNavController(this).navigate(navDirections); + if (isSafeToNavigate(navController)) { + final NavDirections navDirections = MorePreferencesFragmentDirections.actionMorePreferencesFragmentToSettingsPreferencesFragment(); + navController.navigate(navDirections); + } return true; })); screen.addPreference(getPreference(R.string.backup_and_restore, R.drawable.ic_settings_backup_restore_24, preference -> { - final NavDirections navDirections = MorePreferencesFragmentDirections.actionMorePreferencesFragmentToBackupPreferencesFragment(); - NavHostFragment.findNavController(this).navigate(navDirections); + if (isSafeToNavigate(navController)) { + final NavDirections navDirections = MorePreferencesFragmentDirections.actionMorePreferencesFragmentToBackupPreferencesFragment(); + navController.navigate(navDirections); + } return true; })); screen.addPreference(getPreference(R.string.action_about, R.drawable.ic_outline_info_24, preference1 -> { - final NavDirections navDirections = MorePreferencesFragmentDirections.actionMorePreferencesFragmentToAboutFragment(); - NavHostFragment.findNavController(this).navigate(navDirections); + if (isSafeToNavigate(navController)) { + final NavDirections navDirections = MorePreferencesFragmentDirections.actionMorePreferencesFragmentToAboutFragment(); + navController.navigate(navDirections); + } return true; })); @@ -187,6 +203,11 @@ public class MorePreferencesFragment extends BasePreferencesFragment { screen.addPreference(reminderPreference); } + private boolean isSafeToNavigate(final NavController navController) { + return navController.getCurrentDestination() != null + && navController.getCurrentDestination().getId() == R.id.morePreferencesFragment; + } + @Override public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { if (resultCode == Constants.LOGIN_RESULT_CODE) { diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/NotificationsPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/NotificationsPreferencesFragment.java new file mode 100644 index 00000000..72f69f79 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/NotificationsPreferencesFragment.java @@ -0,0 +1,43 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; + +public class NotificationsPreferencesFragment extends BasePreferencesFragment { + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + screen.addPreference(getActivityNotificationsPreference(context)); + screen.addPreference(getDMNotificationsPreference(context)); + } + + private Preference getActivityNotificationsPreference(@NonNull final Context context) { + return PreferenceHelper.getSwitchPreference( + context, + Constants.CHECK_ACTIVITY, + R.string.activity_setting, + -1, + false, + (preference, newValue) -> { + shouldRecreate(); + return true; + }); + } + + private Preference getDMNotificationsPreference(@NonNull final Context context) { + return PreferenceHelper.getSwitchPreference( + context, + PreferenceKeys.PREF_ENABLE_DM_NOTIFICATIONS, + R.string.enable_dm_notifications, + -1, + false, + null); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java new file mode 100644 index 00000000..07012fe9 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/PostPreferencesFragment.java @@ -0,0 +1,49 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreferenceCompat; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class PostPreferencesFragment extends BasePreferencesFragment { + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + // generalCategory.addPreference(getAutoPlayVideosPreference(context)); + screen.addPreference(getAlwaysMuteVideosPreference(context)); + screen.addPreference(getShowCaptionPreference(context)); + } + + private Preference getAutoPlayVideosPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.AUTOPLAY_VIDEOS); + preference.setTitle(R.string.post_viewer_autoplay_video); + preference.setIconSpaceReserved(false); + return preference; + } + + private Preference getAlwaysMuteVideosPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.MUTED_VIDEOS); + preference.setTitle(R.string.post_viewer_muted_autoplay); + preference.setIconSpaceReserved(false); + return preference; + } + + private Preference getShowCaptionPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.SHOW_CAPTIONS); + preference.setDefaultValue(true); + preference.setTitle(R.string.post_viewer_show_captions); + preference.setIconSpaceReserved(false); + return preference; + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceHelper.java b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceHelper.java new file mode 100644 index 00000000..824c18d7 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceHelper.java @@ -0,0 +1,30 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.SwitchPreferenceCompat; + +public final class PreferenceHelper { + + public static SwitchPreferenceCompat getSwitchPreference(@NonNull final Context context, + @NonNull final String key, + @StringRes final int titleResId, + @StringRes final int summaryResId, + final boolean iconSpaceReserved, + final OnPreferenceChangeListener onPreferenceChangeListener) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(key); + preference.setTitle(titleResId); + preference.setIconSpaceReserved(iconSpaceReserved); + if (summaryResId != -1) { + preference.setSummary(summaryResId); + } + if (onPreferenceChangeListener != null) { + preference.setOnPreferenceChangeListener(onPreferenceChangeListener); + } + return preference; + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.java b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.java new file mode 100644 index 00000000..3f481685 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.java @@ -0,0 +1,8 @@ +package awais.instagrabber.fragments.settings; + +public final class PreferenceKeys { + public static final String PREF_ENABLE_DM_NOTIFICATIONS = "enable_dm_notifications"; + public static final String PREF_ENABLE_DM_AUTO_REFRESH = "enable_dm_auto_refresh"; + public static final String PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT = "enable_dm_auto_refresh_freq_unit"; + public static final String PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER = "enable_dm_auto_refresh_freq_number"; +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java index 00628d3f..b45739a0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java @@ -1,91 +1,55 @@ package awais.instagrabber.fragments.settings; import android.content.Context; -import android.content.res.TypedArray; -import android.view.View; import androidx.annotation.NonNull; -import androidx.appcompat.widget.AppCompatButton; -import androidx.appcompat.widget.AppCompatTextView; +import androidx.annotation.StringRes; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; -import androidx.preference.ListPreference; import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import androidx.preference.PreferenceViewHolder; -import androidx.preference.SwitchPreferenceCompat; -import com.google.android.material.switchmaterial.SwitchMaterial; +import com.google.common.collect.ImmutableList; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.util.List; import awais.instagrabber.R; -import awais.instagrabber.dialogs.TimeSettingsDialog; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.DirectoryChooser; -import awais.instagrabber.utils.LocaleUtils; import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.UserAgentUtils; -import awais.instagrabber.utils.Utils; -import static awais.instagrabber.utils.Constants.FOLDER_PATH; -import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToDm; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToDownloads; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToGeneral; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToLocale; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToNotifications; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToPost; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToStories; +import static awais.instagrabber.fragments.settings.SettingsPreferencesFragmentDirections.actionSettingsToTheme; import static awais.instagrabber.utils.Utils.settingsHelper; public class SettingsPreferencesFragment extends BasePreferencesFragment { - private static final String TAG = "SettingsPrefsFrag"; - private boolean isLoggedIn; + private static final String TAG = SettingsPreferencesFragment.class.getSimpleName(); + private static final List screens = ImmutableList.of( + new SettingScreen(R.string.pref_category_general, actionSettingsToGeneral()), + new SettingScreen(R.string.pref_category_theme, actionSettingsToTheme()), + new SettingScreen(R.string.pref_category_locale, actionSettingsToLocale()), + new SettingScreen(R.string.pref_category_post, actionSettingsToPost()), + new SettingScreen(R.string.pref_category_stories, actionSettingsToStories(), true), + new SettingScreen(R.string.pref_category_dm, actionSettingsToDm(), true), + new SettingScreen(R.string.pref_category_notifications, actionSettingsToNotifications(), true), + new SettingScreen(R.string.pref_category_downloads, actionSettingsToDownloads()) + ); @Override void setupPreferenceScreen(final PreferenceScreen screen) { - final String cookie = settingsHelper.getString(Constants.COOKIE); - isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; final Context context = getContext(); if (context == null) return; - final PreferenceCategory generalCategory = new PreferenceCategory(context); - screen.addPreference(generalCategory); - generalCategory.setTitle(R.string.pref_category_general); - generalCategory.setIconSpaceReserved(false); - generalCategory.addPreference(getThemePreference(context)); - generalCategory.addPreference(getDefaultTabPreference()); - generalCategory.addPreference(getUpdateCheckPreference()); - // generalCategory.addPreference(getAutoPlayVideosPreference()); - generalCategory.addPreference(getAlwaysMuteVideosPreference()); - generalCategory.addPreference(getShowCaptionPreference()); - - // screen.addPreference(getDivider(context)); - // final PreferenceCategory themeCategory = new PreferenceCategory(context); - // screen.addPreference(themeCategory); - // themeCategory.setTitle(R.string.pref_category_theme); - // themeCategory.setIconSpaceReserved(false); - // themeCategory.addPreference(getAmoledThemePreference()); - - final PreferenceCategory downloadsCategory = new PreferenceCategory(context); - screen.addPreference(downloadsCategory); - downloadsCategory.setTitle(R.string.pref_category_downloads); - downloadsCategory.setIconSpaceReserved(false); - downloadsCategory.addPreference(getDownloadUserFolderPreference()); - downloadsCategory.addPreference(getSaveToCustomFolderPreference()); - - final PreferenceCategory localeCategory = new PreferenceCategory(context); - screen.addPreference(localeCategory); - localeCategory.setTitle(R.string.pref_category_locale); - localeCategory.setIconSpaceReserved(false); - localeCategory.addPreference(getLanguagePreference()); - localeCategory.addPreference(getPostTimePreference()); - - if (isLoggedIn) { - final PreferenceCategory loggedInUsersPreferenceCategory = new PreferenceCategory(context); - screen.addPreference(loggedInUsersPreferenceCategory); - loggedInUsersPreferenceCategory.setIconSpaceReserved(false); - loggedInUsersPreferenceCategory.setTitle(R.string.login_settings); - loggedInUsersPreferenceCategory.addPreference(getStorySortPreference()); - loggedInUsersPreferenceCategory.addPreference(getMarkStoriesSeenPreference()); - loggedInUsersPreferenceCategory.addPreference(getMarkDMSeenPreference()); - loggedInUsersPreferenceCategory.addPreference(getEnableActivityNotificationsPreference()); + final String cookie = settingsHelper.getString(Constants.COOKIE); + final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) > 0; + for (final SettingScreen settingScreen : screens) { + if (settingScreen.isLoginRequired() && !isLoggedIn) continue; + screen.addPreference(getNavPreference(context, settingScreen)); } // else { // final PreferenceCategory anonUsersPreferenceCategory = new PreferenceCategory(context); @@ -95,275 +59,43 @@ public class SettingsPreferencesFragment extends BasePreferencesFragment { // } } - private Preference getLanguagePreference() { - final Context context = getContext(); - if (context == null) return null; - final ListPreference preference = new ListPreference(context); - preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); - final int length = getResources().getStringArray(R.array.languages).length; - final String[] values = new String[length]; - for (int i = 0; i < length; i++) { - values[i] = String.valueOf(i); - } - preference.setKey(Constants.APP_LANGUAGE); - preference.setTitle(R.string.select_language); - preference.setDialogTitle(R.string.select_language); - preference.setEntries(R.array.languages); - preference.setIconSpaceReserved(false); - preference.setEntryValues(values); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - shouldRecreate(); - final int appUaCode = settingsHelper.getInteger(Constants.APP_UA_CODE); - final String appUa = UserAgentUtils.generateAppUA(appUaCode, LocaleUtils.getCurrentLocale().getLanguage()); - settingsHelper.putString(Constants.APP_UA, appUa); - return true; - }); - return preference; - } - - private Preference getDefaultTabPreference() { - final Context context = getContext(); - if (context == null) return null; - final ListPreference preference = new ListPreference(context); - preference.setEnabled(isLoggedIn); - preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); - final TypedArray mainNavIds = getResources().obtainTypedArray(R.array.main_nav_ids); - final int length = mainNavIds.length(); - final String[] values = new String[length]; - for (int i = 0; i < length; i++) { - final int resourceId = mainNavIds.getResourceId(i, -1); - if (resourceId < 0) continue; - values[i] = getResources().getResourceEntryName(resourceId); - } - mainNavIds.recycle(); - preference.setKey(Constants.DEFAULT_TAB); - preference.setTitle(R.string.pref_start_screen); - preference.setDialogTitle(R.string.pref_start_screen); - preference.setEntries(R.array.main_nav_ids_values); - preference.setEntryValues(values); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getUpdateCheckPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.CHECK_UPDATES); - preference.setTitle(R.string.update_check); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getThemePreference(@NonNull final Context context) { + private Preference getNavPreference(@NonNull final Context context, + @NonNull final SettingScreen settingScreen) { final Preference preference = new Preference(context); - preference.setTitle(R.string.pref_category_theme); - // preference.setIcon(R.drawable.ic_format_paint_24); + preference.setTitle(settingScreen.getTitleResId()); preference.setIconSpaceReserved(false); preference.setOnPreferenceClickListener(preference1 -> { - final NavDirections navDirections = SettingsPreferencesFragmentDirections.actionSettingsPreferencesFragmentToThemePreferencesFragment(); - NavHostFragment.findNavController(this).navigate(navDirections); + NavHostFragment.findNavController(this).navigate(settingScreen.getDirections()); return true; }); return preference; } - private Preference getDownloadUserFolderPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.DOWNLOAD_USER_FOLDER); - preference.setTitle(R.string.download_user_folder); - preference.setIconSpaceReserved(false); - return preference; - } + private static class SettingScreen { + private final int titleResId; + private final NavDirections directions; + private final boolean loginRequired; - private Preference getSaveToCustomFolderPreference() { - final Context context = getContext(); - if (context == null) return null; - return new SaveToCustomFolderPreference(context, (resultCallback) -> new DirectoryChooser() - .setInitialDirectory(settingsHelper.getString(FOLDER_PATH)) - .setInteractionListener(file -> { - settingsHelper.putString(FOLDER_PATH, file.getAbsolutePath()); - resultCallback.onResult(file.getAbsolutePath()); - }) - .show(getParentFragmentManager(), null)); - } - - private Preference getAutoPlayVideosPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.AUTOPLAY_VIDEOS); - preference.setTitle(R.string.post_viewer_autoplay_video); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getAlwaysMuteVideosPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.MUTED_VIDEOS); - preference.setTitle(R.string.post_viewer_muted_autoplay); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getShowCaptionPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.SHOW_CAPTIONS); - preference.setDefaultValue(true); - preference.setTitle(R.string.post_viewer_show_captions); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getStorySortPreference() { - final Context context = getContext(); - if (context == null) return null; - final ListPreference preference = new ListPreference(context); - preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); - final int length = getResources().getStringArray(R.array.story_sorts).length; - final String[] values = new String[length]; - for (int i = 0; i < length; i++) { - values[i] = String.valueOf(i); - } - preference.setKey(Constants.STORY_SORT); - preference.setTitle(R.string.story_sort_setting); - preference.setDialogTitle(R.string.story_sort_setting); - preference.setEntries(R.array.story_sorts); - preference.setIconSpaceReserved(false); - preference.setEntryValues(values); - return preference; - } - - private Preference getMarkStoriesSeenPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.MARK_AS_SEEN); - preference.setTitle(R.string.mark_as_seen_setting); - preference.setSummary(R.string.mark_as_seen_setting_summary); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getMarkDMSeenPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.DM_MARK_AS_SEEN); - preference.setTitle(R.string.dm_mark_as_seen_setting); - preference.setSummary(R.string.dm_mark_as_seen_setting_summary); - preference.setIconSpaceReserved(false); - return preference; - } - - private Preference getEnableActivityNotificationsPreference() { - final Context context = getContext(); - if (context == null) return null; - final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.CHECK_ACTIVITY); - preference.setTitle(R.string.activity_setting); - preference.setIconSpaceReserved(false); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - shouldRecreate(); - return true; - }); - return preference; - } - - private Preference getPostTimePreference() { - final Context context = getContext(); - if (context == null) return null; - final Preference preference = new Preference(context); - preference.setTitle(R.string.time_settings); - preference.setSummary(Utils.datetimeParser.format(new Date())); - preference.setIconSpaceReserved(false); - preference.setOnPreferenceClickListener(preference1 -> { - new TimeSettingsDialog( - settingsHelper.getBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED), - settingsHelper.getString(Constants.CUSTOM_DATE_TIME_FORMAT), - settingsHelper.getString(Constants.DATE_TIME_SELECTION), - settingsHelper.getBoolean(Constants.SWAP_DATE_TIME_FORMAT_ENABLED), - (isCustomFormat, - formatSelection, - spTimeFormatSelectedItemPosition, - spSeparatorSelectedItemPosition, - spDateFormatSelectedItemPosition, - selectedFormat, - currentFormat, - swapDateTime) -> { - if (isCustomFormat) { - settingsHelper.putString(Constants.CUSTOM_DATE_TIME_FORMAT, formatSelection); - } else { - final String formatSelectionUpdated = spTimeFormatSelectedItemPosition + ";" - + spSeparatorSelectedItemPosition + ';' - + spDateFormatSelectedItemPosition; // time;separator;date - settingsHelper.putString(Constants.DATE_TIME_FORMAT, selectedFormat); - settingsHelper.putString(Constants.DATE_TIME_SELECTION, formatSelectionUpdated); - } - settingsHelper.putBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED, isCustomFormat); - settingsHelper.putBoolean(Constants.SWAP_DATE_TIME_FORMAT_ENABLED, swapDateTime); - Utils.datetimeParser = (SimpleDateFormat) currentFormat.clone(); - preference.setSummary(Utils.datetimeParser.format(new Date())); - } - ).show(getParentFragmentManager(), null); - return true; - }); - return preference; - } - - public static class SaveToCustomFolderPreference extends Preference { - private AppCompatTextView customPathTextView; - private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener; - private final String key; - - public SaveToCustomFolderPreference(final Context context, final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) { - super(context); - this.onSelectFolderButtonClickListener = onSelectFolderButtonClickListener; - key = Constants.FOLDER_SAVE_TO; - setLayoutResource(R.layout.pref_custom_folder); - setKey(key); - setTitle(R.string.save_to_folder); - setIconSpaceReserved(false); + public SettingScreen(@StringRes final int titleResId, final NavDirections directions) { + this(titleResId, directions, false); } - @Override - public void onBindViewHolder(final PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - final SwitchMaterial cbSaveTo = (SwitchMaterial) holder.findViewById(R.id.cbSaveTo); - final View buttonContainer = holder.findViewById(R.id.button_container); - customPathTextView = (AppCompatTextView) holder.findViewById(R.id.custom_path); - cbSaveTo.setOnCheckedChangeListener((buttonView, isChecked) -> { - settingsHelper.putBoolean(FOLDER_SAVE_TO, isChecked); - buttonContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE); - final String customPath = settingsHelper.getString(FOLDER_PATH); - customPathTextView.setText(customPath); - }); - final boolean savedToEnabled = settingsHelper.getBoolean(key); - holder.itemView.setOnClickListener(v -> cbSaveTo.toggle()); - cbSaveTo.setChecked(savedToEnabled); - buttonContainer.setVisibility(savedToEnabled ? View.VISIBLE : View.GONE); - final AppCompatButton btnSaveTo = (AppCompatButton) holder.findViewById(R.id.btnSaveTo); - btnSaveTo.setOnClickListener(v -> { - if (onSelectFolderButtonClickListener == null) return; - onSelectFolderButtonClickListener.onClick(result -> { - if (TextUtils.isEmpty(result)) return; - customPathTextView.setText(result); - }); - }); + public SettingScreen(@StringRes final int titleResId, final NavDirections directions, final boolean loginRequired) { + this.titleResId = titleResId; + this.directions = directions; + this.loginRequired = loginRequired; } - public interface ResultCallback { - void onResult(String result); + public int getTitleResId() { + return titleResId; } - public interface OnSelectFolderButtonClickListener { - void onClick(ResultCallback resultCallback); + public NavDirections getDirections() { + return directions; + } + + public boolean isLoginRequired() { + return loginRequired; } } } diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/StoriesPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/StoriesPreferencesFragment.java new file mode 100644 index 00000000..8337f7c3 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/StoriesPreferencesFragment.java @@ -0,0 +1,48 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreferenceCompat; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; + +public class StoriesPreferencesFragment extends BasePreferencesFragment { + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) return; + screen.addPreference(getStorySortPreference(context)); + screen.addPreference(getMarkStoriesSeenPreference(context)); + } + + private Preference getStorySortPreference(@NonNull final Context context) { + final ListPreference preference = new ListPreference(context); + preference.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()); + final int length = getResources().getStringArray(R.array.story_sorts).length; + final String[] values = new String[length]; + for (int i = 0; i < length; i++) { + values[i] = String.valueOf(i); + } + preference.setKey(Constants.STORY_SORT); + preference.setTitle(R.string.story_sort_setting); + preference.setDialogTitle(R.string.story_sort_setting); + preference.setEntries(R.array.story_sorts); + preference.setIconSpaceReserved(false); + preference.setEntryValues(values); + return preference; + } + + private Preference getMarkStoriesSeenPreference(@NonNull final Context context) { + final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); + preference.setKey(Constants.MARK_AS_SEEN); + preference.setTitle(R.string.mark_as_seen_setting); + preference.setSummary(R.string.mark_as_seen_setting_summary); + preference.setIconSpaceReserved(false); + return preference; + } +} 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..a41f0fc9 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/managers/InboxManager.java @@ -0,0 +1,360 @@ +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(); + if (currentDirectInbox == null) return; + 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(); + if (currentDirectInbox == null) return; + 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..ab31122a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java @@ -0,0 +1,1798 @@ +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.repositories.responses.giphy.GiphyGif; +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 Boolean isGroup = this.isGroup.getValue(); + if (isGroup == null || !isGroup) return; + 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 LiveData> sendAnimatedMedia(@NonNull final GiphyGif giphyGif) { + final MutableLiveData> data = new MutableLiveData<>(); + final Long userId = getCurrentUserId(data); + if (userId == null) return data; + final String clientContext = UUID.randomUUID().toString(); + final DirectItem directItem = DirectItemFactory.createAnimatedMedia(userId, clientContext, giphyGif); + directItem.setPending(true); + addItems(0, Collections.singletonList(directItem)); + data.postValue(Resource.loading(directItem)); + final Call request = service.broadcastAnimatedMedia( + clientContext, + threadIdOrUserIds, + giphyGif + ); + enqueueRequest(request, data, directItem); + 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<>(); + 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())); + } + + @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<>(); + 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/models/enums/BroadcastItemType.java b/app/src/main/java/awais/instagrabber/models/enums/BroadcastItemType.java index 7c399d0b..7fd57da0 100644 --- a/app/src/main/java/awais/instagrabber/models/enums/BroadcastItemType.java +++ b/app/src/main/java/awais/instagrabber/models/enums/BroadcastItemType.java @@ -7,7 +7,8 @@ public enum BroadcastItemType { IMAGE("configure_photo"), LINK("link"), VIDEO("configure_video"), - VOICE("share_voice"); + VOICE("share_voice"), + ANIMATED_MEDIA("animated_media"); private final String value; diff --git a/app/src/main/java/awais/instagrabber/repositories/GifRepository.java b/app/src/main/java/awais/instagrabber/repositories/GifRepository.java new file mode 100644 index 00000000..cf99e59f --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/GifRepository.java @@ -0,0 +1,14 @@ +package awais.instagrabber.repositories; + +import awais.instagrabber.repositories.responses.giphy.GiphyGifResponse; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + +public interface GifRepository { + + @GET("/api/v1/creatives/story_media_search_keyed_format/") + Call searchGiphyGifs(@Query("request_surface") final String requestSurface, + @Query("q") final String query, + @Query("media_types") final String mediaTypes); +} diff --git a/app/src/main/java/awais/instagrabber/repositories/requests/directmessages/AnimatedMediaBroadcastOptions.java b/app/src/main/java/awais/instagrabber/repositories/requests/directmessages/AnimatedMediaBroadcastOptions.java new file mode 100644 index 00000000..8b71f194 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/requests/directmessages/AnimatedMediaBroadcastOptions.java @@ -0,0 +1,27 @@ +package awais.instagrabber.repositories.requests.directmessages; + +import java.util.HashMap; +import java.util.Map; + +import awais.instagrabber.models.enums.BroadcastItemType; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; + +public class AnimatedMediaBroadcastOptions extends BroadcastOptions { + + private final GiphyGif giphyGif; + + public AnimatedMediaBroadcastOptions(final String clientContext, + final ThreadIdOrUserIds threadIdOrUserIds, + final GiphyGif giphyGif) { + super(clientContext, threadIdOrUserIds, BroadcastItemType.ANIMATED_MEDIA); + this.giphyGif = giphyGif; + } + + @Override + public Map getFormMap() { + final Map form = new HashMap<>(); + form.put("is_sticker", String.valueOf(giphyGif.isSticker())); + form.put("id", giphyGif.getId()); + return form; + } +} 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 3d19077e..1be7b245 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/User.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/User.java @@ -194,17 +194,40 @@ public class User implements Serializable { return profileContextLinksWithUserIds; } + @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..35f8994d 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 @@ -2,8 +2,12 @@ package awais.instagrabber.repositories.responses.directmessages; import androidx.annotation.NonNull; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Date; import java.util.List; +import java.util.Objects; import awais.instagrabber.models.enums.DirectItemType; import awais.instagrabber.repositories.responses.Location; @@ -40,6 +44,7 @@ public class DirectItem implements Cloneable { private Date date; private boolean isPending; private boolean showForwardAttribution; + private LocalDateTime localDateTime; public DirectItem(final String itemId, final long userId, @@ -213,6 +218,13 @@ public class DirectItem implements Cloneable { return date; } + public LocalDateTime getLocalDateTime() { + if (localDateTime == null) { + localDateTime = Instant.ofEpochMilli(timestamp / 1000).atZone(ZoneId.systemDefault()).toLocalDateTime();; + } + return localDateTime; + } + public void setItemId(final String itemId) { this.itemId = itemId; } @@ -239,21 +251,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/repositories/responses/giphy/GiphyGif.java b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGif.java new file mode 100644 index 00000000..53332f3e --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGif.java @@ -0,0 +1,70 @@ +package awais.instagrabber.repositories.responses.giphy; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +public class GiphyGif { + private final String type; + private final String id; + private final String title; + private final int isSticker; + private final GiphyGifImages images; + + public GiphyGif(final String type, final String id, final String title, final int isSticker, final GiphyGifImages images) { + this.type = type; + this.id = id; + this.title = title; + this.isSticker = isSticker; + this.images = images; + } + + public String getType() { + return type; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public boolean isSticker() { + return isSticker == 1; + } + + public GiphyGifImages getImages() { + return images; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GiphyGif giphyGif = (GiphyGif) o; + return isSticker == giphyGif.isSticker && + Objects.equals(type, giphyGif.type) && + Objects.equals(id, giphyGif.id) && + Objects.equals(title, giphyGif.title) && + Objects.equals(images, giphyGif.images); + } + + @Override + public int hashCode() { + return Objects.hash(type, id, title, isSticker, images); + } + + @NonNull + @Override + public String toString() { + return "GiphyGif{" + + "type='" + type + '\'' + + ", id='" + id + '\'' + + ", title='" + title + '\'' + + ", isSticker=" + isSticker() + + ", images=" + images + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImage.java b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImage.java new file mode 100644 index 00000000..d3659fe4 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImage.java @@ -0,0 +1,59 @@ +package awais.instagrabber.repositories.responses.giphy; + +import java.util.Objects; + +public class GiphyGifImage { + private final int height; + private final int width; + private final long webpSize; + private final String webp; + + public GiphyGifImage(final int height, final int width, final long webpSize, final String webp) { + this.height = height; + this.width = width; + this.webpSize = webpSize; + this.webp = webp; + } + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + public long getWebpSize() { + return webpSize; + } + + 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 GiphyGifImage that = (GiphyGifImage) o; + return height == that.height && + width == that.width && + webpSize == that.webpSize && + Objects.equals(webp, that.webp); + } + + @Override + public int hashCode() { + return Objects.hash(height, width, webpSize, webp); + } + + @Override + public String toString() { + return "GiphyGifImage{" + + "height=" + height + + ", width=" + width + + ", webpSize=" + webpSize + + ", webp='" + webp + '\'' + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImages.java b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImages.java new file mode 100644 index 00000000..230b17a1 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImages.java @@ -0,0 +1,40 @@ +package awais.instagrabber.repositories.responses.giphy; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +import awais.instagrabber.repositories.responses.AnimatedMediaFixedHeight; + +public class GiphyGifImages { + private final AnimatedMediaFixedHeight fixedHeight; + + public GiphyGifImages(final AnimatedMediaFixedHeight fixedHeight) { + this.fixedHeight = fixedHeight; + } + + 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 GiphyGifImages that = (GiphyGifImages) o; + return Objects.equals(fixedHeight, that.fixedHeight); + } + + @Override + public int hashCode() { + return Objects.hash(fixedHeight); + } + + @NonNull + @Override + public String toString() { + return "GiphyGifImages{" + + "fixedHeight=" + fixedHeight + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResponse.java new file mode 100644 index 00000000..d9fa5d0c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResponse.java @@ -0,0 +1,46 @@ +package awais.instagrabber.repositories.responses.giphy; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +public class GiphyGifResponse { + private final GiphyGifResults results; + private final String status; + + public GiphyGifResponse(final GiphyGifResults results, final String status) { + this.results = results; + this.status = status; + } + + public GiphyGifResults getResults() { + return results; + } + + public String getStatus() { + return status; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GiphyGifResponse that = (GiphyGifResponse) o; + return Objects.equals(results, that.results) && + Objects.equals(status, that.status); + } + + @Override + public int hashCode() { + return Objects.hash(results, status); + } + + @NonNull + @Override + public String toString() { + return "GiphyGifResponse{" + + "results=" + results + + ", status='" + status + '\'' + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResults.java b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResults.java new file mode 100644 index 00000000..3f6fd94d --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResults.java @@ -0,0 +1,47 @@ +package awais.instagrabber.repositories.responses.giphy; + +import androidx.annotation.NonNull; + +import java.util.List; +import java.util.Objects; + +public class GiphyGifResults { + private final List giphyGifs; + private final List giphy; + + public GiphyGifResults(final List giphyGifs, final List giphy) { + this.giphyGifs = giphyGifs; + this.giphy = giphy; + } + + public List getGiphyGifs() { + return giphyGifs; + } + + public List getGiphy() { + return giphy; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GiphyGifResults that = (GiphyGifResults) o; + return Objects.equals(giphyGifs, that.giphyGifs) && + Objects.equals(giphy, that.giphy); + } + + @Override + public int hashCode() { + return Objects.hash(giphyGifs, giphy); + } + + @NonNull + @Override + public String toString() { + return "GiphyGifResults{" + + "giphyGifs=" + giphyGifs + + ", giphy=" + giphy + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java b/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java index bdadac87..03f2c9b2 100644 --- a/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java +++ b/app/src/main/java/awais/instagrabber/services/ActivityCheckerService.java @@ -126,6 +126,6 @@ public class ActivityCheckerService extends Service { final Intent intent = new Intent(getApplicationContext(), MainActivity.class) .setAction(Constants.ACTION_SHOW_ACTIVITY) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); - return PendingIntent.getActivity(getApplicationContext(), 1738, intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getActivity(getApplicationContext(), Constants.SHOW_ACTIVITY_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT); } } diff --git a/app/src/main/java/awais/instagrabber/services/BootCompletedReceiver.java b/app/src/main/java/awais/instagrabber/services/BootCompletedReceiver.java new file mode 100644 index 00000000..8594d8b3 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/services/BootCompletedReceiver.java @@ -0,0 +1,27 @@ +package awais.instagrabber.services; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import java.util.Objects; + +import awais.instagrabber.fragments.settings.PreferenceKeys; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.TextUtils; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class BootCompletedReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, final Intent intent) { + if (!Objects.equals(intent.getAction(), "android.intent.action.BOOT_COMPLETED")) return; + final boolean enabled = settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH); + if (!enabled) return; + final String cookie = settingsHelper.getString(Constants.COOKIE); + final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0; + if (!isLoggedIn) return; + DMSyncAlarmReceiver.setAlarm(context); + } +} diff --git a/app/src/main/java/awais/instagrabber/services/DMSyncAlarmReceiver.java b/app/src/main/java/awais/instagrabber/services/DMSyncAlarmReceiver.java new file mode 100644 index 00000000..aec895b9 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/services/DMSyncAlarmReceiver.java @@ -0,0 +1,87 @@ +package awais.instagrabber.services; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; + +import awais.instagrabber.fragments.settings.PreferenceKeys; +import awais.instagrabber.utils.Constants; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class DMSyncAlarmReceiver extends BroadcastReceiver { + private static final String TAG = DMSyncAlarmReceiver.class.getSimpleName(); + + @Override + public void onReceive(final Context context, final Intent intent) { + final boolean enabled = settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH); + if (!enabled) { + // If somehow the alarm was triggered even when auto refresh is disabled + cancelAlarm(context); + return; + } + try { + final Context applicationContext = context.getApplicationContext(); + ContextCompat.startForegroundService(applicationContext, new Intent(applicationContext, DMSyncService.class)); + } catch (Exception e) { + Log.e(TAG, "onReceive: ", e); + } + } + + public static void setAlarm(@NonNull final Context context) { + Log.d(TAG, "setting DMSyncService Alarm"); + final AlarmManager alarmManager = getAlarmManager(context); + if (alarmManager == null) return; + final PendingIntent pendingIntent = getPendingIntent(context); + alarmManager.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(), getIntervalMillis(), pendingIntent); + } + + public static void cancelAlarm(@NonNull final Context context) { + Log.d(TAG, "cancelling DMSyncService Alarm"); + final AlarmManager alarmManager = getAlarmManager(context); + if (alarmManager == null) return; + final PendingIntent pendingIntent = getPendingIntent(context); + alarmManager.cancel(pendingIntent); + } + + private static AlarmManager getAlarmManager(@NonNull final Context context) { + return (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE); + } + + private static PendingIntent getPendingIntent(@NonNull final Context context) { + final Context applicationContext = context.getApplicationContext(); + final Intent intent = new Intent(applicationContext, DMSyncAlarmReceiver.class); + return PendingIntent.getBroadcast(applicationContext, + Constants.DM_SYNC_SERVICE_REQUEST_CODE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + private static long getIntervalMillis() { + int amount = settingsHelper.getInteger(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER); + if (amount <= 0) { + amount = 30; + } + final String unit = settingsHelper.getString(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT); + final TemporalUnit temporalUnit; + switch (unit) { + case "mins": + temporalUnit = ChronoUnit.MINUTES; + break; + default: + case "secs": + temporalUnit = ChronoUnit.SECONDS; + } + return Duration.of(amount, temporalUnit).toMillis(); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/services/DMSyncService.java b/app/src/main/java/awais/instagrabber/services/DMSyncService.java new file mode 100644 index 00000000..f885a559 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/services/DMSyncService.java @@ -0,0 +1,259 @@ +package awais.instagrabber.services; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.lifecycle.LifecycleService; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import awais.instagrabber.R; +import awais.instagrabber.activities.MainActivity; +import awais.instagrabber.db.datasources.DMLastNotifiedDataSource; +import awais.instagrabber.db.entities.DMLastNotified; +import awais.instagrabber.db.repositories.DMLastNotifiedRepository; +import awais.instagrabber.db.repositories.RepositoryCallback; +import awais.instagrabber.fragments.settings.PreferenceKeys; +import awais.instagrabber.managers.DirectMessagesManager; +import awais.instagrabber.managers.InboxManager; +import awais.instagrabber.models.Resource; +import awais.instagrabber.repositories.responses.directmessages.DirectInbox; +import awais.instagrabber.repositories.responses.directmessages.DirectItem; +import awais.instagrabber.repositories.responses.directmessages.DirectThread; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.DMUtils; +import awais.instagrabber.utils.DateUtils; +import awais.instagrabber.utils.TextUtils; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class DMSyncService extends LifecycleService { + private static final String TAG = DMSyncService.class.getSimpleName(); + + private InboxManager inboxManager; + private DMLastNotifiedRepository dmLastNotifiedRepository; + private Map dmLastNotifiedMap; + + @Override + public void onCreate() { + super.onCreate(); + startForeground(Constants.DM_CHECK_NOTIFICATION_ID, buildForegroundNotification()); + Log.d(TAG, "onCreate: Service created"); + final DirectMessagesManager directMessagesManager = DirectMessagesManager.getInstance(); + inboxManager = directMessagesManager.getInboxManager(); + dmLastNotifiedRepository = DMLastNotifiedRepository.getInstance(DMLastNotifiedDataSource.getInstance(getApplicationContext())); + } + + private void parseUnread(@NonNull final DirectInbox directInbox) { + dmLastNotifiedRepository.getAllDMDmLastNotified(new RepositoryCallback>() { + @Override + public void onSuccess(final List result) { + dmLastNotifiedMap = result != null + ? result.stream().collect(Collectors.toMap(DMLastNotified::getThreadId, Function.identity())) + : Collections.emptyMap(); + parseUnreadActual(directInbox); + } + + @Override + public void onDataNotAvailable() { + dmLastNotifiedMap = Collections.emptyMap(); + parseUnreadActual(directInbox); + } + }); + // Log.d(TAG, "inbox observer: " + directInbox); + } + + private void parseUnreadActual(@NonNull final DirectInbox directInbox) { + final List threads = directInbox.getThreads(); + final ImmutableMap.Builder> unreadMessagesMapBuilder = ImmutableMap.builder(); + if (threads == null) { + stopSelf(); + return; + } + for (final DirectThread thread : threads) { + if (thread.isMuted()) continue; + final boolean read = DMUtils.isRead(thread); + if (read) continue; + final List unreadMessages = getUnreadMessages(thread); + if (unreadMessages.isEmpty()) continue; + unreadMessagesMapBuilder.put(thread.getThreadId(), unreadMessages); + } + final Map> unreadMessagesMap = unreadMessagesMapBuilder.build(); + if (unreadMessagesMap.isEmpty()) { + stopSelf(); + return; + } + showNotification(directInbox, unreadMessagesMap); + final LocalDateTime now = LocalDateTime.now(); + // Update db + final ImmutableList.Builder lastNotifiedListBuilder = ImmutableList.builder(); + for (final Map.Entry> unreadMessagesEntry : unreadMessagesMap.entrySet()) { + final List unreadItems = unreadMessagesEntry.getValue(); + final DirectItem latestItem = unreadItems.get(unreadItems.size() - 1); + lastNotifiedListBuilder.add(new DMLastNotified(0, + unreadMessagesEntry.getKey(), + latestItem.getLocalDateTime(), + now)); + } + dmLastNotifiedRepository.insertOrUpdateDMLastNotified( + lastNotifiedListBuilder.build(), + new RepositoryCallback() { + @Override + public void onSuccess(final Void result) { + stopSelf(); + } + + @Override + public void onDataNotAvailable() { + stopSelf(); + } + } + ); + } + + @NonNull + private List getUnreadMessages(@NonNull final DirectThread thread) { + final List items = thread.getItems(); + if (items == null) return Collections.emptyList(); + final DMLastNotified dmLastNotified = dmLastNotifiedMap.get(thread.getThreadId()); + final long viewerId = thread.getViewerId(); + final Map lastSeenAt = thread.getLastSeenAt(); + final ImmutableList.Builder unreadListBuilder = ImmutableList.builder(); + int count = 0; + for (final DirectItem item : items) { + if (item == null) continue; + if (item.getUserId() == viewerId) break; // Reached a message from the viewer, it is assumed the viewer has read the next messages + final boolean read = DMUtils.isRead(item, lastSeenAt, Collections.singletonList(viewerId)); + if (read) break; + if (dmLastNotified != null && dmLastNotified.getLastNotifiedMsgTs() != null) { + if (count == 0 && DateUtils.isBeforeOrEqual(item.getLocalDateTime(), dmLastNotified.getLastNotifiedMsgTs())) { + // The first unread item has been notified and hence all subsequent items can be ignored + // since the items are in desc timestamp order + break; + } + } + unreadListBuilder.add(item); + count++; + // Inbox style notification only allows 6 lines + if (count >= 6) break; + } + // Reversing, so that oldest messages are on top + return unreadListBuilder.build().reverse(); + } + + private void showNotification(final DirectInbox directInbox, + final Map> unreadMessagesMap) { + final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) return; + for (final Map.Entry> unreadMessagesEntry : unreadMessagesMap.entrySet()) { + final Optional directThreadOptional = getThread(directInbox, unreadMessagesEntry.getKey()); + if (!directThreadOptional.isPresent()) continue; + final DirectThread thread = directThreadOptional.get(); + final DirectItem firstDirectItem = thread.getFirstDirectItem(); + if (firstDirectItem == null) continue; + final List unreadMessages = unreadMessagesEntry.getValue(); + final NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); + inboxStyle.setBigContentTitle(thread.getThreadTitle()); + for (final DirectItem item : unreadMessages) { + inboxStyle.addLine(DMUtils.getMessageString(thread, getResources(), thread.getViewerId(), item)); + } + final Notification notification = new NotificationCompat.Builder(this, Constants.DM_UNREAD_CHANNEL_ID) + .setStyle(inboxStyle) + .setSmallIcon(R.drawable.ic_round_mode_comment_24) + .setContentTitle(thread.getThreadTitle()) + .setContentText(DMUtils.getMessageString(thread, getResources(), thread.getViewerId(), firstDirectItem)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setGroup(Constants.GROUP_KEY_DM) + .setAutoCancel(true) + .setContentIntent(getThreadPendingIntent(thread.getThreadId(), thread.getThreadTitle())) + .build(); + notificationManager.notify(Constants.DM_UNREAD_PARENT_NOTIFICATION_ID, notification); + } + } + + private Optional getThread(@NonNull final DirectInbox directInbox, final String threadId) { + return directInbox.getThreads() + .stream() + .filter(thread -> Objects.equals(thread.getThreadId(), threadId)) + .findFirst(); + } + + @NonNull + private PendingIntent getThreadPendingIntent(final String threadId, final String threadTitle) { + final Intent intent = new Intent(getApplicationContext(), MainActivity.class) + .setAction(Constants.ACTION_SHOW_DM_THREAD) + .putExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_ID, threadId) + .putExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_TITLE, threadTitle) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); + return PendingIntent.getActivity(getApplicationContext(), Constants.SHOW_DM_THREAD, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + @Override + public int onStartCommand(final Intent intent, final int flags, final int startId) { + super.onStartCommand(intent, flags, startId); + final String cookie = settingsHelper.getString(Constants.COOKIE); + final boolean isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != 0; + if (!isLoggedIn) { + stopSelf(); + return START_NOT_STICKY; + } + // Need to setup here if service was started by the boot completed receiver + CookieUtils.setupCookies(cookie); + final boolean notificationsEnabled = settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_NOTIFICATIONS); + inboxManager.getInbox().observe(this, inboxResource -> { + if (!notificationsEnabled || inboxResource == null || inboxResource.status != Resource.Status.SUCCESS) { + stopSelf(); + return; + } + final DirectInbox directInbox = inboxResource.data; + if (directInbox == null) { + stopSelf(); + return; + } + parseUnread(directInbox); + }); + Log.d(TAG, "onStartCommand: refreshing inbox"); + inboxManager.refresh(); + return START_NOT_STICKY; + } + + @Override + public IBinder onBind(@NonNull final Intent intent) { + super.onBind(intent); + return null; + } + + private Notification buildForegroundNotification() { + final Resources resources = getResources(); + return new NotificationCompat.Builder(this, Constants.SILENT_NOTIFICATIONS_CHANNEL_ID) + .setOngoing(true) + .setSound(null) + .setContentTitle(resources.getString(R.string.app_name)) + .setContentText(resources.getString(R.string.checking_for_new_messages)) + .setSmallIcon(R.mipmap.ic_launcher) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setGroup(Constants.GROUP_KEY_SILENT_NOTIFICATIONS) + .build(); + } +} \ No newline at end of file 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/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index 1b06283c..62e5bf5a 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -58,6 +58,8 @@ public final class Constants { // Notification ids public static final int ACTIVITY_NOTIFICATION_ID = 10; + public static final int DM_UNREAD_PARENT_NOTIFICATION_ID = 20; + public static final int DM_CHECK_NOTIFICATION_ID = 11; // see https://github.com/dilame/instagram-private-api/blob/master/src/core/constants.ts public static final String SUPPORTED_CAPABILITIES = "[ { \"name\": \"SUPPORTED_SDK_VERSIONS\", \"value\":" + @@ -74,12 +76,6 @@ public final class Constants { public static final String FDROID_SHA1_FINGERPRINT = "C1661EB8FD09F618307E687786D5E5056F65084D"; public static final String SKIPPED_VERSION = "skipped_version"; public static final String DEFAULT_TAB = "default_tab"; - public static final String ACTIVITY_CHANNEL_ID = "activity"; - public static final String DOWNLOAD_CHANNEL_ID = "download"; - public static final String ACTIVITY_CHANNEL_NAME = "Activity"; - public static final String DOWNLOAD_CHANNEL_NAME = "Downloads"; - public static final String NOTIF_GROUP_NAME = "awais.instagrabber.InstaNotif"; - public static final String ACTION_SHOW_ACTIVITY = "show_activity"; public static final String PREF_DARK_THEME = "dark_theme"; public static final String PREF_LIGHT_THEME = "light_theme"; public static final String DEFAULT_HASH_TAG_PIC = "https://www.instagram.com/static/images/hashtag/search-hashtag-default-avatar.png/1d8417c9a4f5.png"; @@ -94,4 +90,27 @@ public final class Constants { public static final String PREF_SAVED_POSTS_LAYOUT = "saved_posts_layout"; public static final String PREF_EMOJI_VARIANTS = "emoji_variants"; public static final String PREF_REACTIONS = "reactions"; + + public static final String ACTIVITY_CHANNEL_ID = "activity"; + public static final String ACTIVITY_CHANNEL_NAME = "Activity"; + public static final String DOWNLOAD_CHANNEL_ID = "download"; + public static final String DOWNLOAD_CHANNEL_NAME = "Downloads"; + public static final String DM_UNREAD_CHANNEL_ID = "dmUnread"; + public static final String DM_UNREAD_CHANNEL_NAME = "Messages"; + public static final String SILENT_NOTIFICATIONS_CHANNEL_ID = "silentNotifications"; + public static final String SILENT_NOTIFICATIONS_CHANNEL_NAME = "Silent notifications"; + + public static final String NOTIF_GROUP_NAME = "awais.instagrabber.InstaNotif"; + public static final String GROUP_KEY_DM = "awais.instagrabber.MESSAGES"; + public static final String GROUP_KEY_SILENT_NOTIFICATIONS = "awais.instagrabber.SILENT_NOTIFICATIONS"; + + public static final int SHOW_ACTIVITY_REQUEST_CODE = 1738; + public static final int SHOW_DM_THREAD = 2000; + public static final int DM_SYNC_SERVICE_REQUEST_CODE = 3000; + + public static final String ACTION_SHOW_ACTIVITY = "show_activity"; + public static final String ACTION_SHOW_DM_THREAD = "show_dm_thread"; + + public static final String DM_THREAD_ACTION_EXTRA_THREAD_ID = "thread_id"; + public static final String DM_THREAD_ACTION_EXTRA_THREAD_TITLE = "thread_title"; } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/DMUtils.java b/app/src/main/java/awais/instagrabber/utils/DMUtils.java new file mode 100644 index 00000000..81429ff7 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/DMUtils.java @@ -0,0 +1,285 @@ +package awais.instagrabber.utils; + +import android.content.res.Resources; + +import androidx.annotation.NonNull; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import awais.instagrabber.R; +import awais.instagrabber.models.enums.DirectItemType; +import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.repositories.responses.directmessages.DirectItem; +import awais.instagrabber.repositories.responses.directmessages.DirectItemAnimatedMedia; +import awais.instagrabber.repositories.responses.directmessages.DirectItemReelShare; +import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia; +import awais.instagrabber.repositories.responses.directmessages.DirectThread; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadLastSeenAt; +import awais.instagrabber.repositories.responses.directmessages.RavenExpiringMediaActionSummary; + +public final class DMUtils { + public static boolean isRead(final DirectItem item, + @NonNull final Map lastSeenAt, + final List userIdsToCheck) { + // Further check if directStory exists + // 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 boolean isRead(@NonNull final DirectThread thread) { + final boolean read; + if (thread.getDirectStory() != null) { + return false; + } + final DirectItem item = thread.getFirstDirectItem(); + final long viewerId = thread.getViewerId(); + if (item != null && item.getUserId() == viewerId) { + // 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 = isRead(item, lastSeenAtMap, Collections.singletonList(viewerId)); + } + return read; + } + + public static String getMessageString(@NonNull final DirectThread thread, + final Resources resources, + final long viewerId, + final DirectItem item) { + final long senderId = item.getUserId(); + final DirectItemType itemType = item.getItemType(); + String subtitle = null; + final String username = getUsername(thread.getUsers(), senderId, viewerId, resources); + String message = ""; + if (itemType == null) { + message = resources.getString(R.string.dms_inbox_raven_message_unknown); + } else { + switch (itemType) { + case TEXT: + message = item.getText(); + break; + case LIKE: + message = item.getLike(); + break; + case LINK: + message = item.getLink().getText(); + break; + case PLACEHOLDER: + message = item.getPlaceholder().getMessage(); + break; + case MEDIA_SHARE: + final User mediaShareUser = item.getMediaShare().getUser(); + subtitle = resources.getString(R.string.dms_inbox_shared_post, + username != null ? username : "", + mediaShareUser == null ? "" : mediaShareUser.getUsername()); + break; + case ANIMATED_MEDIA: + final DirectItemAnimatedMedia animatedMedia = item.getAnimatedMedia(); + subtitle = resources.getString(animatedMedia.isSticker() ? R.string.dms_inbox_shared_sticker + : R.string.dms_inbox_shared_gif, + username != null ? username : ""); + break; + case PROFILE: + subtitle = resources + .getString(R.string.dms_inbox_shared_profile, username != null ? username : "", item.getProfile().getUsername()); + break; + case LOCATION: + subtitle = resources + .getString(R.string.dms_inbox_shared_location, username != null ? username : "", item.getLocation().getName()); + break; + case MEDIA: { + final MediaItemType mediaType = item.getMedia().getMediaType(); + subtitle = getMediaSpecificSubtitle(username, resources, mediaType); + break; + } + case STORY_SHARE: { + final String reelType = item.getStoryShare().getReelType(); + if (reelType == null) { + subtitle = item.getStoryShare().getTitle(); + } else { + final int format = reelType.equals("highlight_reel") + ? R.string.dms_inbox_shared_highlight + : R.string.dms_inbox_shared_story; + final User storyShareMediaUser = item.getStoryShare().getMedia().getUser(); + subtitle = resources.getString(format, + username != null ? username : "", + storyShareMediaUser == null ? "" : storyShareMediaUser.getUsername()); + } + break; + } + case VOICE_MEDIA: + subtitle = resources.getString(R.string.dms_inbox_shared_voice, username != null ? username : ""); + break; + case ACTION_LOG: + subtitle = item.getActionLog().getDescription(); + break; + case VIDEO_CALL_EVENT: + subtitle = item.getVideoCallEvent().getDescription(); + break; + case CLIP: + final User clipUser = item.getClip().getClip().getUser(); + subtitle = resources.getString(R.string.dms_inbox_shared_clip, + username != null ? username : "", + clipUser == null ? "" : clipUser.getUsername()); + break; + case FELIX_SHARE: + final User felixShareVideoUser = item.getFelixShare().getVideo().getUser(); + subtitle = resources.getString(R.string.dms_inbox_shared_igtv, + username != null ? username : "", + felixShareVideoUser == null ? "" : felixShareVideoUser.getUsername()); + break; + case RAVEN_MEDIA: + subtitle = getRavenMediaSubtitle(item, resources, username); + break; + case REEL_SHARE: + final DirectItemReelShare reelShare = item.getReelShare(); + if (reelShare == null) { + subtitle = ""; + break; + } + final String reelType = reelShare.getType(); + switch (reelType) { + case "reply": + if (viewerId == item.getUserId()) { + subtitle = resources.getString(R.string.dms_inbox_replied_story_outgoing, reelShare.getText()); + } else { + subtitle = resources + .getString(R.string.dms_inbox_replied_story_incoming, username != null ? username : "", reelShare.getText()); + } + break; + case "mention": + if (viewerId == item.getUserId()) { + // You mentioned the other person + final long mentionedUserId = item.getReelShare().getMentionedUserId(); + final String otherUsername = getUsername(thread.getUsers(), mentionedUserId, viewerId, resources); + subtitle = resources.getString(R.string.dms_inbox_mentioned_story_outgoing, otherUsername); + } else { + // They mentioned you + subtitle = resources.getString(R.string.dms_inbox_mentioned_story_incoming, username != null ? username : ""); + } + break; + case "reaction": + if (viewerId == item.getUserId()) { + subtitle = resources.getString(R.string.dms_inbox_reacted_story_outgoing, reelShare.getText()); + } else { + subtitle = resources + .getString(R.string.dms_inbox_reacted_story_incoming, username != null ? username : "", reelShare.getText()); + } + break; + default: + subtitle = ""; + break; + } + break; + default: + message = resources.getString(R.string.dms_inbox_raven_message_unknown); + } + } + if (subtitle == null) { + if (thread.isGroup() || (!thread.isGroup() && senderId == viewerId)) { + subtitle = String.format("%s: %s", username != null ? username : "", message); + } else { + subtitle = message; + } + } + return subtitle; + } + + public static String getUsername(final List users, + final long userId, + final long viewerId, + final Resources resources) { + if (userId == viewerId) { + return resources.getString(R.string.you); + } + final Optional senderOptional = users.stream() + .filter(Objects::nonNull) + .filter(user -> user.getPk() == userId) + .findFirst(); + return senderOptional.map(User::getUsername).orElse(null); + } + + public static String getMediaSpecificSubtitle(final String username, final Resources resources, final MediaItemType mediaType) { + final String userSharedAnImage = resources.getString(R.string.dms_inbox_shared_image, username != null ? username : ""); + final String userSharedAVideo = resources.getString(R.string.dms_inbox_shared_video, username != null ? username : ""); + final String userSentAMessage = resources.getString(R.string.dms_inbox_shared_message, username != null ? username : ""); + String subtitle; + switch (mediaType) { + case MEDIA_TYPE_IMAGE: + subtitle = userSharedAnImage; + break; + case MEDIA_TYPE_VIDEO: + subtitle = userSharedAVideo; + break; + default: + subtitle = userSentAMessage; + break; + } + return subtitle; + } + + private static String getRavenMediaSubtitle(final DirectItem item, + final Resources resources, + final String username) { + String subtitle = "↗ "; + final DirectItemVisualMedia visualMedia = item.getVisualMedia(); + final RavenExpiringMediaActionSummary summary = visualMedia.getExpiringMediaActionSummary(); + if (summary != null) { + final RavenExpiringMediaActionSummary.ActionType expiringMediaType = summary.getType(); + int textRes = 0; + switch (expiringMediaType) { + case DELIVERED: + textRes = R.string.dms_inbox_raven_media_delivered; + break; + case SENT: + textRes = R.string.dms_inbox_raven_media_sent; + break; + case OPENED: + textRes = R.string.dms_inbox_raven_media_opened; + break; + case REPLAYED: + textRes = R.string.dms_inbox_raven_media_replayed; + break; + case SENDING: + textRes = R.string.dms_inbox_raven_media_sending; + break; + case BLOCKED: + textRes = R.string.dms_inbox_raven_media_blocked; + break; + case SUGGESTED: + textRes = R.string.dms_inbox_raven_media_suggested; + break; + case SCREENSHOT: + textRes = R.string.dms_inbox_raven_media_screenshot; + break; + case CANNOT_DELIVER: + textRes = R.string.dms_inbox_raven_media_cant_deliver; + break; + } + if (textRes > 0) { + subtitle += resources.getString(textRes); + } + return subtitle; + } + final MediaItemType mediaType = visualMedia.getMedia().getMediaType(); + subtitle = getMediaSpecificSubtitle(username, resources, mediaType); + return subtitle; + } +} diff --git a/app/src/main/java/awais/instagrabber/utils/DateUtils.java b/app/src/main/java/awais/instagrabber/utils/DateUtils.java index 648fbc59..6f8fdcd1 100644 --- a/app/src/main/java/awais/instagrabber/utils/DateUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/DateUtils.java @@ -2,6 +2,7 @@ package awais.instagrabber.utils; import androidx.annotation.NonNull; +import java.time.LocalDateTime; import java.util.Calendar; import java.util.Date; import java.util.Locale; @@ -34,4 +35,8 @@ public final class DateUtils { final Calendar calendar = Calendar.getInstance(Locale.getDefault()); return -(calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000); } + + public static boolean isBeforeOrEqual(final LocalDateTime localDateTime, final LocalDateTime comparedTo) { + return localDateTime.isBefore(comparedTo) || localDateTime.isEqual(comparedTo); + } } diff --git a/app/src/main/java/awais/instagrabber/utils/DirectItemFactory.java b/app/src/main/java/awais/instagrabber/utils/DirectItemFactory.java index 4b319032..85e6f9ae 100644 --- a/app/src/main/java/awais/instagrabber/utils/DirectItemFactory.java +++ b/app/src/main/java/awais/instagrabber/utils/DirectItemFactory.java @@ -8,13 +8,16 @@ import java.util.UUID; import awais.instagrabber.models.enums.DirectItemType; import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.repositories.responses.AnimatedMediaImages; import awais.instagrabber.repositories.responses.Audio; import awais.instagrabber.repositories.responses.ImageVersions2; import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.MediaCandidate; import awais.instagrabber.repositories.responses.VideoVersion; import awais.instagrabber.repositories.responses.directmessages.DirectItem; +import awais.instagrabber.repositories.responses.directmessages.DirectItemAnimatedMedia; import awais.instagrabber.repositories.responses.directmessages.DirectItemVoiceMedia; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; public final class DirectItemFactory { @@ -213,4 +216,45 @@ public final class DirectItemFactory { 0, false); } + + public static DirectItem createAnimatedMedia(final long userId, + final String clientContext, + final GiphyGif giphyGif) { + final AnimatedMediaImages animatedImages = new AnimatedMediaImages(giphyGif.getImages().getFixedHeight()); + final DirectItemAnimatedMedia animateMedia = new DirectItemAnimatedMedia( + giphyGif.getId(), + animatedImages, + false, + giphyGif.isSticker() + ); + return new DirectItem( + UUID.randomUUID().toString(), + userId, + System.currentTimeMillis() * 1000, + DirectItemType.ANIMATED_MEDIA, + null, + null, + null, + clientContext, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + animateMedia, + null, + null, + null, + null, + 0, + false + ); + } } diff --git a/app/src/main/java/awais/instagrabber/utils/FlavorTown.java b/app/src/main/java/awais/instagrabber/utils/FlavorTown.java index de4f7cb6..ee859700 100755 --- a/app/src/main/java/awais/instagrabber/utils/FlavorTown.java +++ b/app/src/main/java/awais/instagrabber/utils/FlavorTown.java @@ -20,7 +20,6 @@ import androidx.appcompat.app.AppCompatActivity; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Locale; import java.util.concurrent.ThreadLocalRandom; import javax.security.cert.CertificateException; @@ -106,11 +105,11 @@ public final class FlavorTown { if (settingsHelper.getInteger(Constants.PREV_INSTALL_VERSION) < BuildConfig.VERSION_CODE) { int appUaCode = settingsHelper.getInteger(Constants.APP_UA_CODE); int browserUaCode = settingsHelper.getInteger(Constants.BROWSER_UA_CODE); - if (browserUaCode == -1) { + if (browserUaCode == -1 || browserUaCode >= UserAgentUtils.browsers.length) { browserUaCode = ThreadLocalRandom.current().nextInt(0, UserAgentUtils.browsers.length); settingsHelper.putInteger(Constants.BROWSER_UA_CODE, browserUaCode); } - if (appUaCode == -1) { + if (appUaCode == -1 || appUaCode >= UserAgentUtils.devices.length) { appUaCode = ThreadLocalRandom.current().nextInt(0, UserAgentUtils.devices.length); settingsHelper.putInteger(Constants.APP_UA_CODE, appUaCode); } diff --git a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java index f1281d76..77cc3502 100644 --- a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java @@ -13,7 +13,6 @@ import org.json.JSONObject; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import awais.instagrabber.BuildConfig; import awais.instagrabber.models.StoryModel; @@ -31,9 +30,6 @@ import awais.instagrabber.repositories.responses.Media; 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; public final class ResponseBodyUtils { @@ -1125,36 +1121,15 @@ public final class ResponseBodyUtils { return candidate.getUrl(); } - 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; - }); - // Further check if directStory exists - if (read && directStory != null) { - read = false; - } - return read; - } - 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/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index dd96f41e..e3426f99 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -8,6 +8,10 @@ import androidx.annotation.NonNull; import androidx.annotation.StringDef; import androidx.appcompat.app.AppCompatDelegate; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_NOTIFICATIONS; import static awais.instagrabber.utils.Constants.APP_LANGUAGE; import static awais.instagrabber.utils.Constants.APP_THEME; import static awais.instagrabber.utils.Constants.APP_UA; @@ -130,14 +134,14 @@ public final class SettingsHelper { CUSTOM_DATE_TIME_FORMAT, DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT, PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT, PREF_LOCATION_POSTS_LAYOUT, PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT, - STORY_SORT, PREF_EMOJI_VARIANTS, PREF_REACTIONS}) + STORY_SORT, PREF_EMOJI_VARIANTS, PREF_REACTIONS, PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT}) public @interface StringSettings {} @StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS, SHOW_CAPTIONS, CUSTOM_DATE_TIME_FORMAT_ENABLED, MARK_AS_SEEN, DM_MARK_AS_SEEN, CHECK_ACTIVITY, - CHECK_UPDATES, SWAP_DATE_TIME_FORMAT_ENABLED}) + CHECK_UPDATES, SWAP_DATE_TIME_FORMAT_ENABLED, PREF_ENABLE_DM_NOTIFICATIONS, PREF_ENABLE_DM_AUTO_REFRESH}) public @interface BooleanSettings {} - @StringDef({PREV_INSTALL_VERSION, BROWSER_UA_CODE, APP_UA_CODE}) + @StringDef({PREV_INSTALL_VERSION, BROWSER_UA_CODE, APP_UA_CODE, PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER}) public @interface IntegerSettings {} } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index 3e45a21c..b69c01d1 100644 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -10,6 +10,7 @@ import android.content.Intent; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.media.MediaScannerConnection; import android.media.MediaScannerConnection.OnScanCompletedListener; import android.net.Uri; @@ -28,10 +29,13 @@ import android.view.inputmethod.InputMethodManager; import android.webkit.MimeTypeMap; import android.widget.Toast; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; import com.google.android.exoplayer2.database.ExoDatabaseProvider; import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; @@ -347,4 +351,15 @@ public final class Utils { Log.e(TAG, "hideKeyboard: ", e); } } + + public static Drawable getAnimatableDrawable(@NonNull final Context context, + @DrawableRes final int drawableResId) { + final Drawable drawable; + if (Build.VERSION.SDK_INT >= 24) { + drawable = ContextCompat.getDrawable(context, drawableResId); + } else { + drawable = AnimatedVectorDrawableCompat.create(context, drawableResId); + } + return drawable; + } } 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..f2646504 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java @@ -5,610 +5,155 @@ 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.repositories.responses.giphy.GiphyGif; 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 +176,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 +208,26 @@ 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) { + return threadManager.unsend(item); } - 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> sendAnimatedMedia(@NonNull final GiphyGif giphyGif) { + return threadManager.sendAnimatedMedia(giphyGif); } 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 +252,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/GifPickerViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/GifPickerViewModel.java new file mode 100644 index 00000000..e78b19db --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/GifPickerViewModel.java @@ -0,0 +1,121 @@ +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 com.google.common.collect.ImmutableList; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import awais.instagrabber.models.Resource; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; +import awais.instagrabber.repositories.responses.giphy.GiphyGifResponse; +import awais.instagrabber.repositories.responses.giphy.GiphyGifResults; +import awais.instagrabber.webservices.GifService; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class GifPickerViewModel extends ViewModel { + private static final String TAG = GifPickerViewModel.class.getSimpleName(); + + private final MutableLiveData>> images = new MutableLiveData<>(Resource.success(Collections.emptyList())); + private final GifService gifService; + + private Call searchRequest; + + public GifPickerViewModel() { + gifService = GifService.getInstance(); + search(null); + } + + public LiveData>> getImages() { + return images; + } + + public void search(final String query) { + final Resource> currentValue = images.getValue(); + if (currentValue != null && currentValue.status == Resource.Status.LOADING) { + cancelSearchRequest(); + } + images.postValue(Resource.loading(getCurrentImages())); + searchRequest = gifService.searchGiphyGifs(query, query != null); + searchRequest.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (response.isSuccessful()) { + parseResponse(response); + 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); + images.postValue(Resource.error(msg, getCurrentImages())); + Log.e(TAG, msg); + } catch (IOException e) { + images.postValue(Resource.error(e.getMessage(), getCurrentImages())); + Log.e(TAG, "onResponse: ", e); + } + } + images.postValue(Resource.error("request was not successful and response error body was null", getCurrentImages())); + } + + @Override + public void onFailure(@NonNull final Call call, + @NonNull final Throwable t) { + images.postValue(Resource.error(t.getMessage(), getCurrentImages())); + Log.e(TAG, "enqueueRequest: onFailure: ", t); + } + }); + } + + private void parseResponse(final Response response) { + final GiphyGifResponse giphyGifResponse = response.body(); + if (giphyGifResponse == null) { + images.postValue(Resource.error("Response body was null", getCurrentImages())); + return; + } + final GiphyGifResults results = giphyGifResponse.getResults(); + images.postValue(Resource.success( + ImmutableList.builder() + .addAll(results.getGiphy() == null ? Collections.emptyList() : results.getGiphy()) + .addAll(results.getGiphyGifs() == null ? Collections.emptyList() : results.getGiphyGifs()) + .build() + )); + } + + // @NonNull + // private List getGiphyGifImages(@NonNull final List giphy) { + // return giphy.stream() + // .map(giphyGif -> { + // final GiphyGifImages images = giphyGif.getImages(); + // if (images == null) return null; + // return images.getOriginal(); + // }) + // .filter(Objects::nonNull) + // .collect(Collectors.toList()); + // } + + private List getCurrentImages() { + final Resource> value = images.getValue(); + return value == null ? Collections.emptyList() : value.data; + } + + public void cancelSearchRequest() { + if (searchRequest == null) return; + searchRequest.cancel(); + } +} 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/java/awais/instagrabber/webservices/DirectMessagesService.java b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java index 2b3670c9..3d70c240 100644 --- a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java @@ -17,6 +17,7 @@ import java.util.UUID; import java.util.stream.Collectors; import awais.instagrabber.repositories.DirectMessagesRepository; +import awais.instagrabber.repositories.requests.directmessages.AnimatedMediaBroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds; import awais.instagrabber.repositories.requests.directmessages.LinkBroadcastOptions; @@ -34,6 +35,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThreadDeta import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import retrofit2.Call; @@ -182,6 +184,11 @@ public class DirectMessagesService extends BaseService { return broadcast(new ReactionBroadcastOptions(clientContext, threadIdOrUserIds, itemId, emoji, delete)); } + public Call broadcastAnimatedMedia(final String clientContext, + final ThreadIdOrUserIds threadIdOrUserIds, + final GiphyGif giphyGif) { + return broadcast(new AnimatedMediaBroadcastOptions(clientContext, threadIdOrUserIds, giphyGif)); + } private Call broadcast(@NonNull final BroadcastOptions broadcastOptions) { if (TextUtils.isEmpty(broadcastOptions.getClientContext())) { diff --git a/app/src/main/java/awais/instagrabber/webservices/GifService.java b/app/src/main/java/awais/instagrabber/webservices/GifService.java new file mode 100644 index 00000000..6485efd1 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/GifService.java @@ -0,0 +1,33 @@ +package awais.instagrabber.webservices; + +import awais.instagrabber.repositories.GifRepository; +import awais.instagrabber.repositories.responses.giphy.GiphyGifResponse; +import retrofit2.Call; +import retrofit2.Retrofit; + +public class GifService extends BaseService { + + private final GifRepository repository; + + private static GifService instance; + + private GifService() { + final Retrofit retrofit = getRetrofitBuilder() + .baseUrl("https://i.instagram.com") + .build(); + repository = retrofit.create(GifRepository.class); + } + + public static GifService getInstance() { + if (instance == null) { + instance = new GifService(); + } + return instance; + } + + public Call searchGiphyGifs(final String query, + final boolean includeGifs) { + final String mediaTypes = includeGifs ? "[\"giphy_gifs\",\"giphy\"]" : "[\"giphy\"]"; + return repository.searchGiphyGifs("direct", query, mediaTypes); + } +} diff --git a/app/src/main/res/drawable/ic_round_gif_24.xml b/app/src/main/res/drawable/ic_round_gif_24.xml new file mode 100644 index 00000000..c2b5340c --- /dev/null +++ b/app/src/main/res/drawable/ic_round_gif_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_mode_comment_24.xml b/app/src/main/res/drawable/ic_round_mode_comment_24.xml new file mode 100644 index 00000000..366bca72 --- /dev/null +++ b/app/src/main/res/drawable/ic_round_mode_comment_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d4c86892..866770aa 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -29,6 +29,7 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" + android:background="?attr/colorSurface" app:layout_collapseMode="pin" app:title="@string/app_name" tools:menu="@menu/main_menu" /> diff --git a/app/src/main/res/layout/fragment_direct_messages_thread.xml b/app/src/main/res/layout/fragment_direct_messages_thread.xml index 79a1091c..56674d95 100644 --- a/app/src/main/res/layout/fragment_direct_messages_thread.xml +++ b/app/src/main/res/layout/fragment_direct_messages_thread.xml @@ -120,7 +120,8 @@ app:layout_constraintBottom_toBottomOf="@id/input" app:layout_constraintEnd_toStartOf="@id/send" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@id/input" /> + app:layout_constraintTop_toTopOf="@id/input" + tools:visibility="visible" /> + app:strokeWidth="1dp" + tools:visibility="visible" /> + app:layout_goneMarginEnd="24dp" + tools:visibility="visible" /> + + + tools:visibility="visible" /> + tools:visibility="visible" /> + tools:visibility="visible" /> + app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Button.Circle" + tools:visibility="visible" /> + app:layout_constraintStart_toStartOf="parent" + tools:visibility="visible" /> + tools:visibility="gone" /> + tools:visibility="gone" /> + tools:visibility="gone" /> \ No newline at end of file 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/layout/layout_gif_picker.xml b/app/src/main/res/layout/layout_gif_picker.xml new file mode 100644 index 00000000..b8b736b9 --- /dev/null +++ b/app/src/main/res/layout/layout_gif_picker.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_profile_details.xml b/app/src/main/res/layout/layout_profile_details.xml index c20bcfe3..1d747107 100644 --- a/app/src/main/res/layout/layout_profile_details.xml +++ b/app/src/main/res/layout/layout_profile_details.xml @@ -16,12 +16,12 @@ android:transitionName="profile_pic" android:visibility="invisible" app:actualImageScaleType="centerCrop" + app:layout_constraintBottom_toBottomOf="@id/btnTagged" app:layout_constraintEnd_toStartOf="@id/btnFollow" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="@id/btnTagged" - tools:visibility="visible" - tools:foreground="@mipmap/ic_launcher" /> + tools:foreground="@mipmap/ic_launcher" + tools:visibility="visible" /> @@ -48,8 +48,8 @@ android:gravity="center" android:visibility="gone" app:chipBackgroundColor="@null" - app:layout_constraintStart_toEndOf="@id/btnFollow" app:layout_constraintBottom_toTopOf="@id/fav_chip" + app:layout_constraintStart_toEndOf="@id/btnFollow" tools:text="omg what do u expect" tools:visibility="visible" /> @@ -64,8 +64,8 @@ app:chipBackgroundColor="@null" app:chipIcon="@drawable/ic_outline_class_24" app:chipIconTint="@color/blue_700" - app:layout_constraintStart_toEndOf="@id/mainStatus" app:layout_constraintBottom_toTopOf="@id/fav_chip" + app:layout_constraintStart_toEndOf="@id/mainStatus" app:rippleColor="@color/blue_A400" tools:visibility="visible" /> @@ -79,8 +79,8 @@ app:chipBackgroundColor="@null" app:chipIcon="@drawable/ic_like" app:chipIconTint="@color/red_600" - app:layout_constraintStart_toEndOf="@id/btnSaved" app:layout_constraintBottom_toTopOf="@id/fav_chip" + app:layout_constraintStart_toEndOf="@id/btnSaved" app:rippleColor="@color/red_300" tools:visibility="visible" /> @@ -136,8 +136,8 @@ android:ellipsize="marquee" android:paddingStart="8dp" android:paddingTop="4dp" - android:paddingBottom="4dp" android:paddingEnd="4dp" + android:paddingBottom="4dp" android:singleLine="true" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textStyle="bold" @@ -189,8 +189,8 @@ android:layout_height="wrap_content" android:background="?android:selectableItemBackground" android:paddingStart="8dp" - android:paddingEnd="8dp" android:paddingTop="4dp" + android:paddingEnd="8dp" android:paddingBottom="4dp" android:textAppearance="@style/TextAppearance.AppCompat.Body1" app:layout_constraintBottom_toTopOf="@id/mainUrl" @@ -206,8 +206,8 @@ android:layout_below="@id/mainBiography" android:ellipsize="marquee" android:paddingStart="8dp" - android:paddingEnd="8dp" android:paddingTop="4dp" + android:paddingEnd="8dp" android:paddingBottom="4dp" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:visibility="gone" @@ -232,32 +232,33 @@ android:textSize="12sp" android:textStyle="italic" android:visibility="gone" + app:layout_constraintBottom_toTopOf="@id/counts_barrier" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/mainUrl" - app:layout_constraintBottom_toTopOf="@id/counts_barrier" tools:text="Followed by @instagram, @facebook + 69 more" tools:visibility="visible" /> - + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/more_nav_graph.xml b/app/src/main/res/navigation/more_nav_graph.xml index f4861fc3..c0c8770c 100644 --- a/app/src/main/res/navigation/more_nav_graph.xml +++ b/app/src/main/res/navigation/more_nav_graph.xml @@ -81,8 +81,29 @@ android:name="awais.instagrabber.fragments.settings.SettingsPreferencesFragment" android:label="@string/action_settings"> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 49bcf692..e9d9d6d6 100755 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -124,4 +124,12 @@ @style/AppTheme.Dark.Black @style/AppTheme.Dark.MaterialDark + + secs + mins + + + @string/secs + @string/mins + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ab473cd0..da8ae5ea 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -185,6 +185,7 @@ %s shared a video %s sent a message %s shared a gif + %s shared a sticker %s shared a profile: @%s %s shared a location: %s %s shared a story highlight by @%s @@ -479,4 +480,16 @@ Decline Accept You + No pending requests + Checking for new messages + Stories + DM + Notifications + Post + Enable DM notifications + Auto refresh messages + Auto refresh every + secs + mins + Search GIPHY diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 188c046c..bf8fdcaf 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -125,8 +125,8 @@ @color/purple_600 @color/green_500 @color/green_200 - @color/grey_600_a20 - @color/grey_600_a20 + @color/grey_900 + @color/grey_900 @color/grey_900 @color/red_200 @color/black diff --git a/build.gradle b/build.gradle index 1c106f22..6e5fab7e 100755 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.1.2' - def nav_version = "2.3.2" + def nav_version = "2.3.3" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6b0a820c..a93e645f 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,9 @@ +#Sun Feb 28 01:04:23 JST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=1433372d903ffba27496f8d5af24265310d2da0d78bf6b4e5138831d4fe066e9 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-all.zip -# https://gradle.org/releases/ -# https://gradle.org/release-checksums/ zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-all.zip +distributionSha256Sum=1433372d903ffba27496f8d5af24265310d2da0d78bf6b4e5138831d4fe066e9 +# https://gradle.org/releases/ +# https://gradle.org/release-checksums/ \ No newline at end of file