1
0
mirror of https://github.com/KokaKiwi/BarInsta synced 2024-11-24 23:57:30 +00:00
This commit is contained in:
Austin Huang 2021-06-29 12:58:23 -04:00
commit 53b0301385
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
44 changed files with 2049 additions and 1644 deletions

View File

@ -111,8 +111,10 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
override fun onCreate(savedInstanceState: Bundle?) {
try {
DownloadUtils.init(this,
Utils.settingsHelper.getString(PreferenceKeys.PREF_BARINSTA_DIR_URI))
DownloadUtils.init(
this,
Utils.settingsHelper.getString(PreferenceKeys.PREF_BARINSTA_DIR_URI)
)
} catch (e: ReselectDocumentTreeException) {
super.onCreate(savedInstanceState)
val intent = Intent(this, DirectorySelectActivity::class.java)
@ -324,6 +326,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
// } catch (e: Exception) {
// Log.e(TAG, "onDestroy: ", e)
// }
DownloadUtils.destroy()
instance = null
}
@ -358,21 +361,27 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val notificationManager = NotificationManagerCompat.from(applicationContext)
notificationManager.createNotificationChannel(NotificationChannel(
Constants.DOWNLOAD_CHANNEL_ID,
Constants.DOWNLOAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
))
notificationManager.createNotificationChannel(NotificationChannel(
Constants.ACTIVITY_CHANNEL_ID,
Constants.ACTIVITY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
))
notificationManager.createNotificationChannel(NotificationChannel(
Constants.DM_UNREAD_CHANNEL_ID,
Constants.DM_UNREAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
))
notificationManager.createNotificationChannel(
NotificationChannel(
Constants.DOWNLOAD_CHANNEL_ID,
Constants.DOWNLOAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
)
notificationManager.createNotificationChannel(
NotificationChannel(
Constants.ACTIVITY_CHANNEL_ID,
Constants.ACTIVITY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
)
notificationManager.createNotificationChannel(
NotificationChannel(
Constants.DM_UNREAD_CHANNEL_ID,
Constants.DM_UNREAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
)
val silentNotificationChannel = NotificationChannel(
Constants.SILENT_NOTIFICATIONS_CHANNEL_ID,
Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME,
@ -404,7 +413,8 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
supportFragmentManager,
R.id.main_nav_host,
intent,
firstFragmentGraphIndex)
firstFragmentGraphIndex
)
navControllerLiveData.observe(this, { navController: NavController? -> setupNavigation(binding.toolbar, navController) })
currentNavControllerLiveData = navControllerLiveData
}
@ -432,27 +442,33 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
private fun setupAnonBottomNav(): List<Tab> {
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),
false,
"favorites_nav_graph",
R.navigation.favorites_nav_graph,
R.id.favorites_nav_graph,
R.id.favoritesFragment)
val profileTab = Tab(R.drawable.ic_person_24,
R.id.favoritesFragment
)
val profileTab = Tab(
R.drawable.ic_person_24,
getString(R.string.profile),
false,
"profile_nav_graph",
R.navigation.profile_nav_graph,
R.id.profile_nav_graph,
R.id.profileFragment)
val moreTab = Tab(R.drawable.ic_more_horiz_24,
R.id.profileFragment
)
val moreTab = Tab(
R.drawable.ic_more_horiz_24,
getString(R.string.more),
false,
"more_nav_graph",
R.navigation.more_nav_graph,
R.id.more_nav_graph,
R.id.morePreferencesFragment)
R.id.morePreferencesFragment
)
val menu = binding.bottomNavView.menu
menu.clear()
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) {
// Set the thread title earlier for better ux
val title = arguments.getString("title")
val actionBar = supportActionBar
if (actionBar != null && !isEmpty(title)) {
actionBar.title = title
if (!title.isNullOrBlank()) {
supportActionBar?.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
@ -764,7 +786,8 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL
"com.google.android.gms.fonts",
"com.google.android.gms",
"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)
config.setReplaceAll(true) // .setUseEmojiAsDefaultStyle(true)
.registerInitCallback(object : InitCallback() {

View File

@ -4,8 +4,8 @@ import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.adapters.FeedStoriesAdapter;
import awais.instagrabber.databinding.ItemHighlightBinding;
import awais.instagrabber.repositories.responses.stories.Story;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.stories.Story;
public final class FeedStoryViewHolder extends RecyclerView.ViewHolder {

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package awais.instagrabber.fragments;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Rect;
@ -93,8 +92,8 @@ import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Caption;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.MediaCandidate;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.NullSafePair;

View File

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

View File

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

View File

@ -12,7 +12,6 @@ import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.text.Editable;
import android.util.Log;
import android.util.Pair;

View File

@ -1,7 +1,6 @@
package awais.instagrabber.fragments.main;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
@ -12,7 +11,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
@ -45,8 +43,8 @@ import awais.instagrabber.dialogs.PostsLayoutPreferencesDialogFragment;
import awais.instagrabber.fragments.PostViewV2Fragment;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.repositories.requests.StoryViewerOptions;
import awais.instagrabber.repositories.responses.stories.Story;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.stories.Story;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CoroutineUtilsKt;

View File

@ -0,0 +1,979 @@
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.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(),
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

@ -23,7 +23,6 @@ import java.nio.charset.StandardCharsets;
import awais.instagrabber.R;
import awais.instagrabber.dialogs.ConfirmDialogFragment;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;

View File

@ -4,11 +4,11 @@ import android.content.ContentResolver
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.models.Resource
import awais.instagrabber.models.Resource.Companion.error
import awais.instagrabber.models.Resource.Companion.loading
import awais.instagrabber.models.Resource.Companion.success
import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.repositories.requests.directmessages.ThreadIdsOrUserIds
import awais.instagrabber.repositories.responses.User
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.getCsrfTokenFromCookie
import awais.instagrabber.utils.getUserIdFromCookie
import awais.instagrabber.webservices.DirectMessagesService
import awais.instagrabber.webservices.DirectMessagesRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -31,6 +31,7 @@ object DirectMessagesManager {
private val viewerId: Long
private val deviceUuid: String
private val csrfToken: String
private val directMessagesRepository by lazy { DirectMessagesRepository.getInstance() }
fun moveThreadFromPending(threadId: String) {
val pendingThreads = pendingInboxManager.threads.value ?: return
@ -66,7 +67,8 @@ object DirectMessagesManager {
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, secondId: String?, itemType: BroadcastItemType, scope: CoroutineScope) {
sendMedia(setOf(recipient), mediaId, secondId, itemType, scope)
@ -79,9 +81,9 @@ object DirectMessagesManager {
itemType: BroadcastItemType,
scope: CoroutineScope,
) {
val threadIds = recipients.mapNotNull{ it.thread?.threadId }
val userIdsTemp = recipients.mapNotNull{ it.user?.pk }
val userIds = userIdsTemp.map{ listOf(it.toString(10)) }
val threadIds = recipients.mapNotNull { it.thread?.threadId }
val userIdsTemp = recipients.mapNotNull { it.user?.pk }
val userIds = userIdsTemp.map { listOf(it.toString(10)) }
sendMedia(threadIds, userIds, mediaId, secondId, itemType, scope) {
inboxManager.refresh(scope)
}
@ -101,7 +103,7 @@ object DirectMessagesManager {
scope.launch(Dispatchers.IO) {
try {
if (itemType == BroadcastItemType.MEDIA_SHARE)
DirectMessagesService.broadcastMediaShare(
directMessagesRepository.broadcastMediaShare(
csrfToken,
viewerId,
deviceUuid,
@ -111,7 +113,7 @@ object DirectMessagesManager {
secondId
)
if (itemType == BroadcastItemType.PROFILE)
DirectMessagesService.broadcastProfile(
directMessagesRepository.broadcastProfile(
csrfToken,
viewerId,
deviceUuid,

View File

@ -13,7 +13,7 @@ import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.*
import awais.instagrabber.utils.*
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.CacheLoader
import com.google.common.collect.ImmutableList
@ -25,8 +25,7 @@ import java.util.*
import java.util.concurrent.TimeUnit
class InboxManager(private val pending: Boolean) {
// private val fetchInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner()
// private val fetchPendingInboxControlledRunner: ControlledRunner<Resource<DirectInbox>> = ControlledRunner()
private val directMessagesRepository by lazy { DirectMessagesRepository.getInstance() }
private val inbox = MutableLiveData<Resource<DirectInbox?>>(success(null))
private val unseenCount = MutableLiveData<Resource<Int?>>()
private val pendingRequestsTotal = MutableLiveData(0)
@ -58,9 +57,9 @@ class InboxManager(private val pending: Boolean) {
scope.launch(Dispatchers.IO) {
try {
val inboxValue = if (pending) {
DirectMessagesService.fetchPendingInbox(cursor, seqId)
directMessagesRepository.fetchPendingInbox(cursor, seqId)
} else {
DirectMessagesService.fetchInbox(cursor, seqId)
directMessagesRepository.fetchInbox(cursor, seqId)
}
parseInboxResponse(inboxValue)
} catch (e: Exception) {
@ -77,7 +76,7 @@ class InboxManager(private val pending: Boolean) {
unseenCount.postValue(loading(currentUnseenCount))
scope.launch(Dispatchers.IO) {
try {
val directBadgeCount = DirectMessagesService.fetchUnseenCount()
val directBadgeCount = directMessagesRepository.fetchUnseenCount()
unseenCount.postValue(success(directBadgeCount.badgeCount))
} catch (e: Exception) {
Log.e(TAG, "Failed fetching unseen count", e)
@ -253,7 +252,7 @@ class InboxManager(private val pending: Boolean) {
try {
val clone = currentDirectInbox.clone() as DirectInbox
clone.threads = threadsCopy
inbox.setValue(success(clone))
inbox.postValue(success(clone))
} catch (e: CloneNotSupportedException) {
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.TextUtils.isEmpty
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.MediaRepository
import com.google.common.collect.ImmutableList
@ -39,7 +39,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call
import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.util.*
@ -64,6 +63,7 @@ class ThreadManager(
private val threadIdsOrUserIds: ThreadIdsOrUserIds = of(threadId)
private val friendshipRepository: FriendshipRepository by lazy { FriendshipRepository.getInstance() }
private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() }
private val directMessagesRepository by lazy { DirectMessagesRepository.getInstance() }
val thread: LiveData<DirectThread?> by lazy {
distinctUntilChanged(map(inboxManager.getInbox()) { inboxResource: Resource<DirectInbox?>? ->
@ -128,7 +128,7 @@ class ThreadManager(
_fetching.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val threadFeedResponse = DirectMessagesService.fetchThread(threadId, cursor)
val threadFeedResponse = directMessagesRepository.fetchThread(threadId, cursor)
if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") {
_fetching.postValue(error(R.string.generic_not_ok_response, null))
return@launch
@ -156,7 +156,7 @@ class ThreadManager(
if (isGroup == null || !isGroup) return
scope.launch(Dispatchers.IO) {
try {
val response = DirectMessagesService.participantRequests(threadId, 1)
val response = directMessagesRepository.participantRequests(threadId, 1)
_pendingRequests.postValue(response)
} catch (e: Exception) {
Log.e(TAG, "fetchPendingRequests: ", e)
@ -348,7 +348,7 @@ class ThreadManager(
val repliedToClientContext = replyToItemValue?.clientContext
scope.launch(Dispatchers.IO) {
try {
val response = DirectMessagesService.broadcastText(
val response = directMessagesRepository.broadcastText(
csrfToken,
viewerId,
deviceUuid,
@ -395,7 +395,7 @@ class ThreadManager(
data.postValue(loading(directItem))
scope.launch(Dispatchers.IO) {
try {
val request = DirectMessagesService.broadcastAnimatedMedia(
val request = directMessagesRepository.broadcastAnimatedMedia(
csrfToken,
userId,
deviceUuid,
@ -444,7 +444,7 @@ class ThreadManager(
null
)
mediaRepository.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions)
val broadcastResponse = DirectMessagesService.broadcastVoice(
val broadcastResponse = directMessagesRepository.broadcastVoice(
csrfToken,
viewerId,
deviceUuid,
@ -488,7 +488,7 @@ class ThreadManager(
}
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.broadcastReaction(
directMessagesRepository.broadcastReaction(
csrfToken,
userId,
deviceUuid,
@ -528,7 +528,7 @@ class ThreadManager(
}
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.broadcastReaction(
directMessagesRepository.broadcastReaction(
csrfToken,
viewerId,
deviceUuid,
@ -556,7 +556,7 @@ class ThreadManager(
}
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.deleteItem(csrfToken, deviceUuid, threadId, itemId)
directMessagesRepository.deleteItem(csrfToken, deviceUuid, threadId, itemId)
} catch (e: Exception) {
// add the item back if unsuccessful
addItems(index, listOf(item))
@ -632,7 +632,7 @@ class ThreadManager(
}
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.forward(
directMessagesRepository.forward(
thread.threadId,
itemTypeName,
threadId,
@ -651,7 +651,7 @@ class ThreadManager(
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.approveRequest(csrfToken, deviceUuid, threadId)
directMessagesRepository.approveRequest(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "acceptRequest: ", e)
@ -665,7 +665,7 @@ class ThreadManager(
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.declineRequest(csrfToken, deviceUuid, threadId)
directMessagesRepository.declineRequest(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "declineRequest: ", e)
@ -721,7 +721,7 @@ class ThreadManager(
if (handleInvalidResponse(data, response)) return@launch
val response1 = response.response ?: return@launch
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)
} catch (e: Exception) {
data.postValue(error(e.message, null))
@ -782,7 +782,7 @@ class ThreadManager(
VideoOptions(duration / 1000f, emptyList(), 0, false)
)
mediaRepository.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions)
val broadcastResponse = DirectMessagesService.broadcastVideo(
val broadcastResponse = directMessagesRepository.broadcastVideo(
csrfToken,
viewerId,
deviceUuid,
@ -912,7 +912,7 @@ class ThreadManager(
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
val response = DirectMessagesService.updateTitle(csrfToken, deviceUuid, threadId, newTitle.trim())
val response = directMessagesRepository.updateTitle(csrfToken, deviceUuid, threadId, newTitle.trim())
handleDetailsChangeResponse(data, response)
} catch (e: Exception) {
}
@ -924,7 +924,7 @@ class ThreadManager(
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
val response = DirectMessagesService.addUsers(
val response = directMessagesRepository.addUsers(
csrfToken,
deviceUuid,
threadId,
@ -943,7 +943,7 @@ class ThreadManager(
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.removeUsers(csrfToken, deviceUuid, threadId, setOf(user.pk))
directMessagesRepository.removeUsers(csrfToken, deviceUuid, threadId, setOf(user.pk))
data.postValue(success(Any()))
var activeUsers = users.value
var leftUsersValue = leftUsers.value
@ -978,7 +978,7 @@ class ThreadManager(
if (isAdmin(user)) return data
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.addAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
directMessagesRepository.addAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
val currentAdminIds = adminUserIds.value
val updatedAdminIds = ImmutableList.builder<Long>()
.addAll(currentAdminIds ?: emptyList())
@ -1006,7 +1006,7 @@ class ThreadManager(
if (!isAdmin(user)) return data
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.removeAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
directMessagesRepository.removeAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
val currentAdmins = adminUserIds.value ?: return@launch
val updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk }
val currentThread = thread.value ?: return@launch
@ -1036,7 +1036,7 @@ class ThreadManager(
}
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.mute(csrfToken, deviceUuid, threadId)
directMessagesRepository.mute(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
@ -1064,7 +1064,7 @@ class ThreadManager(
}
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.unmute(csrfToken, deviceUuid, threadId)
directMessagesRepository.unmute(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
@ -1092,7 +1092,7 @@ class ThreadManager(
}
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.muteMentions(csrfToken, deviceUuid, threadId)
directMessagesRepository.muteMentions(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
@ -1120,7 +1120,7 @@ class ThreadManager(
}
scope.launch(Dispatchers.IO) {
try {
DirectMessagesService.unmuteMentions(csrfToken, deviceUuid, threadId)
directMessagesRepository.unmuteMentions(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
@ -1199,7 +1199,7 @@ class ThreadManager(
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val response = DirectMessagesService.approveParticipantRequests(
val response = directMessagesRepository.approveParticipantRequests(
csrfToken,
deviceUuid,
threadId,
@ -1220,7 +1220,7 @@ class ThreadManager(
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val response = DirectMessagesService.declineParticipantRequests(
val response = directMessagesRepository.declineParticipantRequests(
csrfToken,
deviceUuid,
threadId,
@ -1262,7 +1262,7 @@ class ThreadManager(
}
scope.launch(Dispatchers.IO) {
try {
val response = DirectMessagesService.approvalRequired(csrfToken, deviceUuid, threadId)
val response = directMessagesRepository.approvalRequired(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, response)
val currentThread = thread.value ?: return@launch
try {
@ -1290,7 +1290,7 @@ class ThreadManager(
}
scope.launch(Dispatchers.IO) {
try {
val request = DirectMessagesService.approvalNotRequired(csrfToken, deviceUuid, threadId)
val request = directMessagesRepository.approvalNotRequired(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request)
val currentThread = thread.value ?: return@launch
try {
@ -1313,7 +1313,7 @@ class ThreadManager(
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val request = DirectMessagesService.leave(csrfToken, deviceUuid, threadId)
val request = directMessagesRepository.leave(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request)
} catch (e: Exception) {
Log.e(TAG, "leave: ", e)
@ -1328,7 +1328,7 @@ class ThreadManager(
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val request = DirectMessagesService.end(csrfToken, deviceUuid, threadId)
val request = directMessagesRepository.end(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request)
val currentThread = thread.value ?: return@launch
try {
@ -1365,7 +1365,7 @@ class ThreadManager(
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val response = DirectMessagesService.markAsSeen(csrfToken, deviceUuid, threadId, directItem)
val response = directMessagesRepository.markAsSeen(csrfToken, deviceUuid, threadId, directItem)
if (response == null) {
data.postValue(error(R.string.generic_null_response, null))
return@launch

View File

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

View File

@ -4,7 +4,6 @@ import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.repositories.responses.feed.EndOfFeedDemarcator
import awais.instagrabber.utils.TextUtils
import java.io.Serializable
import java.util.*
data class Media(
val pk: String? = null,

View File

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

View File

@ -1,7 +1,7 @@
package awais.instagrabber.repositories.responses.stories
import java.io.Serializable
import awais.instagrabber.repositories.responses.User
import java.io.Serializable
data class Broadcast(
val id: String?,

View File

@ -1,29 +1,29 @@
package awais.instagrabber.repositories.responses.stories
import java.io.Serializable
import awais.instagrabber.repositories.responses.ImageUrl
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.utils.TextUtils
import java.io.Serializable
data class Story(
// universal
val id: String?,
val latestReelMedia: Long?, // = timestamp
val mediaCount: Int?,
val id: String? = null,
val latestReelMedia: Long? = null, // = timestamp
val mediaCount: Int? = null,
// for stories and highlights
var seen: Long?,
val user: User?,
var seen: Long? = null,
val user: User? = null,
// for stories
val muted: Boolean?,
val hasBestiesMedia: Boolean?,
val items: List<StoryMedia>?, // may be null
val muted: Boolean? = null,
val hasBestiesMedia: Boolean? = null,
val items: List<StoryMedia>? = null, // may be null
// for highlights
val coverMedia: CoverMedia?,
val title: String?,
val coverMedia: CoverMedia? = null,
val title: String? = null,
// for archives
val coverImageVersion: ImageUrl?,
val coverImageVersion: ImageUrl? = null,
// invented fields
val broadcast: Broadcast? // does not naturally occur
val broadcast: Broadcast? = null, // does not naturally occur
) : Serializable {
val dateTime: String
get() = if (latestReelMedia != null) TextUtils.epochSecondToString(latestReelMedia) else ""

View File

@ -1,13 +1,11 @@
package awais.instagrabber.repositories.responses.stories
import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.utils.TextUtils
import awais.instagrabber.repositories.responses.ImageVersions2
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.MediaCandidate
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.utils.TextUtils
import java.io.Serializable
import java.util.*
data class StoryMedia(
// inherited from Media

View File

@ -1,6 +1,5 @@
package awais.instagrabber.utils
import android.Manifest
import android.content.Context
import android.content.DialogInterface
import android.content.UriPermission

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

@ -21,7 +21,6 @@ import awais.instagrabber.models.stickers.QuestionModel;
import awais.instagrabber.models.stickers.QuizModel;
import awais.instagrabber.models.stickers.SliderModel;
import awais.instagrabber.models.stickers.SwipeUpModel;
import awais.instagrabber.repositories.responses.stories.StoryMedia;
import awais.instagrabber.repositories.responses.Caption;
import awais.instagrabber.repositories.responses.FriendshipStatus;
import awais.instagrabber.repositories.responses.ImageVersions2;
@ -29,7 +28,7 @@ import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.MediaCandidate;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.MediaCandidate;
import awais.instagrabber.repositories.responses.stories.StoryMedia;
public final class ResponseBodyUtils {
private static final String TAG = "ResponseBodyUtils";

View File

@ -5,26 +5,26 @@ import android.content.SharedPreferences
import android.os.Build
import androidx.annotation.StringDef
import androidx.appcompat.app.AppCompatDelegate
import awais.instagrabber.fragments.settings.PreferenceKeys
import java.util.*
import awais.instagrabber.fragments.settings.PreferenceKeys
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 {
val stringDefault = getStringDefault(key)
return if (sharedPreferences != null) sharedPreferences.getString(
return sharedPreferences?.getString(
key,
stringDefault
)!! else stringDefault
) ?: stringDefault
}
fun getStringSet(@StringSetSettings key: String?): Set<String>? {
fun getStringSet(@StringSetSettings key: String?): Set<String> {
val stringSetDefault: Set<String> = HashSet()
return if (sharedPreferences != null) sharedPreferences.getStringSet(
return sharedPreferences?.getStringSet(
key,
stringSetDefault
) else stringSetDefault
) ?: stringSetDefault
}
fun getInteger(@IntegerSettings key: String): Int {
@ -49,15 +49,16 @@ class SettingsHelper(context: Context) {
fun getThemeCode(fromHelper: Boolean): Int {
var themeCode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
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) {
1 -> themeCode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
3 -> themeCode = AppCompatDelegate.MODE_NIGHT_NO
0 -> themeCode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
}
if (themeCode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM && Build.VERSION.SDK_INT < 29) themeCode =
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
if (themeCode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM && Build.VERSION.SDK_INT < 29) {
themeCode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
}
return themeCode
}
@ -78,7 +79,7 @@ class SettingsHelper(context: Context) {
}
fun hasPreference(key: String?): Boolean {
return sharedPreferences != null && sharedPreferences.contains(key)
return sharedPreferences?.contains(key) ?: false
}
@StringDef(
@ -149,8 +150,4 @@ class SettingsHelper(context: Context) {
@StringDef(PreferenceKeys.KEYWORD_FILTERS)
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.MutableLiveData;
import awais.instagrabber.db.repositories.AccountRepository;
import awais.instagrabber.models.Resource;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.CoroutineUtilsKt;
@ -26,6 +28,8 @@ public class AppStateViewModel extends AndroidViewModel {
private final String cookie;
private final MutableLiveData<Resource<User>> currentUser = new MutableLiveData<>(Resource.loading(null));
private AccountRepository accountRepository;
private UserRepository userRepository;
public AppStateViewModel(@NonNull final Application application) {
@ -38,7 +42,7 @@ public class AppStateViewModel extends AndroidViewModel {
return;
}
userRepository = UserRepository.Companion.getInstance();
// final AccountRepository accountRepository = AccountRepository.getInstance(AccountDataSource.getInstance(application));
accountRepository = AccountRepository.Companion.getInstance(application);
fetchProfileDetails();
}
@ -61,13 +65,26 @@ public class AppStateViewModel extends AndroidViewModel {
userRepository.getUserInfo(uid, CoroutineUtilsKt.getContinuation((user, throwable) -> {
if (throwable != null) {
Log.e(TAG, "onFailure: ", throwable);
final User backup = currentUser.getValue().data != null ?
currentUser.getValue().data :
new User(uid);
final Resource<User> userResource = currentUser.getValue();
final User backup = userResource != null && userResource.data != null ? userResource.data : new User(uid);
currentUser.postValue(Resource.error(throwable.getMessage(), backup));
return;
}
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()));
}
}

View File

@ -1,6 +1,5 @@
package awais.instagrabber.viewmodels
import android.R.attr
import android.app.Application
import android.content.ContentResolver
import android.net.Uri
@ -23,7 +22,6 @@ import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener
import awais.instagrabber.utils.MediaUtils.VideoInfo
import awais.instagrabber.utils.VoiceRecorder.VoiceRecorderCallback
import awais.instagrabber.utils.VoiceRecorder.VoiceRecordingResult
import java.util.*
class DirectThreadViewModel(

View File

@ -3,7 +3,6 @@ package awais.instagrabber.viewmodels;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.ArrayList;
import java.util.List;
import awais.instagrabber.repositories.responses.stories.Story;

View File

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

View File

@ -5,7 +5,6 @@ import android.util.Log
import androidx.lifecycle.*
import androidx.savedstate.SavedStateRegistryOwner
import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.db.repositories.AccountRepository
import awais.instagrabber.db.repositories.FavoriteRepository
import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.models.Resource
@ -13,59 +12,91 @@ import awais.instagrabber.models.StoryModel
import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.FriendshipStatus
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.UserProfileContextLink
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.repositories.responses.stories.Story
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.isReallyPrivate
import awais.instagrabber.viewmodels.ProfileFragmentViewModel.ProfileAction.*
import awais.instagrabber.viewmodels.ProfileFragmentViewModel.ProfileEvent.*
import awais.instagrabber.webservices.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalDateTime
class ProfileFragmentViewModel(
state: SavedStateHandle,
userRepository: UserRepository,
friendshipRepository: FriendshipRepository,
private val state: SavedStateHandle,
private val csrfToken: String?,
private val deviceUuid: String?,
private val userRepository: UserRepository,
private val friendshipRepository: FriendshipRepository,
private val storiesRepository: StoriesRepository,
mediaRepository: MediaRepository,
graphQLRepository: GraphQLRepository,
accountRepository: AccountRepository,
private val mediaRepository: MediaRepository,
private val graphQLRepository: GraphQLRepository,
private val favoriteRepository: FavoriteRepository,
private val directMessagesRepository: DirectMessagesRepository,
private val messageManager: DirectMessagesManager?,
ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val _currentUser = MutableLiveData<Resource<User?>>(Resource.loading(null))
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 isLoggedIn: LiveData<Boolean> = currentUser.map { it.data != null }
val isFavorite: LiveData<Boolean> = _isFavorite
val eventLiveData: LiveData<Event<ProfileEvent>?> = _eventLiveData
private val currentUserAndStateUsernameLiveData: LiveData<Pair<Resource<User?>, Resource<String?>>> =
object : MediatorLiveData<Pair<Resource<User?>, Resource<String?>>>() {
private val currentUserStateUsernameActionLiveData: LiveData<Triple<Resource<User?>, Resource<String?>, ProfileAction>> =
object : MediatorLiveData<Triple<Resource<User?>, Resource<String?>, ProfileAction>>() {
var user: Resource<User?> = Resource.loading(null)
var stateUsername: Resource<String?> = Resource.loading(null)
var action: ProfileAction = INIT
init {
addSource(currentUser) { currentUser ->
this.user = currentUser
value = currentUser to stateUsername
value = Triple(currentUser, stateUsername, action)
}
addSource(state.getLiveData<String?>("username")) { username ->
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")) {
this.stateUsername = Resource.success(null)
value = user to this.stateUsername
value = Triple(user, this.stateUsername, action)
}
}
}
private val profileFetchControlledRunner = ControlledRunner<User?>()
val profile: LiveData<Resource<User?>> = currentUserAndStateUsernameLiveData.switchMap {
val (currentUserResource, stateUsernameResource) = it
val profile: LiveData<Resource<User?>> = currentUserStateUsernameActionLiveData.switchMap {
val (currentUserResource, stateUsernameResource, action) = it
liveData<Resource<User?>>(context = viewModelScope.coroutineContext + ioDispatcher) {
if (currentUserResource.status == Resource.Status.LOADING || stateUsernameResource.status == Resource.Status.LOADING) {
emit(Resource.loading(null))
@ -78,33 +109,67 @@ class ProfileFragmentViewModel(
return@liveData
}
try {
val fetchedUser = profileFetchControlledRunner.cancelPreviousThenRun {
return@cancelPreviousThenRun fetchUser(currentUser, userRepository, stateUsername, graphQLRepository)
}
emit(Resource.success(fetchedUser))
if (fetchedUser != null) {
checkAndInsertFavorite(fetchedUser)
when (action) {
INIT, REFRESH -> {
val fetchedUser = profileFetchControlledRunner.cancelPreviousThenRun { fetchUser(currentUser, stateUsername) }
emit(Resource.success(fetchedUser))
if (fetchedUser != null) {
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) {
emit(Resource.error(e.message, null))
emit(Resource.error(e.message, profileCopy.value?.data))
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>?>()
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) {
val (currentUserResource, profileResource, action) = currentUserAndProfilePair
if (action != INIT && action != REFRESH) {
return@liveData
}
// don't fetch if not logged in
if (isLoggedIn.value != true) {
if (currentUserResource.data == null) {
emit(Resource.success(null))
return@liveData
}
if (userResource.status == Resource.Status.LOADING) {
if (currentUserResource.status == Resource.Status.LOADING || profileResource.status == Resource.Status.LOADING) {
emit(Resource.loading(null))
return@liveData
}
val user = userResource.data
val user = profileResource.data
if (user == null) {
emit(Resource.success(null))
return@liveData
@ -120,18 +185,22 @@ class ProfileFragmentViewModel(
}
private val highlightsFetchControlledRunner = ControlledRunner<List<Story>?>()
val userHighlights: LiveData<Resource<List<Story>?>> = profile.switchMap { userResource ->
val userHighlights: LiveData<Resource<List<Story>?>> = currentUserProfileActionLiveData.switchMap { currentUserAndProfilePair ->
liveData<Resource<List<Story>?>>(context = viewModelScope.coroutineContext + ioDispatcher) {
val (currentUserResource, profileResource, action) = currentUserAndProfilePair
if (action != INIT && action != REFRESH) {
return@liveData
}
// don't fetch if not logged in
if (isLoggedIn.value != true) {
if (currentUserResource.data == null) {
emit(Resource.success(null))
return@liveData
}
if (userResource.status == Resource.Status.LOADING) {
if (currentUserResource.status == Resource.Status.LOADING || profileResource.status == Resource.Status.LOADING) {
emit(Resource.loading(null))
return@liveData
}
val user = userResource.data
val user = profileResource.data
if (user == null) {
emit(Resource.success(null))
return@liveData
@ -141,24 +210,25 @@ class ProfileFragmentViewModel(
emit(Resource.success(fetchedHighlights))
} catch (e: Exception) {
emit(Resource.error(e.message, null))
Log.e(TAG, "fetching story: ", e)
Log.e(TAG, "fetching highlights: ", e)
}
}
}
private suspend fun fetchUser(
currentUser: User?,
userRepository: UserRepository,
stateUsername: String,
graphQLRepository: GraphQLRepository
) = if (currentUser != null) {
// logged in
val tempUser = userRepository.getUsernameInfo(stateUsername)
tempUser.friendshipStatus = userRepository.getUserFriendship(tempUser.pk)
tempUser
} else {
): User {
if (currentUser != null) {
// logged in
val tempUser = userRepository.getUsernameInfo(stateUsername)
if (!tempUser.isReallyPrivate(currentUser)) {
tempUser.friendshipStatus = userRepository.getUserFriendship(tempUser.pk)
}
return tempUser
}
// anonymous
graphQLRepository.fetchUser(stateUsername)
return graphQLRepository.fetchUser(stateUsername)
}
private suspend fun fetchUserStory(fetchedUser: User): List<StoryModel> = storiesRepository.getUserStory(
@ -167,7 +237,7 @@ class ProfileFragmentViewModel(
private suspend fun fetchUserHighlights(fetchedUser: User): List<Story> = storiesRepository.fetchHighlights(fetchedUser.pk)
private suspend fun checkAndInsertFavorite(fetchedUser: User) {
private suspend fun checkAndUpdateFavorite(fetchedUser: User) {
try {
val favorite = favoriteRepository.getFavorite(fetchedUser.username, FavoriteType.USER)
if (favorite == null) {
@ -187,7 +257,260 @@ class ProfileFragmentViewModel(
)
} catch (e: Exception) {
_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), null, BroadcastItemType.PROFILE, viewModelScope)
}
fun shareDm(recipients: Set<RankedRecipient>) {
val mediaId = profile.value?.data?.pk ?: return
messageManager?.sendMedia(recipients, mediaId.toString(10), null, 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,45 +519,90 @@ class ProfileFragmentViewModel(
*/
val username: LiveData<String> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.LOADING, Resource.Status.ERROR -> ""
Resource.Status.SUCCESS -> it.data?.username ?: ""
Resource.Status.ERROR -> ""
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.username ?: ""
}
}
init {
// Log.d(TAG, "${state.keys()} $userRepository $friendshipRepository $storiesRepository $mediaRepository")
}
fun setCurrentUser(currentUser: Resource<User?>) {
_currentUser.postValue(currentUser)
}
fun shareDm(result: RankedRecipient) {
if (messageManager == null) {
messageManager = DirectMessagesManager
val profilePicUrl: LiveData<String?> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> null
Resource.Status.LOADING, Resource.Status.SUCCESS -> it.data?.profilePicUrl
}
val mediaId = profile.value?.data?.pk ?: return
messageManager?.sendMedia(result, mediaId.toString(10), null, BroadcastItemType.PROFILE, viewModelScope)
}
fun shareDm(recipients: Set<RankedRecipient>) {
if (messageManager == null) {
messageManager = DirectMessagesManager
val fullName: LiveData<String?> = Transformations.map(profile) {
return@map when (it.status) {
Resource.Status.ERROR -> ""
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), null, BroadcastItemType.PROFILE, viewModelScope)
}
}
@Suppress("UNCHECKED_CAST")
class ProfileFragmentViewModelFactory(
private val csrfToken: String?,
private val deviceUuid: String?,
private val userRepository: UserRepository,
private val friendshipRepository: FriendshipRepository,
private val storiesRepository: StoriesRepository,
private val mediaRepository: MediaRepository,
private val graphQLRepository: GraphQLRepository,
private val accountRepository: AccountRepository,
private val favoriteRepository: FavoriteRepository,
private val directMessagesRepository: DirectMessagesRepository,
private val messageManager: DirectMessagesManager?,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null,
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
@ -245,13 +613,16 @@ class ProfileFragmentViewModelFactory(
): T {
return ProfileFragmentViewModel(
handle,
csrfToken,
deviceUuid,
userRepository,
friendshipRepository,
storiesRepository,
mediaRepository,
graphQLRepository,
accountRepository,
favoriteRepository,
directMessagesRepository,
messageManager,
Dispatchers.IO,
) as T
}

View File

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

View File

@ -1,6 +1,6 @@
package awais.instagrabber.webservices
import awais.instagrabber.repositories.DirectMessagesRepository
import awais.instagrabber.repositories.DirectMessagesService
import awais.instagrabber.repositories.requests.directmessages.*
import awais.instagrabber.repositories.responses.directmessages.*
import awais.instagrabber.repositories.responses.giphy.GiphyGif
@ -9,8 +9,7 @@ import awais.instagrabber.utils.Utils
import org.json.JSONArray
import java.util.*
object DirectMessagesService {
private val repository: DirectMessagesRepository = RetrofitFactory.retrofit.create(DirectMessagesRepository::class.java)
open class DirectMessagesRepository(private val service: DirectMessagesService) {
suspend fun fetchInbox(
cursor: String?,
@ -29,7 +28,7 @@ object DirectMessagesService {
if (seqId != 0L) {
queryMap["seq_id"] = seqId.toString()
}
return repository.fetchInbox(queryMap)
return service.fetchInbox(queryMap)
}
suspend fun fetchThread(
@ -44,10 +43,10 @@ object DirectMessagesService {
if (!cursor.isNullOrBlank()) {
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(
csrfToken: String,
@ -61,7 +60,17 @@ object DirectMessagesService {
): DirectThreadBroadcastResponse {
val urls = extractUrls(text)
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)
if (!repliedToItemId.isNullOrBlank() && !repliedToClientContext.isNullOrBlank()) {
@ -212,7 +221,7 @@ object DirectMessagesService {
form.putAll(broadcastOptions.formMap)
form["action"] = "send_item"
// val signedForm = Utils.sign(form)
return repository.broadcast(broadcastOptions.itemType.value, form)
return service.broadcast(broadcastOptions.itemType.value, form)
}
suspend fun addUsers(
@ -226,7 +235,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid,
"user_ids" to JSONArray(userIds).toString(),
)
return repository.addUsers(threadId, form)
return service.addUsers(threadId, form)
}
suspend fun removeUsers(
@ -240,7 +249,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid,
"user_ids" to JSONArray(userIds).toString(),
)
return repository.removeUsers(threadId, form)
return service.removeUsers(threadId, form)
}
suspend fun updateTitle(
@ -254,7 +263,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid,
"title" to title,
)
return repository.updateTitle(threadId, form)
return service.updateTitle(threadId, form)
}
suspend fun addAdmins(
@ -268,7 +277,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid,
"user_ids" to JSONArray(userIds).toString(),
)
return repository.addAdmins(threadId, form)
return service.addAdmins(threadId, form)
}
suspend fun removeAdmins(
@ -282,7 +291,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid,
"user_ids" to JSONArray(userIds).toString(),
)
return repository.removeAdmins(threadId, form)
return service.removeAdmins(threadId, form)
}
suspend fun deleteItem(
@ -295,7 +304,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid,
)
return repository.deleteItem(threadId, itemId, form)
return service.deleteItem(threadId, itemId, form)
}
suspend fun rankedRecipients(
@ -317,7 +326,7 @@ object DirectMessagesService {
if (showThreads != null) {
queryMap["showThreads"] = showThreads.toString()
}
return repository.rankedRecipients(queryMap)
return service.rankedRecipients(queryMap)
}
suspend fun forward(
@ -333,7 +342,7 @@ object DirectMessagesService {
"forwarded_from_thread_id" to fromThreadId,
"forwarded_from_thread_item_id" to itemId,
)
return repository.forward(form)
return service.forward(form)
}
suspend fun createThread(
@ -354,7 +363,7 @@ object DirectMessagesService {
form["thread_title"] = threadTitle
}
val signedForm = Utils.sign(form)
return repository.createThread(signedForm)
return service.createThread(signedForm)
}
suspend fun mute(
@ -366,7 +375,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid
)
return repository.mute(threadId, form)
return service.mute(threadId, form)
}
suspend fun unmute(
@ -378,7 +387,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid,
)
return repository.unmute(threadId, form)
return service.unmute(threadId, form)
}
suspend fun muteMentions(
@ -390,7 +399,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid,
)
return repository.muteMentions(threadId, form)
return service.muteMentions(threadId, form)
}
suspend fun unmuteMentions(
@ -402,7 +411,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid,
)
return repository.unmuteMentions(threadId, form)
return service.unmuteMentions(threadId, form)
}
suspend fun participantRequests(
@ -410,7 +419,7 @@ object DirectMessagesService {
pageSize: Int,
cursor: String? = null,
): DirectThreadParticipantRequestsResponse {
return repository.participantRequests(threadId, pageSize, cursor)
return service.participantRequests(threadId, pageSize, cursor)
}
suspend fun approveParticipantRequests(
@ -425,7 +434,7 @@ object DirectMessagesService {
"user_ids" to JSONArray(userIds).toString(),
// "share_join_chat_story" to String.valueOf(true)
)
return repository.approveParticipantRequests(threadId, form)
return service.approveParticipantRequests(threadId, form)
}
suspend fun declineParticipantRequests(
@ -439,7 +448,7 @@ object DirectMessagesService {
"_uuid" to deviceUuid,
"user_ids" to JSONArray(userIds).toString(),
)
return repository.declineParticipantRequests(threadId, form)
return service.declineParticipantRequests(threadId, form)
}
suspend fun approvalRequired(
@ -451,7 +460,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid,
)
return repository.approvalRequired(threadId, form)
return service.approvalRequired(threadId, form)
}
suspend fun approvalNotRequired(
@ -463,7 +472,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid,
)
return repository.approvalNotRequired(threadId, form)
return service.approvalNotRequired(threadId, form)
}
suspend fun leave(
@ -475,7 +484,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid,
)
return repository.leave(threadId, form)
return service.leave(threadId, form)
}
suspend fun end(
@ -487,7 +496,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid,
)
return repository.end(threadId, form)
return service.end(threadId, form)
}
suspend fun fetchPendingInbox(cursor: String?, seqId: Long): DirectInboxResponse {
@ -504,7 +513,7 @@ object DirectMessagesService {
if (seqId != 0L) {
queryMap["seq_id"] = seqId.toString()
}
return repository.fetchPendingInbox(queryMap)
return service.fetchPendingInbox(queryMap)
}
suspend fun approveRequest(
@ -516,7 +525,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid,
)
return repository.approveRequest(threadId, form)
return service.approveRequest(threadId, form)
}
suspend fun declineRequest(
@ -528,7 +537,7 @@ object DirectMessagesService {
"_csrftoken" to csrfToken,
"_uuid" to deviceUuid,
)
return repository.declineRequest(threadId, form)
return service.declineRequest(threadId, form)
}
suspend fun markAsSeen(
@ -546,6 +555,18 @@ object DirectMessagesService {
"thread_id" to threadId,
"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(
id: String,
type: String, // 1 caption 2 comment 3 bio
): String {
): String? {
val form = mapOf(
"id" to id,
"type" to type,
)
val response = service.translate(form)
val jsonObject = JSONObject(response)
return jsonObject.optString("translation")
if (!jsonObject.has("translation") || jsonObject.isNull("translation")) {
return null
}
return jsonObject.getString("translation")
}
suspend fun uploadFinish(

View File

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

View File

@ -7,7 +7,6 @@ import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.stories.ArchiveResponse
import awais.instagrabber.repositories.responses.stories.Story
import awais.instagrabber.repositories.responses.stories.StoryStickerResponse
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.ResponseBodyUtils
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.Utils

View File

@ -5,7 +5,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
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
android:id="@+id/header"

View File

@ -93,6 +93,7 @@
<string name="story_mentions">Mentions</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="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="empty_acc">This Account has No 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,9 @@ import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.repositories.*
import awais.instagrabber.repositories.responses.*
import awais.instagrabber.repositories.responses.directmessages.*
import awais.instagrabber.repositories.responses.stories.ArchiveResponse
import awais.instagrabber.repositories.responses.stories.ReelsTrayResponse
import awais.instagrabber.repositories.responses.stories.StoryStickerResponse
open class UserServiceAdapter : UserService {
@ -48,15 +51,15 @@ open class StoriesServiceAdapter : StoriesService {
TODO("Not yet implemented")
}
override suspend fun getFeedStories(): String {
override suspend fun getFeedStories(): ReelsTrayResponse? {
TODO("Not yet implemented")
}
override suspend fun fetchHighlights(uid: Long): String {
override suspend fun fetchHighlights(uid: Long): ReelsTrayResponse? {
TODO("Not yet implemented")
}
override suspend fun fetchArchive(queryParams: Map<String, String>): String {
override suspend fun fetchArchive(queryParams: Map<String, String>): ArchiveResponse? {
TODO("Not yet implemented")
}
@ -168,3 +171,118 @@ open class FavoriteDaoAdapter : FavoriteDao {
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 awais.instagrabber.MainCoroutineScopeRule
import awais.instagrabber.common.*
import awais.instagrabber.db.datasources.AccountDataSource
import awais.instagrabber.db.datasources.FavoriteDataSource
import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.db.repositories.AccountRepository
import awais.instagrabber.db.repositories.FavoriteRepository
import awais.instagrabber.getOrAwaitValue
import awais.instagrabber.models.Resource
@ -21,6 +19,7 @@ import awais.instagrabber.repositories.responses.stories.Story
import awais.instagrabber.webservices.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.json.JSONException
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.jupiter.api.Assertions.*
@ -37,30 +36,41 @@ internal class ProfileFragmentViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
private val testPublicUser = User(
pk = 100,
username = "test",
fullName = "Test user"
)
private lateinit var testPublicUser: User
private lateinit var testPublicUser1: User
private val testPublicUser1 = User(
pk = 101,
username = "test1",
fullName = "Test1 user1"
)
private val csrfToken = "csrfToken"
private val deviceUuid = "deviceUuid"
@Before
fun setup() {
testPublicUser = User(
pk = 100,
username = "test",
fullName = "Test user"
)
testPublicUser1 = User(
pk = 101,
username = "test1",
fullName = "Test1 user1"
)
}
@ExperimentalCoroutinesApi
@Test
fun `no state username and null current user`() {
val viewModel = ProfileFragmentViewModel(
SavedStateHandle(),
null,
deviceUuid,
UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()),
GraphQLRepository(GraphQLServiceAdapter()),
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher,
)
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
@ -76,13 +86,16 @@ internal class ProfileFragmentViewModelTest {
fun `no state username with current user provided`() {
val viewModel = ProfileFragmentViewModel(
SavedStateHandle(),
csrfToken,
deviceUuid,
UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()),
GraphQLRepository(GraphQLServiceAdapter()),
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher,
)
assertEquals(false, viewModel.isLoggedIn.getOrAwaitValue())
@ -128,13 +141,16 @@ internal class ProfileFragmentViewModelTest {
}
val viewModel = ProfileFragmentViewModel(
state,
null,
deviceUuid,
UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()),
graphQLRepository,
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher,
)
viewModel.setCurrentUser(Resource.success(null))
@ -179,13 +195,16 @@ internal class ProfileFragmentViewModelTest {
}
val viewModel = ProfileFragmentViewModel(
state,
csrfToken,
deviceUuid,
userRepository,
FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()),
GraphQLRepository(GraphQLServiceAdapter()),
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher,
)
viewModel.setCurrentUser(Resource.success(User()))
@ -215,13 +234,16 @@ internal class ProfileFragmentViewModelTest {
}
val viewModel = ProfileFragmentViewModel(
state,
null,
deviceUuid,
UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()),
graphQLRepository,
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher,
)
viewModel.setCurrentUser(Resource.success(null))
@ -267,13 +289,16 @@ internal class ProfileFragmentViewModelTest {
}))
val viewModel = ProfileFragmentViewModel(
state,
null,
deviceUuid,
UserRepository(UserServiceAdapter()),
FriendshipRepository(FriendshipServiceAdapter()),
StoriesRepository(StoriesServiceAdapter()),
MediaRepository(MediaServiceAdapter()),
graphQLRepository,
AccountRepository(AccountDataSource(AccountDaoAdapter())),
favoriteRepository,
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher,
)
viewModel.setCurrentUser(Resource.success(null))
@ -302,17 +327,20 @@ internal class ProfileFragmentViewModelTest {
}
val storiesRepository = object : StoriesRepository(StoriesServiceAdapter()) {
override suspend fun getUserStory(options: StoryViewerOptions): List<StoryModel> = testUserStories
override suspend fun fetchHighlights(profileId: Long): List<HighlightModel> = testUserHighlights
override suspend fun fetchHighlights(profileId: Long): List<Story> = testUserHighlights
}
val viewModel = ProfileFragmentViewModel(
state,
csrfToken,
deviceUuid,
userRepository,
FriendshipRepository(FriendshipServiceAdapter()),
storiesRepository,
MediaRepository(MediaServiceAdapter()),
GraphQLRepository(GraphQLServiceAdapter()),
AccountRepository(AccountDataSource(AccountDaoAdapter())),
FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())),
DirectMessagesRepository(DirectMessagesServiceAdapter()),
null,
coroutineScope.dispatcher,
)
viewModel.setCurrentUser(Resource.success(User()))
@ -332,4 +360,45 @@ internal class ProfileFragmentViewModelTest {
}
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)
}
}

View File

@ -10,7 +10,7 @@
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Tue May 04 02:33:38 JST 2021
#Wed Jun 30 00:40:18 JST 2021
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -XX:+UseParallelGC
android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx1536M"