This commit is contained in:
Austin Huang 2021-06-24 11:27:48 -04:00
commit 38cc804a3f
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
6 changed files with 152 additions and 32 deletions

View File

@ -1,14 +1,13 @@
package awais.instagrabber.models package awais.instagrabber.models
import awais.instagrabber.utils.TextUtils import awais.instagrabber.utils.TextUtils
import java.util.*
data class HighlightModel( data class HighlightModel(
val title: String?, val title: String? = null,
val id: String, val id: String = "",
val thumbnailUrl: String, val thumbnailUrl: String = "",
val timestamp: Long, val timestamp: Long = 0,
val mediaCount: Int val mediaCount: Int = 0,
) { ) {
val dateTime: String val dateTime: String
get() = TextUtils.epochSecondToString(timestamp) get() = TextUtils.epochSecondToString(timestamp)

View File

@ -5,14 +5,14 @@ import awais.instagrabber.models.stickers.*
import java.io.Serializable import java.io.Serializable
data class StoryModel( data class StoryModel(
val storyMediaId: String?, val storyMediaId: String? = null,
val storyUrl: String?, val storyUrl: String? = null,
var thumbnail: String?, var thumbnail: String? = null,
val itemType: MediaItemType?, val itemType: MediaItemType? = null,
val timestamp: Long, val timestamp: Long = 0,
val username: String?, val username: String? = null,
val userId: Long, val userId: Long = 0,
val canReply: Boolean val canReply: Boolean = false,
) : Serializable { ) : Serializable {
var videoUrl: String? = null var videoUrl: String? = null
var tappableShortCode: String? = null var tappableShortCode: String? = null

View File

@ -8,8 +8,11 @@ import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.db.repositories.AccountRepository import awais.instagrabber.db.repositories.AccountRepository
import awais.instagrabber.db.repositories.FavoriteRepository import awais.instagrabber.db.repositories.FavoriteRepository
import awais.instagrabber.managers.DirectMessagesManager import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.models.HighlightModel
import awais.instagrabber.models.Resource import awais.instagrabber.models.Resource
import awais.instagrabber.models.StoryModel
import awais.instagrabber.models.enums.FavoriteType import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.User import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.utils.ControlledRunner import awais.instagrabber.utils.ControlledRunner
@ -22,7 +25,7 @@ class ProfileFragmentViewModel(
state: SavedStateHandle, state: SavedStateHandle,
userRepository: UserRepository, userRepository: UserRepository,
friendshipRepository: FriendshipRepository, friendshipRepository: FriendshipRepository,
storiesRepository: StoriesRepository, private val storiesRepository: StoriesRepository,
mediaRepository: MediaRepository, mediaRepository: MediaRepository,
graphQLRepository: GraphQLRepository, graphQLRepository: GraphQLRepository,
accountRepository: AccountRepository, accountRepository: AccountRepository,
@ -61,27 +64,21 @@ class ProfileFragmentViewModel(
private val profileFetchControlledRunner = ControlledRunner<User?>() private val profileFetchControlledRunner = ControlledRunner<User?>()
val profile: LiveData<Resource<User?>> = currentUserAndStateUsernameLiveData.switchMap { val profile: LiveData<Resource<User?>> = currentUserAndStateUsernameLiveData.switchMap {
val (userResource, stateUsernameResource) = it val (currentUserResource, stateUsernameResource) = it
liveData<Resource<User?>>(context = viewModelScope.coroutineContext + ioDispatcher) { liveData<Resource<User?>>(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)) emit(Resource.loading(null))
return@liveData return@liveData
} }
val user = userResource.data val currentUser = currentUserResource.data
val stateUsername = stateUsernameResource.data val stateUsername = stateUsernameResource.data
if (stateUsername.isNullOrBlank()) { if (stateUsername.isNullOrBlank()) {
emit(Resource.success(user)) emit(Resource.success(currentUser))
return@liveData return@liveData
} }
try { try {
val fetchedUser = profileFetchControlledRunner.cancelPreviousThenRun { val fetchedUser = profileFetchControlledRunner.cancelPreviousThenRun {
return@cancelPreviousThenRun if (user != null) { return@cancelPreviousThenRun fetchUser(currentUser, userRepository, stateUsername, graphQLRepository)
val tempUser = userRepository.getUsernameInfo(stateUsername) // logged in
tempUser.friendshipStatus = userRepository.getUserFriendship(tempUser.pk)
return@cancelPreviousThenRun tempUser
} else {
graphQLRepository.fetchUser(stateUsername) // anonymous
}
} }
emit(Resource.success(fetchedUser)) emit(Resource.success(fetchedUser))
if (fetchedUser != null) { if (fetchedUser != null) {
@ -94,6 +91,81 @@ class ProfileFragmentViewModel(
} }
} }
private val storyFetchControlledRunner = ControlledRunner<List<StoryModel>?>()
val userStories: LiveData<Resource<List<StoryModel>?>> = profile.switchMap { userResource ->
liveData<Resource<List<StoryModel>?>>(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<List<HighlightModel>?>()
val userHighlights: LiveData<Resource<List<HighlightModel>?>> = profile.switchMap { userResource ->
liveData<Resource<List<HighlightModel>?>>(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<StoryModel> = storiesRepository.getUserStory(
StoryViewerOptions.forUser(fetchedUser.pk, fetchedUser.fullName)
)
private suspend fun fetchUserHighlights(fetchedUser: User): List<HighlightModel> = storiesRepository.fetchHighlights(fetchedUser.pk)
private suspend fun checkAndInsertFavorite(fetchedUser: User) { private suspend fun checkAndInsertFavorite(fetchedUser: User) {
try { try {
val favorite = favoriteRepository.getFavorite(fetchedUser.username, FavoriteType.USER) val favorite = favoriteRepository.getFavorite(fetchedUser.username, FavoriteType.USER)

View File

@ -19,7 +19,7 @@ import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.util.* import java.util.*
class StoriesRepository(private val service: StoriesService) { open class StoriesRepository(private val service: StoriesService) {
suspend fun fetch(mediaId: Long): StoryModel { suspend fun fetch(mediaId: Long): StoryModel {
val response = service.fetch(mediaId) val response = service.fetch(mediaId)
@ -99,7 +99,7 @@ class StoriesRepository(private val service: StoriesService) {
return sort(feedStoryModels) return sort(feedStoryModels)
} }
suspend fun fetchHighlights(profileId: Long): List<HighlightModel> { open suspend fun fetchHighlights(profileId: Long): List<HighlightModel> {
val response = service.fetchHighlights(profileId) val response = service.fetchHighlights(profileId)
val highlightsReel = JSONObject(response).getJSONArray("tray") val highlightsReel = JSONObject(response).getJSONArray("tray")
val length = highlightsReel.length() 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")) return ArchiveFetchResponse(highlightModels, data.getBoolean("more_available"), data.getString("max_id"))
} }
suspend fun getUserStory(options: StoryViewerOptions): List<StoryModel> { open suspend fun getUserStory(options: StoryViewerOptions): List<StoryModel> {
val url = buildUrl(options) ?: return emptyList() val url = buildUrl(options) ?: return emptyList()
val response = service.getUserStory(url) val response = service.getUserStory(url)
val isLocOrHashtag = options.type == StoryViewerOptions.Type.LOCATION || options.type == StoryViewerOptions.Type.HASHTAG val isLocOrHashtag = options.type == StoryViewerOptions.Type.LOCATION || options.type == StoryViewerOptions.Type.HASHTAG

View File

@ -17,9 +17,7 @@ open class UserServiceAdapter : UserService {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun getUserFriendship(uid: Long): FriendshipStatus { override suspend fun getUserFriendship(uid: Long): FriendshipStatus = FriendshipStatus()
TODO("Not yet implemented")
}
override suspend fun search(timezoneOffset: Float, query: String): UserSearchResponse { override suspend fun search(timezoneOffset: Float, query: String): UserSearchResponse {
TODO("Not yet implemented") TODO("Not yet implemented")

View File

@ -11,8 +11,11 @@ import awais.instagrabber.db.entities.Favorite
import awais.instagrabber.db.repositories.AccountRepository import awais.instagrabber.db.repositories.AccountRepository
import awais.instagrabber.db.repositories.FavoriteRepository import awais.instagrabber.db.repositories.FavoriteRepository
import awais.instagrabber.getOrAwaitValue import awais.instagrabber.getOrAwaitValue
import awais.instagrabber.models.HighlightModel
import awais.instagrabber.models.Resource import awais.instagrabber.models.Resource
import awais.instagrabber.models.StoryModel
import awais.instagrabber.models.enums.FavoriteType import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.FriendshipStatus import awais.instagrabber.repositories.responses.FriendshipStatus
import awais.instagrabber.repositories.responses.User import awais.instagrabber.repositories.responses.User
import awais.instagrabber.webservices.* import awais.instagrabber.webservices.*
@ -279,6 +282,54 @@ internal class ProfileFragmentViewModelTest {
while (profile.status == Resource.Status.LOADING) { while (profile.status == Resource.Status.LOADING) {
profile = viewModel.profile.getOrAwaitValue() profile = viewModel.profile.getOrAwaitValue()
} }
assertEquals(true, viewModel.isFavorite.getOrAwaitValue())
assertTrue(updateFavoriteCalled) assertTrue(updateFavoriteCalled)
} }
@ExperimentalCoroutinesApi
@Test
fun `should fetch user stories and highlights when logged in`() {
val state = SavedStateHandle(
mutableMapOf<String, Any?>(
"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<StoryModel> = testUserStories
override suspend fun fetchHighlights(profileId: Long): List<HighlightModel> = 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)
}
} }