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