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.appcompat.widget.TooltipCompat 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.navigation.fragment.findNavController 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.* import awais.instagrabber.dialogs.ConfirmDialogFragment.ConfirmDialogFragmentCallback import awais.instagrabber.dialogs.MultiOptionDialogFragment.MultiOptionDialogSingleCallback import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option import awais.instagrabber.fragments.UserSearchMode 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 { private var backStackSavedStateResultLiveData: MutableLiveData? = 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? = 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 userRepository by lazy { UserRepository.getInstance() } private val friendshipRepository by lazy { FriendshipRepository.getInstance() } private val storiesRepository by lazy { StoriesRepository.getInstance() } private val mediaRepository by lazy { MediaRepository.getInstance() } private val graphQLRepository by lazy { GraphQLRepository.getInstance() } private val favoriteRepository by lazy { FavoriteRepository.getInstance(requireContext()) } private val directMessagesRepository by lazy { DirectMessagesRepository.getInstance() } 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) { openPostDialog(media, -1) } override fun onProfilePicClick(media: Media) { navigateToProfile(media.user?.username) } override fun onNameClick(media: Media) { navigateToProfile(media.user?.username) } override fun onLocationClick(media: Media?) { try { val action = ProfileFragmentDirections.actionToLocation(media?.location?.pk ?: return) findNavController().navigate(action) } catch (e: Exception) { Log.e(TAG, "onLocationClick: ", e) } } override fun onMentionClick(mention: String?) { navigateToProfile(mention?.trimAll() ?: return) } override fun onHashtagClick(hashtag: String?) { try { val action = ProfileFragmentDirections.actionToHashtag(hashtag ?: return) findNavController().navigate(action) } catch (e: Exception) { Log.e(TAG, "onHashtagClick: ", e) } } override fun onCommentsClick(media: Media?) { try { val commentsAction = ProfileFragmentDirections.actionToComments( media?.code ?: return, media.pk ?: return, media.user?.pk ?: return ) findNavController().navigate(commentsAction) } catch (e: Exception) { Log.e(TAG, "onCommentsClick: ", e) } } override fun onDownloadClick(media: Media?, childPosition: Int, popupLocation: View) { DownloadUtils.showDownloadDialog(context ?: return, media ?: return, childPosition, popupLocation) } 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?) { 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 != null 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.actionToFollowViewer( viewModel.profile.value?.data?.pk ?: return@OnClickListener, true, viewModel.profile.value?.data?.username ?: return@OnClickListener ) findNavController().navigate(action) } catch (e: Exception) { Log.e(TAG, "onFollowersClickListener: ", e) } } private val onFollowingClickListener = View.OnClickListener { try { val action = ProfileFragmentDirections.actionToFollowViewer( viewModel.profile.value?.data?.pk ?: return@OnClickListener, false, viewModel.profile.value?.data?.username ?: return@OnClickListener ) findNavController().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 actionToHashtag = ProfileFragmentDirections.actionToHashtag(it.originalText.trimAll()) findNavController().navigate(actionToHashtag) } 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 { 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) } catch (e: Exception) { Log.e(TAG, "share: ", e) } } // clear result backStackSavedStateResultLiveData?.postValue(null) } private fun openPostDialog(media: Media, position: Int) { try { val actionToPost = ProfileFragmentDirections.actionToPost(media, position) findNavController().navigate(actionToPost) } 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, friendshipRepository, storiesRepository, mediaRepository, graphQLRepository, favoriteRepository, directMessagesRepository, if (isLoggedIn) DirectMessagesManager else null, this, arguments ) ).get(ProfileFragmentViewModel::class.java) setHasOptionsMenu(true) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentProfileBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { init() } 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() mainActivity.setToolbar(binding.toolbar, this) try { val backStackEntry = NavHostFragment.findNavController(this).currentBackStackEntry if (backStackEntry != null) { backStackSavedStateResultLiveData = backStackEntry.savedStateHandle.getLiveData("result") backStackSavedStateResultLiveData?.observe(viewLifecycleOwner, backStackSavedStateObserver) } mainActivity.supportActionBar?.title = viewModel.username.value val (currentUserData, profileData) = viewModel.currentUserProfileActionLiveData.value!! setupOptionsMenuItems(currentUserData.data, profileData.data) } catch (e: Exception) { Log.e(TAG, "onResume: ", e) } } override fun onStop() { super.onStop() mainActivity.resetToolbar(this) } override fun onDestroyView() { super.onDestroyView() setupPostsDone = false } private fun shareProfileViaDm() { try { val actionToUserSearch = ProfileFragmentDirections.actionToUserSearch().apply { title = getString(R.string.share) actionLabel = getString(R.string.send) showGroups = true multiple = true searchMode = UserSearchMode.RAVEN } findNavController().navigate(actionToUserSearch) } 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 try { val actionToNotifications = ProfileFragmentDirections.actionToNotifications().apply { type = "chaining" targetId = profile.pk } findNavController().navigate(actionToNotifications) } catch (e: Exception) { Log.e(TAG, "navigateToChaining: ", e) } } private fun init() { binding.swipeRefreshLayout.setOnRefreshListener(this) disableDm = !isNavRootInCurrentTabs("direct_messages_nav_graph") setupHighlights() setupObservers() } private fun setupObservers() { appStateViewModel.currentUserLiveData.observe(viewLifecycleOwner, viewModel::setCurrentUser) viewModel.isLoggedIn.observe(viewLifecycleOwner) { // observe so that `isLoggedIn.value` is correct Log.d(TAG, "setupObservers: $it") } 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 } setupFavChip(profile, currentUser) setupFavButton(currentUser, profile) setupSavedButton(currentUser, profile) setupTaggedButton(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 == null) 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.privatePage.visibility = VISIBLE binding.privatePage.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT 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 } private fun setupProfileContext(contextPair: Pair?>) { 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.unmute_stories else R.string.mute_stories) mutePostsMenuItem?.isVisible = true mutePostsMenuItem?.setTitle(if (profile.friendshipStatus?.muting == true) R.string.unmute_posts else R.string.mute_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) if (count >= 1000) { TooltipCompat.setTooltipText(binding.header.mainPostCount, count.toString(10)) } } 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) if (count >= 1000) { TooltipCompat.setTooltipText(binding.header.mainFollowing, count.toString(10)) } } 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) if (count >= 1000) { TooltipCompat.setTooltipText(binding.header.mainFollowers, count.toString(10)) } } 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.actionToSaved( viewModel.profile.value?.data?.username ?: return@setOnClickListener, viewModel.profile.value?.data?.pk ?: return@setOnClickListener, PostItemType.LIKED ) findNavController().navigate(action) } catch (e: Exception) { Log.e(TAG, "setupLikedButton: ", e) } } } private fun setupTaggedButton(profile: User?) { val visibility = if (profile?.usertagsCount == 0L) View.GONE else View.VISIBLE binding.header.btnTagged.visibility = visibility if (visibility == View.GONE) { binding.header.btnTagged.setOnClickListener(null) return } binding.header.btnTagged.setOnClickListener { try { val action = ProfileFragmentDirections.actionToSaved( viewModel.profile.value?.data?.username ?: return@setOnClickListener, viewModel.profile.value?.data?.pk ?: return@setOnClickListener, PostItemType.TAGGED ) findNavController().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.actionToSavedCollections().apply { isSaving = false } findNavController().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() { binding.header.root.visibility = GONE binding.swipeRefreshLayout.visibility = GONE binding.privatePage.visibility = VISIBLE binding.privatePage1.visibility = VISIBLE binding.privatePage2.visibility = 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.user!!.pk, "").apply { currentFeedStoryIndex = position } val action = ProfileFragmentDirections.actionToStory(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 { AppExecutors.mainThread.execute { 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) // if (!canScrollVertically) { // (binding.collapsingToolbarLayout.layoutParams as AppBarLayout.LayoutParams).scrollFlags = 0 // } // } // }) setupPostsDone = true } private fun navigateToProfile(username: String?) { try { val username1 = username ?: return val actionToProfile = ProfileFragmentDirections.actionToProfile().apply { this.username = username1 } findNavController().navigate(actionToProfile) } 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.actionToStory( StoryViewerOptions.forUser( viewModel.profile.value?.data?.pk ?: return, viewModel.profile.value?.data?.username ?: return, ) ) findNavController().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) { layoutPreferences = it Handler(Looper.getMainLooper()).postDelayed( { binding.postsRecyclerView.layoutPreferences = it }, 200 ) } fragment.show(childFragmentManager, PostsLayoutPreferencesDialogFragment::class.java.simpleName) } }