From 751259d4302fd47622a13d2645e2d4080ea17118 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 7 Jul 2021 15:09:33 +0900 Subject: [PATCH 1/3] Re-add ProfileFragmentViewModel params and fix tests --- .../fragments/main/ProfileFragment.kt | 18 +++++- .../viewmodels/ProfileFragmentViewModel.kt | 59 +++++++++++-------- .../awais/instagrabber/common/Adapters.kt | 18 ++++-- .../ProfileFragmentViewModelTest.kt | 5 +- 4 files changed, 66 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt index 3eb27b33..ac0b5be0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt @@ -85,6 +85,14 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall 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 @@ -309,7 +317,15 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall viewModel = ViewModelProvider( this, ProfileFragmentViewModelFactory( - FavoriteRepository.getInstance(requireContext()), + csrfToken, + deviceUuid, + userRepository, + friendshipRepository, + storiesRepository, + mediaRepository, + graphQLRepository, + favoriteRepository, + directMessagesRepository, if (isLoggedIn) DirectMessagesManager else null, this, arguments diff --git a/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt index 33550f70..28e8ef6b 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt @@ -16,12 +16,9 @@ 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.Constants import awais.instagrabber.utils.ControlledRunner import awais.instagrabber.utils.Event -import awais.instagrabber.utils.getCsrfTokenFromCookie import awais.instagrabber.utils.SingleRunner -import awais.instagrabber.utils.Utils import awais.instagrabber.utils.extensions.TAG import awais.instagrabber.utils.extensions.isReallyPrivate import awais.instagrabber.viewmodels.ProfileFragmentViewModel.ProfileAction.* @@ -34,20 +31,18 @@ import java.time.LocalDateTime class ProfileFragmentViewModel( private val state: SavedStateHandle, - private val favoriteRepository: FavoriteRepository?, + 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 favoriteRepository: FavoriteRepository, + private val directMessagesRepository: DirectMessagesRepository, private val messageManager: DirectMessagesManager?, ioDispatcher: CoroutineDispatcher, ) : ViewModel() { - private val cookie: String = Utils.settingsHelper.getString(Constants.COOKIE) - private val csrfToken: String? = getCsrfTokenFromCookie(cookie) - private val deviceUuid: String = Utils.settingsHelper.getString(Constants.DEVICE_UUID) - private val userRepository: UserRepository by lazy { UserRepository.getInstance() } - private val friendshipRepository: FriendshipRepository by lazy { FriendshipRepository.getInstance() } - private val storiesRepository: StoriesRepository by lazy { StoriesRepository.getInstance() } - private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() } - private val graphQLRepository: GraphQLRepository by lazy { GraphQLRepository.getInstance() } - private val directMessagesRepository: DirectMessagesRepository by lazy { DirectMessagesRepository.getInstance() } - private val _currentUser = MutableLiveData>(Resource.loading(null)) private val _isFavorite = MutableLiveData(false) private val profileAction = MutableLiveData(INIT) @@ -247,7 +242,7 @@ class ProfileFragmentViewModel( private suspend fun checkAndUpdateFavorite(fetchedUser: User) { try { - val favorite = favoriteRepository!!.getFavorite(fetchedUser.username, FavoriteType.USER) + val favorite = favoriteRepository.getFavorite(fetchedUser.username, FavoriteType.USER) if (favorite == null) { _isFavorite.postValue(false) return @@ -295,7 +290,7 @@ class ProfileFragmentViewModel( viewModelScope.launch(Dispatchers.IO) { toggleFavoriteControlledRunner.afterPrevious { try { - val favorite = favoriteRepository!!.getFavorite(username, FavoriteType.USER) + val favorite = favoriteRepository.getFavorite(username, FavoriteType.USER) if (favorite == null) { // insert favoriteRepository.insertOrUpdateFavorite( @@ -330,7 +325,7 @@ class ProfileFragmentViewModel( val currentUserId = currentUser.value?.data?.pk ?: return@afterPrevious val targetUserId = profile.value?.data?.pk ?: return@afterPrevious val csrfToken = csrfToken ?: return@afterPrevious - val deviceUuid = deviceUuid + val deviceUuid = deviceUuid ?: return@afterPrevious if (following) { if (!confirmed) { _eventLiveData.postValue(Event(ShowConfirmUnfollowDialog)) @@ -369,7 +364,7 @@ class ProfileFragmentViewModel( val currentUserId = currentUser.value?.data?.pk ?: return@afterPrevious val targetUserId = profile.value?.data?.pk ?: return@afterPrevious val csrfToken = csrfToken ?: return@afterPrevious - val deviceUuid = deviceUuid + val deviceUuid = deviceUuid ?: return@afterPrevious val username = profile.value?.data?.username ?: return@afterPrevious val thread = directMessagesRepository.createThread( csrfToken, @@ -403,7 +398,7 @@ class ProfileFragmentViewModel( val profile = profile.value?.data ?: return@afterPrevious friendshipRepository.toggleRestrict( csrfToken ?: return@afterPrevious, - deviceUuid, + deviceUuid ?: return@afterPrevious, profile.pk, !(profile.friendshipStatus?.isRestricted ?: false), ) @@ -425,7 +420,7 @@ class ProfileFragmentViewModel( friendshipRepository.changeBlock( csrfToken ?: return@afterPrevious, currentUser.value?.data?.pk ?: return@afterPrevious, - deviceUuid, + deviceUuid ?: return@afterPrevious, profile.friendshipStatus?.blocking ?: return@afterPrevious, profile.pk ) @@ -447,7 +442,7 @@ class ProfileFragmentViewModel( friendshipRepository.changeMute( csrfToken ?: return@afterPrevious, currentUser.value?.data?.pk ?: return@afterPrevious, - deviceUuid, + deviceUuid ?: return@afterPrevious, profile.friendshipStatus?.isMutingReel ?: return@afterPrevious, profile.pk, true @@ -470,7 +465,7 @@ class ProfileFragmentViewModel( friendshipRepository.changeMute( csrfToken ?: return@afterPrevious, currentUser.value?.data?.pk ?: return@afterPrevious, - deviceUuid, + deviceUuid ?: return@afterPrevious, profile.friendshipStatus?.muting ?: return@afterPrevious, profile.pk, false @@ -492,7 +487,7 @@ class ProfileFragmentViewModel( friendshipRepository.removeFollower( csrfToken ?: return@afterPrevious, currentUser.value?.data?.pk ?: return@afterPrevious, - deviceUuid, + deviceUuid ?: return@afterPrevious, profile.value?.data?.pk ?: return@afterPrevious ) profileAction.postValue(REFRESH_FRIENDSHIP) @@ -601,7 +596,15 @@ class ProfileFragmentViewModel( @Suppress("UNCHECKED_CAST") class ProfileFragmentViewModelFactory( - private val favoriteRepository: FavoriteRepository?, + 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 favoriteRepository: FavoriteRepository, + private val directMessagesRepository: DirectMessagesRepository, private val messageManager: DirectMessagesManager?, owner: SavedStateRegistryOwner, defaultArgs: Bundle? = null, @@ -613,7 +616,15 @@ class ProfileFragmentViewModelFactory( ): T { return ProfileFragmentViewModel( handle, + csrfToken, + deviceUuid, + userRepository, + friendshipRepository, + storiesRepository, + mediaRepository, + graphQLRepository, favoriteRepository, + directMessagesRepository, messageManager, Dispatchers.IO, ) as T diff --git a/app/src/test/java/awais/instagrabber/common/Adapters.kt b/app/src/test/java/awais/instagrabber/common/Adapters.kt index 5735e342..f0892232 100644 --- a/app/src/test/java/awais/instagrabber/common/Adapters.kt +++ b/app/src/test/java/awais/instagrabber/common/Adapters.kt @@ -8,9 +8,7 @@ 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 +import awais.instagrabber.repositories.responses.stories.* open class UserServiceAdapter : UserService { override suspend fun getUserInfo(uid: Long): WrappedUser { @@ -47,7 +45,7 @@ open class FriendshipServiceAdapter : FriendshipService { } open class StoriesServiceAdapter : StoriesService { - override suspend fun fetch(mediaId: Long): String { + override suspend fun fetch(mediaId: Long): StoryMediaResponse { TODO("Not yet implemented") } @@ -63,11 +61,19 @@ open class StoriesServiceAdapter : StoriesService { TODO("Not yet implemented") } - override suspend fun getUserStory(url: String): String { + override suspend fun getReelsMedia(id: String): ReelsMediaResponse { TODO("Not yet implemented") } - override suspend fun respondToSticker(storyId: String, stickerId: String, action: String, form: Map): StoryStickerResponse { + override suspend fun getStories(type: String, id: String): ReelsResponse { + TODO("Not yet implemented") + } + + override suspend fun getUserStories(id: Long): ReelsResponse { + TODO("Not yet implemented") + } + + override suspend fun respondToSticker(storyId: Long, stickerId: Long, action: String, form: Map): StoryStickerResponse { TODO("Not yet implemented") } diff --git a/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt b/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt index cb3344d8..3977eee9 100644 --- a/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt +++ b/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt @@ -15,7 +15,6 @@ import awais.instagrabber.repositories.requests.StoryViewerOptions import awais.instagrabber.repositories.responses.FriendshipStatus import awais.instagrabber.repositories.responses.User import awais.instagrabber.repositories.responses.stories.Story -import awais.instagrabber.repositories.responses.stories.StoryMedia import awais.instagrabber.webservices.* import kotlinx.coroutines.ExperimentalCoroutinesApi import org.json.JSONException @@ -320,13 +319,13 @@ internal class ProfileFragmentViewModelTest { "username" to testPublicUser.username ) ) - val testUserStories = listOf(StoryMedia()) + val testUserStories = Story() val testUserHighlights = listOf(Story()) val userRepository = object : UserRepository(UserServiceAdapter()) { override suspend fun getUsernameInfo(username: String): User = testPublicUser } val storiesRepository = object : StoriesRepository(StoriesServiceAdapter()) { - override suspend fun getStories(options: StoryViewerOptions): List = testUserStories + override suspend fun getStories(options: StoryViewerOptions): Story = testUserStories override suspend fun fetchHighlights(profileId: Long): List = testUserHighlights } val viewModel = ProfileFragmentViewModel( From 64600ceb046a73da4800dd7f5df7ea70eaeb1caf Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 7 Jul 2021 16:29:44 +0900 Subject: [PATCH 2/3] Convert UserSearchFragment to kotlin and fix UserSearchMode --- .../fragments/UserSearchFragment.java | 312 ------------------ .../fragments/UserSearchFragment.kt | 252 ++++++++++++++ .../instagrabber/fragments/UserSearchMode.kt | 2 +- .../viewmodels/UserSearchViewModel.java | 2 +- 4 files changed, 254 insertions(+), 314 deletions(-) delete mode 100644 app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.kt diff --git a/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.java b/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.java deleted file mode 100644 index 417b7237..00000000 --- a/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.java +++ /dev/null @@ -1,312 +0,0 @@ -package awais.instagrabber.fragments; - -import android.content.Context; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.core.util.Pair; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.SavedStateHandle; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.NavBackStackEntry; -import androidx.navigation.NavController; -import androidx.navigation.fragment.NavHostFragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.transition.TransitionManager; - -import com.google.android.material.chip.Chip; -import com.google.android.material.snackbar.Snackbar; - -import java.util.Objects; -import java.util.Set; - -import awais.instagrabber.activities.MainActivity; -import awais.instagrabber.adapters.UserSearchResultsAdapter; -import awais.instagrabber.customviews.helpers.TextWatcherAdapter; -import awais.instagrabber.databinding.FragmentUserSearchBinding; -import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; -import awais.instagrabber.utils.ViewUtils; -import awais.instagrabber.viewmodels.UserSearchViewModel; - -public class UserSearchFragment extends Fragment { - private static final String TAG = UserSearchFragment.class.getSimpleName(); - - private FragmentUserSearchBinding binding; - private UserSearchViewModel viewModel; - private UserSearchResultsAdapter resultsAdapter; - private int paddingOffset; - - private final int windowWidth = Utils.displayMetrics.widthPixels; - private final int minInputWidth = Utils.convertDpToPx(50); - private String actionLabel; - private String title; - private boolean multiple; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - binding = FragmentUserSearchBinding.inflate(inflater, container, false); - viewModel = new ViewModelProvider(this).get(UserSearchViewModel.class); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - paddingOffset = binding.search.getPaddingStart() + binding.search.getPaddingEnd() + binding.group - .getPaddingStart() + binding.group.getPaddingEnd() + binding.group.getChipSpacingHorizontal(); - init(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - viewModel.cleanup(); - } - - private void init() { - final Bundle arguments = getArguments(); - if (arguments != null) { - final UserSearchFragmentArgs fragmentArgs = UserSearchFragmentArgs.fromBundle(arguments); - actionLabel = fragmentArgs.getActionLabel(); - title = fragmentArgs.getTitle(); - multiple = fragmentArgs.getMultiple(); - viewModel.setHideThreadIds(fragmentArgs.getHideThreadIds()); - viewModel.setHideUserIds(fragmentArgs.getHideUserIds()); - viewModel.setSearchMode(fragmentArgs.getSearchMode()); - viewModel.setShowGroups(fragmentArgs.getShowGroups()); - } - setupTitles(); - setupInput(); - setupResults(); - setupObservers(); - // show cached results - viewModel.showCachedResults(); - } - - private void setupTitles() { - if (!TextUtils.isEmpty(actionLabel)) { - binding.done.setText(actionLabel); - } - if (!TextUtils.isEmpty(title)) { - final MainActivity activity = (MainActivity) getActivity(); - if (activity != null) { - final ActionBar actionBar = activity.getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(title); - } - } - } - } - - private void setupResults() { - final Context context = getContext(); - if (context == null) return; - binding.results.setLayoutManager(new LinearLayoutManager(context)); - resultsAdapter = new UserSearchResultsAdapter(multiple, (position, recipient, selected) -> { - if (!multiple) { - final NavController navController = NavHostFragment.findNavController(this); - if (!setResult(navController, recipient)) return; - navController.navigateUp(); - return; - } - viewModel.setSelectedRecipient(recipient, !selected); - resultsAdapter.setSelectedRecipient(recipient, !selected); - if (!selected) { - createChip(recipient); - return; - } - final View chip = findChip(recipient); - if (chip == null) return; - removeChipFromGroup(chip); - }); - binding.results.setAdapter(resultsAdapter); - binding.done.setOnClickListener(v -> { - final NavController navController = NavHostFragment.findNavController(this); - if (!setResult(navController, viewModel.getSelectedRecipients())) return; - navController.navigateUp(); - }); - } - - private boolean setResult(@NonNull final NavController navController, final RankedRecipient rankedRecipient) { - final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry(); - if (navBackStackEntry == null) return false; - final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); - savedStateHandle.set("result", rankedRecipient); - return true; - } - - private boolean setResult(@NonNull final NavController navController, final Set rankedRecipients) { - final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry(); - if (navBackStackEntry == null) return false; - final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); - savedStateHandle.set("result", rankedRecipients); - return true; - } - - private void setupInput() { - binding.search.addTextChangedListener(new TextWatcherAdapter() { - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { - // if (TextUtils.isEmpty(s)) { - // viewModel.cancelSearch(); - // viewModel.clearResults(); - // return; - // } - viewModel.search(s == null ? null : s.toString().trim()); - } - }); - binding.search.setOnKeyListener((v, keyCode, event) -> { - if (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) { - final View chip = getLastChip(); - if (chip == null) return false; - removeChip(chip); - } - return false; - }); - binding.group.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { - @Override - public void onChildViewAdded(final View parent, final View child) {} - - @Override - public void onChildViewRemoved(final View parent, final View child) { - binding.group.post(() -> { - TransitionManager.beginDelayedTransition(binding.getRoot()); - calculateInputWidth(0); - }); - } - }); - - } - - private void setupObservers() { - viewModel.getRecipients().observe(getViewLifecycleOwner(), results -> { - if (results == null) return; - switch (results.status) { - case SUCCESS: - if (results.data != null) { - resultsAdapter.submitList(results.data); - } - break; - case ERROR: - if (results.message != null) { - Snackbar.make(binding.getRoot(), results.message, Snackbar.LENGTH_LONG).show(); - } - if (results.resId != 0) { - Snackbar.make(binding.getRoot(), results.resId, Snackbar.LENGTH_LONG).show(); - } - if (results.data != null) { - resultsAdapter.submitList(results.data); - } - break; - case LOADING: - //noinspection DuplicateBranchesInSwitch - if (results.data != null) { - resultsAdapter.submitList(results.data); - } - break; - } - }); - viewModel.showAction().observe(getViewLifecycleOwner(), showAction -> binding.done.setVisibility(showAction ? View.VISIBLE : View.GONE)); - } - - private void createChip(final RankedRecipient recipient) { - final Context context = getContext(); - if (context == null) return; - final Chip chip = new Chip(context); - chip.setTag(recipient); - chip.setText(getRecipientText(recipient)); - chip.setCloseIconVisible(true); - chip.setOnCloseIconClickListener(v -> removeChip(chip)); - binding.group.post(() -> { - final Pair measure = ViewUtils.measure(chip, binding.group); - TransitionManager.beginDelayedTransition(binding.getRoot()); - calculateInputWidth(measure.second != null ? measure.second : 0); - binding.group.addView(chip, binding.group.getChildCount() - 1); - }); - } - - private String getRecipientText(final RankedRecipient recipient) { - if (recipient == null) return null; - if (recipient.getUser() != null) { - return recipient.getUser().getFullName(); - } - if (recipient.getThread() != null) { - return recipient.getThread().getThreadTitle(); - } - return null; - } - - private void removeChip(@NonNull final View chip) { - final RankedRecipient recipient = (RankedRecipient) chip.getTag(); - if (recipient == null) return; - viewModel.setSelectedRecipient(recipient, false); - resultsAdapter.setSelectedRecipient(recipient, false); - removeChipFromGroup(chip); - } - - private View findChip(final RankedRecipient recipient) { - if (recipient == null || recipient.getUser() == null && recipient.getThread() == null) return null; - boolean isUser = recipient.getUser() != null; - final int childCount = binding.group.getChildCount(); - if (childCount == 0) return null; - for (int i = childCount - 1; i >= 0; i--) { - final View child = binding.group.getChildAt(i); - if (child == null) continue; - final RankedRecipient tag = (RankedRecipient) child.getTag(); - if (tag == null || isUser && tag.getUser() == null || !isUser && tag.getThread() == null) continue; - if ((isUser && tag.getUser().getPk() == recipient.getUser().getPk()) - || (!isUser && Objects.equals(tag.getThread().getThreadId(), recipient.getThread().getThreadId()))) { - return child; - } - } - return null; - } - - private void removeChipFromGroup(final View chip) { - binding.group.post(() -> { - TransitionManager.beginDelayedTransition(binding.getRoot()); - binding.group.removeView(chip); - }); - } - - private void calculateInputWidth(final int newChipWidth) { - final View lastChip = getLastChip(); - int lastRight = lastChip != null ? lastChip.getRight() : 0; - final int remainingSpaceInRow = windowWidth - lastRight; - if (remainingSpaceInRow < newChipWidth) { - // next chip will go to the next row, so assume no chips present - lastRight = 0; - } - final int newRight = lastRight + newChipWidth; - final int newInputWidth = windowWidth - newRight - paddingOffset; - binding.search.getLayoutParams().width = newInputWidth < minInputWidth ? windowWidth : newInputWidth; - binding.search.requestLayout(); - } - - private View getLastChip() { - final int childCount = binding.group.getChildCount(); - if (childCount == 0) { - return null; - } - for (int i = childCount - 1; i >= 0; i--) { - final View child = binding.group.getChildAt(i); - if (child instanceof Chip) { - return child; - } - } - return null; - } -} diff --git a/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.kt b/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.kt new file mode 100644 index 00000000..a72be70a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.kt @@ -0,0 +1,252 @@ +package awais.instagrabber.fragments + +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.OnHierarchyChangeListener +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.transition.TransitionManager +import awais.instagrabber.activities.MainActivity +import awais.instagrabber.adapters.UserSearchResultsAdapter +import awais.instagrabber.customviews.helpers.TextWatcherAdapter +import awais.instagrabber.databinding.FragmentUserSearchBinding +import awais.instagrabber.models.Resource +import awais.instagrabber.repositories.responses.directmessages.RankedRecipient +import awais.instagrabber.utils.Utils +import awais.instagrabber.utils.extensions.trimAll +import awais.instagrabber.utils.measure +import awais.instagrabber.viewmodels.UserSearchViewModel +import com.google.android.material.chip.Chip +import com.google.android.material.snackbar.Snackbar + +class UserSearchFragment : Fragment() { + + private lateinit var binding: FragmentUserSearchBinding + + private var resultsAdapter: UserSearchResultsAdapter? = null + private var paddingOffset = 0 + private var actionLabel: String? = null + private var title: String? = null + private var multiple = false + + private val viewModel: UserSearchViewModel by viewModels() + private val windowWidth = Utils.displayMetrics.widthPixels + private val minInputWidth = Utils.convertDpToPx(50f) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentUserSearchBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + paddingOffset = with(binding) { + search.paddingStart + search.paddingEnd + group.paddingStart + group.paddingEnd + group.chipSpacingHorizontal + } + init() + } + + override fun onDestroyView() { + super.onDestroyView() + viewModel.cleanup() + } + + private fun init() { + val arguments = arguments + if (arguments != null) { + val fragmentArgs = UserSearchFragmentArgs.fromBundle(arguments) + actionLabel = fragmentArgs.actionLabel + title = fragmentArgs.title + multiple = fragmentArgs.multiple + viewModel.setHideThreadIds(fragmentArgs.hideThreadIds) + viewModel.setHideUserIds(fragmentArgs.hideUserIds) + viewModel.setSearchMode(fragmentArgs.searchMode) + viewModel.setShowGroups(fragmentArgs.showGroups) + } + setupTitles() + setupInput() + setupResults() + setupObservers() + // show cached results + viewModel.showCachedResults() + } + + private fun setupTitles() { + if (!actionLabel.isNullOrBlank()) { + binding.done.text = actionLabel + } + if (title.isNullOrBlank()) return + (activity as MainActivity?)?.supportActionBar?.title = title + } + + private fun setupResults() { + val context = context ?: return + binding.results.layoutManager = LinearLayoutManager(context) + resultsAdapter = UserSearchResultsAdapter(multiple) { _: Int, recipient: RankedRecipient, selected: Boolean -> + if (!multiple) { + val navController = NavHostFragment.findNavController(this) + if (!setResult(navController, recipient)) return@UserSearchResultsAdapter + navController.navigateUp() + return@UserSearchResultsAdapter + } + viewModel.setSelectedRecipient(recipient, !selected) + resultsAdapter?.setSelectedRecipient(recipient, !selected) + if (!selected) { + createChip(recipient) + return@UserSearchResultsAdapter + } + val chip = findChip(recipient) ?: return@UserSearchResultsAdapter + removeChipFromGroup(chip) + } + binding.results.adapter = resultsAdapter + binding.done.setOnClickListener { + val navController = NavHostFragment.findNavController(this) + if (!setResult(navController, viewModel.selectedRecipients)) return@setOnClickListener + navController.navigateUp() + } + } + + private fun setResult(navController: NavController, rankedRecipient: RankedRecipient): Boolean { + navController.previousBackStackEntry?.savedStateHandle?.set("result", rankedRecipient) ?: return false + return true + } + + private fun setResult(navController: NavController, rankedRecipients: Set): Boolean { + navController.previousBackStackEntry?.savedStateHandle?.set("result", rankedRecipients) ?: return false + return true + } + + private fun setupInput() { + binding.search.addTextChangedListener(object : TextWatcherAdapter() { + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + viewModel.search(s.toString().trimAll()) + } + }) + binding.search.setOnKeyListener { _: View?, _: Int, event: KeyEvent? -> + if (event != null && event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_DEL) { + val chip = lastChip ?: return@setOnKeyListener false + removeChip(chip) + } + false + } + binding.group.setOnHierarchyChangeListener(object : OnHierarchyChangeListener { + override fun onChildViewAdded(parent: View, child: View) {} + override fun onChildViewRemoved(parent: View, child: View) { + binding.group.post { + TransitionManager.beginDelayedTransition(binding.root) + calculateInputWidth(0) + } + } + }) + } + + private fun setupObservers() { + viewModel.recipients.observe(viewLifecycleOwner) { + if (it == null) return@observe + when (it.status) { + Resource.Status.SUCCESS -> if (it.data != null) { + resultsAdapter?.submitList(it.data) + } + Resource.Status.ERROR -> { + if (it.message != null) { + Snackbar.make(binding.root, it.message, Snackbar.LENGTH_LONG).show() + } + if (it.resId != 0) { + Snackbar.make(binding.root, it.resId, Snackbar.LENGTH_LONG).show() + } + if (it.data != null) { + resultsAdapter?.submitList(it.data) + } + } + Resource.Status.LOADING -> if (it.data != null) { + resultsAdapter?.submitList(it.data) + } + } + } + viewModel.showAction().observe(viewLifecycleOwner) { binding.done.visibility = if (it) View.VISIBLE else View.GONE } + } + + private fun createChip(recipient: RankedRecipient) { + val context = context ?: return + val chip = Chip(context).apply { + tag = recipient + text = getRecipientText(recipient) + isCloseIconVisible = true + setOnCloseIconClickListener { removeChip(this) } + } + binding.group.post { + val measure = measure(chip, binding.group) + TransitionManager.beginDelayedTransition(binding.root) + calculateInputWidth(if (measure.second != null) measure.second else 0) + binding.group.addView(chip, binding.group.childCount - 1) + } + } + + private fun getRecipientText(recipient: RankedRecipient?): String? = when { + recipient == null -> null + recipient.user != null -> recipient.user.fullName + recipient.thread != null -> recipient.thread.threadTitle + else -> null + } + + private fun removeChip(chip: View) { + val recipient = chip.tag as RankedRecipient + viewModel.setSelectedRecipient(recipient, false) + resultsAdapter?.setSelectedRecipient(recipient, false) + removeChipFromGroup(chip) + } + + private fun findChip(recipient: RankedRecipient?): View? { + if (recipient == null || recipient.user == null && recipient.thread == null) return null + val isUser = recipient.user != null + val childCount = binding.group.childCount + if (childCount == 0) return null + for (i in childCount - 1 downTo 0) { + val child = binding.group.getChildAt(i) ?: continue + val tag = child.tag as RankedRecipient + if (isUser && tag.user == null || !isUser && tag.thread == null) continue + if (isUser && tag.user?.pk == recipient.user?.pk || !isUser && tag.thread?.threadId == recipient.thread?.threadId) { + return child + } + } + return null + } + + private fun removeChipFromGroup(chip: View) { + binding.group.post { + TransitionManager.beginDelayedTransition(binding.root) + binding.group.removeView(chip) + } + } + + private fun calculateInputWidth(newChipWidth: Int) { + var lastRight = lastChip?.right ?: 0 + val remainingSpaceInRow = windowWidth - lastRight + if (remainingSpaceInRow < newChipWidth) { + // next chip will go to the next row, so assume no chips present + lastRight = 0 + } + val newRight = lastRight + newChipWidth + val newInputWidth = windowWidth - newRight - paddingOffset + binding.search.layoutParams.width = if (newInputWidth < minInputWidth) windowWidth else newInputWidth + binding.search.requestLayout() + } + + private val lastChip: View? + get() { + val childCount = binding.group.childCount + if (childCount == 0) return null + for (i in childCount - 1 downTo 0) { + val child = binding.group.getChildAt(i) + if (child is Chip) { + return child + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/UserSearchMode.kt b/app/src/main/java/awais/instagrabber/fragments/UserSearchMode.kt index 8d0da314..fc0e9191 100644 --- a/app/src/main/java/awais/instagrabber/fragments/UserSearchMode.kt +++ b/app/src/main/java/awais/instagrabber/fragments/UserSearchMode.kt @@ -1,6 +1,6 @@ package awais.instagrabber.fragments -enum class UserSearchMode(name: String) { +enum class UserSearchMode(val mode: String) { USER_SEARCH("user_name"), RAVEN("raven"), RESHARE("reshare"); diff --git a/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java index 63161b94..c9bac545 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java @@ -192,7 +192,7 @@ public class UserSearchViewModel extends ViewModel { private void rankedRecipientSearch() { directMessagesRepository.rankedRecipients( - searchMode.name(), + searchMode.getMode(), showGroups, currentQuery, CoroutineUtilsKt.getContinuation((response, throwable) -> { From 74434aa3b33a9f87105db8ead4696cd28c163d7a Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sun, 11 Jul 2021 02:37:29 +0900 Subject: [PATCH 3/3] Fix re-init of fragments on tab change --- .../instagrabber/activities/MainActivity.kt | 148 ++--- .../customviews/PostsRecyclerView.java | 77 +-- .../customviews/helpers/PostFetcher.java | 29 +- .../dialogs/PostLoadingDialogFragment.kt | 78 +++ .../TabOrderPreferenceDialogFragment.java | 2 +- .../fragments/TopicPostsFragment.java | 2 +- .../DirectMessageInboxFragment.kt | 49 +- .../fragments/main/ProfileFragment.kt | 39 +- .../settings/GeneralPreferencesFragment.java | 2 +- .../java/awais/instagrabber/models/Tab.kt | 6 + .../utils/BarinstaDeepLinkHelper.kt | 23 + .../instagrabber/utils/KeywordsFilterUtils.kt | 33 +- .../instagrabber/utils/NavigationHelper.kt | 30 +- .../viewmodels/MediaViewModel.java | 97 ++- .../viewmodels/ProfileFragmentViewModel.kt | 2 + app/src/main/res/layout/activity_main.xml | 3 +- app/src/main/res/menu/bottom_nav_menu.xml | 28 + ...raph.xml => direct_messages_nav_graph.xml} | 333 +--------- .../res/navigation/discover_nav_graph.xml | 518 ++++++++++++++++ .../res/navigation/favorites_nav_graph.xml | 484 +++++++++++++++ .../main/res/navigation/feed_nav_graph.xml | 520 ++++++++++++++++ .../main/res/navigation/more_nav_graph.xml | 539 +++++++++++++++++ .../notification_viewer_nav_graph.xml | 495 +++++++++++++++ .../main/res/navigation/profile_nav_graph.xml | 567 ++++++++++++++++++ .../main/res/navigation/root_nav_graph.xml | 12 + .../res/navigation/settings_nav_graph.xml | 108 ---- app/src/main/res/values/ids.xml | 9 - 27 files changed, 3528 insertions(+), 705 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/dialogs/PostLoadingDialogFragment.kt create mode 100644 app/src/main/java/awais/instagrabber/utils/BarinstaDeepLinkHelper.kt create mode 100644 app/src/main/res/menu/bottom_nav_menu.xml rename app/src/main/res/navigation/{nav_graph.xml => direct_messages_nav_graph.xml} (65%) create mode 100644 app/src/main/res/navigation/discover_nav_graph.xml create mode 100644 app/src/main/res/navigation/favorites_nav_graph.xml create mode 100644 app/src/main/res/navigation/feed_nav_graph.xml create mode 100644 app/src/main/res/navigation/more_nav_graph.xml create mode 100644 app/src/main/res/navigation/notification_viewer_nav_graph.xml create mode 100644 app/src/main/res/navigation/profile_nav_graph.xml create mode 100644 app/src/main/res/navigation/root_nav_graph.xml diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.kt b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt index dd0a7298..fbf89442 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.kt +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt @@ -14,8 +14,6 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.WindowManager -import android.widget.Toast -import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.app.NotificationManagerCompat @@ -26,9 +24,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat.InitCallback import androidx.emoji.text.FontRequestEmojiCompatConfig -import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavGraph @@ -36,7 +32,6 @@ import androidx.navigation.NavGraphNavigator import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.* import awais.instagrabber.BuildConfig -import awais.instagrabber.NavGraphDirections import awais.instagrabber.R import awais.instagrabber.customviews.emoji.EmojiVariantManager import awais.instagrabber.customviews.helpers.RootViewDeferringInsetsCallback @@ -54,37 +49,28 @@ import awais.instagrabber.utils.* import awais.instagrabber.utils.AppExecutors.tasksThread import awais.instagrabber.utils.DownloadUtils.ReselectDocumentTreeException import awais.instagrabber.utils.TextUtils.isEmpty -import awais.instagrabber.utils.TextUtils.shortcodeToId import awais.instagrabber.utils.emoji.EmojiParser import awais.instagrabber.viewmodels.AppStateViewModel import awais.instagrabber.viewmodels.DirectInboxViewModel -import awais.instagrabber.webservices.GraphQLRepository -import awais.instagrabber.webservices.MediaRepository import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.textfield.TextInputLayout import com.google.common.collect.ImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.util.* -class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedListener { +class MainActivity : BaseLanguageActivity() { private lateinit var binding: ActivityMainBinding private lateinit var navController: NavController private lateinit var appBarConfiguration: AppBarConfiguration - // private var currentNavControllerLiveData: LiveData? = null private var searchMenuItem: MenuItem? = null private var startNavRootId: Int = 0 - // private var firstFragmentGraphIndex = 0 private var lastSelectedNavMenuId = 0 private var isActivityCheckerServiceBound = false - private var isBackStackEmpty = false private var isLoggedIn = false private var deviceUuid: String? = null private var csrfToken: String? = null @@ -106,8 +92,6 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL isActivityCheckerServiceBound = false } } - private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() } - private val graphQLRepository: GraphQLRepository by lazy { GraphQLRepository.getInstance() } override fun onCreate(savedInstanceState: Bundle?) { try { @@ -156,7 +140,6 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL if (isLoggedIn && Utils.settingsHelper.getBoolean(PreferenceKeys.CHECK_ACTIVITY)) { bindActivityCheckerService() } - supportFragmentManager.addOnBackStackChangedListener(this) // Initialise the internal map tasksThread.execute { EmojiParser.getInstance(this) @@ -235,7 +218,6 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.main_menu, menu) searchMenuItem = menu.findItem(R.id.search) - // val navController = currentNavControllerLiveData?.value val currentDestination = navController.currentDestination if (currentDestination != null) { val backStack = navController.backQueue @@ -246,9 +228,8 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.search) { - // val navController = currentNavControllerLiveData?.value ?: return false try { - navController.navigate(R.id.action_global_search) + navController.navigate(getSearchDeepLink()) return true } catch (e: Exception) { Log.e(TAG, "onOptionsItemSelected: ", e) @@ -301,27 +282,24 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL instance = null } - override fun onBackPressed() { - val backStack = navController.backQueue - val currentNavControllerBackStack = backStack.size - if (isTaskRoot && isBackStackEmpty && currentNavControllerBackStack == 2) { - finishAfterTransition() - return - } - if (!isFinishing) { - try { - super.onBackPressed() - } catch (e: Exception) { - Log.e(TAG, "onBackPressed: ", e) - finish() - } - } - } - - override fun onBackStackChanged() { - val backStackEntryCount = supportFragmentManager.backStackEntryCount - isBackStackEmpty = backStackEntryCount == 0 - } + // override fun onBackPressed() { + // Log.d(TAG, "onBackPressed: ") + // navController.navigateUp() + // val backStack = navController.backQueue + // val currentNavControllerBackStack = backStack.size + // if (isTaskRoot && isBackStackEmpty && currentNavControllerBackStack == 2) { + // finishAfterTransition() + // return + // } + // if (!isFinishing) { + // try { + // super.onBackPressed() + // } catch (e: Exception) { + // Log.e(TAG, "onBackPressed: ", e) + // finish() + // } + // } + // } private fun createNotificationChannels() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return @@ -374,16 +352,10 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL val navigator = navigatorProvider.getNavigator("navigation") val rootNavGraph = NavGraph(navigator) val navInflater = navController.navInflater - val destinations = currentTabs.map { - val navGraph = navInflater.inflate(R.navigation.nav_graph) - navGraph.id = it.navigationRootId - navGraph.label = "${it.title}_nav_graph".lowercase(Locale.getDefault()) - navGraph.setStartDestination(it.startDestinationFragmentId) - return@map navGraph - } + val topLevelDestinations = currentTabs.map { navInflater.inflate(it.navigationResId) } rootNavGraph.id = R.id.root_nav_graph rootNavGraph.label = "root_nav_graph" - rootNavGraph.addDestinations(destinations) + rootNavGraph.addDestinations(topLevelDestinations) rootNavGraph.setStartDestination(if (startNavRootId != 0) startNavRootId else R.id.profile_nav_graph) navController.graph = rootNavGraph binding.bottomNavView.setupWithNavController(navController) @@ -536,8 +508,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL fun navigateToThread(threadId: String?, threadTitle: String?) { if (threadId == null || threadTitle == null) return try { - val action = NavGraphDirections.actionGlobalDirectThread(threadId, threadTitle) - navController.navigate(action) + navController.navigate(getDirectThreadDeepLink(threadId, threadTitle)) } catch (e: Exception) { Log.e(TAG, "navigateToThread: ", e) } @@ -564,8 +535,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL private fun showProfileView(intentModel: IntentModel) { try { val username = intentModel.text - val action = NavGraphDirections.actionGlobalProfile().setUsername(username) - navController.navigate(action) + navController.navigate(getProfileDeepLink(username)) } catch (e: Exception) { Log.e(TAG, "showProfileView: ", e) } @@ -574,33 +544,10 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL private fun showPostView(intentModel: IntentModel) { val shortCode = intentModel.text // Log.d(TAG, "shortCode: " + shortCode); - val alertDialog = AlertDialog.Builder(this) - .setCancelable(false) - .setView(R.layout.dialog_opening_post) - .create() - alertDialog.show() - lifecycleScope.launch(Dispatchers.IO) { - try { - val media = if (isLoggedIn) mediaRepository.fetch(shortcodeToId(shortCode)) else graphQLRepository.fetchPost(shortCode) - withContext(Dispatchers.Main) { - if (media == null) { - Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show() - return@withContext - } - try { - val action = NavGraphDirections.actionGlobalPost(media, 0) - navController.navigate(action) - } catch (e: Exception) { - Log.e(TAG, "showPostView: ", e) - } - } - } catch (e: Exception) { - Log.e(TAG, "showPostView: ", e) - } finally { - withContext(Dispatchers.Main) { - alertDialog.dismiss() - } - } + try { + navController.navigate(getPostDeepLink(shortCode)) + } catch (e: Exception) { + Log.e(TAG, "showPostView: ", e) } } @@ -608,8 +555,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL val locationId = intentModel.text // Log.d(TAG, "locationId: " + locationId); try { - val action = NavGraphDirections.actionGlobalLocation(locationId.toLong()) - navController.navigate(action) + navController.navigate(getLocationDeepLink(locationId)) } catch (e: Exception) { Log.e(TAG, "showLocationView: ", e) } @@ -619,8 +565,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL val hashtag = intentModel.text // Log.d(TAG, "hashtag: " + hashtag); try { - val action = NavGraphDirections.actionGlobalHashTag(hashtag) - navController.navigate(action) + navController.navigate(getHashtagDeepLink(hashtag)) } catch (e: Exception) { Log.e(TAG, "showHashtagView: ", e) } @@ -628,8 +573,7 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL private fun showActivityView() { try { - val action = NavGraphDirections.actionGlobalNotifications().apply { type = "notif" } - navController.navigate(action) + navController.navigate(getNotificationsDeepLink("notif")) } catch (e: Exception) { Log.e(TAG, "showActivityView: ", e) } @@ -649,21 +593,21 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL val bottomNavView: BottomNavigationView get() = binding.bottomNavView - fun setCollapsingView(view: View) { - try { - binding.collapsingToolbarLayout.addView(view, 0) - } catch (e: Exception) { - Log.e(TAG, "setCollapsingView: ", e) - } - } - - fun removeCollapsingView(view: View) { - try { - binding.collapsingToolbarLayout.removeView(view) - } catch (e: Exception) { - Log.e(TAG, "removeCollapsingView: ", e) - } - } + // fun setCollapsingView(view: View) { + // try { + // binding.collapsingToolbarLayout.addView(view, 0) + // } catch (e: Exception) { + // Log.e(TAG, "setCollapsingView: ", e) + // } + // } + // + // fun removeCollapsingView(view: View) { + // try { + // binding.collapsingToolbarLayout.removeView(view) + // } catch (e: Exception) { + // Log.e(TAG, "removeCollapsingView: ", e) + // } + // } fun resetToolbar() { binding.appBarLayout.visibility = View.VISIBLE diff --git a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java index 6b05d2fe..698cbb94 100644 --- a/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java +++ b/app/src/main/java/awais/instagrabber/customviews/PostsRecyclerView.java @@ -3,12 +3,11 @@ package awais.instagrabber.customviews; import android.content.Context; import android.util.AttributeSet; import android.util.Log; -import android.view.View; -import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelStoreOwner; import androidx.recyclerview.widget.LinearSmoothScroller; @@ -27,24 +26,18 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.function.Function; import awais.instagrabber.adapters.FeedAdapterV2; import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; import awais.instagrabber.customviews.helpers.PostFetcher; import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge; -import awais.instagrabber.fragments.settings.PreferenceKeys; -import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.repositories.responses.Media; -import awais.instagrabber.utils.KeywordsFilterUtils; import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.MediaViewModel; import awais.instagrabber.workers.DownloadWorker; -import static awais.instagrabber.utils.Utils.settingsHelper; - public class PostsRecyclerView extends RecyclerView { private static final String TAG = "PostsRecyclerView"; @@ -52,7 +45,6 @@ public class PostsRecyclerView extends RecyclerView { private PostsLayoutPreferences layoutPreferences; private PostFetcher.PostFetchService postFetchService; private Transition transition; - private PostFetcher postFetcher; private ViewModelStoreOwner viewModelStoreOwner; private FeedAdapterV2 feedAdapter; private LifecycleOwner lifeCycleOwner; @@ -63,40 +55,9 @@ public class PostsRecyclerView extends RecyclerView { private FeedAdapterV2.FeedItemCallback feedItemCallback; private boolean shouldScrollToTop; private FeedAdapterV2.SelectionModeCallback selectionModeCallback; - private Function headerViewCreator; - private Function headerBinder; - private boolean refresh = true; private final List fetchStatusChangeListeners = new ArrayList<>(); - private final FetchListener> fetchListener = new FetchListener>() { - @Override - public void onResult(final List result) { - if (refresh) { - refresh = false; - mediaViewModel.getList().postValue(result); - shouldScrollToTop = true; - dispatchFetchStatus(); - return; - } - final List models = mediaViewModel.getList().getValue(); - final List modelsCopy = models == null ? new ArrayList<>() : new ArrayList<>(models); - if (settingsHelper.getBoolean(PreferenceKeys.TOGGLE_KEYWORD_FILTER)) { - final ArrayList items = new ArrayList<>(settingsHelper.getStringSet(PreferenceKeys.KEYWORD_FILTERS)); - modelsCopy.addAll(new KeywordsFilterUtils(items).filter(result)); - } else { - modelsCopy.addAll(result); - } - mediaViewModel.getList().postValue(modelsCopy); - dispatchFetchStatus(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - } - }; - private final RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(getContext()) { @Override protected int getVerticalSnapPreference() { @@ -199,18 +160,22 @@ public class PostsRecyclerView extends RecyclerView { private void initSelf() { try { - mediaViewModel = new ViewModelProvider(viewModelStoreOwner).get(MediaViewModel.class); + mediaViewModel = new ViewModelProvider( + viewModelStoreOwner, + new MediaViewModel.ViewModelFactory(postFetchService) + ).get(MediaViewModel.class); } catch (Exception e) { Log.e(TAG, "initSelf: ", e); } if (mediaViewModel == null) return; - mediaViewModel.getList().observe(lifeCycleOwner, list -> feedAdapter.submitList(list, () -> { + final LiveData> mediaListLiveData = mediaViewModel.getList(); + mediaListLiveData.observe(lifeCycleOwner, list -> feedAdapter.submitList(list, () -> { + dispatchFetchStatus(); // postDelayed(this::fetchMoreIfPossible, 1000); if (!shouldScrollToTop) return; shouldScrollToTop = false; post(() -> smoothScrollToPosition(0)); })); - postFetcher = new PostFetcher(postFetchService, fetchListener); if (layoutPreferences.getHasGap()) { addItemDecoration(gridSpacingItemDecoration); } @@ -218,18 +183,20 @@ public class PostsRecyclerView extends RecyclerView { setNestedScrollingEnabled(true); setItemAnimator(null); lazyLoader = new RecyclerLazyLoaderAtEdge(layoutManager, (page) -> { - if (postFetcher.hasMore()) { - postFetcher.fetch(); + if (mediaViewModel.hasMore()) { + mediaViewModel.fetch(); dispatchFetchStatus(); } }); addOnScrollListener(lazyLoader); - postFetcher.fetch(); - dispatchFetchStatus(); + if (mediaListLiveData.getValue() == null || mediaListLiveData.getValue().isEmpty()) { + mediaViewModel.fetch(); + dispatchFetchStatus(); + } } private void fetchMoreIfPossible() { - if (!postFetcher.hasMore()) return; + if (!mediaViewModel.hasMore()) return; if (feedAdapter.getItemCount() == 0) return; final LayoutManager layoutManager = getLayoutManager(); if (!(layoutManager instanceof StaggeredGridLayoutManager)) return; @@ -238,7 +205,7 @@ public class PostsRecyclerView extends RecyclerView { if (allNoPosition) return; final boolean match = Arrays.stream(itemPositions).anyMatch(position -> position == feedAdapter.getItemCount() - 1); if (!match) return; - postFetcher.fetch(); + mediaViewModel.fetch(); dispatchFetchStatus(); } @@ -268,6 +235,7 @@ public class PostsRecyclerView extends RecyclerView { private List getDisplayUrl(final Media feedModel) { List urls = Collections.emptyList(); + if (feedModel == null || feedModel.getType() == null) return urls; switch (feedModel.getType()) { case MEDIA_TYPE_IMAGE: case MEDIA_TYPE_VIDEO: @@ -320,20 +288,18 @@ public class PostsRecyclerView extends RecyclerView { } public void refresh() { - refresh = true; + shouldScrollToTop = true; if (lazyLoader != null) { lazyLoader.resetState(); } - if (postFetcher != null) { - // mediaViewModel.getList().postValue(Collections.emptyList()); - postFetcher.reset(); - postFetcher.fetch(); + if (mediaViewModel != null) { + mediaViewModel.refresh(); } dispatchFetchStatus(); } public boolean isFetching() { - return postFetcher != null && postFetcher.isFetching(); + return mediaViewModel != null && mediaViewModel.isFetching(); } public PostsRecyclerView addFetchStatusChangeListener(final FetchStatusChangeListener fetchStatusChangeListener) { @@ -369,6 +335,7 @@ public class PostsRecyclerView extends RecyclerView { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); lifeCycleOwner = null; + initCalled = false; } @Override diff --git a/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java b/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java index 367b6544..a017ebaa 100644 --- a/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java +++ b/app/src/main/java/awais/instagrabber/customviews/helpers/PostFetcher.java @@ -21,21 +21,20 @@ public class PostFetcher { } public void fetch() { - if (!fetching) { - fetching = true; - postFetchService.fetch(new FetchListener>() { - @Override - public void onResult(final List result) { - fetching = false; - fetchListener.onResult(result); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: ", t); - } - }); - } + if (fetching) return; + fetching = true; + postFetchService.fetch(new FetchListener>() { + @Override + public void onResult(final List result) { + fetching = false; + fetchListener.onResult(result); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "onFailure: ", t); + } + }); } public void reset() { diff --git a/app/src/main/java/awais/instagrabber/dialogs/PostLoadingDialogFragment.kt b/app/src/main/java/awais/instagrabber/dialogs/PostLoadingDialogFragment.kt new file mode 100644 index 00000000..66cea86a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/dialogs/PostLoadingDialogFragment.kt @@ -0,0 +1,78 @@ +package awais.instagrabber.dialogs + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import awais.instagrabber.R +import awais.instagrabber.utils.* +import awais.instagrabber.utils.extensions.TAG +import awais.instagrabber.webservices.GraphQLRepository +import awais.instagrabber.webservices.MediaRepository +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.* + +class PostLoadingDialogFragment : DialogFragment() { + private var isLoggedIn: Boolean = false + + private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() } + private val graphQLRepository: GraphQLRepository by lazy { GraphQLRepository.getInstance() } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val cookie = Utils.settingsHelper.getString(Constants.COOKIE) + var userId: Long = 0 + var csrfToken: String? = null + if (cookie.isNotBlank()) { + userId = getUserIdFromCookie(cookie) + csrfToken = getCsrfTokenFromCookie(cookie) + } + if (cookie.isBlank() || userId == 0L || csrfToken.isNullOrBlank()) { + isLoggedIn = false + return + } + isLoggedIn = true + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext()) + .setCancelable(false) + .setView(R.layout.dialog_opening_post) + .create() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + val arguments = PostLoadingDialogFragmentArgs.fromBundle(arguments ?: return) + val shortCode = arguments.shortCode + lifecycleScope.launch(Dispatchers.IO) { + try { + val media = if (isLoggedIn) mediaRepository.fetch(TextUtils.shortcodeToId(shortCode)) else graphQLRepository.fetchPost(shortCode) + withContext(Dispatchers.Main) { + if (media == null) { + Toast.makeText(context, R.string.post_not_found, Toast.LENGTH_SHORT).show() + return@withContext + } + try { + findNavController().navigate(PostLoadingDialogFragmentDirections.actionToPost(media, 0)) + } catch (e: Exception) { + Log.e(TAG, "showPostView: ", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "showPostView: ", e) + } finally { + withContext(Dispatchers.Main) { + dismiss() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/dialogs/TabOrderPreferenceDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/TabOrderPreferenceDialogFragment.java index 4ed6e8c5..ba6fbab0 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/TabOrderPreferenceDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/TabOrderPreferenceDialogFragment.java @@ -238,7 +238,7 @@ public class TabOrderPreferenceDialogFragment extends DialogFragment { private void saveNewOrder() { final String newOrderString = newOrderTabs .stream() - .map(tab -> NavigationHelperKt.geNavGraphNameForNavRootId(tab.getNavigationRootId())) + .map(tab -> NavigationHelperKt.getNavGraphNameForNavRootId(tab.getNavigationRootId())) .collect(Collectors.joining(",")); Utils.settingsHelper.putString(PreferenceKeys.PREF_TAB_ORDER, newOrderString); } diff --git a/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java b/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java index bcc8a1e2..08a09cba 100644 --- a/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java @@ -170,7 +170,7 @@ public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.O private void openPostDialog(final Media feedModel, final int position) { try { - final NavDirections action = TopicPostsFragmentDirections.actionGlobalPost(feedModel, position); + final NavDirections action = TopicPostsFragmentDirections.actionToPost(feedModel, position); NavHostFragment.findNavController(TopicPostsFragment.this).navigate(action); } catch (Exception e) { Log.e(TAG, "openPostDialog: ", e); diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.kt b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.kt index f55e07d7..78aa6488 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.kt +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.kt @@ -7,10 +7,8 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.view.* -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener @@ -33,18 +31,16 @@ class DirectMessageInboxFragment : Fragment(), OnRefreshListener { private val viewModel: DirectInboxViewModel by activityViewModels() private lateinit var fragmentActivity: MainActivity - private lateinit var root: CoordinatorLayout private lateinit var binding: FragmentDirectMessagesInboxBinding - private lateinit var inboxAdapter: DirectMessageInboxAdapter private lateinit var lazyLoader: RecyclerLazyLoaderAtEdge - private var shouldRefresh = true private var scrollToTop = false private var navigating = false - private var threadsObserver: Observer>? = null + private var pendingRequestsMenuItem: MenuItem? = null private var pendingRequestTotalBadgeDrawable: BadgeDrawable? = null private var isPendingRequestTotalBadgeAttached = false + private var inboxAdapter: DirectMessageInboxAdapter? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -57,17 +53,11 @@ class DirectMessageInboxFragment : Fragment(), OnRefreshListener { container: ViewGroup?, savedInstanceState: Bundle?, ): View { - if (this::root.isInitialized) { - shouldRefresh = false - return root - } binding = FragmentDirectMessagesInboxBinding.inflate(inflater, container, false) - root = binding.root - return root + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - if (!shouldRefresh) return init() } @@ -90,11 +80,6 @@ class DirectMessageInboxFragment : Fragment(), OnRefreshListener { } } - override fun onResume() { - super.onResume() - setupObservers() - } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.dm_inbox_menu, menu) pendingRequestsMenuItem = menu.findItem(R.id.pending_requests) @@ -119,23 +104,14 @@ class DirectMessageInboxFragment : Fragment(), OnRefreshListener { init() } - override fun onDestroy() { - super.onDestroy() - removeViewModelObservers() - viewModel.onDestroy() - } - private fun setupObservers() { - removeViewModelObservers() - threadsObserver = Observer { list: List -> - if (!this::inboxAdapter.isInitialized) return@Observer - inboxAdapter.submitList(list) { + viewModel.threads.observe(viewLifecycleOwner, { list: List -> + inboxAdapter?.submitList(list) { if (!scrollToTop) return@submitList binding.inboxList.post { binding.inboxList.smoothScrollToPosition(0) } scrollToTop = false } - } - threadsObserver?.let { viewModel.threads.observe(fragmentActivity, it) } + }) viewModel.inbox.observe(viewLifecycleOwner, { inboxResource: Resource? -> if (inboxResource == null) return@observe when (inboxResource.status) { @@ -191,11 +167,6 @@ class DirectMessageInboxFragment : Fragment(), OnRefreshListener { } } - private fun removeViewModelObservers() { - threadsObserver?.let { viewModel.threads.removeObserver(it) } - // no need to explicitly remove observers whose lifecycle owner is getViewLifecycleOwner - } - private fun init() { val context = context ?: return setupObservers() @@ -218,10 +189,12 @@ class DirectMessageInboxFragment : Fragment(), OnRefreshListener { } } navigating = false + }.also { + it.setHasStableIds(true) } - inboxAdapter.setHasStableIds(true) binding.inboxList.adapter = inboxAdapter - lazyLoader = RecyclerLazyLoaderAtEdge(layoutManager) { viewModel.fetchInbox() } - lazyLoader.let { binding.inboxList.addOnScrollListener(it) } + lazyLoader = RecyclerLazyLoaderAtEdge(layoutManager) { viewModel.fetchInbox() }.also { + binding.inboxList.addOnScrollListener(it) + } } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt index ac0b5be0..d7e54ac1 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.kt @@ -13,7 +13,6 @@ 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 @@ -75,12 +74,14 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall private var selectedMedia: List? = null private var actionMode: ActionMode? = null private var disableDm: Boolean = false - private var shouldRefresh: Boolean = true + + // 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 root: MotionLayout private lateinit var binding: FragmentProfileBinding private lateinit var appStateViewModel: AppStateViewModel private lateinit var viewModel: ProfileFragmentViewModel @@ -335,23 +336,12 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall } 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 + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - if (!shouldRefresh) { - setupObservers() - return - } init() - shouldRefresh = false } override fun onRefresh() { @@ -399,6 +389,11 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall } } + override fun onDestroyView() { + super.onDestroyView() + setupPostsDone = false + } + private fun shareProfileViaDm() { try { val actionToUserSearch = ProfileFragmentDirections.actionToUserSearch().apply { @@ -444,7 +439,11 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall } private fun setupObservers() { - viewModel.isLoggedIn.observe(viewLifecycleOwner) {} // observe so that `isLoggedIn.value` is correct + 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) { @@ -469,7 +468,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall context?.let { ctx -> Toast.makeText(ctx, R.string.error_loading_profile, Toast.LENGTH_LONG).show() } return@observe } - root.loadLayoutDescription(R.xml.header_list_scene) + binding.root.loadLayoutDescription(R.xml.header_list_scene) setupFavChip(profile, currentUser) setupFavButton(currentUser, profile) setupSavedButton(currentUser, profile) @@ -549,7 +548,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall binding.privatePage2.visibility = VISIBLE binding.postsRecyclerView.visibility = GONE binding.swipeRefreshLayout.isRefreshing = false - root.getTransition(R.id.transition)?.setEnable(false) + binding.root.getTransition(R.id.transition)?.setEnable(false) } private fun setupProfileContext(contextPair: Pair?>) { @@ -853,7 +852,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall } private fun showDefaultMessage() { - root.loadLayoutDescription(R.xml.profile_fragment_no_acc_layout) + binding.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) @@ -884,7 +883,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall 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) + binding.root.getTransition(R.id.transition)?.setEnable(!canScrollVertically) } }) setupPostsDone = true diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java index 73fc94a0..52e7fdd6 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/GeneralPreferencesFragment.java @@ -59,7 +59,7 @@ public class GeneralPreferencesFragment extends BasePreferencesFragment implemen .map(Tab::getTitle) .toArray(String[]::new); final String[] navGraphFileNames = tabs.stream() - .map(tab -> NavigationHelperKt.geNavGraphNameForNavRootId(tab.getNavigationRootId())) + .map(tab -> NavigationHelperKt.getNavGraphNameForNavRootId(tab.getNavigationRootId())) .toArray(String[]::new); preference.setKey(Constants.DEFAULT_TAB); preference.setTitle(R.string.pref_start_screen); diff --git a/app/src/main/java/awais/instagrabber/models/Tab.kt b/app/src/main/java/awais/instagrabber/models/Tab.kt index 4edfee0d..d31352a1 100644 --- a/app/src/main/java/awais/instagrabber/models/Tab.kt +++ b/app/src/main/java/awais/instagrabber/models/Tab.kt @@ -2,12 +2,18 @@ package awais.instagrabber.models import androidx.annotation.DrawableRes import androidx.annotation.IdRes +import androidx.annotation.NavigationRes data class Tab( @param:DrawableRes val iconResId: Int, val title: String, val isRemovable: Boolean, + /** + * This is the actual resource id of the navigation resource (R.navigation.graphName = navigationResId) + */ + @param:NavigationRes val navigationResId: Int, + /** * This is the resource id of the root navigation tag of the navigation resource. * diff --git a/app/src/main/java/awais/instagrabber/utils/BarinstaDeepLinkHelper.kt b/app/src/main/java/awais/instagrabber/utils/BarinstaDeepLinkHelper.kt new file mode 100644 index 00000000..1517e157 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/BarinstaDeepLinkHelper.kt @@ -0,0 +1,23 @@ +package awais.instagrabber.utils + +import android.net.Uri +import androidx.core.net.toUri + +private const val domain = "barinsta" + +fun getDirectThreadDeepLink(threadId: String, threadTitle: String, isPending: Boolean = false): Uri = + "$domain://dm_thread/$threadId/$threadTitle?pending=${isPending}".toUri() + +fun getProfileDeepLink(username: String): Uri = "$domain://profile/$username".toUri() + +fun getPostDeepLink(shortCode: String): Uri = "$domain://post/$shortCode".toUri() + +fun getLocationDeepLink(locationId: Long): Uri = "$domain://location/$locationId".toUri() + +fun getLocationDeepLink(locationId: String): Uri = "$domain://location/$locationId".toUri() + +fun getHashtagDeepLink(hashtag: String): Uri = "$domain://hashtag/$hashtag".toUri() + +fun getNotificationsDeepLink(type: String, targetId: Long = 0): Uri = "$domain://notifications/$type?targetId=$targetId".toUri() + +fun getSearchDeepLink(): Uri = "$domain://search".toUri() \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/KeywordsFilterUtils.kt b/app/src/main/java/awais/instagrabber/utils/KeywordsFilterUtils.kt index 1a972f8b..355144da 100644 --- a/app/src/main/java/awais/instagrabber/utils/KeywordsFilterUtils.kt +++ b/app/src/main/java/awais/instagrabber/utils/KeywordsFilterUtils.kt @@ -2,8 +2,8 @@ package awais.instagrabber.utils import awais.instagrabber.repositories.responses.Media import java.util.* +import kotlin.collections.ArrayList -class KeywordsFilterUtils(private val keywords: ArrayList) { // fun filter(caption: String?): Boolean { // if (caption == null) return false // if (keywords.isEmpty()) return false @@ -14,24 +14,17 @@ class KeywordsFilterUtils(private val keywords: ArrayList) { // return false // } - fun filter(media: Media?): Boolean { - if (media == null) return false - val (_, text) = media.caption ?: return false - if (keywords.isEmpty()) return false - val temp = text!!.lowercase(Locale.getDefault()) - for (s in keywords) { - if (temp.contains(s)) return true - } - return false - } +private fun containsAnyKeyword(keywords: List, media: Media?): Boolean { + if (media == null || keywords.isEmpty()) return false + val (_, text) = media.caption ?: return false + val temp = text!!.lowercase(Locale.getDefault()) + return keywords.any { temp.contains(it) } +} - fun filter(media: List?): List? { - if (keywords.isEmpty()) return media - if (media == null) return ArrayList() - val result: MutableList = ArrayList() - for (m in media) { - if (!filter(m)) result.add(m) - } - return result - } +fun filter(keywords: List, media: List?): List? { + if (keywords.isEmpty()) return media + if (media == null) return ArrayList() + val result: MutableList = ArrayList() + media.filterNotTo(result) { containsAnyKeyword(keywords, it) } + return result } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/NavigationHelper.kt b/app/src/main/java/awais/instagrabber/utils/NavigationHelper.kt index 77735937..0f474bf6 100644 --- a/app/src/main/java/awais/instagrabber/utils/NavigationHelper.kt +++ b/app/src/main/java/awais/instagrabber/utils/NavigationHelper.kt @@ -28,7 +28,8 @@ private fun getTabs( navRootIds: IntArray, isAnon: Boolean = false, ): Pair, MutableList> { - val navGraphNames = getResIdsForNavRootIds(navRootIds, ::geNavGraphNameForNavRootId) + val navGraphNames = getResIdsForNavRootIds(navRootIds, ::getNavGraphNameForNavRootId) + val navGraphResIds = getResIdsForNavRootIds(navRootIds, ::getNavGraphResIdForNavRootId) val titleArray = getResIdsForNavRootIds(navRootIds, ::getTitleResIdForNavRootId) val iconIds = getResIdsForNavRootIds(navRootIds, ::getIconResIdForNavRootId) val startDestFragIds = getResIdsForNavRootIds(navRootIds, ::getStartDestFragIdForNavRootId) @@ -37,15 +38,15 @@ private fun getTabs( val otherTabs = mutableListOf() // Will contain tabs not in current list for (i in navRootIds.indices) { val navRootId = navRootIds[i] - val navGraphName = navGraphNames[i] val tab = Tab( iconIds[i], context.getString(titleArray[i]), - if(isAnon) false else !NON_REMOVABLE_NAV_ROOT_IDS.contains(navRootId), + if (isAnon) false else !NON_REMOVABLE_NAV_ROOT_IDS.contains(navRootId), + navGraphResIds[i], navRootId, startDestFragIds[i] ) - if (!isAnon && !orderedGraphNames.contains(navGraphName)) { + if (!isAnon && !orderedGraphNames.contains(navGraphNames[i])) { otherTabs.add(tab) continue } @@ -109,7 +110,7 @@ private fun getStartDestFragIdForNavRootId(id: Int): Int = when (id) { else -> 0 } -fun geNavGraphNameForNavRootId(id: Int): String = when (id) { +fun getNavGraphNameForNavRootId(id: Int): String = when (id) { R.id.direct_messages_nav_graph -> "direct_messages_nav_graph" R.id.feed_nav_graph -> "feed_nav_graph" R.id.profile_nav_graph -> "profile_nav_graph" @@ -120,7 +121,18 @@ fun geNavGraphNameForNavRootId(id: Int): String = when (id) { else -> "" } -private fun geNavGraphNameForNavRootId(navGraphName: String): Int = when (navGraphName) { +fun getNavGraphResIdForNavRootId(id: Int): Int = when (id) { + R.id.direct_messages_nav_graph -> R.navigation.direct_messages_nav_graph + R.id.feed_nav_graph -> R.navigation.feed_nav_graph + R.id.profile_nav_graph -> R.navigation.profile_nav_graph + R.id.discover_nav_graph -> R.navigation.discover_nav_graph + R.id.more_nav_graph -> R.navigation.more_nav_graph + R.id.favorites_nav_graph -> R.navigation.favorites_nav_graph + R.id.notification_viewer_nav_graph -> R.navigation.notification_viewer_nav_graph + else -> 0 +} + +private fun getNavRootIdForGraphName(navGraphName: String): Int = when (navGraphName) { "direct_messages_nav_graph" -> R.id.direct_messages_nav_graph "feed_nav_graph" -> R.id.feed_nav_graph "profile_nav_graph" -> R.id.profile_nav_graph @@ -139,9 +151,9 @@ private fun getOrderedNavRootIdsFromPref(navGraphNames: List): Pair): Pair> list; + private static final String TAG = MediaViewModel.class.getSimpleName(); + + private boolean refresh = true; + + private final PostFetcher postFetcher; + private final MutableLiveData> list = new MutableLiveData<>(); + + public MediaViewModel(@NonNull final PostFetcher.PostFetchService postFetchService) { + final FetchListener> fetchListener = new FetchListener>() { + @Override + public void onResult(final List result) { + if (refresh) { + list.postValue(filterResult(result, true)); + refresh = false; + return; + } + list.postValue(filterResult(result, false)); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "onFailure: ", t); + } + }; + postFetcher = new PostFetcher(postFetchService, fetchListener); + } - public MutableLiveData> getList() { - if (list == null) { - list = new MutableLiveData<>(); + @NonNull + private List filterResult(final List result, final boolean isRefresh) { + final List models = list.getValue(); + final List modelsCopy = models == null || isRefresh ? new ArrayList<>() : new ArrayList<>(models); + if (settingsHelper.getBoolean(PreferenceKeys.TOGGLE_KEYWORD_FILTER)) { + final List keywords = new ArrayList<>(settingsHelper.getStringSet(PreferenceKeys.KEYWORD_FILTERS)); + final List filter = KeywordsFilterUtilsKt.filter(keywords, result); + if (filter != null) { + modelsCopy.addAll(filter); + } + return modelsCopy; } + modelsCopy.addAll(result); + return modelsCopy; + } + + public LiveData> getList() { return list; } + + public boolean hasMore() { + return postFetcher.hasMore(); + } + + public void fetch() { + postFetcher.fetch(); + } + + public void reset() { + postFetcher.reset(); + } + + public boolean isFetching() { + return postFetcher.isFetching(); + } + + public void refresh() { + refresh = true; + reset(); + fetch(); + } + + public static class ViewModelFactory implements ViewModelProvider.Factory { + + @NonNull + private final PostFetcher.PostFetchService postFetchService; + + public ViewModelFactory(@NonNull final PostFetcher.PostFetchService postFetchService) { + this.postFetchService = postFetchService; + } + + @NonNull + @Override + public T create(@NonNull final Class modelClass) { + //noinspection unchecked + return (T) new MediaViewModel(postFetchService); + } + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt index 28e8ef6b..a3d169ce 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt @@ -26,6 +26,7 @@ import awais.instagrabber.viewmodels.ProfileFragmentViewModel.ProfileEvent.* import awais.instagrabber.webservices.* import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.time.LocalDateTime @@ -380,6 +381,7 @@ class ProfileFragmentViewModel( } val threadId = thread.threadId ?: return@afterPrevious _eventLiveData.postValue(Event(NavigateToThread(threadId, username))) + delay(200) // Add delay so that the postValue in finally does not overwrite the NavigateToThread event } catch (e: Exception) { Log.e(TAG, "sendDm: ", e) } finally { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9a0c155b..a56989a4 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -73,5 +73,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" - app:labelVisibilityMode="auto" /> + app:labelVisibilityMode="auto" + tools:menu="@menu/bottom_nav_menu" /> \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml new file mode 100644 index 00000000..3714cd6c --- /dev/null +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/direct_messages_nav_graph.xml similarity index 65% rename from app/src/main/res/navigation/nav_graph.xml rename to app/src/main/res/navigation/direct_messages_nav_graph.xml index 2107acf6..62d459f3 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/direct_messages_nav_graph.xml @@ -2,70 +2,22 @@ - - - - - - - - - - - - - - + android:id="@+id/direct_messages_nav_graph" + app:startDestination="@id/directMessagesInboxFragment"> - - - - - - - - - - + android:id="@+id/directMessagesInboxFragment" + android:name="awais.instagrabber.fragments.directmessages.DirectMessageInboxFragment" + android:label="@string/action_dms" + tools:layout="@layout/fragment_direct_messages_inbox"> + android:id="@+id/action_to_thread" + app:destination="@id/directMessagesThreadFragment" /> + android:id="@+id/action_to_pending_inbox" + app:destination="@id/directPendingInboxFragment" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -470,126 +346,6 @@ app:destination="@id/profile_non_top" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -677,41 +433,6 @@ app:destination="@id/profile_non_top" /> - - - - - - - - - - - - - - - - + + @@ -863,36 +586,4 @@ app:argType="string[]" app:nullable="true" /> - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/discover_nav_graph.xml b/app/src/main/res/navigation/discover_nav_graph.xml new file mode 100644 index 00000000..cd2c844e --- /dev/null +++ b/app/src/main/res/navigation/discover_nav_graph.xml @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/favorites_nav_graph.xml b/app/src/main/res/navigation/favorites_nav_graph.xml new file mode 100644 index 00000000..32ee328f --- /dev/null +++ b/app/src/main/res/navigation/favorites_nav_graph.xml @@ -0,0 +1,484 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/feed_nav_graph.xml b/app/src/main/res/navigation/feed_nav_graph.xml new file mode 100644 index 00000000..5eaa30ca --- /dev/null +++ b/app/src/main/res/navigation/feed_nav_graph.xml @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/more_nav_graph.xml b/app/src/main/res/navigation/more_nav_graph.xml new file mode 100644 index 00000000..61d6f0ef --- /dev/null +++ b/app/src/main/res/navigation/more_nav_graph.xml @@ -0,0 +1,539 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/notification_viewer_nav_graph.xml b/app/src/main/res/navigation/notification_viewer_nav_graph.xml new file mode 100644 index 00000000..edd216d6 --- /dev/null +++ b/app/src/main/res/navigation/notification_viewer_nav_graph.xml @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/profile_nav_graph.xml b/app/src/main/res/navigation/profile_nav_graph.xml new file mode 100644 index 00000000..c71167e7 --- /dev/null +++ b/app/src/main/res/navigation/profile_nav_graph.xml @@ -0,0 +1,567 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/root_nav_graph.xml b/app/src/main/res/navigation/root_nav_graph.xml new file mode 100644 index 00000000..39619106 --- /dev/null +++ b/app/src/main/res/navigation/root_nav_graph.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/settings_nav_graph.xml b/app/src/main/res/navigation/settings_nav_graph.xml index d8ecbccf..caed28fa 100644 --- a/app/src/main/res/navigation/settings_nav_graph.xml +++ b/app/src/main/res/navigation/settings_nav_graph.xml @@ -4,110 +4,6 @@ android:id="@+id/settings_nav_graph" app:startDestination="@id/settingsPreferencesFragment"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 23eec836..66f1464c 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -11,15 +11,6 @@ - - - - - - - - -