Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thumbs-up function 👍 #626

Merged
merged 45 commits into from
Feb 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
44b8b10
Implement posting thumbs-up count to firestore
mkeeda Jan 26, 2020
b49b8b0
Fix multiple counter to single counter, and adopt realtime updates
mkeeda Jan 29, 2020
4a2de25
Apply some fixes for thumbs up
takahirom Feb 2, 2020
286d6bd
Debugging thumbs-up function of firestore
mkeeda Feb 1, 2020
bf57b49
Success to show thumbs-up counts if shards are already exists
mkeeda Feb 2, 2020
da8e92f
Remove a white space
mkeeda Feb 2, 2020
075e3b2
Change processing whether shard exists into checking a shard that sha…
mkeeda Feb 3, 2020
a4c3287
Bind error to SystemViewModel
mkeeda Feb 3, 2020
6f6b1c0
Apply some thumbs-up button designs
mkeeda Feb 3, 2020
3cf94b8
Add a thumbs-up icon
mkeeda Feb 3, 2020
0b0483b
Fix indent and variable name
mkeeda Feb 3, 2020
0cd8484
Adjust thumbs-up button styles
mkeeda Feb 4, 2020
707fb7b
Show outlined button if thumbs-up count is zero
mkeeda Feb 4, 2020
f0db2dd
Fix grradlew.bat
mkeeda Feb 5, 2020
856e3ac
Merge branch 'master' into thumb-up
mkeeda Feb 5, 2020
6ebcbfa
Change number of shards; 10 -> 5
mkeeda Feb 5, 2020
e478ca9
Remove unnecessary changes
mkeeda Feb 5, 2020
ab883b9
Fix SessionDetailViewModelTest
mkeeda Feb 8, 2020
3bac614
Add test to get thumbs-up count
mkeeda Feb 8, 2020
8e26ef0
Count up at intervals for FireStore charges
takahirom Feb 7, 2020
59aad5a
Apply ktlintFormat
mkeeda Feb 8, 2020
67e699a
Separate incremented count flow from total count flow
mkeeda Feb 9, 2020
6f07494
Display incremented thumbs-up count by own
mkeeda Feb 11, 2020
7d102fd
Pop-up and drop-out animation
mkeeda Feb 12, 2020
ea1e3d4
Use resource string placeholder
mkeeda Feb 13, 2020
8c64427
Apply ktlintFormat
mkeeda Feb 14, 2020
54940d7
Pass lintDebug task
mkeeda Feb 14, 2020
035602c
Fix thumbsUpCount test
mkeeda Feb 14, 2020
5f1c524
Bind error of incrementation thumbs-up
mkeeda Feb 14, 2020
c88056a
Merge the 2 increment liveDatas
mkeeda Feb 14, 2020
f9d5066
Appley ktlintFormat
mkeeda Feb 15, 2020
7e54b27
Fix SessionDetailViewModelTest; separate error
mkeeda Feb 15, 2020
bc6dad4
Extract animation functions from SessionDetailTileItem
mkeeda Feb 15, 2020
6d1c674
Suppress useless cast for lintDebug task
mkeeda Feb 15, 2020
5889f99
Merge branch 'master' into thumb-up
takahirom Feb 15, 2020
e1cba55
Tweak thumb up animation
takahirom Feb 15, 2020
dbe94a2
Merge branch 'thumb-up' of github.com:mkeeda/conference-app-2020 into…
mkeeda Feb 16, 2020
e556cac
Use UiModel.error
takahirom Feb 15, 2020
489fb21
Scale animation when users continually press thumbs-up
mkeeda Feb 16, 2020
69c94e9
Apply ktlintFormat
mkeeda Feb 16, 2020
521ba2d
Restore error checking
mkeeda Feb 16, 2020
eb5daa5
Minor adjustment ui design
mkeeda Feb 16, 2020
48c71ba
Remove ⭐ and rewrite debug logging messages
mkeeda Feb 16, 2020
3d4c670
LintDebug task can success without type cast
mkeeda Feb 16, 2020
808f2b2
Fix stroke color of blue thumbs-up button
mkeeda Feb 16, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.github.droidkaigi.confsched2020.ext

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume

// This function is copied from https://medium.com/androiddevelopers/suspending-over-views-19de9ebd7020

