mirror of
https://github.com/KokaKiwi/BarInsta
synced 2024-11-24 15:47:30 +00:00
Merge branch 'master' of https://github.com/austinhuang0131/instagrabber
This commit is contained in:
commit
53b0301385
@ -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() {
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
)
|
@ -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?,
|
||||
|
@ -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 ""
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,5 @@
|
||||
package awais.instagrabber.utils
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.UriPermission
|
||||
|
27
app/src/main/java/awais/instagrabber/utils/Event.kt
Normal file
27
app/src/main/java/awais/instagrabber/utils/Event.kt
Normal 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
|
||||
}
|
@ -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";
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package awais.instagrabber.utils.extensions
|
||||
|
||||
fun String.trimAll() = this.trim { it <= ' ' }
|
@ -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
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -28,7 +28,7 @@ object RetrofitFactory {
|
||||
addInterceptor(AddCookiesInterceptor())
|
||||
addInterceptor(igErrorsInterceptor)
|
||||
if (BuildConfig.DEBUG) {
|
||||
// addInterceptor(new LoggingInterceptor());
|
||||
// addInterceptor(LoggingInterceptor())
|
||||
}
|
||||
}
|
||||
val gson = GsonBuilder().apply {
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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 -> 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>
|
||||
|
28
app/src/main/res/xml/profile_fragment_no_acc_layout.xml
Normal file
28
app/src/main/res/xml/profile_fragment_no_acc_layout.xml
Normal 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>
|
@ -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")
|
||||
}
|
||||
|
||||
@ -167,4 +170,119 @@ open class FavoriteDaoAdapter : FavoriteDao {
|
||||
override suspend fun deleteFavorites(vararg favorites: Favorite) {}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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"
|
||||
android.useAndroidX=true
|
Loading…
Reference in New Issue
Block a user