From daefa0dca89a15ce50c70b7f77552823fafb7158 Mon Sep 17 00:00:00 2001
From: Martin Bonnin <martin@mbonnin.net>
Date: Wed, 17 Apr 2024 09:17:13 +0200
Subject: [PATCH 1/3] make the app work offline

---
 .../store/graphql/ApolloExtensions.kt         |  27 +++++
 .../store/graphql/RoomsGraphQLRepository.kt   |   8 +-
 .../graphql/SessionsGraphQLRepository.kt      | 100 +++---------------
 .../graphql/SpeakersGraphQLRepository.kt      |  27 ++---
 4 files changed, 59 insertions(+), 103 deletions(-)

diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/ApolloExtensions.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/ApolloExtensions.kt
index c8070979..1d9a3c8f 100644
--- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/ApolloExtensions.kt
+++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/ApolloExtensions.kt
@@ -1,10 +1,16 @@
 package fr.androidmakers.store.graphql
 
+import com.apollographql.apollo3.ApolloCall
 import com.apollographql.apollo3.api.ApolloResponse
 import com.apollographql.apollo3.api.Operation
+import com.apollographql.apollo3.cache.normalized.FetchPolicy
+import com.apollographql.apollo3.cache.normalized.fetchPolicy
+import com.apollographql.apollo3.exception.ApolloException
 import com.apollographql.apollo3.exception.CacheMissException
+import com.apollographql.apollo3.exception.DefaultApolloException
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.flow
 
 internal fun <T : Operation.Data> Flow<ApolloResponse<T>>.ignoreCacheMisses(): Flow<ApolloResponse<T>> {
   return filterNot {
@@ -12,3 +18,24 @@ internal fun <T : Operation.Data> Flow<ApolloResponse<T>>.ignoreCacheMisses(): F
     it.exception is CacheMissException
   }
 }
+
+
+internal fun <T : Operation.Data> ApolloCall<T>.cacheAndNetwork(): Flow<Result<T>> {
+  return flow {
+    var hasData = false
+    var exception: ApolloException? = null
+    fetchPolicy(FetchPolicy.CacheAndNetwork)
+      .toFlow()
+      .collect {
+        if (it.data != null) {
+          hasData = true
+          emit(Result.success(it.data!!))
+        } else {
+          exception = it.exception ?: DefaultApolloException("No data found")
+        }
+      }
+    if (!hasData) {
+      emit(Result.failure(exception!!))
+    }
+  }
+}
diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt
index 8d4c39ab..77fb60a8 100644
--- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt
+++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt
@@ -18,11 +18,7 @@ class RoomsGraphQLRepository(private val apolloClient: ApolloClient): RoomsRepos
 
   override fun getRooms(): Flow<Result<List<Room>>> {
     return apolloClient.query(GetRoomsQuery())
-        .fetchPolicy(FetchPolicy.NetworkFirst)
-        .watch()
-        .map {
-          it.dataAssertNoErrors.rooms.map { it.roomDetails.toRoom() }
-        }
-        .toResultFlow()
+        .cacheAndNetwork()
+        .map { it.map { it.rooms.map { it.roomDetails.toRoom() } } }
   }
 }
diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt
index e5652417..f25daaf7 100644
--- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt
+++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt
@@ -1,113 +1,45 @@
 package fr.androidmakers.store.graphql
 
 import com.apollographql.apollo3.ApolloClient
-import com.apollographql.apollo3.api.Mutation
 import com.apollographql.apollo3.cache.normalized.FetchPolicy
-import com.apollographql.apollo3.cache.normalized.apolloStore
 import com.apollographql.apollo3.cache.normalized.fetchPolicy
-import com.apollographql.apollo3.cache.normalized.optimisticUpdates
-import com.apollographql.apollo3.cache.normalized.refetchPolicy
-import com.apollographql.apollo3.cache.normalized.watch
 import fr.androidmakers.domain.model.Session
 import fr.androidmakers.domain.repo.SessionsRepository
