Migrate ProfileFragment to kotlin and use viewmodel

This commit is contained in:
Ammar Githam 2021-06-30 00:21:10 +09:00
parent bdad254f5d
commit 27d919e6b2
30 changed files with 2024 additions and 1620 deletions

View File

@ -111,8 +111,10 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
try { try {
DownloadUtils.init(this, DownloadUtils.init(
Utils.settingsHelper.getString(PreferenceKeys.PREF_BARINSTA_DIR_URI)) this,
Utils.settingsHelper.getString(PreferenceKeys.PREF_BARINSTA_DIR_URI)
)
} catch (e: ReselectDocumentTreeException) { } catch (e: ReselectDocumentTreeException) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val intent = Intent(this, DirectorySelectActivity::class.java) val intent = Intent(this, DirectorySelectActivity::class.java)
@ -324,6 +326,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
// } catch (e: Exception) { // } catch (e: Exception) {
// Log.e(TAG, "onDestroy: ", e) // Log.e(TAG, "onDestroy: ", e)
// } // }
DownloadUtils.destroy()
instance = null instance = null
} }
@ -358,21 +361,27 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
private fun createNotificationChannels() { private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val notificationManager = NotificationManagerCompat.from(applicationContext) val notificationManager = NotificationManagerCompat.from(applicationContext)
notificationManager.createNotificationChannel(NotificationChannel( notificationManager.createNotificationChannel(
Constants.DOWNLOAD_CHANNEL_ID, NotificationChannel(
Constants.DOWNLOAD_CHANNEL_NAME, Constants.DOWNLOAD_CHANNEL_ID,
NotificationManager.IMPORTANCE_DEFAULT Constants.DOWNLOAD_CHANNEL_NAME,
)) NotificationManager.IMPORTANCE_DEFAULT
notificationManager.createNotificationChannel(NotificationChannel( )
Constants.ACTIVITY_CHANNEL_ID, )
Constants.ACTIVITY_CHANNEL_NAME, notificationManager.createNotificationChannel(
NotificationManager.IMPORTANCE_DEFAULT NotificationChannel(
)) Constants.ACTIVITY_CHANNEL_ID,
notificationManager.createNotificationChannel(NotificationChannel( Constants.ACTIVITY_CHANNEL_NAME,
Constants.DM_UNREAD_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT
Constants.DM_UNREAD_CHANNEL_NAME, )
NotificationManager.IMPORTANCE_DEFAULT )
)) notificationManager.createNotificationChannel(
NotificationChannel(
Constants.DM_UNREAD_CHANNEL_ID,
Constants.DM_UNREAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
)
val silentNotificationChannel = NotificationChannel( val silentNotificationChannel = NotificationChannel(
Constants.SILENT_NOTIFICATIONS_CHANNEL_ID, Constants.SILENT_NOTIFICATIONS_CHANNEL_ID,
Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME, Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME,
@ -404,7 +413,8 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
supportFragmentManager, supportFragmentManager,
R.id.main_nav_host, R.id.main_nav_host,
intent, intent,
firstFragmentGraphIndex) firstFragmentGraphIndex
)
navControllerLiveData.observe(this, { navController: NavController? -> setupNavigation(binding.toolbar, navController) }) navControllerLiveData.observe(this, { navController: NavController? -> setupNavigation(binding.toolbar, navController) })
currentNavControllerLiveData = navControllerLiveData currentNavControllerLiveData = navControllerLiveData
} }
@ -432,27 +442,33 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
private fun setupAnonBottomNav(): List<Tab> { private fun setupAnonBottomNav(): List<Tab> {
val selectedItemId = binding.bottomNavView.selectedItemId val selectedItemId = binding.bottomNavView.selectedItemId
val favoriteTab = Tab(R.drawable.ic_star_24, val favoriteTab = Tab(
R.drawable.ic_star_24,
getString(R.string.title_favorites), getString(R.string.title_favorites),
false, false,
"favorites_nav_graph", "favorites_nav_graph",
R.navigation.favorites_nav_graph, R.navigation.favorites_nav_graph,
R.id.favorites_nav_graph, R.id.favorites_nav_graph,
R.id.favoritesFragment) R.id.favoritesFragment
val profileTab = Tab(R.drawable.ic_person_24, )
val profileTab = Tab(
R.drawable.ic_person_24,
getString(R.string.profile), getString(R.string.profile),
false, false,
"profile_nav_graph", "profile_nav_graph",
R.navigation.profile_nav_graph, R.navigation.profile_nav_graph,
R.id.profile_nav_graph, R.id.profile_nav_graph,
R.id.profileFragment) R.id.profileFragment
val moreTab = Tab(R.drawable.ic_more_horiz_24, )
val moreTab = Tab(
R.drawable.ic_more_horiz_24,
getString(R.string.more), getString(R.string.more),
false, false,
"more_nav_graph", "more_nav_graph",
R.navigation.more_nav_graph, R.navigation.more_nav_graph,
R.id.more_nav_graph, R.id.more_nav_graph,
R.id.morePreferencesFragment) R.id.morePreferencesFragment
)
val menu = binding.bottomNavView.menu val menu = binding.bottomNavView.menu
menu.clear() menu.clear()
menu.add(0, favoriteTab.navigationRootId, 0, favoriteTab.title).setIcon(favoriteTab.iconResId) menu.add(0, favoriteTab.navigationRootId, 0, favoriteTab.title).setIcon(favoriteTab.iconResId)
@ -489,9 +505,15 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
if (destination.id == R.id.directMessagesThreadFragment && arguments != null) { if (destination.id == R.id.directMessagesThreadFragment && arguments != null) {
// Set the thread title earlier for better ux // Set the thread title earlier for better ux
val title = arguments.getString("title") val title = arguments.getString("title")
val actionBar = supportActionBar if (!title.isNullOrBlank()) {
if (actionBar != null && !isEmpty(title)) { supportActionBar?.title = title
actionBar.title = title }
}
if (destination.id == R.id.profileFragment && arguments != null) {
// Set the title to username
val username = arguments.getString("username")
if (!username.isNullOrBlank()) {
supportActionBar?.title = username.substringAfter("@")
} }
} }
// below is a hack to check if we are at the end of the current stack, to setup the search view // below is a hack to check if we are at the end of the current stack, to setup the search view
@ -764,7 +786,8 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
"com.google.android.gms.fonts", "com.google.android.gms.fonts",
"com.google.android.gms", "com.google.android.gms",
"Noto Color Emoji Compat", "Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs) R.array.com_google_android_gms_fonts_certs
)
val config: EmojiCompat.Config = FontRequestEmojiCompatConfig(applicationContext, fontRequest) val config: EmojiCompat.Config = FontRequestEmojiCompatConfig(applicationContext, fontRequest)
config.setReplaceAll(true) // .setUseEmojiAsDefaultStyle(true) config.setReplaceAll(true) // .setUseEmojiAsDefaultStyle(true)
.registerInitCallback(object : InitCallback() { .registerInitCallback(object : InitCallback() {

View File

@ -156,6 +156,13 @@ public class RamboTextViewV2 extends AutoLinkTextView {
onEmailClickListeners.clear(); onEmailClickListeners.clear();
} }
public void clearAllAutoLinkListeners() {
clearOnMentionClickListeners();
clearOnHashtagClickListeners();
clearOnURLClickListeners();
clearOnEmailClickListeners();
}
public interface OnMentionClickListener { public interface OnMentionClickListener {
void onMentionClick(final AutoLinkItem autoLinkItem); void onMentionClick(final AutoLinkItem autoLinkItem);
} }

View File

@ -10,6 +10,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.primitives.Booleans; import com.google.common.primitives.Booleans;
@ -36,18 +38,21 @@ public class MultiOptionDialogFragment<T extends Serializable> extends DialogFra
private List<Option<?>> options; private List<Option<?>> options;
@NonNull @NonNull
public static <E extends Serializable> MultiOptionDialogFragment<E> newInstance(@StringRes final int title, public static <E extends Serializable> MultiOptionDialogFragment<E> newInstance(final int requestCode,
@StringRes final int title,
@NonNull final ArrayList<Option<E>> options) { @NonNull final ArrayList<Option<E>> options) {
return newInstance(title, 0, 0, options, Type.SINGLE); return newInstance(requestCode, title, 0, 0, options, Type.SINGLE);
} }
@NonNull @NonNull
public static <E extends Serializable> MultiOptionDialogFragment<E> newInstance(@StringRes final int title, public static <E extends Serializable> MultiOptionDialogFragment<E> newInstance(final int requestCode,
@StringRes final int title,
@StringRes final int positiveButtonText, @StringRes final int positiveButtonText,
@StringRes final int negativeButtonText, @StringRes final int negativeButtonText,
@NonNull final ArrayList<Option<E>> options, @NonNull final ArrayList<Option<E>> options,
@NonNull final Type type) { @NonNull final Type type) {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putInt("requestCode", requestCode);
args.putInt("title", title); args.putInt("title", title);
args.putInt("positiveButtonText", positiveButtonText); args.putInt("positiveButtonText", positiveButtonText);
args.putInt("negativeButtonText", negativeButtonText); args.putInt("negativeButtonText", negativeButtonText);
@ -58,10 +63,28 @@ public class MultiOptionDialogFragment<T extends Serializable> extends DialogFra
return fragment; return fragment;
} }
@SuppressWarnings({"rawtypes", "unchecked"})
@Override @Override
public void onAttach(@NonNull final Context context) { public void onAttach(@NonNull final Context context) {
super.onAttach(context); super.onAttach(context);
this.context = context; this.context = context;
final Fragment parentFragment = getParentFragment();
if (parentFragment != null) {
if (parentFragment instanceof MultiOptionDialogCallback) {
callback = (MultiOptionDialogCallback) parentFragment;
}
if (parentFragment instanceof MultiOptionDialogSingleCallback) {
singleCallback = (MultiOptionDialogSingleCallback) parentFragment;
}
return;
}
final FragmentActivity fragmentActivity = getActivity();
if (fragmentActivity instanceof MultiOptionDialogCallback) {
callback = (MultiOptionDialogCallback) fragmentActivity;
}
if (fragmentActivity instanceof MultiOptionDialogSingleCallback) {
singleCallback = (MultiOptionDialogSingleCallback) fragmentActivity;
}
} }
@NonNull @NonNull
@ -69,12 +92,15 @@ public class MultiOptionDialogFragment<T extends Serializable> extends DialogFra
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
final Bundle arguments = getArguments(); final Bundle arguments = getArguments();
int title = 0; int title = 0;
int rc = 0;
if (arguments != null) { if (arguments != null) {
rc = arguments.getInt("requestCode");
title = arguments.getInt("title"); title = arguments.getInt("title");
type = (Type) arguments.getSerializable("type"); type = (Type) arguments.getSerializable("type");
} }
final int requestCode = rc;
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
if (title > 0) { if (title != 0) {
builder.setTitle(title); builder.setTitle(title);
} }
try { try {
@ -89,11 +115,11 @@ public class MultiOptionDialogFragment<T extends Serializable> extends DialogFra
if (negativeButtonText > 0) { if (negativeButtonText > 0) {
builder.setNegativeButton(negativeButtonText, (dialog, which) -> { builder.setNegativeButton(negativeButtonText, (dialog, which) -> {
if (callback != null) { if (callback != null) {
callback.onCancel(); callback.onCancel(requestCode);
return; return;
} }
if (singleCallback != null) { if (singleCallback != null) {
singleCallback.onCancel(); singleCallback.onCancel(requestCode);
} }
}); });
} }
@ -113,7 +139,7 @@ public class MultiOptionDialogFragment<T extends Serializable> extends DialogFra
final Option<T> option = (Option<T>) options.get(position); final Option<T> option = (Option<T>) options.get(position);
selected.add(option.value); selected.add(option.value);
} }
callback.onMultipleSelect(selected); callback.onMultipleSelect(requestCode, selected);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "onCreateDialog: ", e); Log.e(TAG, "onCreateDialog: ", e);
} }
@ -133,7 +159,7 @@ public class MultiOptionDialogFragment<T extends Serializable> extends DialogFra
try { try {
final Option<?> option = options.get(which); final Option<?> option = options.get(which);
//noinspection unchecked //noinspection unchecked
callback.onCheckChange((T) option.value, isChecked); callback.onCheckChange(requestCode, (T) option.value, isChecked);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "onCreateDialog: ", e); Log.e(TAG, "onCreateDialog: ", e);
} }
@ -157,7 +183,7 @@ public class MultiOptionDialogFragment<T extends Serializable> extends DialogFra
try { try {
final Option<?> option = options.get(which); final Option<?> option = options.get(which);
//noinspection unchecked //noinspection unchecked
callback.onCheckChange((T) option.value, true); callback.onCheckChange(requestCode, (T) option.value, true);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "onCreateDialog: ", e); Log.e(TAG, "onCreateDialog: ", e);
} }
@ -168,7 +194,7 @@ public class MultiOptionDialogFragment<T extends Serializable> extends DialogFra
try { try {
final Option<?> option = options.get(which); final Option<?> option = options.get(which);
//noinspection unchecked //noinspection unchecked
singleCallback.onSelect((T) option.value); singleCallback.onSelect(requestCode, (T) option.value);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "onCreateDialog: ", e); Log.e(TAG, "onCreateDialog: ", e);
} }
@ -190,19 +216,19 @@ public class MultiOptionDialogFragment<T extends Serializable> extends DialogFra
} }
public interface MultiOptionDialogCallback<T> { public interface MultiOptionDialogCallback<T> {
void onSelect(T result); void onSelect(int requestCode, T result);
void onMultipleSelect(List<T> result); void onMultipleSelect(int requestCode, List<T> result);
void onCheckChange(T item, boolean isChecked); void onCheckChange(int requestCode, T item, boolean isChecked);
void onCancel(); void onCancel(int requestCode);
} }
public interface MultiOptionDialogSingleCallback<T> { public interface MultiOptionDialogSingleCallback<T> {
void onSelect(T result); void onSelect(int requestCode, T result);
void onCancel(); void onCancel(int requestCode);
} }
public static class Option<T extends Serializable> { public static class Option<T extends Serializable> {

View File

@ -2,7 +2,6 @@ package awais.instagrabber.fragments;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
@ -31,8 +30,6 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat; import androidx.core.view.GestureDetectorCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
@ -98,7 +95,7 @@ import awais.instagrabber.viewmodels.ArchivesViewModel;
import awais.instagrabber.viewmodels.FeedStoriesViewModel; import awais.instagrabber.viewmodels.FeedStoriesViewModel;
import awais.instagrabber.viewmodels.HighlightsViewModel; import awais.instagrabber.viewmodels.HighlightsViewModel;
import awais.instagrabber.viewmodels.StoriesViewModel; import awais.instagrabber.viewmodels.StoriesViewModel;
import awais.instagrabber.webservices.DirectMessagesService; import awais.instagrabber.webservices.DirectMessagesRepository;
import awais.instagrabber.webservices.MediaRepository; import awais.instagrabber.webservices.MediaRepository;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.StoriesRepository; import awais.instagrabber.webservices.StoriesRepository;
@ -148,7 +145,7 @@ public class StoryViewerFragment extends Fragment {
// private boolean isHighlight; // private boolean isHighlight;
// private boolean isArchive; // private boolean isArchive;
// private boolean isNotification; // private boolean isNotification;
private DirectMessagesService directMessagesService; private DirectMessagesRepository directMessagesRepository;
private StoryViewerOptions options; private StoryViewerOptions options;
private String csrfToken; private String csrfToken;
private String deviceId; private String deviceId;
@ -164,7 +161,7 @@ public class StoryViewerFragment extends Fragment {
fragmentActivity = (AppCompatActivity) requireActivity(); fragmentActivity = (AppCompatActivity) requireActivity();
storiesRepository = StoriesRepository.Companion.getInstance(); storiesRepository = StoriesRepository.Companion.getInstance();
mediaRepository = MediaRepository.Companion.getInstance(); mediaRepository = MediaRepository.Companion.getInstance();
directMessagesService = DirectMessagesService.INSTANCE; directMessagesRepository = DirectMessagesRepository.Companion.getInstance();
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@ -218,7 +215,7 @@ public class StoryViewerFragment extends Fragment {
final AlertDialog ad = new AlertDialog.Builder(context) final AlertDialog ad = new AlertDialog.Builder(context)
.setTitle(R.string.reply_story) .setTitle(R.string.reply_story)
.setView(input) .setView(input)
.setPositiveButton(R.string.confirm, (d, w) -> directMessagesService.broadcastStoryReply( .setPositiveButton(R.string.confirm, (d, w) -> directMessagesRepository.broadcastStoryReply(
csrfToken, csrfToken,
userId, userId,
deviceId, deviceId,

View File

@ -308,9 +308,9 @@ class DirectMessageSettingsFragment : Fragment(), ConfirmDialogFragmentCallback
{ _: Int, user: User? -> { _: Int, user: User? ->
val options = viewModel.createUserOptions(user) val options = viewModel.createUserOptions(user)
if (options.isEmpty()) return@DirectUsersAdapter true if (options.isEmpty()) return@DirectUsersAdapter true
val fragment = MultiOptionDialogFragment.newInstance(-1, options) val fragment = MultiOptionDialogFragment.newInstance(0, -1, options)
fragment.setSingleCallback(object : MultiOptionDialogSingleCallback<String?> { fragment.setSingleCallback(object : MultiOptionDialogSingleCallback<String?> {
override fun onSelect(action: String?) { override fun onSelect(requestCode: Int, action: String?) {
if (action == null) return if (action == null) return
val resourceLiveData = viewModel.doAction(user, action) val resourceLiveData = viewModel.doAction(user, action)
if (resourceLiveData != null) { if (resourceLiveData != null) {
@ -318,7 +318,7 @@ class DirectMessageSettingsFragment : Fragment(), ConfirmDialogFragmentCallback
} }
} }
override fun onCancel() {} override fun onCancel(requestCode: Int) {}
}) })
val fragmentManager = childFragmentManager val fragmentManager = childFragmentManager
fragment.show(fragmentManager, "actions") fragment.show(fragmentManager, "actions")

View File

@ -0,0 +1,981 @@
package awais.instagrabber.fragments.main
import android.content.Intent
import android.graphics.Typeface
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.SpannableStringBuilder
import android.text.style.RelativeSizeSpan
import android.text.style.StyleSpan
import android.util.Log
import android.view.*
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.NavHostFragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import awais.instagrabber.R
import awais.instagrabber.activities.MainActivity
import awais.instagrabber.adapters.FeedAdapterV2
import awais.instagrabber.adapters.HighlightsAdapter
import awais.instagrabber.asyncs.ProfilePostFetchService
import awais.instagrabber.customviews.PrimaryActionModeCallback
import awais.instagrabber.customviews.RamboTextViewV2
import awais.instagrabber.customviews.RamboTextViewV2.*
import awais.instagrabber.databinding.FragmentProfileBinding
import awais.instagrabber.db.repositories.AccountRepository
import awais.instagrabber.db.repositories.FavoriteRepository
import awais.instagrabber.dialogs.ConfirmDialogFragment
import awais.instagrabber.dialogs.ConfirmDialogFragment.ConfirmDialogFragmentCallback
import awais.instagrabber.dialogs.MultiOptionDialogFragment
import awais.instagrabber.dialogs.MultiOptionDialogFragment.MultiOptionDialogSingleCallback
import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option
import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment
import awais.instagrabber.dialogs.ProfilePicDialogFragment
import awais.instagrabber.fragments.HashTagFragment.ARG_HASHTAG
import awais.instagrabber.fragments.PostViewV2Fragment
import awais.instagrabber.fragments.UserSearchFragment
import awais.instagrabber.fragments.UserSearchFragmentDirections
import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.models.Resource
import awais.instagrabber.models.enums.PostItemType
import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.FriendshipStatus
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.UserProfileContextLink
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.utils.*
import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.utils.extensions.isReallyPrivate
import awais.instagrabber.utils.extensions.trimAll
import awais.instagrabber.viewmodels.AppStateViewModel
import awais.instagrabber.viewmodels.ProfileFragmentViewModel
import awais.instagrabber.viewmodels.ProfileFragmentViewModel.ProfileEvent.*
import awais.instagrabber.viewmodels.ProfileFragmentViewModelFactory
import awais.instagrabber.webservices.*
class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCallback, MultiOptionDialogSingleCallback<String> {
private var backStackSavedStateResultLiveData: MutableLiveData<Any?>? = null
private var shareDmMenuItem: MenuItem? = null
private var shareLinkMenuItem: MenuItem? = null
private var removeFollowerMenuItem: MenuItem? = null
private var chainingMenuItem: MenuItem? = null
private var mutePostsMenuItem: MenuItem? = null
private var muteStoriesMenuItem: MenuItem? = null
private var restrictMenuItem: MenuItem? = null
private var blockMenuItem: MenuItem? = null
private var setupPostsDone: Boolean = false
private var selectedMedia: List<Media>? = null
private var actionMode: ActionMode? = null
private var disableDm: Boolean = false
private var shouldRefresh: Boolean = true
private var highlightsAdapter: HighlightsAdapter? = null
private var layoutPreferences = Utils.getPostsLayoutPreferences(Constants.PREF_PROFILE_POSTS_LAYOUT)
private lateinit var mainActivity: MainActivity
private lateinit var root: MotionLayout
private lateinit var binding: FragmentProfileBinding
private lateinit var appStateViewModel: AppStateViewModel
private lateinit var viewModel: ProfileFragmentViewModel
private val confirmDialogFragmentRequestCode = 100
private val ppOptsDialogRequestCode = 101
private val bioDialogRequestCode = 102
private val translationDialogRequestCode = 103
private val feedItemCallback: FeedAdapterV2.FeedItemCallback = object : FeedAdapterV2.FeedItemCallback {
override fun onPostClick(media: Media?, profilePicView: View?, mainPostImage: View?) {
openPostDialog(media ?: return, -1)
}
override fun onProfilePicClick(media: Media?, profilePicView: View?) {
navigateToProfile(media?.user?.username)
}
override fun onNameClick(media: Media?, profilePicView: View?) {
navigateToProfile(media?.user?.username)
}
override fun onLocationClick(media: Media?) {
val action = FeedFragmentDirections.actionGlobalLocationFragment(media?.location?.pk ?: return)
NavHostFragment.findNavController(this@ProfileFragment).navigate(action)
}
override fun onMentionClick(mention: String?) {
navigateToProfile(mention?.trimAll() ?: return)
}
override fun onHashtagClick(hashtag: String?) {
val action = FeedFragmentDirections.actionGlobalHashTagFragment(hashtag ?: return)
NavHostFragment.findNavController(this@ProfileFragment).navigate(action)
}
override fun onCommentsClick(media: Media?) {
val commentsAction = ProfileFragmentDirections.actionGlobalCommentsViewerFragment(
media?.code ?: return,
media.pk ?: return,
media.user?.pk ?: return
)
NavHostFragment.findNavController(this@ProfileFragment).navigate(commentsAction)
}
override fun onDownloadClick(media: Media?, childPosition: Int) {
DownloadUtils.showDownloadDialog(context ?: return, media ?: return, childPosition)
}
override fun onEmailClick(emailId: String?) {
Utils.openEmailAddress(context ?: return, emailId ?: return)
}
override fun onURLClick(url: String?) {
Utils.openURL(context ?: return, url ?: return)
}
override fun onSliderClick(media: Media?, position: Int) {
openPostDialog(media ?: return, position)
}
}
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
binding.postsRecyclerView.endSelection()
}
}
private val multiSelectAction = PrimaryActionModeCallback(
R.menu.multi_select_download_menu,
object : PrimaryActionModeCallback.CallbacksHelper() {
override fun onDestroy(mode: ActionMode?) {
binding.postsRecyclerView.endSelection()
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
val item1 = item ?: return false
if (item1.itemId == R.id.action_download) {
val selectedMedia = this@ProfileFragment.selectedMedia ?: return false
val context = context ?: return false
DownloadUtils.download(context, selectedMedia)
binding.postsRecyclerView.endSelection()
return true
}
return false
}
}
)
private val selectionModeCallback = object : FeedAdapterV2.SelectionModeCallback {
override fun onSelectionStart() {
if (!onBackPressedCallback.isEnabled) {
onBackPressedCallback.isEnabled = true
mainActivity.onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback)
}
if (actionMode == null) {
actionMode = mainActivity.startActionMode(multiSelectAction)
}
}
override fun onSelectionChange(mediaSet: Set<Media>?) {
if (mediaSet == null) {
selectedMedia = null
return
}
val title = getString(R.string.number_selected, mediaSet.size)
actionMode?.title = title
selectedMedia = mediaSet.toList()
}
override fun onSelectionEnd() {
if (onBackPressedCallback.isEnabled) {
onBackPressedCallback.isEnabled = false
onBackPressedCallback.remove()
}
(actionMode ?: return).finish()
actionMode = null
}
}
private val onProfilePicClickListener = View.OnClickListener {
val hasStories = viewModel.userStories.value?.data?.isNotEmpty() ?: false
if (!hasStories) {
showProfilePicDialog()
return@OnClickListener
}
val dialog = MultiOptionDialogFragment.newInstance(
ppOptsDialogRequestCode,
0,
arrayListOf(
Option(getString(R.string.view_pfp), "profile_pic"),
Option(getString(R.string.show_stories), "show_stories")
)
)
dialog.show(childFragmentManager, MultiOptionDialogFragment::class.java.simpleName)
}
private val onFollowersClickListener = View.OnClickListener {
try {
val action = ProfileFragmentDirections.actionProfileFragmentToFollowViewerFragment(
viewModel.profile.value?.data?.pk ?: return@OnClickListener,
true,
viewModel.profile.value?.data?.username ?: return@OnClickListener
)
NavHostFragment.findNavController(this).navigate(action)
} catch (e: Exception) {
Log.e(TAG, "onFollowersClickListener: ", e)
}
}
private val onFollowingClickListener = View.OnClickListener {
try {
val action = ProfileFragmentDirections.actionProfileFragmentToFollowViewerFragment(
viewModel.profile.value?.data?.pk ?: return@OnClickListener,
false,
viewModel.profile.value?.data?.username ?: return@OnClickListener
)
NavHostFragment.findNavController(this).navigate(action)
} catch (e: Exception) {
Log.e(TAG, "onFollowersClickListener: ", e)
}
}
private val onEmailClickListener = OnEmailClickListener {
Utils.openEmailAddress(context ?: return@OnEmailClickListener, it.originalText.trimAll())
}
private val onHashtagClickListener = OnHashtagClickListener {
try {
val bundle = Bundle()
bundle.putString(ARG_HASHTAG, it.originalText.trimAll())
NavHostFragment.findNavController(this).navigate(R.id.action_global_hashTagFragment, bundle)
} catch (e: Exception) {
Log.e(TAG, "onHashtagClickListener: ", e)
}
}
private val onMentionClickListener = OnMentionClickListener {
navigateToProfile(it.originalText.trimAll())
}
private val onURLClickListener = OnURLClickListener {
Utils.openURL(context ?: return@OnURLClickListener, it.originalText.trimAll())
}
@Suppress("UNCHECKED_CAST")
private val backStackSavedStateObserver = Observer<Any?> { result ->
if (result == null) return@Observer
if ((result is RankedRecipient)) {
if (context != null) {
Toast.makeText(context, R.string.sending, Toast.LENGTH_SHORT).show()
}
viewModel.shareDm(result)
} else if ((result is Set<*>)) {
try {
if (context != null) {
Toast.makeText(context, R.string.sending, Toast.LENGTH_SHORT).show()
}
viewModel.shareDm(result as Set<RankedRecipient>)
} catch (e: Exception) {
Log.e(TAG, "share: ", e)
}
}
// clear result
backStackSavedStateResultLiveData?.postValue(null)
}
private fun openPostDialog(media: Media, position: Int) {
val bundle = Bundle().apply {
putSerializable(PostViewV2Fragment.ARG_MEDIA, media)
putInt(PostViewV2Fragment.ARG_SLIDER_POSITION, position)
}
try {
val navController = NavHostFragment.findNavController(this)
navController.navigate(R.id.action_global_post_view, bundle)
} catch (e: Exception) {
Log.e(TAG, "openPostDialog: ", e)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivity = requireActivity() as MainActivity
appStateViewModel = ViewModelProvider(mainActivity).get(AppStateViewModel::class.java)
val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
val deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
val csrfToken = getCsrfTokenFromCookie(cookie)
val userId = getUserIdFromCookie(cookie)
val isLoggedIn = !csrfToken.isNullOrBlank() && userId != 0L && deviceUuid.isNotBlank()
viewModel = ViewModelProvider(
this,
ProfileFragmentViewModelFactory(
csrfToken,
deviceUuid,
UserRepository.getInstance(),
FriendshipRepository.getInstance(),
StoriesRepository.getInstance(),
MediaRepository.getInstance(),
GraphQLRepository.getInstance(),
AccountRepository.getInstance(requireContext()),
FavoriteRepository.getInstance(requireContext()),
DirectMessagesRepository.getInstance(),
if (isLoggedIn) DirectMessagesManager else null,
this,
arguments
)
).get(ProfileFragmentViewModel::class.java)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
if (this::root.isInitialized) {
shouldRefresh = false
return root
}
appStateViewModel.currentUserLiveData.observe(viewLifecycleOwner, viewModel::setCurrentUser)
binding = FragmentProfileBinding.inflate(inflater, container, false)
root = binding.root
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!shouldRefresh) {
setupObservers()
return
}
init()
shouldRefresh = false
}
override fun onRefresh() {
viewModel.refresh()
binding.postsRecyclerView.refresh()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.profile_menu, menu)
blockMenuItem = menu.findItem(R.id.block)
restrictMenuItem = menu.findItem(R.id.restrict)
muteStoriesMenuItem = menu.findItem(R.id.mute_stories)
mutePostsMenuItem = menu.findItem(R.id.mute_posts)
chainingMenuItem = menu.findItem(R.id.chaining)
removeFollowerMenuItem = menu.findItem(R.id.remove_follower)
shareLinkMenuItem = menu.findItem(R.id.share_link)
shareDmMenuItem = menu.findItem(R.id.share_dm)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.layout -> showPostsLayoutPreferences()
R.id.restrict -> viewModel.restrictUser()
R.id.block -> viewModel.blockUser()
R.id.chaining -> navigateToChaining()
R.id.mute_stories -> viewModel.muteStories()
R.id.mute_posts -> viewModel.mutePosts()
R.id.remove_follower -> viewModel.removeFollower()
R.id.share_link -> shareProfileLink()
R.id.share_dm -> shareProfileViaDm()
}
return true
}
override fun onResume() {
super.onResume()
try {
val backStackEntry = NavHostFragment.findNavController(this).currentBackStackEntry
if (backStackEntry != null) {
backStackSavedStateResultLiveData = backStackEntry.savedStateHandle.getLiveData("result")
backStackSavedStateResultLiveData?.observe(viewLifecycleOwner, backStackSavedStateObserver)
}
} catch (e: Exception) {
Log.e(TAG, "onResume: ", e)
}
}
private fun shareProfileViaDm() {
val actionGlobalUserSearch = UserSearchFragmentDirections.actionGlobalUserSearch().apply {
setTitle(getString(R.string.share))
setActionLabel(getString(R.string.send))
showGroups = true
multiple = true
setSearchMode(UserSearchFragment.SearchMode.RAVEN)
}
try {
val navController = NavHostFragment.findNavController(this@ProfileFragment)
navController.navigate(actionGlobalUserSearch)
} catch (e: Exception) {
Log.e(TAG, "shareProfileViaDm: ", e)
}
}
private fun shareProfileLink() {
val profile = viewModel.profile.value?.data ?: return
val sharingIntent = Intent(Intent.ACTION_SEND)
sharingIntent.type = "text/plain"
sharingIntent.putExtra(Intent.EXTRA_TEXT, "https://instagram.com/" + profile.username)
startActivity(Intent.createChooser(sharingIntent, null))
}
private fun navigateToChaining() {
viewModel.currentUser.value?.data ?: return
val profile = viewModel.profile.value?.data ?: return
val bundle = Bundle().apply {
putString("type", "chaining")
putLong("targetId", profile.pk)
}
try {
NavHostFragment.findNavController(this).navigate(R.id.action_global_notificationsViewerFragment, bundle)
} catch (e: Exception) {
Log.e(TAG, "navigateToChaining: ", e)
}
}
private fun init() {
binding.swipeRefreshLayout.setOnRefreshListener(this)
disableDm = !Utils.isNavRootInCurrentTabs("direct_messages_nav_graph")
setupHighlights()
setupObservers()
}
private fun setupObservers() {
viewModel.isLoggedIn.observe(viewLifecycleOwner) {} // observe so that `isLoggedIn.value` is correct
viewModel.currentUserProfileActionLiveData.observe(viewLifecycleOwner) {
val (currentUserResource, profileResource) = it
if (currentUserResource.status == Resource.Status.ERROR || profileResource.status == Resource.Status.ERROR) {
context?.let { ctx -> Toast.makeText(ctx, R.string.error_loading_profile, Toast.LENGTH_LONG).show() }
return@observe
}
if (currentUserResource.status == Resource.Status.LOADING || profileResource.status == Resource.Status.LOADING) {
binding.swipeRefreshLayout.isRefreshing = true
return@observe
}
binding.swipeRefreshLayout.isRefreshing = false
val currentUser = currentUserResource.data
val profile = profileResource.data
val stateUsername = arguments?.getString("username")
setupOptionsMenuItems(currentUser, profile)
if (currentUser == null && profile == null && stateUsername.isNullOrBlank()) {
// default anonymous state, show default message
showDefaultMessage()
return@observe
}
if (profile == null && !stateUsername.isNullOrBlank()) {
context?.let { ctx -> Toast.makeText(ctx, R.string.error_loading_profile, Toast.LENGTH_LONG).show() }
return@observe
}
root.loadLayoutDescription(R.xml.header_list_scene)
setupFavChip(profile, currentUser)
setupFavButton(currentUser, profile)
setupSavedButton(currentUser, profile)
setupTaggedButton(currentUser, profile)
setupLikedButton(currentUser, profile)
setupDMButton(currentUser, profile)
if (profile == null) return@observe
if (profile.isReallyPrivate(currentUser)) {
showPrivateAccountMessage()
return@observe
}
if (!setupPostsDone) {
setupPosts(profile, currentUser)
}
}
viewModel.username.observe(viewLifecycleOwner) {
mainActivity.supportActionBar?.title = it
mainActivity.supportActionBar?.subtitle = null
}
viewModel.profilePicUrl.observe(viewLifecycleOwner) {
val visibility = if (it.isNullOrBlank()) View.INVISIBLE else View.VISIBLE
binding.header.mainProfileImage.visibility = visibility
binding.header.mainProfileImage.setImageURI(if (it.isNullOrBlank()) null else it)
binding.header.mainProfileImage.setOnClickListener(if (it.isNullOrBlank()) null else onProfilePicClickListener)
}
viewModel.fullName.observe(viewLifecycleOwner) { binding.header.mainFullName.text = it ?: "" }
viewModel.biography.observe(viewLifecycleOwner, this::setupBiography)
viewModel.url.observe(viewLifecycleOwner, this::setupProfileURL)
viewModel.followersCount.observe(viewLifecycleOwner, this::setupFollowers)
viewModel.followingCount.observe(viewLifecycleOwner, this::setupFollowing)
viewModel.postCount.observe(viewLifecycleOwner, this::setupPostsCount)
viewModel.friendshipStatus.observe(viewLifecycleOwner) {
setupFollowButton(it)
setupMainStatus(it)
}
viewModel.isVerified.observe(viewLifecycleOwner) {
binding.header.isVerified.visibility = if (it == true) View.VISIBLE else View.GONE
}
viewModel.isPrivate.observe(viewLifecycleOwner) {
binding.header.isPrivate.visibility = if (it == true) View.VISIBLE else View.GONE
}
viewModel.isFavorite.observe(viewLifecycleOwner) {
if (!it) {
binding.header.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24)
binding.header.favChip.setText(R.string.add_to_favorites)
return@observe
}
binding.header.favChip.setChipIconResource(R.drawable.ic_star_check_24)
binding.header.favChip.setText(R.string.favorite_short)
}
viewModel.profileContext.observe(viewLifecycleOwner, this::setupProfileContext)
viewModel.userHighlights.observe(viewLifecycleOwner) {
binding.header.highlightsList.visibility = if (it.data.isNullOrEmpty()) View.GONE else View.VISIBLE
highlightsAdapter?.submitList(it.data)
}
viewModel.userStories.observe(viewLifecycleOwner) {
binding.header.mainProfileImage.setStoriesBorder(if (it.data.isNullOrEmpty()) 0 else 1)
}
viewModel.eventLiveData.observe(viewLifecycleOwner) {
val event = it?.getContentIfNotHandled() ?: return@observe
when (event) {
ShowConfirmUnfollowDialog -> showConfirmUnfollowDialog()
is DMButtonState -> binding.header.btnDM.isEnabled = !event.disabled
is NavigateToThread -> mainActivity.navigateToThread(event.threadId, event.username)
is ShowTranslation -> showTranslationDialog(event.result)
}
}
}
private fun showPrivateAccountMessage() {
binding.header.mainFollowers.isClickable = false
binding.header.mainFollowing.isClickable = false
binding.privatePage1.setImageResource(R.drawable.lock)
binding.privatePage2.setText(R.string.priv_acc)
binding.privatePage.visibility = VISIBLE
binding.privatePage1.visibility = VISIBLE
binding.privatePage2.visibility = VISIBLE
binding.postsRecyclerView.visibility = GONE
binding.swipeRefreshLayout.isRefreshing = false
root.getTransition(R.id.transition)?.setEnable(false)
}
private fun setupProfileContext(contextPair: Pair<String?, List<UserProfileContextLink>?>) {
val (profileContext, contextLinkList) = contextPair
if (profileContext == null || contextLinkList == null) {
binding.header.profileContext.visibility = GONE
binding.header.profileContext.clearOnMentionClickListeners()
return
}
var updatedProfileContext: String = profileContext
contextLinkList.forEachIndexed { i, link ->
if (link.username == null) return@forEachIndexed
updatedProfileContext = updatedProfileContext.substring(0, link.start + i) + "@" + updatedProfileContext.substring(link.start + i)
}
binding.header.profileContext.visibility = VISIBLE
binding.header.profileContext.text = updatedProfileContext
binding.header.profileContext.addOnMentionClickListener(onMentionClickListener)
}
private fun setupProfileURL(url: String?) {
if (url.isNullOrBlank()) {
binding.header.mainUrl.visibility = GONE
binding.header.mainUrl.clearOnURLClickListeners()
binding.header.mainUrl.setOnLongClickListener(null)
return
}
binding.header.mainUrl.visibility = VISIBLE
binding.header.mainUrl.text = url
binding.header.mainUrl.addOnURLClickListener { Utils.openURL(context ?: return@addOnURLClickListener, it.originalText.trimAll()) }
binding.header.mainUrl.setOnLongClickListener {
Utils.copyText(context ?: return@setOnLongClickListener false, url.trimAll())
return@setOnLongClickListener true
}
}
private fun showTranslationDialog(result: String) {
val dialog = ConfirmDialogFragment.newInstance(
translationDialogRequestCode,
0,
result,
R.string.ok,
0,
0
)
dialog.show(childFragmentManager, ConfirmDialogFragment::class.java.simpleName)
}
private fun setupBiography(bio: String?) {
if (bio.isNullOrBlank()) {
binding.header.mainBiography.visibility = View.GONE
binding.header.mainBiography.clearAllAutoLinkListeners()
binding.header.mainBiography.setOnLongClickListener(null)
return
}
binding.header.mainBiography.visibility = View.VISIBLE
binding.header.mainBiography.text = bio
setCommonAutoLinkListeners(binding.header.mainBiography)
binding.header.mainBiography.setOnLongClickListener {
val isLoggedIn = viewModel.isLoggedIn.value ?: false
val options = arrayListOf(Option(getString(R.string.bio_copy), "copy"))
if (isLoggedIn) {
options.add(Option(getString(R.string.bio_translate), "translate"))
}
val dialog = MultiOptionDialogFragment.newInstance(
bioDialogRequestCode,
0,
options
)
dialog.show(childFragmentManager, MultiOptionDialogFragment::class.java.simpleName)
return@setOnLongClickListener true
}
}
private fun setCommonAutoLinkListeners(textView: RamboTextViewV2) {
textView.addOnEmailClickListener(onEmailClickListener)
textView.addOnHashtagListener(onHashtagClickListener)
textView.addOnMentionClickListener(onMentionClickListener)
textView.addOnURLClickListener(onURLClickListener)
}
private fun setupOptionsMenuItems(currentUser: User?, profile: User?) {
val isMe = currentUser?.pk == profile?.pk
if (profile == null || (currentUser != null && isMe)) {
hideAllOptionsMenuItems()
return
}
if (currentUser == null) {
hideAllOptionsMenuItems()
shareLinkMenuItem?.isVisible = profile.username.isNotBlank()
return
}
blockMenuItem?.isVisible = true
blockMenuItem?.setTitle(if (profile.friendshipStatus?.blocking == true) R.string.unblock else R.string.block)
restrictMenuItem?.isVisible = true
restrictMenuItem?.setTitle(if (profile.friendshipStatus?.isRestricted == true) R.string.unrestrict else R.string.restrict)
muteStoriesMenuItem?.isVisible = true
muteStoriesMenuItem?.setTitle(if (profile.friendshipStatus?.isMutingReel == true) R.string.mute_stories else R.string.unmute_stories)
mutePostsMenuItem?.isVisible = true
mutePostsMenuItem?.setTitle(if (profile.friendshipStatus?.muting == true) R.string.mute_posts else R.string.unmute_posts)
chainingMenuItem?.isVisible = profile.hasChaining
removeFollowerMenuItem?.isVisible = profile.friendshipStatus?.followedBy ?: false
shareLinkMenuItem?.isVisible = profile.username.isNotBlank()
shareDmMenuItem?.isVisible = profile.pk != 0L
}
private fun hideAllOptionsMenuItems() {
blockMenuItem?.isVisible = false
restrictMenuItem?.isVisible = false
muteStoriesMenuItem?.isVisible = false
mutePostsMenuItem?.isVisible = false
chainingMenuItem?.isVisible = false
removeFollowerMenuItem?.isVisible = false
shareLinkMenuItem?.isVisible = false
shareDmMenuItem?.isVisible = false
}
private fun setupPostsCount(count: Long?) {
if (count == null) {
binding.header.mainPostCount.visibility = View.GONE
return
}
binding.header.mainPostCount.visibility = View.VISIBLE
binding.header.mainPostCount.text = getCountSpan(R.plurals.main_posts_count, abbreviate(count, null), count)
}
private fun setupFollowing(count: Long?) {
if (count == null) {
binding.header.mainFollowing.visibility = View.GONE
return
}
val abbreviate = abbreviate(count, null)
val span = SpannableStringBuilder(getString(R.string.main_posts_following, abbreviate))
binding.header.mainFollowing.visibility = View.VISIBLE
binding.header.mainFollowing.text = getCountSpan(span, abbreviate)
if (count <= 0) {
binding.header.mainFollowing.setOnClickListener(null)
return
}
binding.header.mainFollowing.setOnClickListener(onFollowingClickListener)
}
private fun setupFollowers(count: Long?) {
if (count == null) {
binding.header.mainFollowers.visibility = View.GONE
return
}
binding.header.mainFollowers.visibility = View.VISIBLE
binding.header.mainFollowers.text = getCountSpan(R.plurals.main_posts_followers, abbreviate(count, null), count)
if (count <= 0) {
binding.header.mainFollowers.setOnClickListener(null)
return
}
binding.header.mainFollowers.setOnClickListener(onFollowersClickListener)
}
private fun setupDMButton(currentUser: User?, profile: User?) {
val visibility = if (disableDm || (currentUser != null && profile?.pk == currentUser.pk)) View.GONE else View.VISIBLE
binding.header.btnDM.visibility = visibility
if (visibility == View.GONE) {
binding.header.btnDM.setOnClickListener(null)
return
}
binding.header.btnDM.setOnClickListener { viewModel.sendDm() }
}
private fun setupLikedButton(currentUser: User?, profile: User?) {
val visibility = if (currentUser != null && profile?.pk == currentUser.pk) View.VISIBLE else View.GONE
binding.header.btnLiked.visibility = visibility
if (visibility == View.GONE) {
binding.header.btnLiked.setOnClickListener(null)
return
}
binding.header.btnLiked.setOnClickListener {
try {
val action = ProfileFragmentDirections.actionProfileFragmentToSavedViewerFragment(
viewModel.profile.value?.data?.username ?: return@setOnClickListener,
viewModel.profile.value?.data?.pk ?: return@setOnClickListener,
PostItemType.LIKED
)
NavHostFragment.findNavController(this).navigate(action)
} catch (e: Exception) {
Log.e(TAG, "setupTaggedButton: ", e)
}
}
}
private fun setupTaggedButton(currentUser: User?, profile: User?) {
val visibility = if (currentUser != null && profile?.pk == currentUser.pk) View.VISIBLE else View.GONE
binding.header.btnTagged.visibility = visibility
if (visibility == View.GONE) {
binding.header.btnTagged.setOnClickListener(null)
return
}
binding.header.btnTagged.setOnClickListener {
try {
val action = ProfileFragmentDirections.actionProfileFragmentToSavedViewerFragment(
viewModel.profile.value?.data?.username ?: return@setOnClickListener,
viewModel.profile.value?.data?.pk ?: return@setOnClickListener,
PostItemType.TAGGED
)
NavHostFragment.findNavController(this).navigate(action)
} catch (e: Exception) {
Log.e(TAG, "setupTaggedButton: ", e)
}
}
}
private fun setupSavedButton(currentUser: User?, profile: User?) {
val visibility = if (currentUser != null && profile?.pk == currentUser.pk) View.VISIBLE else View.GONE
binding.header.btnSaved.visibility = visibility
if (visibility == View.GONE) {
binding.header.btnSaved.setOnClickListener(null)
return
}
binding.header.btnSaved.setOnClickListener {
try {
val action = ProfileFragmentDirections.actionGlobalSavedCollectionsFragment(false)
NavHostFragment.findNavController(this).navigate(action)
} catch (e: Exception) {
Log.e(TAG, "setupSavedButton: ", e)
}
}
}
private fun setupFavButton(currentUser: User?, profile: User?) {
val visibility = if (currentUser != null && profile?.pk != currentUser.pk) View.VISIBLE else View.GONE
binding.header.btnFollow.visibility = visibility
if (visibility == View.GONE) {
binding.header.btnFollow.setOnClickListener(null)
return
}
binding.header.btnFollow.setOnClickListener { viewModel.toggleFollow(false) }
}
private fun setupFavChip(profile: User?, currentUser: User?) {
val visibility = if (profile?.pk != currentUser?.pk) View.VISIBLE else View.GONE
binding.header.favChip.visibility = visibility
if (visibility == View.GONE) {
binding.header.favChip.setOnClickListener(null)
return
}
binding.header.favChip.setOnClickListener { viewModel.toggleFavorite() }
}
private fun setupFollowButton(it: FriendshipStatus?) {
if (it == null) return
if (it.following) {
binding.header.btnFollow.setText(R.string.unfollow)
binding.header.btnFollow.setChipIconResource(R.drawable.ic_outline_person_add_disabled_24)
return
}
if (it.outgoingRequest) {
binding.header.btnFollow.setText(R.string.cancel)
binding.header.btnFollow.setChipIconResource(R.drawable.ic_outline_person_add_disabled_24)
return
}
binding.header.btnFollow.setText(R.string.follow)
binding.header.btnFollow.setChipIconResource(R.drawable.ic_outline_person_add_24)
}
private fun setupMainStatus(it: FriendshipStatus?) {
if (it == null || (!it.following && !it.followedBy)) {
binding.header.mainStatus.visibility = View.GONE
return
}
binding.header.mainStatus.visibility = View.VISIBLE
if (it.following && it.followedBy) {
context?.let { ctx ->
binding.header.mainStatus.chipBackgroundColor = AppCompatResources.getColorStateList(ctx, R.color.green_800)
binding.header.mainStatus.setText(R.string.status_mutual)
}
return
}
if (it.following) {
context?.let { ctx ->
binding.header.mainStatus.chipBackgroundColor = AppCompatResources.getColorStateList(ctx, R.color.deep_orange_800)
binding.header.mainStatus.setText(R.string.status_following)
}
return
}
context?.let { ctx ->
binding.header.mainStatus.chipBackgroundColor = AppCompatResources.getColorStateList(ctx, R.color.blue_800)
binding.header.mainStatus.setText(R.string.status_follower)
}
}
private fun getCountSpan(pluralRes: Int, countString: String, count: Long): SpannableStringBuilder {
val span = SpannableStringBuilder(resources.getQuantityString(pluralRes, count.toInt(), countString))
return getCountSpan(span, countString)
}
private fun getCountSpan(span: SpannableStringBuilder, countString: String): SpannableStringBuilder {
span.setSpan(RelativeSizeSpan(1.2f), 0, countString.length, 0)
span.setSpan(StyleSpan(Typeface.BOLD), 0, countString.length, 0)
return span
}
private fun showDefaultMessage() {
root.loadLayoutDescription(R.xml.profile_fragment_no_acc_layout)
binding.privatePage1.visibility = View.VISIBLE
binding.privatePage2.visibility = View.VISIBLE
binding.privatePage1.setImageResource(R.drawable.ic_outline_info_24)
binding.privatePage2.setText(R.string.no_acc)
}
private fun setupHighlights() {
val context = context ?: return
highlightsAdapter = HighlightsAdapter { model, position ->
val options = StoryViewerOptions.forHighlight(model.title)
options.currentFeedStoryIndex = position
val action = ProfileFragmentDirections.actionProfileFragmentToStoryViewerFragment(options)
NavHostFragment.findNavController(this).navigate(action)
}
binding.header.highlightsList.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
binding.header.highlightsList.adapter = highlightsAdapter
}
private fun setupPosts(profile: User, currentUser: User?) {
binding.postsRecyclerView.setViewModelStoreOwner(this)
.setLifeCycleOwner(this)
.setPostFetchService(ProfilePostFetchService(profile, currentUser != null))
.setLayoutPreferences(layoutPreferences)
.addFetchStatusChangeListener { binding.swipeRefreshLayout.isRefreshing = it }
.setFeedItemCallback(feedItemCallback)
.setSelectionModeCallback(selectionModeCallback)
.init()
binding.postsRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val canScrollVertically = recyclerView.canScrollVertically(-1)
root.getTransition(R.id.transition)?.setEnable(!canScrollVertically)
}
})
setupPostsDone = true
}
private fun navigateToProfile(username: String?) {
try {
val bundle = Bundle()
bundle.putString("username", username ?: return)
val navController = NavHostFragment.findNavController(this)
navController.navigate(R.id.action_global_profileFragment, bundle)
} catch (e: Exception) {
Log.e(TAG, "navigateToProfile: ", e)
}
}
private fun showConfirmUnfollowDialog() {
val isPrivate = viewModel.profile.value?.data?.isPrivate ?: return
val titleRes = if (isPrivate) R.string.priv_acc else 0
val messageRes = if (isPrivate) R.string.priv_acc_confirm else R.string.are_you_sure
val dialog = ConfirmDialogFragment.newInstance(
confirmDialogFragmentRequestCode,
titleRes,
messageRes,
R.string.confirm,
R.string.cancel,
0,
)
dialog.show(childFragmentManager, ConfirmDialogFragment::class.java.simpleName)
}
override fun onPositiveButtonClicked(requestCode: Int) {
when (requestCode) {
confirmDialogFragmentRequestCode -> {
viewModel.toggleFollow(true)
}
}
}
override fun onNegativeButtonClicked(requestCode: Int) {}
override fun onNeutralButtonClicked(requestCode: Int) {}
override fun onSelect(requestCode: Int, result: String?) {
val r = result ?: return
when (requestCode) {
ppOptsDialogRequestCode -> onPpOptionSelect(r)
bioDialogRequestCode -> onBioOptionSelect(r)
}
}
private fun onBioOptionSelect(result: String) {
when (result) {
"copy" -> Utils.copyText(context ?: return, viewModel.biography.value ?: return)
"translate" -> viewModel.translateBio()
}
}
private fun onPpOptionSelect(result: String) {
when (result) {
"profile_pic" -> showProfilePicDialog()
"show_stories" -> {
try {
val action = ProfileFragmentDirections.actionProfileFragmentToStoryViewerFragment(
StoryViewerOptions.forUser(
viewModel.profile.value?.data?.pk ?: return,
viewModel.profile.value?.data?.fullName ?: return,
)
)
NavHostFragment.findNavController(this).navigate(action)
} catch (e: Exception) {
Log.e(TAG, "omPpOptionSelect: ", e)
}
}
}
}
override fun onCancel(requestCode: Int) {}
private fun showProfilePicDialog() {
val profile = viewModel.profile.value?.data ?: return
val fragment = ProfilePicDialogFragment.getInstance(
profile.pk,
profile.username,
profile.profilePicUrl ?: return
)
val ft = childFragmentManager.beginTransaction()
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.add(fragment, ProfilePicDialogFragment::class.java.simpleName)
.commit()
}
private fun showPostsLayoutPreferences() {
val fragment = PostsLayoutPreferencesDialogFragment(Constants.PREF_PROFILE_POSTS_LAYOUT) { preferences ->
layoutPreferences = preferences
Handler(Looper.getMainLooper()).postDelayed(
{ binding.postsRecyclerView.layoutPreferences = preferences },
200
)
}
fragment.show(childFragmentManager, PostsLayoutPreferencesDialogFragment::class.java.simpleName)
}
}

View File

@ -4,11 +4,11 @@ import android.content.ContentResolver
import android.util.Log import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.models.Resource import awais.instagrabber.models.Resource
import awais.instagrabber.models.Resource.Companion.error import awais.instagrabber.models.Resource.Companion.error
import awais.instagrabber.models.Resource.Companion.loading import awais.instagrabber.models.Resource.Companion.loading
import awais.instagrabber.models.Resource.Companion.success import awais.instagrabber.models.Resource.Companion.success
import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.repositories.requests.directmessages.ThreadIdsOrUserIds import awais.instagrabber.repositories.requests.directmessages.ThreadIdsOrUserIds
import awais.instagrabber.repositories.responses.User import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.DirectThread import awais.instagrabber.repositories.responses.directmessages.DirectThread
@ -17,7 +17,7 @@ import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.Utils import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.getCsrfTokenFromCookie import awais.instagrabber.utils.getCsrfTokenFromCookie
import awais.instagrabber.utils.getUserIdFromCookie import awais.instagrabber.utils.getUserIdFromCookie
import awais.instagrabber.webservices.DirectMessagesService import awais.instagrabber.webservices.DirectMessagesRepository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -31,6 +31,7 @@ object DirectMessagesManager {
private val viewerId: Long private val viewerId: Long
private val deviceUuid: String private val deviceUuid: String
private val csrfToken: String private val csrfToken: String
private val directMessagesRepository by lazy { DirectMessagesRepository.getInstance() }
fun moveThreadFromPending(threadId: String) { fun moveThreadFromPending(threadId: String) {
val pendingThreads = pendingInboxManager.threads.value ?: return val pendingThreads = pendingInboxManager.threads.value ?: return
@ -66,7 +67,8 @@ object DirectMessagesManager {
return ThreadManager(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid) return ThreadManager(threadId, pending, currentUser, contentResolver, viewerId, csrfToken, deviceUuid)
} }
suspend fun createThread(userPk: Long): DirectThread = DirectMessagesService.createThread(csrfToken, viewerId, deviceUuid, listOf(userPk), null) suspend fun createThread(userPk: Long): DirectThread =
directMessagesRepository.createThread(csrfToken, viewerId, deviceUuid, listOf(userPk), null)
fun sendMedia(recipient: RankedRecipient, mediaId: String, itemType: BroadcastItemType, scope: CoroutineScope) { fun sendMedia(recipient: RankedRecipient, mediaId: String, itemType: BroadcastItemType, scope: CoroutineScope) {
sendMedia(setOf(recipient), mediaId, itemType, scope) sendMedia(setOf(recipient), mediaId, itemType, scope)
@ -78,9 +80,9 @@ object DirectMessagesManager {
itemType: BroadcastItemType, itemType: BroadcastItemType,
scope: CoroutineScope, scope: CoroutineScope,
) { ) {
val threadIds = recipients.mapNotNull{ it.thread?.threadId } val threadIds = recipients.mapNotNull { it.thread?.threadId }
val userIdsTemp = recipients.mapNotNull{ it.user?.pk } val userIdsTemp = recipients.mapNotNull { it.user?.pk }
val userIds = userIdsTemp.map{ listOf(it.toString(10)) } val userIds = userIdsTemp.map { listOf(it.toString(10)) }
sendMedia(threadIds, userIds, mediaId, itemType, scope) { sendMedia(threadIds, userIds, mediaId, itemType, scope) {
inboxManager.refresh(scope) inboxManager.refresh(scope)
} }
@ -99,7 +101,7 @@ object DirectMessagesManager {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
if (itemType == BroadcastItemType.MEDIA_SHARE) if (itemType == BroadcastItemType.MEDIA_SHARE)
DirectMessagesService.broadcastMediaShare( directMessagesRepository.broadcastMediaShare(
csrfToken, csrfToken,
viewerId, viewerId,
deviceUuid, deviceUuid,
@ -108,7 +110,7 @@ object DirectMessagesManager {
mediaId mediaId
) )
if (itemType == BroadcastItemType.PROFILE) if (itemType == BroadcastItemType.PROFILE)
DirectMessagesService.broadcastProfile( directMessagesRepository.broadcastProfile(
csrfToken, csrfToken,
viewerId, viewerId,
deviceUuid, deviceUuid,

View File

@ -13,7 +13,7 @@ import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.* import awais.instagrabber.repositories.responses.directmessages.*
import awais.instagrabber.utils.* import awais.instagrabber.utils.*
import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.webservices.DirectMessagesService import awais.instagrabber.webservices.DirectMessagesRepository
import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader import com.google.common.cache.CacheLoader
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
@ -25,8 +25,7 @@ import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class InboxManager(private val pending: Boolean) { class InboxManager(private val pending: Boolean) {
// private val fetchInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner() private val directMessagesRepository by lazy { DirectMessagesRepository.getInstance() }
// private val fetchPendingInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner()
private val inbox = MutableLiveData<Resource<DirectInbox?>>(success(null)) private val inbox = MutableLiveData<Resource<DirectInbox?>>(success(null))
private val unseenCount = MutableLiveData<Resource<Int?>>() private val unseenCount = MutableLiveData<Resource<Int?>>()
private val pendingRequestsTotal = MutableLiveData(0) private val pendingRequestsTotal = MutableLiveData(0)
@ -58,9 +57,9 @@ class InboxManager(private val pending: Boolean) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val inboxValue = if (pending) { val inboxValue = if (pending) {
DirectMessagesService.fetchPendingInbox(cursor, seqId) directMessagesRepository.fetchPendingInbox(cursor, seqId)
} else { } else {
DirectMessagesService.fetchInbox(cursor, seqId) directMessagesRepository.fetchInbox(cursor, seqId)
} }
parseInboxResponse(inboxValue) parseInboxResponse(inboxValue)
} catch (e: Exception) { } catch (e: Exception) {
@ -77,7 +76,7 @@ class InboxManager(private val pending: Boolean) {
unseenCount.postValue(loading(currentUnseenCount)) unseenCount.postValue(loading(currentUnseenCount))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val directBadgeCount = DirectMessagesService.fetchUnseenCount() val directBadgeCount = directMessagesRepository.fetchUnseenCount()
unseenCount.postValue(success(directBadgeCount.badgeCount)) unseenCount.postValue(success(directBadgeCount.badgeCount))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed fetching unseen count", e) Log.e(TAG, "Failed fetching unseen count", e)
@ -253,7 +252,7 @@ class InboxManager(private val pending: Boolean) {
try { try {
val clone = currentDirectInbox.clone() as DirectInbox val clone = currentDirectInbox.clone() as DirectInbox
clone.threads = threadsCopy clone.threads = threadsCopy
inbox.setValue(success(clone)) inbox.postValue(success(clone))
} catch (e: CloneNotSupportedException) { } catch (e: CloneNotSupportedException) {
Log.e(TAG, "setThread: ", e) Log.e(TAG, "setThread: ", e)
} }

View File

@ -30,7 +30,7 @@ import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener
import awais.instagrabber.utils.MediaUtils.VideoInfo import awais.instagrabber.utils.MediaUtils.VideoInfo
import awais.instagrabber.utils.TextUtils.isEmpty import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.webservices.DirectMessagesService import awais.instagrabber.webservices.DirectMessagesRepository
import awais.instagrabber.webservices.FriendshipRepository import awais.instagrabber.webservices.FriendshipRepository
import awais.instagrabber.webservices.MediaRepository import awais.instagrabber.webservices.MediaRepository
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
@ -64,6 +64,7 @@ class ThreadManager(
private val threadIdsOrUserIds: ThreadIdsOrUserIds = of(threadId) private val threadIdsOrUserIds: ThreadIdsOrUserIds = of(threadId)
private val friendshipRepository: FriendshipRepository by lazy { FriendshipRepository.getInstance() } private val friendshipRepository: FriendshipRepository by lazy { FriendshipRepository.getInstance() }
private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() } private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() }
private val directMessagesRepository by lazy { DirectMessagesRepository.getInstance() }
val thread: LiveData<DirectThread?> by lazy { val thread: LiveData<DirectThread?> by lazy {
distinctUntilChanged(map(inboxManager.getInbox()) { inboxResource: Resource<DirectInbox?>? -> distinctUntilChanged(map(inboxManager.getInbox()) { inboxResource: Resource<DirectInbox?>? ->
@ -128,7 +129,7 @@ class ThreadManager(
_fetching.postValue(loading(null)) _fetching.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val threadFeedResponse = DirectMessagesService.fetchThread(threadId, cursor) val threadFeedResponse = directMessagesRepository.fetchThread(threadId, cursor)
if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") { if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") {
_fetching.postValue(error(R.string.generic_not_ok_response, null)) _fetching.postValue(error(R.string.generic_not_ok_response, null))
return@launch return@launch
@ -156,7 +157,7 @@ class ThreadManager(
if (isGroup == null || !isGroup) return if (isGroup == null || !isGroup) return
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = DirectMessagesService.participantRequests(threadId, 1) val response = directMessagesRepository.participantRequests(threadId, 1)
_pendingRequests.postValue(response) _pendingRequests.postValue(response)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "fetchPendingRequests: ", e) Log.e(TAG, "fetchPendingRequests: ", e)
@ -348,7 +349,7 @@ class ThreadManager(
val repliedToClientContext = replyToItemValue?.clientContext val repliedToClientContext = replyToItemValue?.clientContext
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = DirectMessagesService.broadcastText( val response = directMessagesRepository.broadcastText(
csrfToken, csrfToken,
viewerId, viewerId,
deviceUuid, deviceUuid,
@ -406,7 +407,7 @@ class ThreadManager(
data.postValue(loading(directItem)) data.postValue(loading(directItem))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val request = DirectMessagesService.broadcastAnimatedMedia( val request = directMessagesRepository.broadcastAnimatedMedia(
csrfToken, csrfToken,
userId, userId,
deviceUuid, deviceUuid,
@ -455,7 +456,7 @@ class ThreadManager(
null null
) )
mediaRepository.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions) mediaRepository.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions)
val broadcastResponse = DirectMessagesService.broadcastVoice( val broadcastResponse = directMessagesRepository.broadcastVoice(
csrfToken, csrfToken,
viewerId, viewerId,
deviceUuid, deviceUuid,
@ -499,7 +500,7 @@ class ThreadManager(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.broadcastReaction( directMessagesRepository.broadcastReaction(
csrfToken, csrfToken,
userId, userId,
deviceUuid, deviceUuid,
@ -539,7 +540,7 @@ class ThreadManager(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.broadcastReaction( directMessagesRepository.broadcastReaction(
csrfToken, csrfToken,
viewerId, viewerId,
deviceUuid, deviceUuid,
@ -567,7 +568,7 @@ class ThreadManager(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.deleteItem(csrfToken, deviceUuid, threadId, itemId) directMessagesRepository.deleteItem(csrfToken, deviceUuid, threadId, itemId)
} catch (e: Exception) { } catch (e: Exception) {
// add the item back if unsuccessful // add the item back if unsuccessful
addItems(index, listOf(item)) addItems(index, listOf(item))
@ -643,7 +644,7 @@ class ThreadManager(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.forward( directMessagesRepository.forward(
thread.threadId, thread.threadId,
itemTypeName, itemTypeName,
threadId, threadId,
@ -662,7 +663,7 @@ class ThreadManager(
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.approveRequest(csrfToken, deviceUuid, threadId) directMessagesRepository.approveRequest(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "acceptRequest: ", e) Log.e(TAG, "acceptRequest: ", e)
@ -676,7 +677,7 @@ class ThreadManager(
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.declineRequest(csrfToken, deviceUuid, threadId) directMessagesRepository.declineRequest(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "declineRequest: ", e) Log.e(TAG, "declineRequest: ", e)
@ -732,7 +733,7 @@ class ThreadManager(
if (handleInvalidResponse(data, response)) return@launch if (handleInvalidResponse(data, response)) return@launch
val response1 = response.response ?: return@launch val response1 = response.response ?: return@launch
val uploadId = response1.optString("upload_id") val uploadId = response1.optString("upload_id")
val response2 = DirectMessagesService.broadcastPhoto(csrfToken, viewerId, deviceUuid, clientContext, threadIdsOrUserIds, uploadId) val response2 = directMessagesRepository.broadcastPhoto(csrfToken, viewerId, deviceUuid, clientContext, threadIdsOrUserIds, uploadId)
parseResponse(response2, data, directItem) parseResponse(response2, data, directItem)
} catch (e: Exception) { } catch (e: Exception) {
data.postValue(error(e.message, null)) data.postValue(error(e.message, null))
@ -793,7 +794,7 @@ class ThreadManager(
VideoOptions(duration / 1000f, emptyList(), 0, false) VideoOptions(duration / 1000f, emptyList(), 0, false)
) )
mediaRepository.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions) mediaRepository.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions)
val broadcastResponse = DirectMessagesService.broadcastVideo( val broadcastResponse = directMessagesRepository.broadcastVideo(
csrfToken, csrfToken,
viewerId, viewerId,
deviceUuid, deviceUuid,
@ -923,7 +924,7 @@ class ThreadManager(
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = DirectMessagesService.updateTitle(csrfToken, deviceUuid, threadId, newTitle.trim()) val response = directMessagesRepository.updateTitle(csrfToken, deviceUuid, threadId, newTitle.trim())
handleDetailsChangeResponse(data, response) handleDetailsChangeResponse(data, response)
} catch (e: Exception) { } catch (e: Exception) {
} }
@ -935,7 +936,7 @@ class ThreadManager(
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = DirectMessagesService.addUsers( val response = directMessagesRepository.addUsers(
csrfToken, csrfToken,
deviceUuid, deviceUuid,
threadId, threadId,
@ -954,7 +955,7 @@ class ThreadManager(
val data = MutableLiveData<Resource<Any?>>() val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.removeUsers(csrfToken, deviceUuid, threadId, setOf(user.pk)) directMessagesRepository.removeUsers(csrfToken, deviceUuid, threadId, setOf(user.pk))
data.postValue(success(Any())) data.postValue(success(Any()))
var activeUsers = users.value var activeUsers = users.value
var leftUsersValue = leftUsers.value var leftUsersValue = leftUsers.value
@ -989,7 +990,7 @@ class ThreadManager(
if (isAdmin(user)) return data if (isAdmin(user)) return data
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.addAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk)) directMessagesRepository.addAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
val currentAdminIds = adminUserIds.value val currentAdminIds = adminUserIds.value
val updatedAdminIds = ImmutableList.builder<Long>() val updatedAdminIds = ImmutableList.builder<Long>()
.addAll(currentAdminIds ?: emptyList()) .addAll(currentAdminIds ?: emptyList())
@ -1017,7 +1018,7 @@ class ThreadManager(
if (!isAdmin(user)) return data if (!isAdmin(user)) return data
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.removeAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk)) directMessagesRepository.removeAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
val currentAdmins = adminUserIds.value ?: return@launch val currentAdmins = adminUserIds.value ?: return@launch
val updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk } val updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk }
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
@ -1047,7 +1048,7 @@ class ThreadManager(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.mute(csrfToken, deviceUuid, threadId) directMessagesRepository.mute(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1075,7 +1076,7 @@ class ThreadManager(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.unmute(csrfToken, deviceUuid, threadId) directMessagesRepository.unmute(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1103,7 +1104,7 @@ class ThreadManager(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.muteMentions(csrfToken, deviceUuid, threadId) directMessagesRepository.muteMentions(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1131,7 +1132,7 @@ class ThreadManager(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
DirectMessagesService.unmuteMentions(csrfToken, deviceUuid, threadId) directMessagesRepository.unmuteMentions(csrfToken, deviceUuid, threadId)
data.postValue(success(Any())) data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1210,7 +1211,7 @@ class ThreadManager(
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = DirectMessagesService.approveParticipantRequests( val response = directMessagesRepository.approveParticipantRequests(
csrfToken, csrfToken,
deviceUuid, deviceUuid,
threadId, threadId,
@ -1231,7 +1232,7 @@ class ThreadManager(
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = DirectMessagesService.declineParticipantRequests( val response = directMessagesRepository.declineParticipantRequests(
csrfToken, csrfToken,
deviceUuid, deviceUuid,
threadId, threadId,
@ -1273,7 +1274,7 @@ class ThreadManager(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = DirectMessagesService.approvalRequired(csrfToken, deviceUuid, threadId) val response = directMessagesRepository.approvalRequired(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, response) handleDetailsChangeResponse(data, response)
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1301,7 +1302,7 @@ class ThreadManager(
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val request = DirectMessagesService.approvalNotRequired(csrfToken, deviceUuid, threadId) val request = directMessagesRepository.approvalNotRequired(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request) handleDetailsChangeResponse(data, request)
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1324,7 +1325,7 @@ class ThreadManager(
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val request = DirectMessagesService.leave(csrfToken, deviceUuid, threadId) val request = directMessagesRepository.leave(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request) handleDetailsChangeResponse(data, request)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "leave: ", e) Log.e(TAG, "leave: ", e)
@ -1339,7 +1340,7 @@ class ThreadManager(
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val request = DirectMessagesService.end(csrfToken, deviceUuid, threadId) val request = directMessagesRepository.end(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request) handleDetailsChangeResponse(data, request)
val currentThread = thread.value ?: return@launch val currentThread = thread.value ?: return@launch
try { try {
@ -1376,7 +1377,7 @@ class ThreadManager(
data.postValue(loading(null)) data.postValue(loading(null))
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
val response = DirectMessagesService.markAsSeen(csrfToken, deviceUuid, threadId, directItem) val response = directMessagesRepository.markAsSeen(csrfToken, deviceUuid, threadId, directItem)
if (response == null) { if (response == null) {
data.postValue(error(R.string.generic_null_response, null)) data.postValue(error(R.string.generic_null_response, null))
return@launch return@launch

View File

@ -3,7 +3,7 @@ package awais.instagrabber.repositories
import awais.instagrabber.repositories.responses.directmessages.* import awais.instagrabber.repositories.responses.directmessages.*
import retrofit2.http.* import retrofit2.http.*
interface DirectMessagesRepository { interface DirectMessagesService {
@GET("/api/v1/direct_v2/inbox/") @GET("/api/v1/direct_v2/inbox/")
suspend fun fetchInbox(@QueryMap queryMap: Map<String, String>): DirectInboxResponse suspend fun fetchInbox(@QueryMap queryMap: Map<String, String>): DirectInboxResponse

View File

@ -1,21 +0,0 @@
package awais.instagrabber.repositories.responses;
public class UserProfileContextLink {
private final String username;
private final int start;
private final int end;
public UserProfileContextLink(final String username, final int start, final int end) {
this.username = username;
this.start = start;
this.end = end;
}
public String getUsername() {
return username;
}
public int getStart() {
return start;
}
}

View File

@ -0,0 +1,7 @@
package awais.instagrabber.repositories.responses
data class UserProfileContextLink(
val username: String? = null,
val start: Int = 0,
val end: Int = 0,
)

View File

@ -0,0 +1,27 @@
package awais.instagrabber.utils
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}

View File

@ -5,26 +5,26 @@ import android.content.SharedPreferences
import android.os.Build import android.os.Build
import androidx.annotation.StringDef import androidx.annotation.StringDef
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import awais.instagrabber.fragments.settings.PreferenceKeys
import java.util.* import java.util.*
import awais.instagrabber.fragments.settings.PreferenceKeys
class SettingsHelper(context: Context) { class SettingsHelper(context: Context) {
private val sharedPreferences: SharedPreferences? private val sharedPreferences: SharedPreferences? = context.getSharedPreferences(Constants.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
fun getString(@StringSettings key: String): String { fun getString(@StringSettings key: String): String {
val stringDefault = getStringDefault(key) val stringDefault = getStringDefault(key)
return if (sharedPreferences != null) sharedPreferences.getString( return sharedPreferences?.getString(
key, key,
stringDefault stringDefault
)!! else stringDefault ) ?: stringDefault
} }
fun getStringSet(@StringSetSettings key: String?): Set<String>? { fun getStringSet(@StringSetSettings key: String?): Set<String> {
val stringSetDefault: Set<String> = HashSet() val stringSetDefault: Set<String> = HashSet()
return if (sharedPreferences != null) sharedPreferences.getStringSet( return sharedPreferences?.getStringSet(
key, key,
stringSetDefault stringSetDefault
) else stringSetDefault ) ?: stringSetDefault
} }
fun getInteger(@IntegerSettings key: String): Int { fun getInteger(@IntegerSettings key: String): Int {
@ -49,15 +49,16 @@ class SettingsHelper(context: Context) {
fun getThemeCode(fromHelper: Boolean): Int { fun getThemeCode(fromHelper: Boolean): Int {
var themeCode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM var themeCode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
if (!fromHelper && sharedPreferences != null) { if (!fromHelper && sharedPreferences != null) {
themeCode = sharedPreferences.getString(PreferenceKeys.APP_THEME, themeCode.toString())!!.toInt() themeCode = sharedPreferences.getString(PreferenceKeys.APP_THEME, themeCode.toString())?.toInt() ?: 0
when (themeCode) { when (themeCode) {
1 -> themeCode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY 1 -> themeCode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
3 -> themeCode = AppCompatDelegate.MODE_NIGHT_NO 3 -> themeCode = AppCompatDelegate.MODE_NIGHT_NO
0 -> themeCode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 0 -> themeCode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
} }
} }
if (themeCode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM && Build.VERSION.SDK_INT < 29) themeCode = if (themeCode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM && Build.VERSION.SDK_INT < 29) {
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY themeCode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
}
return themeCode return themeCode
} }
@ -78,7 +79,7 @@ class SettingsHelper(context: Context) {
} }
fun hasPreference(key: String?): Boolean { fun hasPreference(key: String?): Boolean {
return sharedPreferences != null && sharedPreferences.contains(key) return sharedPreferences?.contains(key) ?: false
} }
@StringDef( @StringDef(
@ -149,8 +150,4 @@ class SettingsHelper(context: Context) {
@StringDef(PreferenceKeys.KEYWORD_FILTERS) @StringDef(PreferenceKeys.KEYWORD_FILTERS)
annotation class StringSetSettings annotation class StringSetSettings
init {
sharedPreferences =
context.getSharedPreferences(Constants.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
}
} }

View File

@ -0,0 +1,68 @@
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package awais.instagrabber.utils
import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import awais.instagrabber.utils.extensions.TAG
import java.util.concurrent.atomic.AtomicBoolean
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
*
* Note that only one observer is going to be notified of changes.
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
@MainThread
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
}

View File

@ -0,0 +1,3 @@
package awais.instagrabber.utils.extensions
fun String.trimAll() = this.trim { it <= ' ' }

View File

@ -0,0 +1,9 @@
package awais.instagrabber.utils.extensions
import awais.instagrabber.repositories.responses.User
fun User.isReallyPrivate(currentUser: User? = null): Boolean {
if (currentUser == null) return this.isPrivate
if (this.pk == currentUser.pk) return false
return this.friendshipStatus?.following == false && this.isPrivate
}

View File

@ -9,8 +9,10 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import awais.instagrabber.db.repositories.AccountRepository;
import awais.instagrabber.models.Resource; import awais.instagrabber.models.Resource;
import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.CoroutineUtilsKt;
@ -26,6 +28,8 @@ public class AppStateViewModel extends AndroidViewModel {
private final String cookie; private final String cookie;
private final MutableLiveData<Resource<User>> currentUser = new MutableLiveData<>(Resource.loading(null)); private final MutableLiveData<Resource<User>> currentUser = new MutableLiveData<>(Resource.loading(null));
private AccountRepository accountRepository;
private UserRepository userRepository; private UserRepository userRepository;
public AppStateViewModel(@NonNull final Application application) { public AppStateViewModel(@NonNull final Application application) {
@ -38,7 +42,7 @@ public class AppStateViewModel extends AndroidViewModel {
return; return;
} }
userRepository = UserRepository.Companion.getInstance(); userRepository = UserRepository.Companion.getInstance();
// final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(application)); accountRepository = AccountRepository.Companion.getInstance(application);
fetchProfileDetails(); fetchProfileDetails();
} }
@ -61,13 +65,26 @@ public class AppStateViewModel extends AndroidViewModel {
userRepository.getUserInfo(uid, CoroutineUtilsKt.getContinuation((user, throwable) -> { userRepository.getUserInfo(uid, CoroutineUtilsKt.getContinuation((user, throwable) -> {
if (throwable != null) { if (throwable != null) {
Log.e(TAG, "onFailure: ", throwable); Log.e(TAG, "onFailure: ", throwable);
final User backup = currentUser.getValue().data != null ? final Resource<User> userResource = currentUser.getValue();
currentUser.getValue().data : final User backup = userResource != null && userResource.data != null ? userResource.data : new User(uid);
new User(uid);
currentUser.postValue(Resource.error(throwable.getMessage(), backup)); currentUser.postValue(Resource.error(throwable.getMessage(), backup));
return; return;
} }
currentUser.postValue(Resource.success(user)); currentUser.postValue(Resource.success(user));
if (accountRepository != null && user != null) {
accountRepository.insertOrUpdateAccount(
user.getPk(),
user.getUsername(),
cookie,
user.getFullName() != null ? user.getFullName() : "",
user.getProfilePicUrl(),
CoroutineUtilsKt.getContinuation((account, throwable1) -> AppExecutors.INSTANCE.getMainThread().execute(() -> {
if (throwable1 != null) {
Log.e(TAG, "updateAccountInfo: ", throwable1);
}
}), Dispatchers.getIO())
);
}
}, Dispatchers.getIO())); }, Dispatchers.getIO()));
} }
} }

View File

@ -7,11 +7,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import awais.instagrabber.R import awais.instagrabber.R
import awais.instagrabber.managers.DirectMessagesManager import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.models.Resource import awais.instagrabber.models.Resource
import awais.instagrabber.models.Resource.Companion.error import awais.instagrabber.models.Resource.Companion.error
import awais.instagrabber.models.Resource.Companion.loading import awais.instagrabber.models.Resource.Companion.loading
import awais.instagrabber.models.Resource.Companion.success import awais.instagrabber.models.Resource.Companion.success
import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.models.enums.MediaItemType import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.repositories.responses.Caption import awais.instagrabber.repositories.responses.Caption
import awais.instagrabber.repositories.responses.Location import awais.instagrabber.repositories.responses.Location
@ -280,9 +280,9 @@ class PostViewV2ViewModel : ViewModel() {
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val result = mediaRepository.translate(pk, "1") val result = mediaRepository.translate(pk, "1") ?: return@launch
if (result.isBlank()) { if (result.isBlank()) {
data.postValue(error("", null)) // data.postValue(error("", null))
return@launch return@launch
} }
data.postValue(success(result)) data.postValue(success(result))

View File

@ -14,58 +14,90 @@ import awais.instagrabber.models.StoryModel
import awais.instagrabber.models.enums.BroadcastItemType import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.models.enums.FavoriteType import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.repositories.requests.StoryViewerOptions import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.FriendshipStatus
import awais.instagrabber.repositories.responses.User import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.UserProfileContextLink
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.utils.ControlledRunner import awais.instagrabber.utils.ControlledRunner
import awais.instagrabber.utils.Event
import awais.instagrabber.utils.SingleRunner
import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.utils.extensions.isReallyPrivate
import awais.instagrabber.viewmodels.ProfileFragmentViewModel.ProfileAction.*
import awais.instagrabber.viewmodels.ProfileFragmentViewModel.ProfileEvent.*
import awais.instagrabber.webservices.* import awais.instagrabber.webservices.*
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalDateTime
class ProfileFragmentViewModel( class ProfileFragmentViewModel(
state: SavedStateHandle, private val state: SavedStateHandle,
userRepository: UserRepository, private val csrfToken: String?,
friendshipRepository: FriendshipRepository, private val deviceUuid: String?,
private val userRepository: UserRepository,
private val friendshipRepository: FriendshipRepository,
private val storiesRepository: StoriesRepository, private val storiesRepository: StoriesRepository,
mediaRepository: MediaRepository, private val mediaRepository: MediaRepository,
graphQLRepository: GraphQLRepository, private val graphQLRepository: GraphQLRepository,
accountRepository: AccountRepository,
private val favoriteRepository: FavoriteRepository, private val favoriteRepository: FavoriteRepository,
private val directMessagesRepository: DirectMessagesRepository,
private val messageManager: DirectMessagesManager?,
ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val _currentUser = MutableLiveData<Resource<User?>>(Resource.loading(null)) private val _currentUser = MutableLiveData<Resource<User?>>(Resource.loading(null))
private val _isFavorite = MutableLiveData(false) private val _isFavorite = MutableLiveData(false)
private var messageManager: DirectMessagesManager? = null private val profileAction = MutableLiveData(INIT)
private val _eventLiveData = MutableLiveData<Event<ProfileEvent>?>()
enum class ProfileAction {
INIT,
REFRESH,
REFRESH_FRIENDSHIP,
}
sealed class ProfileEvent {
object ShowConfirmUnfollowDialog : ProfileEvent()
class DMButtonState(val disabled: Boolean) : ProfileEvent()
class NavigateToThread(val threadId: String, val username: String) : ProfileEvent()
class ShowTranslation(val result: String) : ProfileEvent()
}
val currentUser: LiveData<Resource<User?>> = _currentUser val currentUser: LiveData<Resource<User?>> = _currentUser
val isLoggedIn: LiveData<Boolean> = currentUser.map { it.data != null } val isLoggedIn: LiveData<Boolean> = currentUser.map { it.data != null }
val isFavorite: LiveData<Boolean> = _isFavorite val isFavorite: LiveData<Boolean> = _isFavorite
val eventLiveData: LiveData<Event<ProfileEvent>?> = _eventLiveData
private val currentUserAndStateUsernameLiveData: LiveData<Pair<Resource<User?>, Resource<String?>>> = private val currentUserStateUsernameActionLiveData: LiveData<Triple<Resource<User?>, Resource<String?>, ProfileAction>> =
object : MediatorLiveData<Pair<Resource<User?>, Resource<String?>>>() { object : MediatorLiveData<Triple<Resource<User?>, Resource<String?>, ProfileAction>>() {
var user: Resource<User?> = Resource.loading(null) var user: Resource<User?> = Resource.loading(null)
var stateUsername: Resource<String?> = Resource.loading(null) var stateUsername: Resource<String?> = Resource.loading(null)
var action: ProfileAction = INIT
init { init {
addSource(currentUser) { currentUser -> addSource(currentUser) { currentUser ->
this.user = currentUser this.user = currentUser
value = currentUser to stateUsername value = Triple(currentUser, stateUsername, action)
} }
addSource(state.getLiveData<String?>("username")) { username -> addSource(state.getLiveData<String?>("username")) { username ->
this.stateUsername = Resource.success(username.substringAfter('@')) this.stateUsername = Resource.success(username.substringAfter('@'))
value = user to this.stateUsername value = Triple(user, this.stateUsername, action)
} }
// trigger currentUserAndStateUsernameLiveData switch map with a state username success resource addSource(profileAction) { action ->
this.action = action
value = Triple(user, stateUsername, action)
}
// trigger currentUserStateUsernameActionLiveData switch map with a state username success resource
if (!state.contains("username")) { if (!state.contains("username")) {
this.stateUsername = Resource.success(null) this.stateUsername = Resource.success(null)
value = user to this.stateUsername value = Triple(user, this.stateUsername, action)
} }
} }
} }
private val profileFetchControlledRunner = ControlledRunner<User?>() private val profileFetchControlledRunner = ControlledRunner<User?>()
val profile: LiveData<Resource<User?>> = currentUserAndStateUsernameLiveData.switchMap { val profile: LiveData<Resource<User?>> = currentUserStateUsernameActionLiveData.switchMap {
val (currentUserResource, stateUsernameResource) = it val (currentUserResource, stateUsernameResource, action) = it
liveData<Resource<User?>>(context = viewModelScope.coroutineContext + ioDispatcher) { liveData<Resource<User?>>(context = viewModelScope.coroutineContext + ioDispatcher) {
if (currentUserResource.status == Resource.Status.LOADING || stateUsernameResource.status == Resource.Status.LOADING) { if (currentUserResource.status == Resource.Status.LOADING || stateUsernameResource.status == Resource.Status.LOADING) {
emit(Resource.loading(null)) emit(Resource.loading(null))
@ -78,33 +110,67 @@ class ProfileFragmentViewModel(
return@liveData return@liveData
} }
try { try {
val fetchedUser = profileFetchControlledRunner.cancelPreviousThenRun { when (action) {
return@cancelPreviousThenRun fetchUser(currentUser, userRepository, stateUsername, graphQLRepository) INIT, REFRESH -> {
} val fetchedUser = profileFetchControlledRunner.cancelPreviousThenRun { fetchUser(currentUser, stateUsername) }
emit(Resource.success(fetchedUser)) emit(Resource.success(fetchedUser))
if (fetchedUser != null) { if (fetchedUser != null) {
checkAndInsertFavorite(fetchedUser) checkAndUpdateFavorite(fetchedUser)
}
}
REFRESH_FRIENDSHIP -> {
var profile = profileCopy.value?.data ?: return@liveData
profile = profile.copy(friendshipStatus = userRepository.getUserFriendship(profile.pk))
emit(Resource.success(profile))
}
} }
} catch (e: Exception) { } catch (e: Exception) {
emit(Resource.error(e.message, null)) emit(Resource.error(e.message, profileCopy.value?.data))
Log.e(TAG, "fetching user: ", e) Log.e(TAG, "fetching user: ", e)
} }
} }
} }
val profileCopy = profile
val currentUserProfileActionLiveData: LiveData<Triple<Resource<User?>, Resource<User?>, ProfileAction>> =
object : MediatorLiveData<Triple<Resource<User?>, Resource<User?>, ProfileAction>>() {
var currentUser: Resource<User?> = Resource.loading(null)
var profile: Resource<User?> = Resource.loading(null)
var action: ProfileAction = INIT
init {
addSource(this@ProfileFragmentViewModel.currentUser) { currentUser ->
this.currentUser = currentUser
value = Triple(currentUser, profile, action)
}
addSource(this@ProfileFragmentViewModel.profile) { profile ->
this.profile = profile
value = Triple(currentUser, this.profile, action)
}
addSource(profileAction) { action ->
this.action = action
value = Triple(currentUser, this.profile, action)
}
}
}
private val storyFetchControlledRunner = ControlledRunner<List<StoryModel>?>() private val storyFetchControlledRunner = ControlledRunner<List<StoryModel>?>()
val userStories: LiveData<Resource<List<StoryModel>?>> = profile.switchMap { userResource -> val userStories: LiveData<Resource<List<StoryModel>?>> = currentUserProfileActionLiveData.switchMap { currentUserAndProfilePair ->
liveData<Resource<List<StoryModel>?>>(context = viewModelScope.coroutineContext + ioDispatcher) { liveData<Resource<List<StoryModel>?>>(context = viewModelScope.coroutineContext + ioDispatcher) {
val (currentUserResource, profileResource, action) = currentUserAndProfilePair
if (action != INIT && action != REFRESH) {
return@liveData
}
// don't fetch if not logged in // don't fetch if not logged in
if (isLoggedIn.value != true) { if (currentUserResource.data == null) {
emit(Resource.success(null)) emit(Resource.success(null))
return@liveData return@liveData
} }
if (userResource.status == Resource.Status.LOADING) { if (currentUserResource.status == Resource.Status.LOADING || profileResource.status == Resource.Status.LOADING) {
emit(Resource.loading(null)) emit(Resource.loading(null))
return@liveData return@liveData
} }
val user = userResource.data val user = profileResource.data
if (user == null) { if (user == null) {
emit(Resource.success(null)) emit(Resource.success(null))
return@liveData return@liveData
@ -120,18 +186,22 @@ class ProfileFragmentViewModel(
} }
private val highlightsFetchControlledRunner = ControlledRunner<List<HighlightModel>?>() private val highlightsFetchControlledRunner = ControlledRunner<List<HighlightModel>?>()
val userHighlights: LiveData<Resource<List<HighlightModel>?>> = profile.switchMap { userResource -> val userHighlights: LiveData<Resource<List<HighlightModel>?>> = currentUserProfileActionLiveData.switchMap { currentUserAndProfilePair ->
liveData<Resource<List<HighlightModel>?>>(context = viewModelScope.coroutineContext + ioDispatcher) { liveData<Resource<List<HighlightModel>?>>(context = viewModelScope.coroutineContext + ioDispatcher) {
val (currentUserResource, profileResource, action) = currentUserAndProfilePair
if (action != INIT && action != REFRESH) {
return@liveData
}
// don't fetch if not logged in // don't fetch if not logged in
if (isLoggedIn.value != true) { if (currentUserResource.data == null) {
emit(Resource.success(null)) emit(Resource.success(null))
return@liveData return@liveData
} }
if (userResource.status == Resource.Status.LOADING) { if (currentUserResource.status == Resource.Status.LOADING || profileResource.status == Resource.Status.LOADING) {
emit(Resource.loading(null)) emit(Resource.loading(null))
return@liveData return@liveData
} }
val user = userResource.data val user = profileResource.data
if (user == null) { if (user == null) {
emit(Resource.success(null)) emit(Resource.success(null))
return@liveData return@liveData
@ -141,24 +211,25 @@ class ProfileFragmentViewModel(
emit(Resource.success(fetchedHighlights)) emit(Resource.success(fetchedHighlights))
} catch (e: Exception) { } catch (e: Exception) {
emit(Resource.error(e.message, null)) emit(Resource.error(e.message, null))
Log.e(TAG, "fetching story: ", e) Log.e(TAG, "fetching highlights: ", e)
} }
} }
} }
private suspend fun fetchUser( private suspend fun fetchUser(
currentUser: User?, currentUser: User?,
userRepository: UserRepository,
stateUsername: String, stateUsername: String,
graphQLRepository: GraphQLRepository ): User {
) = if (currentUser != null) { if (currentUser != null) {
// logged in // logged in
val tempUser = userRepository.getUsernameInfo(stateUsername) val tempUser = userRepository.getUsernameInfo(stateUsername)
tempUser.friendshipStatus = userRepository.getUserFriendship(tempUser.pk) if (!tempUser.isReallyPrivate(currentUser)) {
tempUser tempUser.friendshipStatus = userRepository.getUserFriendship(tempUser.pk)
} else { }
return tempUser
}
// anonymous // anonymous
graphQLRepository.fetchUser(stateUsername) return graphQLRepository.fetchUser(stateUsername)
} }
private suspend fun fetchUserStory(fetchedUser: User): List<StoryModel> = storiesRepository.getUserStory( private suspend fun fetchUserStory(fetchedUser: User): List<StoryModel> = storiesRepository.getUserStory(
@ -167,7 +238,7 @@ class ProfileFragmentViewModel(
private suspend fun fetchUserHighlights(fetchedUser: User): List<HighlightModel> = storiesRepository.fetchHighlights(fetchedUser.pk) private suspend fun fetchUserHighlights(fetchedUser: User): List<HighlightModel> = storiesRepository.fetchHighlights(fetchedUser.pk)
private suspend fun checkAndInsertFavorite(fetchedUser: User) { private suspend fun checkAndUpdateFavorite(fetchedUser: User) {
try { try {
val favorite = favoriteRepository.getFavorite(fetchedUser.username, FavoriteType.USER) val favorite = favoriteRepository.getFavorite(fetchedUser.username, FavoriteType.USER)
if (favorite == null) { if (favorite == null) {
@ -187,7 +258,260 @@ class ProfileFragmentViewModel(
) )
} catch (e: Exception) { } catch (e: Exception) {
_isFavorite.postValue(false) _isFavorite.postValue(false)
Log.e(TAG, "checkAndInsertFavorite: ", e) Log.e(TAG, "checkAndUpdateFavorite: ", e)
}
}
fun setCurrentUser(currentUser: Resource<User?>) {
_currentUser.postValue(currentUser)
}
fun shareDm(result: RankedRecipient) {
val mediaId = profile.value?.data?.pk ?: return
messageManager?.sendMedia(result, mediaId.toString(10), BroadcastItemType.PROFILE, viewModelScope)
}
fun shareDm(recipients: Set<RankedRecipient>) {
val mediaId = profile.value?.data?.pk ?: return
messageManager?.sendMedia(recipients, mediaId.toString(10), BroadcastItemType.PROFILE, viewModelScope)
}
fun refresh() {
profileAction.postValue(REFRESH)
}
private val toggleFavoriteControlledRunner = SingleRunner()
fun toggleFavorite() {
val username = profile.value?.data?.username ?: return
val fullName = profile.value?.data?.fullName ?: return
val profilePicUrl = profile.value?.data?.profilePicUrl ?: return
viewModelScope.launch(Dispatchers.IO) {
toggleFavoriteControlledRunner.afterPrevious {
try {
val favorite = favoriteRepository.getFavorite(username, FavoriteType.USER)
if (favorite == null) {
// insert
favoriteRepository.insertOrUpdateFavorite(
Favorite(
0,
username,
FavoriteType.USER,
fullName,
profilePicUrl,
LocalDateTime.now()
)
)
_isFavorite.postValue(true)
return@afterPrevious
}
// delete
favoriteRepository.deleteFavorite(username, FavoriteType.USER)
_isFavorite.postValue(false)
} catch (e: Exception) {
Log.e(TAG, "checkAndUpdateFavorite: ", e)
}
}
}
}
private val toggleFollowSingleRunner = SingleRunner()
fun toggleFollow(confirmed: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
toggleFollowSingleRunner.afterPrevious {
try {
val following = profile.value?.data?.friendshipStatus?.following ?: false
val currentUserId = currentUser.value?.data?.pk ?: return@afterPrevious
val targetUserId = profile.value?.data?.pk ?: return@afterPrevious
val csrfToken = csrfToken ?: return@afterPrevious
val deviceUuid = deviceUuid ?: return@afterPrevious
if (following) {
if (!confirmed) {
_eventLiveData.postValue(Event(ShowConfirmUnfollowDialog))
return@afterPrevious
}
// unfollow
friendshipRepository.unfollow(
csrfToken,
currentUserId,
deviceUuid,
targetUserId
)
profileAction.postValue(REFRESH_FRIENDSHIP)
return@afterPrevious
}
friendshipRepository.follow(
csrfToken,
currentUserId,
deviceUuid,
targetUserId
)
profileAction.postValue(REFRESH_FRIENDSHIP)
} catch (e: Exception) {
Log.e(TAG, "toggleFollow: ", e)
}
}
}
}
private val sendDmSingleRunner = SingleRunner()
fun sendDm() {
viewModelScope.launch(Dispatchers.IO) {
sendDmSingleRunner.afterPrevious {
_eventLiveData.postValue(Event(DMButtonState(true)))
try {
val currentUserId = currentUser.value?.data?.pk ?: return@afterPrevious
val targetUserId = profile.value?.data?.pk ?: return@afterPrevious
val csrfToken = csrfToken ?: return@afterPrevious
val deviceUuid = deviceUuid ?: return@afterPrevious
val username = profile.value?.data?.username ?: return@afterPrevious
val thread = directMessagesRepository.createThread(
csrfToken,
currentUserId,
deviceUuid,
listOf(targetUserId),
null,
)
val inboxManager = DirectMessagesManager.inboxManager
if (!inboxManager.containsThread(thread.threadId)) {
thread.isTemp = true
inboxManager.addThread(thread, 0)
}
val threadId = thread.threadId ?: return@afterPrevious
_eventLiveData.postValue(Event(NavigateToThread(threadId, username)))
} catch (e: Exception) {
Log.e(TAG, "sendDm: ", e)
} finally {
_eventLiveData.postValue(Event(DMButtonState(false)))
}
}
}
}
private val restrictUserSingleRunner = SingleRunner()
fun restrictUser() {
if (isLoggedIn.value == false) return
viewModelScope.launch(Dispatchers.IO) {
restrictUserSingleRunner.afterPrevious {
try {
val profile = profile.value?.data ?: return@afterPrevious
friendshipRepository.toggleRestrict(
csrfToken ?: return@afterPrevious,
deviceUuid ?: return@afterPrevious,
profile.pk,
!(profile.friendshipStatus?.isRestricted ?: false),
)
profileAction.postValue(REFRESH_FRIENDSHIP)
} catch (e: Exception) {
Log.e(TAG, "restrictUser: ", e)
}
}
}
}
private val blockUserSingleRunner = SingleRunner()
fun blockUser() {
if (isLoggedIn.value == false) return
viewModelScope.launch(Dispatchers.IO) {
blockUserSingleRunner.afterPrevious {
try {
val profile = profile.value?.data ?: return@afterPrevious
friendshipRepository.changeBlock(
csrfToken ?: return@afterPrevious,
currentUser.value?.data?.pk ?: return@afterPrevious,
deviceUuid ?: return@afterPrevious,
profile.friendshipStatus?.blocking ?: return@afterPrevious,
profile.pk
)
profileAction.postValue(REFRESH_FRIENDSHIP)
} catch (e: Exception) {
Log.e(TAG, "blockUser: ", e)
}
}
}
}
private val muteStoriesSingleRunner = SingleRunner()
fun muteStories() {
if (isLoggedIn.value == false) return
viewModelScope.launch(Dispatchers.IO) {
muteStoriesSingleRunner.afterPrevious {
try {
val profile = profile.value?.data ?: return@afterPrevious
friendshipRepository.changeMute(
csrfToken ?: return@afterPrevious,
currentUser.value?.data?.pk ?: return@afterPrevious,
deviceUuid ?: return@afterPrevious,
profile.friendshipStatus?.isMutingReel ?: return@afterPrevious,
profile.pk,
true
)
profileAction.postValue(REFRESH_FRIENDSHIP)
} catch (e: Exception) {
Log.e(TAG, "muteStories: ", e)
}
}
}
}
private val mutePostsSingleRunner = SingleRunner()
fun mutePosts() {
if (isLoggedIn.value == false) return
viewModelScope.launch(Dispatchers.IO) {
mutePostsSingleRunner.afterPrevious {
try {
val profile = profile.value?.data ?: return@afterPrevious
friendshipRepository.changeMute(
csrfToken ?: return@afterPrevious,
currentUser.value?.data?.pk ?: return@afterPrevious,
deviceUuid ?: return@afterPrevious,
profile.friendshipStatus?.muting ?: return@afterPrevious,
profile.pk,
false
)
profileAction.postValue(REFRESH_FRIENDSHIP)
} catch (e: Exception) {
Log.e(TAG, "mutePosts: ", e)
}
}
}
}
private val removeFollowerSingleRunner = SingleRunner()
fun removeFollower() {
if (isLoggedIn.value == false) return
viewModelScope.launch(Dispatchers.IO) {
removeFollowerSingleRunner.afterPrevious {
try {
friendshipRepository.removeFollower(
csrfToken ?: return@afterPrevious,
currentUser.value?.data?.pk ?: return@afterPrevious,
deviceUuid ?: return@afterPrevious,
profile.value?.data?.pk ?: return@afterPrevious
)
profileAction.postValue(REFRESH_FRIENDSHIP)
} catch (e: Exception) {
Log.e(TAG, "removeFollower: ", e)
}
}
}
}
private val translateBioSingleRunner = SingleRunner()
fun translateBio() {
if (isLoggedIn.value == false) return
viewModelScope.launch(Dispatchers.IO) {
translateBioSingleRunner.afterPrevious {
try {
val result = mediaRepository.translate(
profile.value?.data?.pk?.toString() ?: return@afterPrevious,
"3"
)
if (result.isNullOrBlank()) return@afterPrevious
_eventLiveData.postValue(Event(ShowTranslation(result)))
} catch (e: Exception) {
Log.e(TAG, "translateBio: ", e)
}
}
} }
} }
@ -196,38 +520,82 @@ class ProfileFragmentViewModel(
*/ */
val username: LiveData<String> = Transformations.map(profile) { val username: LiveData<String> = Transformations.map(profile) {
return@map when (it.status) { return@map when (it.status) {
Resource.Status.LOADING, Resource.Status.ERROR -> "" Resource.Status.ERROR -> ""
Resource.Status.SUCCESS -> it.data?.username ?: "" Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.username ?: ""
} }
} }
val profilePicUrl: LiveData<String?> = Transformations.map(profile) {
init { return@map when (it.status) {
// Log.d(TAG, "${state.keys()} $userRepository $friendshipRepository $storiesRepository $mediaRepository") Resource.Status.ERROR -> null
} Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.profilePicUrl
fun setCurrentUser(currentUser: Resource<User?>) {
_currentUser.postValue(currentUser)
}
fun shareDm(result: RankedRecipient) {
if (messageManager == null) {
messageManager = DirectMessagesManager
} }
val mediaId = profile.value?.data?.pk ?: return
messageManager?.sendMedia(result, mediaId.toString(10), BroadcastItemType.PROFILE, viewModelScope)
} }
val fullName: LiveData<String?> = Transformations.map(profile) {
fun shareDm(recipients: Set<RankedRecipient>) { return@map when (it.status) {
if (messageManager == null) { Resource.Status.ERROR -> ""
messageManager = DirectMessagesManager Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.fullName
}
}
val biography: LiveData<String?> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> ""
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.biography
}
}
val url: LiveData<String?> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> ""
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.externalUrl
}
}
val followersCount: LiveData<Long?> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> null
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.followerCount
}
}
val followingCount: LiveData<Long?> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> null
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.followingCount
}
}
val postCount: LiveData<Long?> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> null
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.mediaCount
}
}
val isPrivate: LiveData<Boolean?> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> null
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.isPrivate
}
}
val isVerified: LiveData<Boolean?> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> null
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.isVerified
}
}
val friendshipStatus: LiveData<FriendshipStatus?> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> null
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.friendshipStatus
}
}
val profileContext: LiveData<Pair<String?, List<UserProfileContextLink>?>> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> null to null
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.profileContext to it.data?.profileContextLinksWithUserIds
} }
val mediaId = profile.value?.data?.pk ?: return
messageManager?.sendMedia(recipients, mediaId.toString(10), BroadcastItemType.PROFILE, viewModelScope)
} }
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class ProfileFragmentViewModelFactory( class ProfileFragmentViewModelFactory(
private val csrfToken: String?,
private val deviceUuid: String?,
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val friendshipRepository: FriendshipRepository, private val friendshipRepository: FriendshipRepository,
private val storiesRepository: StoriesRepository, private val storiesRepository: StoriesRepository,
@ -235,6 +603,8 @@ class ProfileFragmentViewModelFactory(
private val graphQLRepository: GraphQLRepository, private val graphQLRepository: GraphQLRepository,
private val accountRepository: AccountRepository, private val accountRepository: AccountRepository,
private val favoriteRepository: FavoriteRepository, private val favoriteRepository: FavoriteRepository,
private val directMessagesRepository: DirectMessagesRepository,
private val messageManager: DirectMessagesManager?,
owner: SavedStateRegistryOwner, owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null, defaultArgs: Bundle? = null,
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) { ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
@ -245,13 +615,16 @@ class ProfileFragmentViewModelFactory(
): T { ): T {
return ProfileFragmentViewModel( return ProfileFragmentViewModel(
handle, handle,
csrfToken,
deviceUuid,
userRepository, userRepository,
friendshipRepository, friendshipRepository,
storiesRepository, storiesRepository,
mediaRepository, mediaRepository,
graphQLRepository, graphQLRepository,
accountRepository,
favoriteRepository, favoriteRepository,
directMessagesRepository,
messageManager,
Dispatchers.IO, Dispatchers.IO,
) as T ) as T
} }

View File

@ -31,7 +31,7 @@ import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.utils.Debouncer; import awais.instagrabber.utils.Debouncer;
import awais.instagrabber.utils.RankedRecipientsCache; import awais.instagrabber.utils.RankedRecipientsCache;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.DirectMessagesService; import awais.instagrabber.webservices.DirectMessagesRepository;
import awais.instagrabber.webservices.UserRepository; import awais.instagrabber.webservices.UserRepository;
import kotlinx.coroutines.Dispatchers; import kotlinx.coroutines.Dispatchers;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
@ -59,7 +59,7 @@ public class UserSearchViewModel extends ViewModel {
private final Debouncer<String> searchDebouncer; private final Debouncer<String> searchDebouncer;
private final Set<RankedRecipient> selectedRecipients = new HashSet<>(); private final Set<RankedRecipient> selectedRecipients = new HashSet<>();
private final UserRepository userRepository; private final UserRepository userRepository;
private final DirectMessagesService directMessagesService; private final DirectMessagesRepository directMessagesRepository;
private final RankedRecipientsCache rankedRecipientsCache; private final RankedRecipientsCache rankedRecipientsCache;
public UserSearchViewModel() { public UserSearchViewModel() {
@ -71,7 +71,7 @@ public class UserSearchViewModel extends ViewModel {
throw new IllegalArgumentException("User is not logged in!"); throw new IllegalArgumentException("User is not logged in!");
} }
userRepository = UserRepository.Companion.getInstance(); userRepository = UserRepository.Companion.getInstance();
directMessagesService = DirectMessagesService.INSTANCE; directMessagesRepository = DirectMessagesRepository.Companion.getInstance();
rankedRecipientsCache = RankedRecipientsCache.INSTANCE; rankedRecipientsCache = RankedRecipientsCache.INSTANCE;
if ((rankedRecipientsCache.isFailed() || rankedRecipientsCache.isExpired()) && !rankedRecipientsCache.isUpdateInitiated()) { if ((rankedRecipientsCache.isFailed() || rankedRecipientsCache.isExpired()) && !rankedRecipientsCache.isUpdateInitiated()) {
updateRankedRecipientCache(); updateRankedRecipientCache();
@ -94,7 +94,7 @@ public class UserSearchViewModel extends ViewModel {
private void updateRankedRecipientCache() { private void updateRankedRecipientCache() {
rankedRecipientsCache.setUpdateInitiated(true); rankedRecipientsCache.setUpdateInitiated(true);
directMessagesService.rankedRecipients( directMessagesRepository.rankedRecipients(
null, null,
null, null,
null, null,
@ -191,7 +191,7 @@ public class UserSearchViewModel extends ViewModel {
} }
private void rankedRecipientSearch() { private void rankedRecipientSearch() {
directMessagesService.rankedRecipients( directMessagesRepository.rankedRecipients(
searchMode.getName(), searchMode.getName(),
showGroups, showGroups,
currentQuery, currentQuery,

View File

@ -1,6 +1,6 @@
package awais.instagrabber.webservices package awais.instagrabber.webservices
import awais.instagrabber.repositories.DirectMessagesRepository import awais.instagrabber.repositories.DirectMessagesService
import awais.instagrabber.repositories.requests.directmessages.* import awais.instagrabber.repositories.requests.directmessages.*
import awais.instagrabber.repositories.responses.directmessages.* import awais.instagrabber.repositories.responses.directmessages.*
import awais.instagrabber.repositories.responses.giphy.GiphyGif import awais.instagrabber.repositories.responses.giphy.GiphyGif
@ -9,8 +9,7 @@ import awais.instagrabber.utils.Utils
import org.json.JSONArray import org.json.JSONArray
import java.util.* import java.util.*
object DirectMessagesService { open class DirectMessagesRepository(private val service: DirectMessagesService) {
private val repository: DirectMessagesRepository = RetrofitFactory.retrofit.create(DirectMessagesRepository::class.java)
suspend fun fetchInbox( suspend fun fetchInbox(
cursor: String?, cursor: String?,
@ -29,7 +28,7 @@ object DirectMessagesService {
if (seqId != 0L) { if (seqId != 0L) {
queryMap["seq_id"] = seqId.toString() queryMap["seq_id"] = seqId.toString()
} }
return repository.fetchInbox(queryMap) return service.fetchInbox(queryMap)
} }
suspend fun fetchThread( suspend fun fetchThread(
@ -44,10 +43,10 @@ object DirectMessagesService {
if (!cursor.isNullOrBlank()) { if (!cursor.isNullOrBlank()) {
queryMap["cursor"] = cursor queryMap["cursor"] = cursor
} }
return repository.fetchThread(threadId, queryMap) return service.fetchThread(threadId, queryMap)
} }
suspend fun fetchUnseenCount(): DirectBadgeCount = repository.fetchUnseenCount() suspend fun fetchUnseenCount(): DirectBadgeCount = service.fetchUnseenCount()
suspend fun broadcastText( suspend fun broadcastText(
csrfToken: String, csrfToken: String,
@ -61,7 +60,17 @@ object DirectMessagesService {
): DirectThreadBroadcastResponse { ): DirectThreadBroadcastResponse {
val urls = extractUrls(text) val urls = extractUrls(text)
if (urls.isNotEmpty()) { if (urls.isNotEmpty()) {
return broadcastLink(csrfToken, userId, deviceUuid, clientContext, threadIdsOrUserIds, text, urls, repliedToItemId, repliedToClientContext) return broadcastLink(
csrfToken,
userId,
deviceUuid,
clientContext,
threadIdsOrUserIds,
text,
urls,
repliedToItemId,
repliedToClientContext
)
} }
val broadcastOptions = TextBroadcastOptions(clientContext, threadIdsOrUserIds, text) val broadcastOptions = TextBroadcastOptions(clientContext, threadIdsOrUserIds, text)
if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) { if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) {
@ -211,7 +220,7 @@ object DirectMessagesService {
form.putAll(broadcastOptions.formMap) form.putAll(broadcastOptions.formMap)
form["action"] = "send_item" form["action"] = "send_item"
// val signedForm = Utils.sign(form) // val signedForm = Utils.sign(form)
return repository.broadcast(broadcastOptions.itemType.value, form) return service.broadcast(broadcastOptions.itemType.value, form)
} }
suspend fun addUsers( suspend fun addUsers(
@ -225,7 +234,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
"user_ids" to JSONArray(userIds).toString(), "user_ids" to JSONArray(userIds).toString(),
) )
return repository.addUsers(threadId, form) return service.addUsers(threadId, form)
} }
suspend fun removeUsers( suspend fun removeUsers(
@ -239,7 +248,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
"user_ids" to JSONArray(userIds).toString(), "user_ids" to JSONArray(userIds).toString(),
) )
return repository.removeUsers(threadId, form) return service.removeUsers(threadId, form)
} }
suspend fun updateTitle( suspend fun updateTitle(
@ -253,7 +262,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
"title" to title, "title" to title,
) )
return repository.updateTitle(threadId, form) return service.updateTitle(threadId, form)
} }
suspend fun addAdmins( suspend fun addAdmins(
@ -267,7 +276,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
"user_ids" to JSONArray(userIds).toString(), "user_ids" to JSONArray(userIds).toString(),
) )
return repository.addAdmins(threadId, form) return service.addAdmins(threadId, form)
} }
suspend fun removeAdmins( suspend fun removeAdmins(
@ -281,7 +290,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
"user_ids" to JSONArray(userIds).toString(), "user_ids" to JSONArray(userIds).toString(),
) )
return repository.removeAdmins(threadId, form) return service.removeAdmins(threadId, form)
} }
suspend fun deleteItem( suspend fun deleteItem(
@ -294,7 +303,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
) )
return repository.deleteItem(threadId, itemId, form) return service.deleteItem(threadId, itemId, form)
} }
suspend fun rankedRecipients( suspend fun rankedRecipients(
@ -316,7 +325,7 @@ object DirectMessagesService {
if (showThreads != null) { if (showThreads != null) {
queryMap["showThreads"] = showThreads.toString() queryMap["showThreads"] = showThreads.toString()
} }
return repository.rankedRecipients(queryMap) return service.rankedRecipients(queryMap)
} }
suspend fun forward( suspend fun forward(
@ -332,7 +341,7 @@ object DirectMessagesService {
"forwarded_from_thread_id" to fromThreadId, "forwarded_from_thread_id" to fromThreadId,
"forwarded_from_thread_item_id" to itemId, "forwarded_from_thread_item_id" to itemId,
) )
return repository.forward(form) return service.forward(form)
} }
suspend fun createThread( suspend fun createThread(
@ -353,7 +362,7 @@ object DirectMessagesService {
form["thread_title"] = threadTitle form["thread_title"] = threadTitle
} }
val signedForm = Utils.sign(form) val signedForm = Utils.sign(form)
return repository.createThread(signedForm) return service.createThread(signedForm)
} }
suspend fun mute( suspend fun mute(
@ -365,7 +374,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid "_uuid" to deviceUuid
) )
return repository.mute(threadId, form) return service.mute(threadId, form)
} }
suspend fun unmute( suspend fun unmute(
@ -377,7 +386,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
) )
return repository.unmute(threadId, form) return service.unmute(threadId, form)
} }
suspend fun muteMentions( suspend fun muteMentions(
@ -389,7 +398,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
) )
return repository.muteMentions(threadId, form) return service.muteMentions(threadId, form)
} }
suspend fun unmuteMentions( suspend fun unmuteMentions(
@ -401,7 +410,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
) )
return repository.unmuteMentions(threadId, form) return service.unmuteMentions(threadId, form)
} }
suspend fun participantRequests( suspend fun participantRequests(
@ -409,7 +418,7 @@ object DirectMessagesService {
pageSize: Int, pageSize: Int,
cursor: String? = null, cursor: String? = null,
): DirectThreadParticipantRequestsResponse { ): DirectThreadParticipantRequestsResponse {
return repository.participantRequests(threadId, pageSize, cursor) return service.participantRequests(threadId, pageSize, cursor)
} }
suspend fun approveParticipantRequests( suspend fun approveParticipantRequests(
@ -424,7 +433,7 @@ object DirectMessagesService {
"user_ids" to JSONArray(userIds).toString(), "user_ids" to JSONArray(userIds).toString(),
// "share_join_chat_story" to String.valueOf(true) // "share_join_chat_story" to String.valueOf(true)
) )
return repository.approveParticipantRequests(threadId, form) return service.approveParticipantRequests(threadId, form)
} }
suspend fun declineParticipantRequests( suspend fun declineParticipantRequests(
@ -438,7 +447,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
"user_ids" to JSONArray(userIds).toString(), "user_ids" to JSONArray(userIds).toString(),
) )
return repository.declineParticipantRequests(threadId, form) return service.declineParticipantRequests(threadId, form)
} }
suspend fun approvalRequired( suspend fun approvalRequired(
@ -450,7 +459,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
) )
return repository.approvalRequired(threadId, form) return service.approvalRequired(threadId, form)
} }
suspend fun approvalNotRequired( suspend fun approvalNotRequired(
@ -462,7 +471,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
) )
return repository.approvalNotRequired(threadId, form) return service.approvalNotRequired(threadId, form)
} }
suspend fun leave( suspend fun leave(
@ -474,7 +483,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
) )
return repository.leave(threadId, form) return service.leave(threadId, form)
} }
suspend fun end( suspend fun end(
@ -486,7 +495,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
) )
return repository.end(threadId, form) return service.end(threadId, form)
} }
suspend fun fetchPendingInbox(cursor: String?, seqId: Long): DirectInboxResponse { suspend fun fetchPendingInbox(cursor: String?, seqId: Long): DirectInboxResponse {
@ -503,7 +512,7 @@ object DirectMessagesService {
if (seqId != 0L) { if (seqId != 0L) {
queryMap["seq_id"] = seqId.toString() queryMap["seq_id"] = seqId.toString()
} }
return repository.fetchPendingInbox(queryMap) return service.fetchPendingInbox(queryMap)
} }
suspend fun approveRequest( suspend fun approveRequest(
@ -515,7 +524,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
) )
return repository.approveRequest(threadId, form) return service.approveRequest(threadId, form)
} }
suspend fun declineRequest( suspend fun declineRequest(
@ -527,7 +536,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken, "_csrftoken" to csrfToken,
"_uuid" to deviceUuid, "_uuid" to deviceUuid,
) )
return repository.declineRequest(threadId, form) return service.declineRequest(threadId, form)
} }
suspend fun markAsSeen( suspend fun markAsSeen(
@ -545,6 +554,18 @@ object DirectMessagesService {
"thread_id" to threadId, "thread_id" to threadId,
"item_id" to itemId, "item_id" to itemId,
) )
return repository.markItemSeen(threadId, itemId, form) return service.markItemSeen(threadId, itemId, form)
}
companion object {
@Volatile
private var INSTANCE: DirectMessagesRepository? = null
fun getInstance(): DirectMessagesRepository {
return INSTANCE ?: synchronized(this) {
val service: DirectMessagesService = RetrofitFactory.retrofit.create(DirectMessagesService::class.java)
DirectMessagesRepository(service).also { INSTANCE = it }
}
}
} }
} }

View File

@ -110,14 +110,17 @@ class MediaRepository(private val service: MediaService) {
suspend fun translate( suspend fun translate(
id: String, id: String,
type: String, // 1 caption 2 comment 3 bio type: String, // 1 caption 2 comment 3 bio
): String { ): String? {
val form = mapOf( val form = mapOf(
"id" to id, "id" to id,
"type" to type, "type" to type,
) )
val response = service.translate(form) val response = service.translate(form)
val jsonObject = JSONObject(response) val jsonObject = JSONObject(response)
return jsonObject.optString("translation") if (!jsonObject.has("translation") || jsonObject.isNull("translation")) {
return null
}
return jsonObject.getString("translation")
} }
suspend fun uploadFinish( suspend fun uploadFinish(

View File

@ -28,7 +28,7 @@ object RetrofitFactory {
addInterceptor(AddCookiesInterceptor()) addInterceptor(AddCookiesInterceptor())
addInterceptor(igErrorsInterceptor) addInterceptor(igErrorsInterceptor)
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
// addInterceptor(new LoggingInterceptor()); // addInterceptor(LoggingInterceptor())
} }
} }
val gson = GsonBuilder().apply { val gson = GsonBuilder().apply {

View File

@ -5,7 +5,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
app:layoutDescription="@xml/header_list_scene"> app:layoutDescription="@xml/header_list_scene"
tools:layoutDescription="@xml/profile_fragment_no_acc_layout">
<include <include
android:id="@+id/header" android:id="@+id/header"

View File

@ -92,6 +92,7 @@
<string name="story_mentions">Mentions</string> <string name="story_mentions">Mentions</string>
<string name="priv_acc">This Account is Private</string> <string name="priv_acc">This Account is Private</string>
<string name="priv_acc_confirm">You won\'t be able to access posts after unfollowing! Are you sure?</string> <string name="priv_acc_confirm">You won\'t be able to access posts after unfollowing! Are you sure?</string>
<string name="are_you_sure">Are you sure?</string>
<string name="no_acc">You can log in via More -&gt; Account on the bottom-right corner or you can view public accounts without login!</string> <string name="no_acc">You can log in via More -&gt; Account on the bottom-right corner or you can view public accounts without login!</string>
<string name="empty_acc">This Account has No Posts</string> <string name="empty_acc">This Account has No Posts</string>
<string name="empty_list">No Such Posts!</string> <string name="empty_list">No Such Posts!</string>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/header"
android:visibility="gone" />
<Constraint
android:id="@+id/privatePage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible" />
<Constraint
android:id="@+id/swipe_refresh_layout"
android:visibility="gone" />
</ConstraintSet>
<ConstraintSet
android:id="@+id/end"
motion:deriveConstraintsFrom="@id/start">
<Constraint android:id="@+id/header" />
<Constraint android:id="@+id/privatePage" />
<Constraint android:id="@+id/swipe_refresh_layout" />
</ConstraintSet>
<Transition
android:id="@+id/transition"
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@+id/start" />
</MotionScene>

View File

@ -7,6 +7,7 @@ import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.models.enums.FavoriteType import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.repositories.* import awais.instagrabber.repositories.*
import awais.instagrabber.repositories.responses.* import awais.instagrabber.repositories.responses.*
import awais.instagrabber.repositories.responses.directmessages.*
open class UserServiceAdapter : UserService { open class UserServiceAdapter : UserService {
override suspend fun getUserInfo(uid: Long): WrappedUser { override suspend fun getUserInfo(uid: Long): WrappedUser {
@ -166,4 +167,119 @@ open class FavoriteDaoAdapter : FavoriteDao {
override suspend fun deleteFavorites(vararg favorites: Favorite) {} override suspend fun deleteFavorites(vararg favorites: Favorite) {}
override suspend fun deleteAllFavorites() {} override suspend fun deleteAllFavorites() {}
}
open class DirectMessagesServiceAdapter: DirectMessagesService {
override suspend fun fetchInbox(queryMap: Map<String, String>): DirectInboxResponse {
TODO("Not yet implemented")
}
override suspend fun fetchPendingInbox(queryMap: Map<String, String>): DirectInboxResponse {
TODO("Not yet implemented")
}
override suspend fun fetchThread(threadId: String, queryMap: Map<String, String>): DirectThreadFeedResponse {
TODO("Not yet implemented")
}
override suspend fun fetchUnseenCount(): DirectBadgeCount {
TODO("Not yet implemented")
}
override suspend fun broadcast(item: String, signedForm: Map<String, String>): DirectThreadBroadcastResponse {
TODO("Not yet implemented")
}
override suspend fun addUsers(threadId: String, form: Map<String, String>): DirectThreadDetailsChangeResponse {
TODO("Not yet implemented")
}
override suspend fun removeUsers(threadId: String, form: Map<String, String>): String {
TODO("Not yet implemented")
}
override suspend fun updateTitle(threadId: String, form: Map<String, String>): DirectThreadDetailsChangeResponse {
TODO("Not yet implemented")
}
override suspend fun addAdmins(threadId: String, form: Map<String, String>): String {
TODO("Not yet implemented")
}
override suspend fun removeAdmins(threadId: String, form: Map<String, String>): String {
TODO("Not yet implemented")
}
override suspend fun deleteItem(threadId: String, itemId: String, form: Map<String, String>): String {
TODO("Not yet implemented")
}
override suspend fun rankedRecipients(queryMap: Map<String, String>): RankedRecipientsResponse {
TODO("Not yet implemented")
}
override suspend fun forward(form: Map<String, String>): DirectThreadBroadcastResponse {
TODO("Not yet implemented")
}
override suspend fun createThread(signedForm: Map<String, String>): DirectThread {
TODO("Not yet implemented")
}
override suspend fun mute(threadId: String, form: Map<String, String>): String {
TODO("Not yet implemented")
}
override suspend fun unmute(threadId: String, form: Map<String, String>): String {
TODO("Not yet implemented")
}
override suspend fun muteMentions(threadId: String, form: Map<String, String?>): String {
TODO("Not yet implemented")
}
override suspend fun unmuteMentions(threadId: String, form: Map<String, String>): String {
TODO("Not yet implemented")
}
override suspend fun participantRequests(threadId: String, pageSize: Int, cursor: String?): DirectThreadParticipantRequestsResponse {
TODO("Not yet implemented")
}
override suspend fun approveParticipantRequests(threadId: String, form: Map<String, String>): DirectThreadDetailsChangeResponse {
TODO("Not yet implemented")
}
override suspend fun declineParticipantRequests(threadId: String, form: Map<String, String>): DirectThreadDetailsChangeResponse {
TODO("Not yet implemented")
}
override suspend fun approvalRequired(threadId: String, form: Map<String, String>): DirectThreadDetailsChangeResponse {
TODO("Not yet implemented")
}
override suspend fun approvalNotRequired(threadId: String, form: Map<String, String>): DirectThreadDetailsChangeResponse {
TODO("Not yet implemented")
}
override suspend fun leave(threadId: String, form: Map<String, String>): DirectThreadDetailsChangeResponse {
TODO("Not yet implemented")
}
override suspend fun end(threadId: String, form: Map<String, String>): DirectThreadDetailsChangeResponse {
TODO("Not yet implemented")
}
override suspend fun approveRequest(threadId: String, form: Map<String, String>): String {
TODO("Not yet implemented")
}
override suspend fun declineRequest(threadId: String, form: Map<String, String>): String {
TODO("Not yet implemented")
}
override suspend fun markItemSeen(threadId: String, itemId: String, form: Map<String, String>): DirectItemSeenResponse {
TODO("Not yet implemented")
}
} }

View File

@ -5,10 +5,8 @@ import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import awais.instagrabber.MainCoroutineScopeRule import awais.instagrabber.MainCoroutineScopeRule
import awais.instagrabber.common.* import awais.instagrabber.common.*
import awais.instagrabber.db.datasources.AccountDataSource
import awais.instagrabber.db.datasources.FavoriteDataSource import awais.instagrabber.db.datasources.FavoriteDataSource
import awais.instagrabber.db.entities.Favorite import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.db.repositories.AccountRepository
import awais.instagrabber.db.repositories.FavoriteRepository import awais.instagrabber.db.repositories.FavoriteRepository
import awais.instagrabber.getOrAwaitValue import awais.instagrabber.getOrAwaitValue
import awais.instagrabber.models.HighlightModel import awais.instagrabber.models.HighlightModel
@ -21,6 +19,7 @@ import awais.instagrabber.repositories.responses.User
import awais.instagrabber.webservices.* import awais.instagrabber.webservices.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.json.JSONException import org.json.JSONException
import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Assertions.*
@ -37,30 +36,41 @@ internal class ProfileFragmentViewModelTest {
@get:Rule @get:Rule
val coroutineScope = MainCoroutineScopeRule() val coroutineScope = MainCoroutineScopeRule()
private val testPublicUser = User( private lateinit var testPublicUser: User
pk = 100, private lateinit var testPublicUser1: User
username = "test",
fullName = "Test user"
)
private val testPublicUser1 = User( private val csrfToken = "csrfToken"
pk = 101, private val deviceUuid = "deviceUuid"
username = "test1",
fullName = "Test1 user1" @Before
) fun setup() {
testPublicUser = User(
pk = 100,
username = "test",
fullName = "Test user"
)
testPublicUser1 = User(
pk = 101,
username = "test1",
fullName = "Test1 user1"
)
}
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@Test @Test
fun `no state username and null current user`() { fun `no state username and null current user`() {
val viewModel = ProfileFragmentViewModel( val viewModel = ProfileFragmentViewModel(
SavedStateHandle(), SavedStateHandle(),
null,
deviceUuid,
UserRepository(UserServiceAdapter()), UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()), FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()), StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()), MediaRepository(MediaServiceAdapter()),
GraphQLRepository(GraphQLServiceAdapter()), GraphQLRepository(GraphQLServiceAdapter()),
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())), FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher, coroutineScope.dispatcher,
) )
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue()) assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
@ -76,13 +86,16 @@ internal class ProfileFragmentViewModelTest {
fun `no state username with current user provided`() { fun `no state username with current user provided`() {
val viewModel = ProfileFragmentViewModel( val viewModel = ProfileFragmentViewModel(
SavedStateHandle(), SavedStateHandle(),
csrfToken,
deviceUuid,
UserRepository(UserServiceAdapter()), UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()), FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()), StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()), MediaRepository(MediaServiceAdapter()),
GraphQLRepository(GraphQLServiceAdapter()), GraphQLRepository(GraphQLServiceAdapter()),
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())), FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher, coroutineScope.dispatcher,
) )
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue()) assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
@ -128,13 +141,16 @@ internal class ProfileFragmentViewModelTest {
} }
val viewModel = ProfileFragmentViewModel( val viewModel = ProfileFragmentViewModel(
state, state,
null,
deviceUuid,
UserRepository(UserServiceAdapter()), UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()), FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()), StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()), MediaRepository(MediaServiceAdapter()),
graphQLRepository, graphQLRepository,
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())), FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher, coroutineScope.dispatcher,
) )
viewModel.setCurrentUser(Resource.success(null)) viewModel.setCurrentUser(Resource.success(null))
@ -179,13 +195,16 @@ internal class ProfileFragmentViewModelTest {
} }
val viewModel = ProfileFragmentViewModel( val viewModel = ProfileFragmentViewModel(
state, state,
csrfToken,
deviceUuid,
userRepository, userRepository,
FriendshipRepository(FriendshipServiceAdapter()), FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()), StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()), MediaRepository(MediaServiceAdapter()),
GraphQLRepository(GraphQLServiceAdapter()), GraphQLRepository(GraphQLServiceAdapter()),
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())), FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher, coroutineScope.dispatcher,
) )
viewModel.setCurrentUser(Resource.success(User())) viewModel.setCurrentUser(Resource.success(User()))
@ -215,13 +234,16 @@ internal class ProfileFragmentViewModelTest {
} }
val viewModel = ProfileFragmentViewModel( val viewModel = ProfileFragmentViewModel(
state, state,
null,
deviceUuid,
UserRepository(UserServiceAdapter()), UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()), FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()), StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()), MediaRepository(MediaServiceAdapter()),
graphQLRepository, graphQLRepository,
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())), FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher, coroutineScope.dispatcher,
) )
viewModel.setCurrentUser(Resource.success(null)) viewModel.setCurrentUser(Resource.success(null))
@ -267,13 +289,16 @@ internal class ProfileFragmentViewModelTest {
})) }))
val viewModel = ProfileFragmentViewModel( val viewModel = ProfileFragmentViewModel(
state, state,
null,
deviceUuid,
UserRepository(UserServiceAdapter()), UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()), FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()), StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()), MediaRepository(MediaServiceAdapter()),
graphQLRepository, graphQLRepository,
AccountRepository(AccountDataSource(AccountDaoAdapter())),
favoriteRepository, favoriteRepository,
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher, coroutineScope.dispatcher,
) )
viewModel.setCurrentUser(Resource.success(null)) viewModel.setCurrentUser(Resource.success(null))
@ -306,13 +331,16 @@ internal class ProfileFragmentViewModelTest {
} }
val viewModel = ProfileFragmentViewModel( val viewModel = ProfileFragmentViewModel(
state, state,
csrfToken,
deviceUuid,
userRepository, userRepository,
FriendshipRepository(FriendshipServiceAdapter()), FriendshipRepository(FriendshipServiceAdapter()),
storiesRepository, storiesRepository,
MediaRepository(MediaServiceAdapter()), MediaRepository(MediaServiceAdapter()),
GraphQLRepository(GraphQLServiceAdapter()), GraphQLRepository(GraphQLServiceAdapter()),
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())), FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher, coroutineScope.dispatcher,
) )
viewModel.setCurrentUser(Resource.success(User())) viewModel.setCurrentUser(Resource.success(User()))
@ -332,4 +360,45 @@ internal class ProfileFragmentViewModelTest {
} }
assertEquals(testUserHighlights, userHighlights.data) assertEquals(testUserHighlights, userHighlights.data)
} }
@ExperimentalCoroutinesApi
@Test
fun `should refresh correctly`() {
val state = SavedStateHandle(
mutableMapOf<String, Any?>(
"username" to testPublicUser.username
)
)
val graphQLRepository = object : GraphQLRepository(GraphQLServiceAdapter()) {
override suspend fun fetchUser(username: String): User = testPublicUser
}
val viewModel = ProfileFragmentViewModel(
state,
null,
deviceUuid,
UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()),
graphQLRepository,
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher,
)
viewModel.setCurrentUser(Resource.success(null))
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
var profile = viewModel.profile.getOrAwaitValue()
while (profile.status == Resource.Status.LOADING) {
profile = viewModel.profile.getOrAwaitValue()
}
assertEquals(testPublicUser, profile.data)
testPublicUser = testPublicUser.copy(biography = "new bio")
viewModel.refresh()
profile = viewModel.profile.getOrAwaitValue()
while (profile.status == Resource.Status.LOADING) {
profile = viewModel.profile.getOrAwaitValue()
}
assertEquals(testPublicUser, profile.data)
}
} }