From f9252327e5ca9bc19a2a6d2add488f5cc10d450d Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Sun, 25 Apr 2021 11:44:31 +0200 Subject: [PATCH 1/5] Add like button to book suggestions --- .../main/res/drawable/ic_like_outlined.xml | 10 +++++++++ app/src/main/res/layout/item_suggestion.xml | 22 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_like_outlined.xml diff --git a/app/src/main/res/drawable/ic_like_outlined.xml b/app/src/main/res/drawable/ic_like_outlined.xml new file mode 100644 index 00000000..0a55226f --- /dev/null +++ b/app/src/main/res/drawable/ic_like_outlined.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/item_suggestion.xml b/app/src/main/res/layout/item_suggestion.xml index 921ee536..faadb006 100644 --- a/app/src/main/res/layout/item_suggestion.xml +++ b/app/src/main/res/layout/item_suggestion.xml @@ -124,13 +124,16 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" + android:layout_marginEnd="32dp" android:layout_marginBottom="16dp" android:gravity="center" android:paddingHorizontal="32dp" - android:text="@string/add_book_to_wishlist" + android:text="@string/wishlist" + app:icon="@drawable/ic_add_round" + app:iconPadding="8dp" + app:iconTint="@color/colorAccent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/iv_item_suggestion_suggester" /> + + Date: Mon, 3 May 2021 21:37:52 +0200 Subject: [PATCH 2/5] Allow liking of books in suggestion screen --- .../dante/suggestions/Suggestion.kt | 4 +++- .../suggestions/SuggestionsRepository.kt | 3 +++ .../firebase/FirebaseSuggestionsApi.kt | 6 +++++ .../firebase/FirebaseSuggestionsRepository.kt | 14 +++++++++++ .../OnSuggestionActionClickedListener.kt | 2 ++ .../suggestions/SuggestionViewHolder.kt | 23 +++++++++++++++++++ .../dante/ui/fragment/SuggestionsFragment.kt | 4 ++++ .../ui/viewmodel/SuggestionsViewModel.kt | 11 +++++++++ app/src/main/res/drawable/ic_like_filled.xml | 9 ++++++++ app/src/main/res/layout/item_suggestion.xml | 19 ++++++++------- 10 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 app/src/main/res/drawable/ic_like_filled.xml diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt b/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt index be9bca30..3fd9d0d8 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt @@ -4,5 +4,7 @@ data class Suggestion( val suggestionId: String, val suggestion: BookSuggestionEntity, val suggester: Suggester, - val recommendation: String + val recommendation: String, + val isLikedByMe: Boolean, + val likesCount: Int ) diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/SuggestionsRepository.kt b/app/src/main/java/at/shockbytes/dante/suggestions/SuggestionsRepository.kt index dfafc601..3369ea6e 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/SuggestionsRepository.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/SuggestionsRepository.kt @@ -2,6 +2,7 @@ package at.shockbytes.dante.suggestions import at.shockbytes.dante.core.book.BookEntity import io.reactivex.Completable +import io.reactivex.Observable import io.reactivex.Single import kotlinx.coroutines.CoroutineScope @@ -14,6 +15,8 @@ interface SuggestionsRepository { fun reportSuggestion(suggestionId: String, scope: CoroutineScope): Completable + fun likeSuggestion(suggestionId: String, isLikedByMe: Boolean, scope: CoroutineScope): Completable + fun getUserReportedSuggestions(): Single> fun suggestBook(bookEntity: BookEntity, recommendation: String): Completable diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsApi.kt b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsApi.kt index f3894c7e..61de16ac 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsApi.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsApi.kt @@ -23,6 +23,12 @@ interface FirebaseSuggestionsApi { @Path("suggestionId") suggestionId: String ): Completable + @POST("suggestions/{suggestionId}/like") + fun likeSuggestion( + @Header("Authorization") bearerToken: String, + @Path("suggestionId") suggestionId: String + ): Completable + @POST("suggestions") fun suggestBook( @Header("Authorization") bearerToken: String, diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt index 4e5c534c..9d0d8dac 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt @@ -11,6 +11,7 @@ import at.shockbytes.dante.util.scheduler.SchedulerFacade import at.shockbytes.tracking.Tracker import at.shockbytes.tracking.event.DanteTrackingEvent import io.reactivex.Completable +import io.reactivex.Observable import io.reactivex.Single import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -95,6 +96,19 @@ class FirebaseSuggestionsRepository( .subscribeOn(schedulers.io) } + override fun likeSuggestion( + suggestionId: String, + isLikedByMe: Boolean, + scope: CoroutineScope + ): Completable { + return loginRepository.getAuthorizationHeader() + .flatMapCompletable { bearerToken -> + firebaseSuggestionsApi.likeSuggestion(bearerToken, suggestionId) + } + .andThen(Completable.fromSingle(loadRemoteSuggestions(scope))) + .subscribeOn(schedulers.io) + } + override fun getUserReportedSuggestions(): Single> { return suggestionsCache.loadReportedSuggestions() } diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt index 736d6125..a60369b5 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt @@ -7,4 +7,6 @@ interface OnSuggestionActionClickedListener { fun onAddSuggestionToWishlist(suggestion: Suggestion) fun onReportBookSuggestion(suggestionId: String, suggestionTitle: String) + + fun onLikeBookSuggestion(suggestionId: String, isLikedByMe: Boolean) } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt index 3dfc490e..63ac9349 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt @@ -3,6 +3,7 @@ package at.shockbytes.dante.ui.adapter.suggestions import android.content.Context import android.view.ViewGroup import android.widget.ImageView +import androidx.core.content.ContextCompat import at.shockbytes.dante.R import at.shockbytes.dante.core.image.ImageLoader import at.shockbytes.dante.databinding.ItemSuggestionBinding @@ -28,6 +29,7 @@ class SuggestionViewHolder( setupSuggester(suggester) setupRecommendation(recommendation) setupBookActionListener(this) + setupLikeButton(suggestionId, isLikedByMe, likesCount) } } @@ -66,6 +68,27 @@ class SuggestionViewHolder( } } + private fun setupLikeButton( + suggestionId: String, + isLikedByMe: Boolean, + likes: Int + ) { + vb.btnLikeItemSuggestion.apply { + text = likes.toString() + + icon = ContextCompat.getDrawable( + context, + if (isLikedByMe) R.drawable.ic_like_filled else R.drawable.ic_like_outlined + ) + + isEnabled = !isLikedByMe + + setOnClickListener { + onSuggestionActionClickedListener.onLikeBookSuggestion(suggestionId, isLikedByMe) + } + } + } + private fun setThumbnailToView( url: String?, view: ImageView, diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index af7b1504..bc0191f5 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -51,6 +51,10 @@ class SuggestionsFragment : BaseFragment() { override fun onReportBookSuggestion(suggestionId: String, suggestionTitle: String) { showReportBookConfirmation(suggestionId, suggestionTitle) } + + override fun onLikeBookSuggestion(suggestionId: String, isLikedByMe: Boolean) { + viewModel.likeSuggestion(suggestionId, isLikedByMe) + } }, onSuggestionExplanationClickedListener = { viewModel.dismissExplanation() } ) diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt index bcb21d70..e16ae8d7 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -188,4 +188,15 @@ class SuggestionsViewModel @Inject constructor( }) .addTo(compositeDisposable) } + + fun likeSuggestion(suggestionId: String, isLikedByMe: Boolean) { + suggestionsRepository.likeSuggestion(suggestionId, isLikedByMe, scope = viewModelScope) + .doOnError(ExceptionHandlers::defaultExceptionHandler) + .subscribe({ + Timber.e("Successfully liked suggestion") + }, { throwable -> + Timber.e("Could not like suggestion: ${throwable.localizedMessage}") + }) + .addTo(compositeDisposable) + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_like_filled.xml b/app/src/main/res/drawable/ic_like_filled.xml new file mode 100644 index 00000000..d76a72c5 --- /dev/null +++ b/app/src/main/res/drawable/ic_like_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/item_suggestion.xml b/app/src/main/res/layout/item_suggestion.xml index faadb006..a46371a0 100644 --- a/app/src/main/res/layout/item_suggestion.xml +++ b/app/src/main/res/layout/item_suggestion.xml @@ -114,7 +114,7 @@ android:layout_width="40dp" android:layout_height="40dp" android:layout_marginTop="24dp" - app:layout_constraintEnd_toEndOf="@id/tv_item_suggestion_recommendation" + app:layout_constraintEnd_toEndOf="@+id/ic_quote2" app:layout_constraintTop_toBottomOf="@id/tv_item_suggestion_recommendation" tools:srcCompat="@drawable/ic_language_norwegian" /> @@ -123,18 +123,15 @@ style="@style/AppTheme.RoundedButton.Outlined" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:layout_marginEnd="32dp" + android:layout_marginStart="32dp" android:layout_marginBottom="16dp" android:gravity="center" android:paddingHorizontal="32dp" - android:text="@string/wishlist" - app:icon="@drawable/ic_add_round" - app:iconPadding="8dp" + app:icon="@drawable/ic_wishlist" app:iconTint="@color/colorAccent" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@+id/iv_item_suggestion_suggester" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/btn_like_item_suggestion" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/iv_item_suggestion_suggester" /> Date: Wed, 5 May 2021 18:50:49 +0200 Subject: [PATCH 3/5] Build flags isLikedByMe and isReportedByMe and let user know if everything worked out fine --- app/build.gradle | 3 +- .../dante/suggestions/Suggestion.kt | 3 +- .../suggestions/SuggestionLikeRequest.kt | 5 ++ .../cache/DataStoreSuggestionsCache.kt | 27 ++++++ .../suggestions/cache/SuggestionsCache.kt | 10 +++ .../firebase/FirebaseSuggestions.kt | 10 --- .../firebase/FirebaseSuggestionsApi.kt | 8 +- .../firebase/FirebaseSuggestionsRepository.kt | 67 +++++++++++++-- .../suggestions/firebase/RawSuggestions.kt | 17 ++++ .../OnSuggestionActionClickedListener.kt | 2 +- .../suggestions/SuggestionViewHolder.kt | 9 +- .../dante/ui/fragment/SuggestionsFragment.kt | 14 ++- .../ui/viewmodel/SuggestionsViewModel.kt | 85 ++++++++++++------- .../main/res/layout/fragment_onboarding.xml | 13 --- .../res/layout/item_stats_empty_indicator.xml | 2 +- core/src/main/res/values-de/strings.xml | 3 + core/src/main/res/values/strings.xml | 3 + 17 files changed, 209 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/at/shockbytes/dante/suggestions/SuggestionLikeRequest.kt delete mode 100644 app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestions.kt create mode 100644 app/src/main/java/at/shockbytes/dante/suggestions/firebase/RawSuggestions.kt diff --git a/app/build.gradle b/app/build.gradle index 44e555c7..4b296613 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'com.google.firebase.crashlytics' android { compileSdkVersion 30 - buildToolsVersion '30.0.0' + buildToolsVersion '30.0.2' defaultConfig { applicationId "at.shockbytes.dante" @@ -120,7 +120,6 @@ dependencies { // debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" - implementation 'com.tbuonomo:creative-viewpager:1.0.1' implementation 'pub.devrel:easypermissions:3.0.0' implementation 'ru.bullyboo.view:circleseekbar:1.0.3' implementation 'com.f2prateek.rx.preferences2:rx-preferences:2.0.0' diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt b/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt index 3fd9d0d8..5c72c9f9 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/Suggestion.kt @@ -5,6 +5,7 @@ data class Suggestion( val suggestion: BookSuggestionEntity, val suggester: Suggester, val recommendation: String, + val likes: Int, val isLikedByMe: Boolean, - val likesCount: Int + val isReportedByMe: Boolean ) diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/SuggestionLikeRequest.kt b/app/src/main/java/at/shockbytes/dante/suggestions/SuggestionLikeRequest.kt new file mode 100644 index 00000000..f9d0e6fc --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/suggestions/SuggestionLikeRequest.kt @@ -0,0 +1,5 @@ +package at.shockbytes.dante.suggestions + +data class SuggestionLikeRequest( + val isLikedByMe: Boolean +) diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/cache/DataStoreSuggestionsCache.kt b/app/src/main/java/at/shockbytes/dante/suggestions/cache/DataStoreSuggestionsCache.kt index c1d84864..9a46eb6d 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/cache/DataStoreSuggestionsCache.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/cache/DataStoreSuggestionsCache.kt @@ -27,6 +27,7 @@ class DataStoreSuggestionsCache( private val suggestionsKey = preferencesKey(KEY_SUGGESTION_CACHE) private val cacheKey = preferencesKey(KEY_CACHE) private val reportedSuggestionsKey = preferencesSetKey(KEY_REPORTED_SUGGESTION_CACHE) + private val likesSuggestionsKey = preferencesSetKey(KEY_LIKED_SUGGESTION_CACHE) override fun lastCacheTimestamp(): Single { return singleOf { @@ -49,6 +50,7 @@ class DataStoreSuggestionsCache( dataStore.data.first()[suggestionsKey] }?.let { data -> gson.fromJson(data) + // TODO Lookup if liked or reported --> For building the UI } ?: Suggestions(listOf()) } } @@ -69,9 +71,34 @@ class DataStoreSuggestionsCache( } } + override suspend fun cacheSuggestionLike(suggestionId: String) { + dataStore.edit { preferences -> + val likes = preferences[likesSuggestionsKey].orEmpty().toMutableSet() + likes.add(suggestionId) + preferences[likesSuggestionsKey] = likes + } + } + + override suspend fun removeSuggestionLike(suggestionId: String) { + dataStore.edit { preferences -> + val likes = preferences[likesSuggestionsKey].orEmpty().toMutableSet() + likes.remove(suggestionId) + preferences[likesSuggestionsKey] = likes + } + } + + override fun loadLikedSuggestions(): Single> { + return singleOf { + runBlocking { + dataStore.data.first()[likesSuggestionsKey].orEmpty().toList() + } + } + } + companion object { private const val KEY_SUGGESTION_CACHE = "key_suggestion_cache" private const val KEY_CACHE = "key_cache" private const val KEY_REPORTED_SUGGESTION_CACHE = "key_reported_suggestion_cache" + private const val KEY_LIKED_SUGGESTION_CACHE = "key_liked_suggestion_cache" } } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/cache/SuggestionsCache.kt b/app/src/main/java/at/shockbytes/dante/suggestions/cache/SuggestionsCache.kt index d1aa6580..c5e2ad4f 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/cache/SuggestionsCache.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/cache/SuggestionsCache.kt @@ -11,7 +11,17 @@ interface SuggestionsCache { fun loadSuggestions(): Single + // ------------ Reports ------------ + suspend fun cacheSuggestionReport(suggestionId: String) fun loadReportedSuggestions(): Single> + + // ------------- Likes ------------- + + suspend fun cacheSuggestionLike(suggestionId: String) + + suspend fun removeSuggestionLike(suggestionId: String) + + fun loadLikedSuggestions(): Single> } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestions.kt b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestions.kt deleted file mode 100644 index e2cce03e..00000000 --- a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestions.kt +++ /dev/null @@ -1,10 +0,0 @@ -package at.shockbytes.dante.suggestions.firebase - -data class FirebaseSuggestions( - val suggestions: List -) { - - data class FirebaseSuggestion( - val title: String - ) -} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsApi.kt b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsApi.kt index 61de16ac..a31faabf 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsApi.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsApi.kt @@ -1,5 +1,6 @@ package at.shockbytes.dante.suggestions.firebase +import at.shockbytes.dante.suggestions.SuggestionLikeRequest import at.shockbytes.dante.suggestions.SuggestionRequest import at.shockbytes.dante.suggestions.Suggestions import io.reactivex.Completable @@ -13,9 +14,9 @@ import retrofit2.http.Path interface FirebaseSuggestionsApi { @GET("suggestions") - fun getSuggestions( + fun getRawSuggestions( @Header("Authorization") bearerToken: String - ): Single + ): Single @POST("suggestions/{suggestionId}/report") fun reportSuggestion( @@ -26,7 +27,8 @@ interface FirebaseSuggestionsApi { @POST("suggestions/{suggestionId}/like") fun likeSuggestion( @Header("Authorization") bearerToken: String, - @Path("suggestionId") suggestionId: String + @Path("suggestionId") suggestionId: String, + @Body suggestionLikeRequest: SuggestionLikeRequest ): Completable @POST("suggestions") diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt index 9d0d8dac..b0121941 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt @@ -3,6 +3,8 @@ package at.shockbytes.dante.suggestions.firebase import at.shockbytes.dante.core.book.BookEntity import at.shockbytes.dante.core.login.LoginRepository import at.shockbytes.dante.suggestions.BookSuggestionEntity +import at.shockbytes.dante.suggestions.Suggestion +import at.shockbytes.dante.suggestions.SuggestionLikeRequest import at.shockbytes.dante.suggestions.SuggestionRequest import at.shockbytes.dante.suggestions.Suggestions import at.shockbytes.dante.suggestions.SuggestionsRepository @@ -11,7 +13,6 @@ import at.shockbytes.dante.util.scheduler.SchedulerFacade import at.shockbytes.tracking.Tracker import at.shockbytes.tracking.event.DanteTrackingEvent import io.reactivex.Completable -import io.reactivex.Observable import io.reactivex.Single import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -27,7 +28,10 @@ class FirebaseSuggestionsRepository( private val tracker: Tracker ) : SuggestionsRepository { - override fun loadSuggestions(accessTimestamp: Long, scope: CoroutineScope): Single { + override fun loadSuggestions( + accessTimestamp: Long, + scope: CoroutineScope + ): Single { return shouldUseRemoteData(accessTimestamp) .flatMap { useRemoteSuggestions -> if (useRemoteSuggestions) { @@ -65,7 +69,8 @@ class FirebaseSuggestionsRepository( private fun loadRemoteSuggestions(scope: CoroutineScope): Single { return loginRepository.getAuthorizationHeader() - .flatMap(firebaseSuggestionsApi::getSuggestions) + .flatMap(firebaseSuggestionsApi::getRawSuggestions) + .flatMap(::mapToSuggestions) .doOnSuccess { suggestions -> if (suggestions.isNotEmpty()) { cacheRemoteSuggestions(suggestions, scope) @@ -74,6 +79,38 @@ class FirebaseSuggestionsRepository( .subscribeOn(schedulers.io) } + private fun mapToSuggestions(rawSuggestions: RawSuggestions): Single { + + return Single + .zip( + suggestionsCache.loadReportedSuggestions(), + suggestionsCache.loadLikedSuggestions(), + { reportedSuggestionIds, likedSuggestionIds -> + + val suggestions = rawSuggestions.suggestions + .map { rawSuggestion -> + + val isLikedByMe = + likedSuggestionIds.contains(rawSuggestion.suggestionId) + val isReportedByMe = + reportedSuggestionIds.contains(rawSuggestion.suggestionId) + + Suggestion( + suggestionId = rawSuggestion.suggestionId, + suggestion = rawSuggestion.suggestion, + suggester = rawSuggestion.suggester, + recommendation = rawSuggestion.recommendation, + likes = rawSuggestion.likes, + isLikedByMe = isLikedByMe, + isReportedByMe = isReportedByMe + ) + } + + Suggestions(suggestions) + } + ) + } + private fun cacheRemoteSuggestions(suggestions: Suggestions, scope: CoroutineScope) { scope.launch { suggestionsCache.cache(suggestions) @@ -103,9 +140,15 @@ class FirebaseSuggestionsRepository( ): Completable { return loginRepository.getAuthorizationHeader() .flatMapCompletable { bearerToken -> - firebaseSuggestionsApi.likeSuggestion(bearerToken, suggestionId) + firebaseSuggestionsApi.likeSuggestion( + bearerToken, + suggestionId, + SuggestionLikeRequest(isLikedByMe) + ) + } + .doOnComplete { + cacheLikedSuggestion(suggestionId, isLikedByMe, scope) } - .andThen(Completable.fromSingle(loadRemoteSuggestions(scope))) .subscribeOn(schedulers.io) } @@ -119,6 +162,20 @@ class FirebaseSuggestionsRepository( } } + private fun cacheLikedSuggestion( + suggestionId: String, + isLikedByMe: Boolean, + scope: CoroutineScope + ) { + scope.launch { + if (isLikedByMe) { + suggestionsCache.cacheSuggestionLike(suggestionId) + } else { + suggestionsCache.removeSuggestionLike(suggestionId) + } + } + } + override fun suggestBook(bookEntity: BookEntity, recommendation: String): Completable { return loginRepository.getAuthorizationHeader() .flatMapCompletable { bearerToken -> diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/RawSuggestions.kt b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/RawSuggestions.kt new file mode 100644 index 00000000..10712a47 --- /dev/null +++ b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/RawSuggestions.kt @@ -0,0 +1,17 @@ +package at.shockbytes.dante.suggestions.firebase + +import at.shockbytes.dante.suggestions.BookSuggestionEntity +import at.shockbytes.dante.suggestions.Suggester + +data class RawSuggestions( + val suggestions: List +) { + + data class RawSuggestion( + val suggestionId: String, + val suggestion: BookSuggestionEntity, + val suggester: Suggester, + val recommendation: String, + val likes: Int + ) +} \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt index a60369b5..d42ccbc1 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/OnSuggestionActionClickedListener.kt @@ -8,5 +8,5 @@ interface OnSuggestionActionClickedListener { fun onReportBookSuggestion(suggestionId: String, suggestionTitle: String) - fun onLikeBookSuggestion(suggestionId: String, isLikedByMe: Boolean) + fun onLikeBookSuggestion(suggestionId: String, suggestionTitle: String, isLikedByMe: Boolean) } \ No newline at end of file diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt index 63ac9349..8623e417 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt @@ -29,7 +29,7 @@ class SuggestionViewHolder( setupSuggester(suggester) setupRecommendation(recommendation) setupBookActionListener(this) - setupLikeButton(suggestionId, isLikedByMe, likesCount) + setupLikeButton(suggestionId, suggestion.title, isLikedByMe, likes) } } @@ -70,6 +70,7 @@ class SuggestionViewHolder( private fun setupLikeButton( suggestionId: String, + suggestionTitle: String, isLikedByMe: Boolean, likes: Int ) { @@ -84,7 +85,11 @@ class SuggestionViewHolder( isEnabled = !isLikedByMe setOnClickListener { - onSuggestionActionClickedListener.onLikeBookSuggestion(suggestionId, isLikedByMe) + onSuggestionActionClickedListener.onLikeBookSuggestion( + suggestionId, + suggestionTitle, + isLikedByMe + ) } } } diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index bc0191f5..be529aaa 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -52,8 +52,12 @@ class SuggestionsFragment : BaseFragment() { showReportBookConfirmation(suggestionId, suggestionTitle) } - override fun onLikeBookSuggestion(suggestionId: String, isLikedByMe: Boolean) { - viewModel.likeSuggestion(suggestionId, isLikedByMe) + override fun onLikeBookSuggestion( + suggestionId: String, + suggestionTitle: String, + isLikedByMe: Boolean + ) { + viewModel.likeSuggestion(suggestionId, suggestionTitle, isLikedByMe) } }, onSuggestionExplanationClickedListener = { viewModel.dismissExplanation() } @@ -126,6 +130,12 @@ class SuggestionsFragment : BaseFragment() { is SuggestionsViewModel.SuggestionEvent.ReportSuggestionEvent.Error -> { showSnackbar(getString(R.string.book_reported_error, event.title)) } + is SuggestionsViewModel.SuggestionEvent.LikeSuggestionEvent.Error -> { + showSnackbar() + } + is SuggestionsViewModel.SuggestionEvent.LikeSuggestionEvent.Success -> { + showSnackbar(getString(event.messageStringRes, event.title)) + } } } diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt index e16ae8d7..abaeca57 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -1,8 +1,10 @@ package at.shockbytes.dante.ui.viewmodel +import androidx.annotation.StringRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import at.shockbytes.dante.R import at.shockbytes.dante.core.book.BookEntity import at.shockbytes.dante.core.book.BookState import at.shockbytes.dante.core.data.BookRepository @@ -53,6 +55,16 @@ class SuggestionsViewModel @Inject constructor( data class Error(val title: String) : ReportSuggestionEvent() } + + sealed class LikeSuggestionEvent : SuggestionEvent() { + + data class Success( + @StringRes val messageStringRes: Int, + val title: String + ) : LikeSuggestionEvent() + + data class Error(val title: String) : LikeSuggestionEvent() + } } private val onSuggestionEvent = PublishSubject.create() @@ -72,7 +84,7 @@ class SuggestionsViewModel @Inject constructor( .doOnSubscribe { suggestionState.postValue(SuggestionsState.Loading) } - .flatMap { (books, reports) -> + .map { (books, reports) -> buildSuggestionsState(books, reports) } .doOnError { throwable -> @@ -94,33 +106,27 @@ class SuggestionsViewModel @Inject constructor( private fun buildSuggestionsState( books: List, suggestions: Suggestions - ): Single { - - return suggestionsRepository.getUserReportedSuggestions() - .map { reports -> - - val suggestedItems = suggestions.suggestions - .sortedBy { it.suggestionId } - .filter { suggestion -> - // Check if book isn't already added in the library - // and if hasn't been reported by this user - val bookAlreadyAdded = books.find { - it.title == suggestion.suggestion.title - } != null - val isReported = reports.contains(suggestion.suggestionId) - !bookAlreadyAdded && !isReported - } - .map(SuggestionsAdapterItem::SuggestedBook) - - when { - suggestedItems.isEmpty() -> SuggestionsState.Empty - explanations.suggestion().show -> { - val items = listOf(SuggestionsAdapterItem.SuggestionHint()) + suggestedItems - SuggestionsState.Present(items) - } - else -> SuggestionsState.Present(suggestedItems) - } + ): SuggestionsState { + val suggestedItems = suggestions.suggestions + .sortedBy { it.suggestionId } + .filter { suggestion -> + // Check if book isn't already added in the library + // and if hasn't been reported by this user + val bookAlreadyAdded = books.find { + it.title == suggestion.suggestion.title + } != null + !bookAlreadyAdded && !suggestion.isReportedByMe } + .map(SuggestionsAdapterItem::SuggestedBook) + + return when { + suggestedItems.isEmpty() -> SuggestionsState.Empty + explanations.suggestion().show -> { + val items = listOf(SuggestionsAdapterItem.SuggestionHint()) + suggestedItems + SuggestionsState.Present(items) + } + else -> SuggestionsState.Present(suggestedItems) + } } fun addSuggestionToWishlist(suggestion: Suggestion) { @@ -163,7 +169,13 @@ class SuggestionsViewModel @Inject constructor( bookTitle: String, suggester: String ) { - tracker.track(DanteTrackingEvent.AddSuggestionToWishlist(suggestionId, bookTitle, suggester)) + tracker.track( + DanteTrackingEvent.AddSuggestionToWishlist( + suggestionId, + bookTitle, + suggester + ) + ) } fun dismissExplanation() { @@ -189,13 +201,22 @@ class SuggestionsViewModel @Inject constructor( .addTo(compositeDisposable) } - fun likeSuggestion(suggestionId: String, isLikedByMe: Boolean) { + fun likeSuggestion(suggestionId: String, suggestionTitle: String, isLikedByMe: Boolean) { suggestionsRepository.likeSuggestion(suggestionId, isLikedByMe, scope = viewModelScope) .doOnError(ExceptionHandlers::defaultExceptionHandler) .subscribe({ - Timber.e("Successfully liked suggestion") - }, { throwable -> - Timber.e("Could not like suggestion: ${throwable.localizedMessage}") + + val msgRes = if (isLikedByMe) { + R.string.suggestion_like_template + } else R.string.suggestion_dislike_template + + onSuggestionEvent.onNext( + SuggestionEvent.LikeSuggestionEvent.Success(msgRes, suggestionTitle) + ) + }, { + onSuggestionEvent.onNext( + SuggestionEvent.LikeSuggestionEvent.Error(suggestionTitle) + ) }) .addTo(compositeDisposable) } diff --git a/app/src/main/res/layout/fragment_onboarding.xml b/app/src/main/res/layout/fragment_onboarding.xml index 8422a2a8..f075d4a1 100644 --- a/app/src/main/res/layout/fragment_onboarding.xml +++ b/app/src/main/res/layout/fragment_onboarding.xml @@ -5,17 +5,4 @@ android:background="@color/white" android:layout_height="match_parent"> - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_stats_empty_indicator.xml b/app/src/main/res/layout/item_stats_empty_indicator.xml index 57be57fe..aa26cdda 100644 --- a/app/src/main/res/layout/item_stats_empty_indicator.xml +++ b/app/src/main/res/layout/item_stats_empty_indicator.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:padding="@dimen/dimens_16dp"> + android:padding="16dp"> Einen Moment. \n Wir loggen dich ein. Buch hinzufügen Farbe ändern + Dir gefällt %s. + Dir gefällt %s nicht mehr. + %s konnte nicht mit gefällt mir markiert werden. Bitte versuche es später nochmals. diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 38c4d71f..c2ace824 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -414,6 +414,8 @@ I\'d like to suggest books Suggestions are available soon Receive some suggestions from book lovers all over the world.\n\nYou will receive weekly updates. Make sure you suggest your favourite books to others too. + You liked %s. + You do not like %s any longer. Open camera right now Enter your recommendation @@ -431,6 +433,7 @@ Do you really want to report the suggestion %s? You reported %s. Thanks for making Dante a better place. There was a problem while reporting %s. Please try again later. + There was a problem while liking %s. Please try again later. We have to apologize. We were unable to load you some suggestions. Make sure you suggest books to others too. One of those books? From a3bb3172a9b11921adc31f907301c447340e10de Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Wed, 5 May 2021 19:24:42 +0200 Subject: [PATCH 4/5] Fix issues with liking books --- README.md | 2 +- .../cache/DataStoreSuggestionsCache.kt | 44 ++++++++++++++----- .../firebase/FirebaseSuggestionsRepository.kt | 4 +- .../suggestions/SuggestionViewHolder.kt | 2 - .../dante/ui/fragment/SuggestionsFragment.kt | 2 +- .../ui/viewmodel/SuggestionsViewModel.kt | 20 ++++++--- core/src/main/res/values/strings.xml | 2 +- 7 files changed, 54 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 11f25289..f2caeb74 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ the user during the login process. - [ ] How to handle local data when switching accounts? https://github.com/realm/realm-java/issues/2153#issuecomment-174613885 ### Version 4.1 - HIGH MAINTENANCE -- [ ] Like book suggestions +- [x] Like book suggestions - [x] Upgrade to Kotlin > 1.4.20 - [x] ViewBinding - [x] Remove Kotterknife usage diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/cache/DataStoreSuggestionsCache.kt b/app/src/main/java/at/shockbytes/dante/suggestions/cache/DataStoreSuggestionsCache.kt index 9a46eb6d..8a787202 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/cache/DataStoreSuggestionsCache.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/cache/DataStoreSuggestionsCache.kt @@ -45,14 +45,37 @@ class DataStoreSuggestionsCache( } override fun loadSuggestions(): Single { - return singleOf { - runBlocking { - dataStore.data.first()[suggestionsKey] - }?.let { data -> - gson.fromJson(data) - // TODO Lookup if liked or reported --> For building the UI - } ?: Suggestions(listOf()) - } + return Single + .zip( + loadReportedSuggestions(), + loadLikedSuggestions(), + { reportedSuggestionIds, likedSuggestionIds -> + + loadSuggestionsFromCache().suggestions + .map { suggestion -> + + val isLikedByMe = + likedSuggestionIds.contains(suggestion.suggestionId) + val isReportedByMe = + reportedSuggestionIds.contains(suggestion.suggestionId) + + suggestion.copy( + isLikedByMe = isLikedByMe, + isReportedByMe = isReportedByMe, + likes = suggestion.likes.inc() + ) + } + .let(::Suggestions) + } + ) + } + + private fun loadSuggestionsFromCache(): Suggestions { + return runBlocking { + dataStore.data.first()[suggestionsKey] + }?.let { data -> + gson.fromJson(data) + } ?: Suggestions(listOf()) } override suspend fun cacheSuggestionReport(suggestionId: String) { @@ -82,8 +105,9 @@ class DataStoreSuggestionsCache( override suspend fun removeSuggestionLike(suggestionId: String) { dataStore.edit { preferences -> val likes = preferences[likesSuggestionsKey].orEmpty().toMutableSet() - likes.remove(suggestionId) - preferences[likesSuggestionsKey] = likes + if (likes.remove(suggestionId)) { + preferences[likesSuggestionsKey] = likes + } } } diff --git a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt index b0121941..3e93d8bc 100644 --- a/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt +++ b/app/src/main/java/at/shockbytes/dante/suggestions/firebase/FirebaseSuggestionsRepository.kt @@ -169,9 +169,9 @@ class FirebaseSuggestionsRepository( ) { scope.launch { if (isLikedByMe) { - suggestionsCache.cacheSuggestionLike(suggestionId) - } else { suggestionsCache.removeSuggestionLike(suggestionId) + } else { + suggestionsCache.cacheSuggestionLike(suggestionId) } } } diff --git a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt index 8623e417..cdd19d92 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/adapter/suggestions/SuggestionViewHolder.kt @@ -82,8 +82,6 @@ class SuggestionViewHolder( if (isLikedByMe) R.drawable.ic_like_filled else R.drawable.ic_like_outlined ) - isEnabled = !isLikedByMe - setOnClickListener { onSuggestionActionClickedListener.onLikeBookSuggestion( suggestionId, diff --git a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt index be529aaa..f22241d7 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/fragment/SuggestionsFragment.kt @@ -131,7 +131,7 @@ class SuggestionsFragment : BaseFragment() { showSnackbar(getString(R.string.book_reported_error, event.title)) } is SuggestionsViewModel.SuggestionEvent.LikeSuggestionEvent.Error -> { - showSnackbar() + showSnackbar(getString(R.string.book_liked_error, event.title)) } is SuggestionsViewModel.SuggestionEvent.LikeSuggestionEvent.Success -> { showSnackbar(getString(event.messageStringRes, event.title)) diff --git a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt index abaeca57..024a9a5c 100644 --- a/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt +++ b/app/src/main/java/at/shockbytes/dante/ui/viewmodel/SuggestionsViewModel.kt @@ -16,6 +16,7 @@ import at.shockbytes.dante.ui.adapter.suggestions.SuggestionsAdapterItem import at.shockbytes.dante.util.ExceptionHandlers import at.shockbytes.dante.util.explanations.Explanations import at.shockbytes.dante.util.addTo +import at.shockbytes.dante.util.scheduler.SchedulerFacade import at.shockbytes.tracking.Tracker import at.shockbytes.tracking.event.DanteTrackingEvent import io.reactivex.Observable @@ -29,7 +30,8 @@ class SuggestionsViewModel @Inject constructor( private val suggestionsRepository: SuggestionsRepository, private val bookRepository: BookRepository, private val tracker: Tracker, - private val explanations: Explanations + private val explanations: Explanations, + private val schedulers: SchedulerFacade ) : BaseViewModel() { sealed class SuggestionsState { @@ -187,12 +189,15 @@ class SuggestionsViewModel @Inject constructor( fun reportBookSuggestion(suggestionId: String, suggestionTitle: String) { suggestionsRepository.reportSuggestion(suggestionId, scope = viewModelScope) .doOnError(ExceptionHandlers::defaultExceptionHandler) + .observeOn(schedulers.ui) + .doOnComplete { + // Reload after a book has been liked + requestSuggestions() + } .subscribe({ onSuggestionEvent.onNext( SuggestionEvent.ReportSuggestionEvent.Success(suggestionTitle) ) - // Reload after a book has been reported - requestSuggestions() }, { onSuggestionEvent.onNext( SuggestionEvent.ReportSuggestionEvent.Error(suggestionTitle) @@ -204,11 +209,16 @@ class SuggestionsViewModel @Inject constructor( fun likeSuggestion(suggestionId: String, suggestionTitle: String, isLikedByMe: Boolean) { suggestionsRepository.likeSuggestion(suggestionId, isLikedByMe, scope = viewModelScope) .doOnError(ExceptionHandlers::defaultExceptionHandler) + .observeOn(schedulers.ui) + .doOnComplete { + // Reload after a book has been liked + requestSuggestions() + } .subscribe({ val msgRes = if (isLikedByMe) { - R.string.suggestion_like_template - } else R.string.suggestion_dislike_template + R.string.suggestion_dislike_template + } else R.string.suggestion_like_template onSuggestionEvent.onNext( SuggestionEvent.LikeSuggestionEvent.Success(msgRes, suggestionTitle) diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index c2ace824..1fdf0bc0 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ Dante - 4.1-dev + 4.1-dev2 https://github.com/shockbytes/Dante From 58aea561cdc29b14b64b995658f3876379c3e476 Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Thu, 10 Jun 2021 20:03:09 +0200 Subject: [PATCH 5/5] Update readme --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f2caeb74..a6a27378 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,19 @@ the user during the login process. - [ ] How to handle local data when switching accounts? https://github.com/realm/realm-java/issues/2153#issuecomment-174613885 ### Version 4.1 - HIGH MAINTENANCE -- [x] Like book suggestions +- [ ] Like book suggestions + - [ ] Bugfixes + - [ ] Split wishlist and suggestions - [x] Upgrade to Kotlin > 1.4.20 - [x] ViewBinding - [x] Remove Kotterknife usage - [ ] Backup file improvements + - [ ] Check Backup pages & labels restore - [ ] Show path to local backup files - [ ] Open file with FileProvider -- [x] Improve main screen +- [ ] Improve main screen + - [ ] Bigger book covers + - [ ] Show stars for read books - [x] Replace buggy SharedElementTransition for DetailPageNavigation - [x] Use lighter UI for labels (outline instead of filled) - [x] New labels screen