Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion shared/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ apollo {
packageName.set("fr.androidmakers.store.graphql")
@OptIn(ApolloExperimental::class)
generateDataBuilders.set(true)
mapScalar("GraphQLLocalDateTime", "kotlinx.datetime.LocalDateTime", "com.apollographql.adapter.datetime.KotlinxLocalDateTimeAdapter")

mapScalar("LocalDateTime", "kotlinx.datetime.LocalDateTime", "com.apollographql.adapter.datetime.KotlinxLocalDateTimeAdapter")
mapScalarToKotlinString("Markdown")

@OptIn(ApolloExperimental::class)
plugin("com.apollographql.cache:normalized-cache-apollo-compiler-plugin:${libs.versions.apollo.cache.get()}") {
Expand Down
3 changes: 3 additions & 0 deletions shared/data/src/commonMain/graphql/extra.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ extend type Room @typePolicy(keyFields: "id")

extend type Session @typePolicy(keyFields: "id")
extend type RootQuery @fieldPolicy(forField: "session", keyArgs: "id")
extend type RootQuery @fieldPolicy(forField: "feedback", keyArgs: "id")

extend type Speaker @typePolicy(keyFields: "id")

extend type Venue @typePolicy(keyFields: "name")

extend type Feedback @typePolicy(keyFields: "id")
22 changes: 22 additions & 0 deletions shared/data/src/commonMain/graphql/feedback.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
query GetFeedback($id: ID!) {
feedback(id: $id) {
id
rating
comment
}
}

mutation UpsertFeedback($feedback: FeedbackInput!) {
upsertFeedback(feedback: $feedback) {
... on FeedbackSuccess {
feedback {
id
rating
comment
}
}
... on FeedbackFailure {
message
}
}
}
73 changes: 62 additions & 11 deletions shared/data/src/commonMain/graphql/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -142,16 +142,16 @@ enum __DirectiveLocation {
INPUT_FIELD_DEFINITION
}

scalar ID

"""
A type representing a formatted kotlinx.datetime.Instant
"""
scalar GraphQLInstant
scalar Instant

scalar GraphQLLocalDate
scalar LocalDate

scalar GraphQLLocalDateTime

scalar ID
scalar LocalDateTime

"""
A Markdown string as described by https://spec.commonmark.org
Expand All @@ -162,6 +162,14 @@ enum ConferenceField {
DAYS
}

enum FeedItemType {
INFO

ALERT

ANNOUNCEMENT
}

enum LinkType {
YouTube

Expand All @@ -178,6 +186,14 @@ enum OrderByDirection {
DESCENDING
}

enum Rating {
Disappointed

Neutral

Happy
}

enum SessionField {
STARTS_AT
}
Expand All @@ -193,7 +209,7 @@ type Conference {

timezone: String!

days: [GraphQLLocalDate!]!
days: [LocalDate!]!

themeColor: String
}
Expand All @@ -207,11 +223,13 @@ type FeatureFlags {
type FeedItem {
id: ID!

createdAt: GraphQLInstant!
type: FeedItemType!

createdAt: Instant!

title: String!

markdown: Markdown!
body: Markdown!
}

type FeedItemFailure {
Expand All @@ -228,6 +246,22 @@ type FeedItemsConnection {
pageInfo: PageInfo!
}

type Feedback {
id: ID!

rating: Rating!

comment: Markdown!
}

type FeedbackFailure {
message: String!
}

type FeedbackSuccess {
feedback: Feedback!
}

type Link {
type: LinkType!

Expand Down Expand Up @@ -266,6 +300,8 @@ type Room {
}

type RootMutation {
upsertFeedback(feedback: FeedbackInput!): FeedbackResult!

updateFeedItem(id: ID!, feedItem: FeedItemInput!): FeedItemResult!

addFeedItem(feedItem: FeedItemInput!): FeedItemResult!
Expand All @@ -283,6 +319,11 @@ type RootMutation {
type RootQuery {
featureFlags: FeatureFlags!

"""
@return null if the Feedback isn't found.
"""
feedback(id: ID!): Feedback

rooms: [Room!]!

sessions(first: Int! = 10, after: String = null, orderBy: SessionOrderBy! = {
Expand Down Expand Up @@ -344,9 +385,9 @@ type Session implements Node {

tags: [String!]!

startsAt: GraphQLLocalDateTime!
startsAt: LocalDateTime!

endsAt: GraphQLLocalDateTime!
endsAt: LocalDateTime!

complexity: String

Expand Down Expand Up @@ -454,6 +495,8 @@ interface Node {

union FeedItemResult = FeedItemFailure|FeedItemSuccess

union FeedbackResult = FeedbackFailure|FeedbackSuccess

input ConferenceOrderBy {
field: ConferenceField!

Expand All @@ -463,7 +506,15 @@ input ConferenceOrderBy {
input FeedItemInput {
title: String

markdown: Markdown
body: Markdown
}

input FeedbackInput {
id: ID!

rating: Rating!

comment: Markdown!
}

input SessionOrderBy {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
package fr.androidmakers.store.firebase

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.auth.auth
import fr.androidmakers.domain.model.User
import fr.androidmakers.domain.repo.UserRepository
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

class FirebaseUserRepository : UserRepository {
class FirebaseUserRepository(
private val dataStore: DataStore<Preferences>,
) : UserRepository {
// Use Firebase.auth as single source of truth

override val currentUser: User?
Expand All @@ -27,4 +36,18 @@ class FirebaseUserRepository : UserRepository {
null
}
}

@OptIn(ExperimentalUuidApi::class)
override suspend fun getInstallationId(): String {
val prefs = dataStore.data.first()
prefs[INSTALLATION_ID_KEY]?.let { return it }

val newId = Uuid.random().toString()
dataStore.edit { it[INSTALLATION_ID_KEY] = newId }
return newId
}

companion object {
private val INSTALLATION_ID_KEY = stringPreferencesKey("installation_id")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package fr.androidmakers.store.graphql

import com.apollographql.apollo.ApolloClient
import com.apollographql.cache.normalized.FetchPolicy
import com.apollographql.cache.normalized.apolloStore
import com.apollographql.cache.normalized.fetchPolicy
import com.apollographql.cache.normalized.isFromCache
import fr.androidmakers.domain.model.Review
import fr.androidmakers.domain.model.ReviewRating
import fr.androidmakers.domain.repo.ReviewRepository
import fr.androidmakers.store.graphql.type.Rating
import fr.androidmakers.store.graphql.type.FeedbackInput
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

class ReviewGraphQLRepository(private val apolloClient: ApolloClient) : ReviewRepository {

override suspend fun getReview(id: String): Result<Review?> {
val response = apolloClient.query(GetFeedbackQuery(id)).execute()
return if (response.exception != null) {
Result.failure(response.exception!!)
} else {
Result.success(response.data?.feedback?.toDomain()).also {
if (response.isFromCache) {
// Update from the network in the background
GlobalScope.launch {
apolloClient.query(GetFeedbackQuery(id)).fetchPolicy(FetchPolicy.NetworkOnly).execute()
}
}
}
}
}

override suspend fun upsertFeedback(id: String, rating: ReviewRating, comment: String): Result<Review> {
val response = apolloClient.mutation(
UpsertFeedbackMutation(
FeedbackInput(
id = id,
rating = rating.toGraphQL(),
comment = comment,
)
)
).execute()

if (response.exception != null) {
return Result.failure(response.exception!!)
}

val data = response.data?.upsertFeedback
return when {
data?.onFeedbackSuccess != null -> {
val feedback = data.onFeedbackSuccess.feedback
/**
* We need to update the cache or else we still have `ROOT_QUERY.foo("$id")` in the cache and that takes
* precedence over the keyargs.
*
* This is so that we can return errors if there are some but here it means we'll have null every time
*
* See also https://github.com/apollographql/apollo-kotlin-normalized-cache/blob/9ad6f2ffaaefc08223d02d5bc0fa3bcb5e49a7cf/normalized-cache/src/commonMain/kotlin/com/apollographql/cache/normalized/api/CacheResolver.kt#L325
*/
apolloClient.apolloStore.writeOperation(
GetFeedbackQuery(id),
GetFeedbackQuery.Data(
feedback = GetFeedbackQuery.Feedback(
__typename = "Feedback",
id = feedback.id,
rating = feedback.rating,
comment = feedback.comment
)
)
)
Result.success(
Review(
id = feedback.id,
rating = feedback.rating.toDomain(),
comment = feedback.comment,
)
)
}

data?.onFeedbackFailure != null -> {
Result.failure(Exception(data.onFeedbackFailure.message))
}

else -> Result.failure(Exception("Unknown error"))
}
}

private fun GetFeedbackQuery.Feedback.toDomain(): Review {
return Review(
id = id,
rating = rating.toDomain(),
comment = comment,
)
}

private fun Rating.toDomain(): ReviewRating = when (this) {
Rating.Disappointed -> ReviewRating.Disappointed
Rating.Neutral -> ReviewRating.Neutral
Rating.Happy -> ReviewRating.Happy
Rating.UNKNOWN__ -> ReviewRating.Neutral
}

private fun ReviewRating.toGraphQL(): Rating = when (this) {
ReviewRating.Disappointed -> Rating.Disappointed
ReviewRating.Neutral -> Rating.Neutral
ReviewRating.Happy -> Rating.Happy
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fun SessionDetails.toSession(): Session {
endsAt = this.endsAt,
startsAt = this.startsAt,
isServiceSession = this.type == "service",
type = type
type = type,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fr.androidmakers.domain.repo.FeatureFlagsRepository
import fr.androidmakers.domain.repo.FeedRepository
import fr.androidmakers.domain.repo.MessagingRepository
import fr.androidmakers.domain.repo.PartnersRepository
import fr.androidmakers.domain.repo.ReviewRepository
import fr.androidmakers.domain.repo.RoomsRepository
import fr.androidmakers.domain.repo.SessionsRepository
import fr.androidmakers.domain.repo.SpeakersRepository
Expand All @@ -14,6 +15,7 @@ import fr.androidmakers.domain.repo.VenueRepository
import fr.androidmakers.store.firebase.FirebaseUserRepository
import fr.androidmakers.store.graphql.FeatureFlagsGraphQLRepository
import fr.androidmakers.store.graphql.PartnersGraphQLRepository
import fr.androidmakers.store.graphql.ReviewGraphQLRepository
import fr.androidmakers.store.graphql.RoomsGraphQLRepository
import fr.androidmakers.store.graphql.SessionsGraphQLRepository
import fr.androidmakers.store.graphql.SpeakersGraphQLRepository
Expand All @@ -33,9 +35,10 @@ val dataModule = module {
single<RoomsRepository> { RoomsGraphQLRepository(get()) }
single<SessionsRepository> { SessionsGraphQLRepository(get()) }
single<SpeakersRepository> { SpeakersGraphQLRepository(get()) }
single<UserRepository> { FirebaseUserRepository() }
single<UserRepository> { FirebaseUserRepository(get()) }
single<VenueRepository> { VenueGraphQLRepository(get()) }
single<FeatureFlagsRepository> { FeatureFlagsGraphQLRepository(get()) }
single<ReviewRepository> { ReviewGraphQLRepository(get()) }

single<BookmarksRepository> { BookmarksDataStoreRepository(get()) }
single<ThemeRepository> { ThemeDataStoreRepository(get()) }
Expand Down
Loading
Loading