suspend fun Animator.awaitEnd() = suspendCancellableCoroutine<Unit> { cont ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// Add an invokeOnCancellation listener. If the coroutine is
// cancelled, cancel the animation too that will notify
// listener's onAnimationCancel() function
cont.invokeOnCancellation { cancel() }

addListener(object : AnimatorListenerAdapter() {
private var endedSuccessfully = true

override fun onAnimationCancel(animation: Animator) {
// Animator has been cancelled, so flip the success flag
endedSuccessfully = false
}

override fun onAnimationEnd(animation: Animator) {
// Make sure we remove the listener so we don't keep
// leak the coroutine continuation
animation.removeListener(this)

if (cont.isActive) {
// If the coroutine is still active...
if (endedSuccessfully) {
// ...and the Animator ended successfully, resume the coroutine
cont.resume(Unit)
} else {
// ...and the Animator was cancelled, cancel the coroutine too
cont.cancel()
}
}
}
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fun Throwable?.toAppError(): AppError? {
fun AppError.stringRes() = when (this) {
is AppError.ApiException.NetworkException -> R.string.error_network
is AppError.ApiException.ServerException -> R.string.error_server
is AppError.ApiException.SessionNotFoundException -> R.string.error_unknown
is AppError.ApiException.UnknownException -> R.string.error_unknown
is AppError.ExternalIntegrationError.NoCalendarIntegrationFoundException
-> R.string.error_no_calendar_integration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.hadilq.liveevent.LiveEvent
import io.github.droidkaigi.confsched2020.model.AppError
import io.github.droidkaigi.confsched2020.model.ErrorGettable

fun <T : Any> LiveData<T>.requireValue() = requireNotNull(value)

Expand Down Expand Up @@ -108,6 +110,43 @@ inline fun <T : Any, LIVE1 : Any, LIVE2 : Any, LIVE3 : Any, LIVE4 : Any> combine
}.distinctUntilChanged()
}

inline fun <T : Any, LIVE1 : Any, LIVE2 : Any, LIVE3 : Any, LIVE4 : Any, LIVE5 : Any> combine(
initialValue: T,
liveData1: LiveData<LIVE1>,
liveData2: LiveData<LIVE2>,
liveData3: LiveData<LIVE3>,
liveData4: LiveData<LIVE4>,
liveData5: LiveData<LIVE5>,
crossinline block: (T, LIVE1, LIVE2, LIVE3, LIVE4, LIVE5) -> T
): LiveData<T> {
return MediatorLiveData<T>().apply {
value = initialValue
listOf(liveData1, liveData2, liveData3, liveData4, liveData5).forEach { liveData ->
addSource(liveData) {
val currentValue = value
val liveData1Value = liveData1.value
val liveData2Value = liveData2.value
val liveData3Value = liveData3.value
val liveData4Value = liveData4.value
val liveData5Value = liveData5.value
if (currentValue != null && liveData1Value != null &&
liveData2Value != null && liveData3Value != null &&
liveData4Value != null && liveData5Value != null
) {
value = block(
currentValue,
liveData1Value,
liveData2Value,
liveData3Value,
liveData4Value,
liveData5Value
)
}
}
}
}.distinctUntilChanged()
}

fun <T> LiveData<T>.setOnEach(mutableLiveData: MutableLiveData<T>): LiveData<T> {
return map {
mutableLiveData.value = it
Expand All @@ -124,3 +163,25 @@ fun <T : Any> LiveData<T?>.toNonNullSingleEvent(): LiveData<T> {
}
return result
}

fun <T> merge(vararg liveDatas: LiveData<T>): LiveData<T> {
return MediatorLiveData<T>().apply {
liveDatas.forEach { liveData ->
addSource(liveData) { value ->
this.value = value
}
}
}
}

fun <T : ErrorGettable> LiveData<T>.toAppError(): LiveData<AppError?> {
return map {
it.getErrorIfExists().toAppError()
}
}

fun <T, R : Result<T>> LiveData<R>.fromResultToAppError(): LiveData<AppError?> {
return map {
it.exceptionOrNull()?.toAppError()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.github.droidkaigi.confsched2020.ext

import android.view.View
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume

// This function is copied from https://medium.com/androiddevelopers/suspending-over-views-19de9ebd7020

suspend fun View.awaitNextLayout() = suspendCancellableCoroutine<Unit> { cont ->
// This lambda is invoked immediately, allowing us to create
// a callback/listener

val listener = object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
// The next layout has happened!
// First remove the listener to not leak the coroutine
v?.removeOnLayoutChangeListener(this)
// Finally resume the continuation, and
// wake the coroutine up
cont.resume(Unit)
}
}
// If the coroutine is cancelled, remove the listener
cont.invokeOnCancellation { removeOnLayoutChangeListener(listener) }
// And finally add the listener to view
addOnLayoutChangeListener(listener)

// The coroutine will now be suspended. It will only be resumed
// when calling cont.resume() in the listener above
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<item name="textAppearanceBody1">@style/TextAppearance.DroidKaigi.Body1</item>
<item name="textAppearanceBody2">@style/TextAppearance.DroidKaigi.Body2</item>
<item name="textAppearanceCaption">@style/TextAppearance.DroidKaigi.Caption</item>
<item name="textAppearanceButton">@style/TextAppearance.DroidKaigi.Button</item>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


<!-- Widget styles -->
<item name="filterChipStyle">@style/Widget.DroidKaigi.FilterChip</item>
Expand Down
6 changes: 6 additions & 0 deletions corecomponent/androidcomponent/src/main/res/values/type.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,10 @@
<item name="android:textColor">?colorOnBackground</item>
</style>

<!-- Notosans Medium 14sp -->
<style name="TextAppearance.DroidKaigi.Button" parent="TextAppearance.MaterialComponents.Button">
<item name="android:fontFamily">@font/notosans_medium</item>
<item name="fontFamily">@font/notosans_medium</item>
<item name="android:textColor">?colorOnBackground</item>
</style>
</resources>
8 changes: 8 additions & 0 deletions data/firestore/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ kotlin {
implementation Dep.Test.KotlinMultiPlatform.commonModuleTestAnnotations
implementation Dep.MockK.common
}
test {
dependsOn commonMain
dependencies {
implementation Dep.Test.KotlinMultiPlatform.jvmModuleTest
implementation Dep.Test.KotlinMultiPlatform.jvmModuleTestJunit
implementation Dep.MockK.jvm
}
}
androidTest {
dependsOn commonMain
dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ import kotlinx.coroutines.flow.Flow
interface Firestore {
fun getFavoriteSessionIds(): Flow<List<String>>
suspend fun toggleFavorite(sessionId: SessionId)
fun getThumbsUpCount(sessionId: SessionId): Flow<Int>
suspend fun incrementThumbsUpCount(sessionId: SessionId, count: Int)
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
package io.github.droidkaigi.confsched2020.data.firestore.internal

import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.QuerySnapshot
import com.google.firebase.firestore.Source
import io.github.droidkaigi.confsched2020.data.firestore.Firestore
import io.github.droidkaigi.confsched2020.model.SessionId
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.tasks.await
import timber.log.Timber
import timber.log.debug
import javax.inject.Inject
import kotlin.math.floor

internal class FirestoreImpl @Inject constructor() : Firestore {

Expand All @@ -35,7 +40,7 @@ internal class FirestoreImpl @Inject constructor() : Firestore {
emit(favoritesRef)
}
val favoritesSnapshotFlow = setupFavorites.flatMapLatest {
it.whereEqualTo("favorite", true).toFlow()
it.whereEqualTo(FAVORITE_VALUE_KEY, true).toFlow()
}
return favoritesSnapshotFlow.mapLatest { favorites ->
Timber.debug { "favoritesSnapshotFlow onNext" }
Expand All @@ -57,12 +62,47 @@ internal class FirestoreImpl @Inject constructor() : Firestore {
} else {
Timber.debug { "toggleFavorite: $sessionId document not exits" }
document.reference
.set(mapOf("favorite" to newFavorite))
.set(mapOf(FAVORITE_VALUE_KEY to newFavorite))
.await()
}
Timber.debug { "toggleFavorite: end" }
}

override fun getThumbsUpCount(sessionId: SessionId): Flow<Int> {
val setupThumbsUp = flow {
signInIfNeeded()
val counterRef = getThumbsUpCounterRef(sessionId)
createShardsIfNeeded(counterRef)
emit(counterRef)
}

val thumbsUpSnapshot = setupThumbsUp.flatMapLatest {
it.toFlow()
}

return thumbsUpSnapshot.map { shards ->
var count = 0
shards.forEach { snap ->
count += snap.get(SHARDS_COUNT_KEY, Int::class.java) ?: 0
}
count
}
}

override suspend fun incrementThumbsUpCount(
sessionId: SessionId,
count: Int
) {
signInIfNeeded()
val counterRef = getThumbsUpCounterRef(sessionId)
createShardsIfNeeded(counterRef)
val shardId = floor(Math.random() * NUM_SHARDS).toInt()
counterRef
.document(shardId.toString())
.update(SHARDS_COUNT_KEY, FieldValue.increment(count.toLong()))
.await()
}

private fun getFavoritesRef(): CollectionReference {
val firebaseAuth = FirebaseAuth.getInstance()
val firebaseUserId = firebaseAuth.currentUser?.uid ?: throw RuntimeException(
Expand All @@ -83,6 +123,42 @@ internal class FirestoreImpl @Inject constructor() : Firestore {
firebaseAuth.signInAnonymously().await()
Timber.debug { "signInIfNeeded end" }
}

private fun getThumbsUpCounterRef(sessionId: SessionId): CollectionReference {
return FirebaseFirestore
.getInstance()
.collection("confsched/2020/sessions/${sessionId.id}/thumbsup_counters")
}

private suspend fun createShardsIfNeeded(counterRef: CollectionReference) {
val lastShardId = NUM_SHARDS - 1
val lastShard = counterRef
.document(lastShardId.toString())
.get(Source.SERVER)
.await()

if (lastShard.exists()) {
Timber.debug { "createShardsIfNeeded shards already exist" }
return
}

val tasks = arrayListOf<Task<Void>>()
(0 until NUM_SHARDS).forEach {
val makeShard = counterRef
.document(it.toString())
.set(mapOf(SHARDS_COUNT_KEY to 0))
tasks.add(makeShard)
}

Tasks.whenAll(tasks).await()
Timber.debug { "createShardsIfNeeded creating shards completed" }
}

companion object {
const val NUM_SHARDS = 5
const val SHARDS_COUNT_KEY = "shards"
const val FAVORITE_VALUE_KEY = "favorite"
}
}

private suspend fun DocumentReference.fastGet(): DocumentSnapshot {
Expand Down
Loading