From a713a72e072bd3a3dfc635afd0a8b1f9cec02c84 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Thu, 24 Jun 2021 20:45:13 +0900 Subject: [PATCH 1/2] Add missed assertion in test --- .../instagrabber/viewmodels/ProfileFragmentViewModelTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt b/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt index fb851c7e..2c425892 100644 --- a/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt +++ b/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt @@ -279,6 +279,7 @@ internal class ProfileFragmentViewModelTest { while (profile.status == Resource.Status.LOADING) { profile = viewModel.profile.getOrAwaitValue() } + assertEquals(true, viewModel.isFavorite.getOrAwaitValue()) assertTrue(updateFavoriteCalled) } } \ No newline at end of file From 50c120eb755fe3cb87a033b68cba49ef8928194b Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Thu, 24 Jun 2021 23:19:21 +0900 Subject: [PATCH 2/2] Add user story and highlights fetch logic --- .../instagrabber/models/HighlightModel.kt | 11 +-- .../awais/instagrabber/models/StoryModel.kt | 16 ++-- .../viewmodels/ProfileFragmentViewModel.kt | 96 ++++++++++++++++--- .../webservices/StoriesRepository.kt | 6 +- .../awais/instagrabber/common/Adapters.kt | 4 +- .../ProfileFragmentViewModelTest.kt | 50 ++++++++++ 6 files changed, 151 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/awais/instagrabber/models/HighlightModel.kt b/app/src/main/java/awais/instagrabber/models/HighlightModel.kt index 79398bcb..73ac96eb 100644 --- a/app/src/main/java/awais/instagrabber/models/HighlightModel.kt +++ b/app/src/main/java/awais/instagrabber/models/HighlightModel.kt @@ -1,14 +1,13 @@ package awais.instagrabber.models import awais.instagrabber.utils.TextUtils -import java.util.* data class HighlightModel( - val title: String?, - val id: String, - val thumbnailUrl: String, - val timestamp: Long, - val mediaCount: Int + val title: String? = null, + val id: String = "", + val thumbnailUrl: String = "", + val timestamp: Long = 0, + val mediaCount: Int = 0, ) { val dateTime: String get() = TextUtils.epochSecondToString(timestamp) diff --git a/app/src/main/java/awais/instagrabber/models/StoryModel.kt b/app/src/main/java/awais/instagrabber/models/StoryModel.kt index c863df70..2858026c 100755 --- a/app/src/main/java/awais/instagrabber/models/StoryModel.kt +++ b/app/src/main/java/awais/instagrabber/models/StoryModel.kt @@ -5,14 +5,14 @@ import awais.instagrabber.models.stickers.* import java.io.Serializable data class StoryModel( - val storyMediaId: String?, - val storyUrl: String?, - var thumbnail: String?, - val itemType: MediaItemType?, - val timestamp: Long, - val username: String?, - val userId: Long, - val canReply: Boolean + val storyMediaId: String? = null, + val storyUrl: String? = null, + var thumbnail: String? = null, + val itemType: MediaItemType? = null, + val timestamp: Long = 0, + val username: String? = null, + val userId: Long = 0, + val canReply: Boolean = false, ) : Serializable { var videoUrl: String? = null var tappableShortCode: String? = null diff --git a/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt b/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt index cd150915..f9543c94 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/ProfileFragmentViewModel.kt @@ -8,8 +8,11 @@ import awais.instagrabber.db.entities.Favorite import awais.instagrabber.db.repositories.AccountRepository import awais.instagrabber.db.repositories.FavoriteRepository import awais.instagrabber.managers.DirectMessagesManager +import awais.instagrabber.models.HighlightModel import awais.instagrabber.models.Resource +import awais.instagrabber.models.StoryModel import awais.instagrabber.models.enums.FavoriteType +import awais.instagrabber.repositories.requests.StoryViewerOptions import awais.instagrabber.repositories.responses.User import awais.instagrabber.repositories.responses.directmessages.RankedRecipient import awais.instagrabber.utils.ControlledRunner @@ -22,7 +25,7 @@ class ProfileFragmentViewModel( state: SavedStateHandle, userRepository: UserRepository, friendshipRepository: FriendshipRepository, - storiesRepository: StoriesRepository, + private val storiesRepository: StoriesRepository, mediaRepository: MediaRepository, graphQLRepository: GraphQLRepository, accountRepository: AccountRepository, @@ -61,27 +64,21 @@ class ProfileFragmentViewModel( private val profileFetchControlledRunner = ControlledRunner() val profile: LiveData> = currentUserAndStateUsernameLiveData.switchMap { - val (userResource, stateUsernameResource) = it + val (currentUserResource, stateUsernameResource) = it liveData>(context = viewModelScope.coroutineContext + ioDispatcher) { - if (userResource.status == Resource.Status.LOADING || stateUsernameResource.status == Resource.Status.LOADING) { + if (currentUserResource.status == Resource.Status.LOADING || stateUsernameResource.status == Resource.Status.LOADING) { emit(Resource.loading(null)) return@liveData } - val user = userResource.data + val currentUser = currentUserResource.data val stateUsername = stateUsernameResource.data if (stateUsername.isNullOrBlank()) { - emit(Resource.success(user)) + emit(Resource.success(currentUser)) return@liveData } try { val fetchedUser = profileFetchControlledRunner.cancelPreviousThenRun { - return@cancelPreviousThenRun if (user != null) { - val tempUser = userRepository.getUsernameInfo(stateUsername) // logged in - tempUser.friendshipStatus = userRepository.getUserFriendship(tempUser.pk) - return@cancelPreviousThenRun tempUser - } else { - graphQLRepository.fetchUser(stateUsername) // anonymous - } + return@cancelPreviousThenRun fetchUser(currentUser, userRepository, stateUsername, graphQLRepository) } emit(Resource.success(fetchedUser)) if (fetchedUser != null) { @@ -94,6 +91,81 @@ class ProfileFragmentViewModel( } } + private val storyFetchControlledRunner = ControlledRunner?>() + val userStories: LiveData?>> = profile.switchMap { userResource -> + liveData?>>(context = viewModelScope.coroutineContext + ioDispatcher) { + // don't fetch if not logged in + if (isLoggedIn.value != true) { + emit(Resource.success(null)) + return@liveData + } + if (userResource.status == Resource.Status.LOADING) { + emit(Resource.loading(null)) + return@liveData + } + val user = userResource.data + if (user == null) { + emit(Resource.success(null)) + return@liveData + } + try { + val fetchedStories = storyFetchControlledRunner.cancelPreviousThenRun { fetchUserStory(user) } + emit(Resource.success(fetchedStories)) + } catch (e: Exception) { + emit(Resource.error(e.message, null)) + Log.e(TAG, "fetching story: ", e) + } + } + } + + private val highlightsFetchControlledRunner = ControlledRunner?>() + val userHighlights: LiveData?>> = profile.switchMap { userResource -> + liveData?>>(context = viewModelScope.coroutineContext + ioDispatcher) { + // don't fetch if not logged in + if (isLoggedIn.value != true) { + emit(Resource.success(null)) + return@liveData + } + if (userResource.status == Resource.Status.LOADING) { + emit(Resource.loading(null)) + return@liveData + } + val user = userResource.data + if (user == null) { + emit(Resource.success(null)) + return@liveData + } + try { + val fetchedHighlights = highlightsFetchControlledRunner.cancelPreviousThenRun { fetchUserHighlights(user) } + emit(Resource.success(fetchedHighlights)) + } catch (e: Exception) { + emit(Resource.error(e.message, null)) + Log.e(TAG, "fetching story: ", e) + } + } + } + + private suspend fun fetchUser( + currentUser: User?, + userRepository: UserRepository, + stateUsername: String, + graphQLRepository: GraphQLRepository + ) = if (currentUser != null) { + // logged in + val tempUser = userRepository.getUsernameInfo(stateUsername) + tempUser.friendshipStatus = userRepository.getUserFriendship(tempUser.pk) + tempUser + } else { + // anonymous + graphQLRepository.fetchUser(stateUsername) + } + + private suspend fun fetchUserStory(fetchedUser: User): List = storiesRepository.getUserStory( + StoryViewerOptions.forUser(fetchedUser.pk, fetchedUser.fullName) + ) + + private suspend fun fetchUserHighlights(fetchedUser: User): List = storiesRepository.fetchHighlights(fetchedUser.pk) + private suspend fun checkAndInsertFavorite(fetchedUser: User) { try { val favorite = favoriteRepository.getFavorite(fetchedUser.username, FavoriteType.USER) diff --git a/app/src/main/java/awais/instagrabber/webservices/StoriesRepository.kt b/app/src/main/java/awais/instagrabber/webservices/StoriesRepository.kt index 37abf8f0..7235ace1 100644 --- a/app/src/main/java/awais/instagrabber/webservices/StoriesRepository.kt +++ b/app/src/main/java/awais/instagrabber/webservices/StoriesRepository.kt @@ -19,7 +19,7 @@ import org.json.JSONArray import org.json.JSONObject import java.util.* -class StoriesRepository(private val service: StoriesService) { +open class StoriesRepository(private val service: StoriesService) { suspend fun fetch(mediaId: Long): StoryModel { val response = service.fetch(mediaId) @@ -99,7 +99,7 @@ class StoriesRepository(private val service: StoriesService) { return sort(feedStoryModels) } - suspend fun fetchHighlights(profileId: Long): List { + open suspend fun fetchHighlights(profileId: Long): List { val response = service.fetchHighlights(profileId) val highlightsReel = JSONObject(response).getJSONArray("tray") val length = highlightsReel.length() @@ -150,7 +150,7 @@ class StoriesRepository(private val service: StoriesService) { return ArchiveFetchResponse(highlightModels, data.getBoolean("more_available"), data.getString("max_id")) } - suspend fun getUserStory(options: StoryViewerOptions): List { + open suspend fun getUserStory(options: StoryViewerOptions): List { val url = buildUrl(options) ?: return emptyList() val response = service.getUserStory(url) val isLocOrHashtag = options.type == StoryViewerOptions.Type.LOCATION || options.type == StoryViewerOptions.Type.HASHTAG diff --git a/app/src/test/java/awais/instagrabber/common/Adapters.kt b/app/src/test/java/awais/instagrabber/common/Adapters.kt index 580bc07a..eac36d61 100644 --- a/app/src/test/java/awais/instagrabber/common/Adapters.kt +++ b/app/src/test/java/awais/instagrabber/common/Adapters.kt @@ -17,9 +17,7 @@ open class UserServiceAdapter : UserService { TODO("Not yet implemented") } - override suspend fun getUserFriendship(uid: Long): FriendshipStatus { - TODO("Not yet implemented") - } + override suspend fun getUserFriendship(uid: Long): FriendshipStatus = FriendshipStatus() override suspend fun search(timezoneOffset: Float, query: String): UserSearchResponse { 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 2c425892..471b9c2a 100644 --- a/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt +++ b/app/src/test/java/awais/instagrabber/viewmodels/ProfileFragmentViewModelTest.kt @@ -11,8 +11,11 @@ import awais.instagrabber.db.entities.Favorite import awais.instagrabber.db.repositories.AccountRepository import awais.instagrabber.db.repositories.FavoriteRepository import awais.instagrabber.getOrAwaitValue +import awais.instagrabber.models.HighlightModel import awais.instagrabber.models.Resource +import awais.instagrabber.models.StoryModel import awais.instagrabber.models.enums.FavoriteType +import awais.instagrabber.repositories.requests.StoryViewerOptions import awais.instagrabber.repositories.responses.FriendshipStatus import awais.instagrabber.repositories.responses.User import awais.instagrabber.webservices.* @@ -282,4 +285,51 @@ internal class ProfileFragmentViewModelTest { assertEquals(true, viewModel.isFavorite.getOrAwaitValue()) assertTrue(updateFavoriteCalled) } + + + @ExperimentalCoroutinesApi + @Test + fun `should fetch user stories and highlights when logged in`() { + val state = SavedStateHandle( + mutableMapOf( + "username" to testPublicUser.username + ) + ) + val testUserStories = listOf(StoryModel()) + val testUserHighlights = listOf(HighlightModel()) + val userRepository = object : UserRepository(UserServiceAdapter()) { + override suspend fun getUsernameInfo(username: String): User = testPublicUser + } + val storiesRepository = object : StoriesRepository(StoriesServiceAdapter()) { + override suspend fun getUserStory(options: StoryViewerOptions): List = testUserStories + override suspend fun fetchHighlights(profileId: Long): List = testUserHighlights + } + val viewModel = ProfileFragmentViewModel( + state, + userRepository, + FriendshipRepository(FriendshipServiceAdapter()), + storiesRepository, + MediaRepository(MediaServiceAdapter()), + GraphQLRepository(GraphQLServiceAdapter()), + AccountRepository(AccountDataSource(AccountDaoAdapter())), + FavoriteRepository(FavoriteDataSource(FavoriteDaoAdapter())), + coroutineScope.dispatcher, + ) + viewModel.setCurrentUser(Resource.success(User())) + assertEquals(true, viewModel.isLoggedIn.getOrAwaitValue()) + var profile = viewModel.profile.getOrAwaitValue() + while (profile.status == Resource.Status.LOADING) { + profile = viewModel.profile.getOrAwaitValue() + } + var userStories = viewModel.userStories.getOrAwaitValue() + while (userStories.status == Resource.Status.LOADING) { + userStories = viewModel.userStories.getOrAwaitValue() + } + assertEquals(testUserStories, userStories.data) + var userHighlights = viewModel.userHighlights.getOrAwaitValue() + while (userHighlights.status == Resource.Status.LOADING) { + userHighlights = viewModel.userHighlights.getOrAwaitValue() + } + assertEquals(testUserHighlights, userHighlights.data) + } } \ No newline at end of file