-import fr.androidmakers.store.graphql.type.buildBookmarkConnection
-import fr.androidmakers.store.graphql.type.buildSession
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
 
 class SessionsGraphQLRepository(private val apolloClient: ApolloClient) : SessionsRepository {
-
-  suspend fun addBookmark(uid: String?, sessionId: String): Boolean {
-    return modifyBookmarks(uid, AddBookmarkMutation(sessionId)) { sessionIds ->
-      AddBookmarkMutation.Data {
-        addBookmark = buildBookmarkConnection {
-          nodes = (sessionIds + sessionId).map {
-            buildSession {
-              this.id = it
-            }
-          }
-        }
-      }
-    }
-  }
-
-  suspend fun removeBookmark(uid: String?, sessionId: String): Boolean {
-    return modifyBookmarks(uid, RemoveBookmarkMutation(sessionId)) { sessionIds ->
-      RemoveBookmarkMutation.Data {
-        removeBookmark = buildBookmarkConnection {
-          nodes = (sessionIds - sessionId).map {
-              buildSession {
-                this.id = it
-              }
-            }
-        }
-      }
-    }
-  }
-
   override suspend fun setBookmark(userId: String, sessionId: String, value: Boolean) {
-    try {
-      if (value) {
-        addBookmark(userId, sessionId)
-      } else {
-        removeBookmark(userId, sessionId)
-      }
-    } catch (e: Exception) {
-      e.printStackTrace()
+    val mutation = if (value) {
+      AddBookmarkMutation(sessionId)
+    } else {
+      RemoveBookmarkMutation(sessionId)
+    }
+    val response = apolloClient.mutation(mutation).execute()
+    if (response.exception != null) {
+      response.exception!!.printStackTrace()
     }
   }
 
   override fun getSession(id: String): Flow<Result<Session>> {
     return apolloClient.query(GetSessionQuery(id))
-      .fetchPolicy(FetchPolicy.CacheAndNetwork)
-      .watch()
-      .ignoreCacheMisses()
-      .map {
-        it.dataAssertNoErrors.session.sessionDetails.toSession()
-      }
-      .toResultFlow()
+      .cacheAndNetwork()
+      .map { it.map { it.session.sessionDetails.toSession() } }
   }
 
   override fun getBookmarks(uid: String): Flow<Result<Set<String>>> {
     return apolloClient.query(BookmarksQuery())
       .fetchPolicy(FetchPolicy.NetworkOnly)
-      .refetchPolicy(FetchPolicy.CacheOnly)
-      .watch().map {
-        it.data!!.bookmarkConnection!!.nodes.map { it.id }.toSet()
-      }.toResultFlow()
-  }
-
-  private suspend fun <D : Mutation.Data> modifyBookmarks(
-    uid: String?,
-    mutation: Mutation<D>,
-    data: (sessionIds: List<String>) -> D
-  ): Boolean {
-    val optimisticData = try {
-      val bookmarks = apolloClient.apolloStore.readOperation(BookmarksQuery()).bookmarkConnection
-      data(bookmarks!!.nodes.map { it.id })
-    } catch (e: Exception) {
-      null
-    }
-    val response = apolloClient.mutation(mutation)
-      .apply {
-        if (optimisticData != null) {
-          optimisticUpdates(optimisticData)
-        }
+      .toFlow()
+      .map {
+        it.dataAssertNoErrors.bookmarkConnection.nodes.map { it.id }.toSet()
       }
-      .execute()
-
-    return response.data != null
+      .toResultFlow()
   }
 
   override fun getSessions(): Flow<Result<List<Session>>> {
     return apolloClient.query(GetSessionsQuery())
-      .fetchPolicy(FetchPolicy.CacheAndNetwork)
-      .watch()
-      .ignoreCacheMisses()
-      .map {
-        it.dataAssertNoErrors.sessions.nodes.map { it.sessionDetails.toSession() }
-      }
-      .toResultFlow()
+      .cacheAndNetwork()
+      .map { it.map { it.sessions.nodes.map { it.sessionDetails.toSession() } } }
   }
-
 }
diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt
index 16d62eb3..9f54f316 100644
--- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt
+++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt
@@ -4,6 +4,7 @@ import com.apollographql.apollo3.ApolloClient
 import com.apollographql.apollo3.cache.normalized.FetchPolicy
 import com.apollographql.apollo3.cache.normalized.fetchPolicy
 import com.apollographql.apollo3.cache.normalized.watch
+import com.apollographql.apollo3.exception.DefaultApolloException
 import fr.androidmakers.domain.model.Speaker
 import fr.androidmakers.domain.repo.SpeakersRepository
 import kotlinx.coroutines.flow.Flow
@@ -13,24 +14,24 @@ class SpeakersGraphQLRepository(private val apolloClient: ApolloClient) : Speake
 
   override fun getSpeakers(): Flow<Result<List<Speaker>>> {
     return apolloClient.query(GetSpeakersQuery())
-      .fetchPolicy(FetchPolicy.CacheAndNetwork)
-      .watch()
-      .ignoreCacheMisses()
-      .map {
-        it.dataAssertNoErrors.speakers.map { it.speakerDetails.toSpeaker() }
-      }.toResultFlow()
+      .cacheAndNetwork()
+      .map { it.map { it.speakers.map { it.speakerDetails.toSpeaker() } } }
   }
 
   override fun getSpeaker(id: String): Flow<Result<Speaker>> {
     return apolloClient.query(GetSpeakersQuery())
-      .fetchPolicy(FetchPolicy.CacheAndNetwork)
-      .watch()
-      .ignoreCacheMisses()
+      .cacheAndNetwork()
       .map {
-        it.dataAssertNoErrors.speakers.map { it.speakerDetails }.singleOrNull { it.id == id }
-          ?.toSpeaker()
-          ?: error("no speaker")
+        if (it.isSuccess) {
+          val speaker = it.getOrThrow().speakers.map { it.speakerDetails }.singleOrNull { it.id == id }?.toSpeaker()
+          if (speaker != null) {
+            Result.success(speaker)
+          } else {
+            Result.failure(DefaultApolloException("Something wrong happend"))
+          }
+        } else {
+          Result.failure(it.exceptionOrNull()!!)
+        }
       }
-      .toResultFlow()
   }
 }

From c74415a5c322b928e6ab31130f3fea509cf798cb Mon Sep 17 00:00:00 2001
From: Martin Bonnin <martin@mbonnin.net>
Date: Wed, 17 Apr 2024 09:19:45 +0200
Subject: [PATCH 2/3] remove watch

---
 .../graphql/PartnersGraphQLRepository.kt      | 24 ++++++++-----------
 .../store/graphql/RoomsGraphQLRepository.kt   |  3 ---
 .../graphql/SpeakersGraphQLRepository.kt      |  3 ---
 .../store/graphql/VenueGraphQLRepository.kt   | 12 ++--------
 4 files changed, 12 insertions(+), 30 deletions(-)

diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/PartnersGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/PartnersGraphQLRepository.kt
index 588325db..4cb3bd28 100644
--- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/PartnersGraphQLRepository.kt
+++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/PartnersGraphQLRepository.kt
@@ -1,9 +1,6 @@
 package fr.androidmakers.store.graphql
 
 import com.apollographql.apollo3.ApolloClient
-import com.apollographql.apollo3.cache.normalized.FetchPolicy
-import com.apollographql.apollo3.cache.normalized.fetchPolicy
-import com.apollographql.apollo3.cache.normalized.watch
 import fr.androidmakers.domain.model.Partner
 import fr.androidmakers.domain.model.PartnerGroup
 import fr.androidmakers.domain.repo.PartnersRepository
@@ -13,24 +10,23 @@ import kotlinx.coroutines.flow.map
 class PartnersGraphQLRepository(private val apolloClient: ApolloClient): PartnersRepository {
   override fun getPartners(): Flow<Result<List<PartnerGroup>>> {
     return apolloClient.query(GetPartnerGroupsQuery())
-        .fetchPolicy(FetchPolicy.CacheAndNetwork)
-        .watch()
-        .ignoreCacheMisses()
+        .cacheAndNetwork()
         .map {
-          it.dataAssertNoErrors.partnerGroups.map { partnerGroup ->
-            PartnerGroup(
+          it.map {
+            it.partnerGroups.map { partnerGroup ->
+              PartnerGroup(
                 title = partnerGroup.title,
                 partners = partnerGroup.partners.map { partner ->
                   Partner(
-                      logoUrl = partner.logoUrlLight,
-                      name = partner.name,
-                      url = partner.url,
-                      logoUrlDark = partner.logoUrlDark
+                    logoUrl = partner.logoUrlLight,
+                    name = partner.name,
+                    url = partner.url,
+                    logoUrlDark = partner.logoUrlDark
                   )
                 }
-            )
+              )
+            }
           }
         }
-        .toResultFlow()
   }
 }
diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt
index 77fb60a8..334faac3 100644
--- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt
+++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt
@@ -1,9 +1,6 @@
 package fr.androidmakers.store.graphql
 
 import com.apollographql.apollo3.ApolloClient
-import com.apollographql.apollo3.cache.normalized.FetchPolicy
-import com.apollographql.apollo3.cache.normalized.fetchPolicy
-import com.apollographql.apollo3.cache.normalized.watch
 import fr.androidmakers.domain.model.Room
 import fr.androidmakers.domain.repo.RoomsRepository
 import kotlinx.coroutines.flow.Flow
diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt
index 9f54f316..6c3e12d9 100644
--- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt
+++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt
@@ -1,9 +1,6 @@
 package fr.androidmakers.store.graphql
 
 import com.apollographql.apollo3.ApolloClient
-import com.apollographql.apollo3.cache.normalized.FetchPolicy
-import com.apollographql.apollo3.cache.normalized.fetchPolicy
-import com.apollographql.apollo3.cache.normalized.watch
 import com.apollographql.apollo3.exception.DefaultApolloException
 import fr.androidmakers.domain.model.Speaker
 import fr.androidmakers.domain.repo.SpeakersRepository
diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/VenueGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/VenueGraphQLRepository.kt
index fc705207..a600c387 100644
--- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/VenueGraphQLRepository.kt
+++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/VenueGraphQLRepository.kt
@@ -1,9 +1,6 @@
 package fr.androidmakers.store.graphql
 
 import com.apollographql.apollo3.ApolloClient
-import com.apollographql.apollo3.cache.normalized.FetchPolicy
-import com.apollographql.apollo3.cache.normalized.fetchPolicy
-import com.apollographql.apollo3.cache.normalized.watch
 import fr.androidmakers.domain.model.Venue
 import fr.androidmakers.domain.repo.VenueRepository
 import kotlinx.coroutines.flow.Flow
@@ -12,12 +9,7 @@ import kotlinx.coroutines.flow.map
 class VenueGraphQLRepository(private val apolloClient: ApolloClient): VenueRepository {
   override fun getVenue(id: String): Flow<Result<Venue>> {
     return apolloClient.query(GetVenueQuery(id))
-        .fetchPolicy(FetchPolicy.CacheAndNetwork)
-        .watch()
-        .ignoreCacheMisses()
-        .map {
-          it.dataAssertNoErrors.venue.toVenue()
-        }
-        .toResultFlow()
+        .cacheAndNetwork()
+        .map { it.map { it.venue.toVenue() } }
   }
 }

From 35608655a84e1a87931bd7381bf057862b2ceda3 Mon Sep 17 00:00:00 2001
From: Martin Bonnin <martin@mbonnin.net>
Date: Wed, 17 Apr 2024 09:26:21 +0200
Subject: [PATCH 3/3] merge bookmarks on signin

---
 .../src/main/java/fr/paug/androidmakers/MainActivity.kt  | 9 ++++++++-
 .../androidmakers/ui/common/navigation/UserViewModel.kt  | 2 ++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt b/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt
index ce0ac2f4..7cfe2fa8 100644
--- a/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt
+++ b/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt
@@ -130,7 +130,14 @@ class MainActivity : AppCompatActivity() {
                 val result = auth.signInWithCredential(firebaseCredential)
                 // Sign in success, update UI with the signed-in user's information
                 lifecycleScope.launch {
-                  UserData().userRepository.setUser(result.user)
+                  UserData().apply {
+                    userRepository.setUser(result.user)
+                    val uid = result.user?.uid
+                    if (uid != null) {
+                      syncBookmarksUseCase(uid)
+                    }
+                  }
+
                   println("user id=${result.user?.uid}")
                   println("idToken=${result.user?.getIdToken(true)}")
                 }
diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt
index 90d875f3..03561fd1 100644
--- a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt
+++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt
@@ -1,11 +1,13 @@
 package com.androidmakers.ui.common.navigation
 
+import fr.androidmakers.domain.interactor.SyncBookmarksUseCase
 import fr.androidmakers.domain.repo.UserRepository
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
 
 class UserData: KoinComponent {
   val userRepository: UserRepository by inject()
+  val syncBookmarksUseCase: SyncBookmarksUseCase by inject()
 }