Skip to content

Commit

Permalink
Add Store5 and create a store for nft tracks (#302)
Browse files Browse the repository at this point in the history
* Add Store5 and create a store for nft tracks

* Add pull to refresh (#303)
  • Loading branch information
newmskywalker authored Aug 5, 2024
1 parent 3fda1e2 commit 19a696e
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import com.slack.circuit.runtime.CircuitUiEvent
import io.newm.shared.public.models.NFTTrack

sealed interface NFTLibraryEvent : CircuitUiEvent {
data object OnRefresh : NFTLibraryEvent

data class PlaySong(val track: NFTTrack) : NFTLibraryEvent

data class OnQueryChange(val newQuery: String) : NFTLibraryEvent

data class OnDownloadTrack(val tackId: String) : NFTLibraryEvent

data class OnApplyFilters(val filters: NFTLibraryFilters) : NFTLibraryEvent
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,6 @@ class NFTLibraryPresenter(

var query by rememberSaveable { mutableStateOf("") }

if (isWalletConnected == true) {
LaunchedEffect(Unit) {
walletNFTTracksUseCase.refresh()
}
}

val nftTracks by remember(isWalletConnected) {
if (isWalletConnected == true) {
walletNFTTracksUseCase.getAllCollectableTracksFlow()
Expand Down Expand Up @@ -114,6 +108,20 @@ class NFTLibraryPresenter(

val isWalletEmpty = isWalletSynced && playList.tracks.isEmpty() && !showZeroResultFound

var refreshing by remember { mutableStateOf(false) }

fun refresh() = scope.launch {
refreshing = true
walletNFTTracksUseCase.refresh()
refreshing = false
}

if (isWalletConnected == true) {
LaunchedEffect(Unit) {
refresh()
}
}

return when {
isLoading -> NFTLibraryState.Loading
isWalletConnected == false -> NFTLibraryState.LinkWallet { newmWalletConnectionId ->
Expand All @@ -128,6 +136,7 @@ class NFTLibraryPresenter(
streamTokenTracks = filteredStreamTokens,
showZeroResultFound = showZeroResultFound,
filters = filters,
refreshing = refreshing,
eventSink = { event ->
when (event) {
is NFTLibraryEvent.OnDownloadTrack -> TODO("Not implemented yet")
Expand All @@ -145,6 +154,7 @@ class NFTLibraryPresenter(
}

is NFTLibraryEvent.OnApplyFilters -> filters = event.filters
NFTLibraryEvent.OnRefresh -> refresh()
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material.swipeable
Expand Down Expand Up @@ -123,6 +127,8 @@ fun NFTLibraryScreenUi(
onQueryChange = { query -> eventSink(OnQueryChange(query)) },
onPlaySong = { track -> eventSink(PlaySong(track)) },
onDownloadSong = { trackId -> eventSink(OnDownloadTrack(trackId)) },
refresh = { eventSink(NFTLibraryEvent.OnRefresh) },
refreshing = state.refreshing,
onApplyFilters = { filters -> eventSink(OnApplyFilters(filters)) }
)
}
Expand All @@ -142,11 +148,16 @@ private fun NFTTracks(
onQueryChange: (String) -> Unit,
onPlaySong: (NFTTrack) -> Unit,
onDownloadSong: (String) -> Unit,
refresh: () -> Unit,
refreshing: Boolean,
onApplyFilters: (NFTLibraryFilters) -> Unit
) {
val scope = rememberCoroutineScope()
val filterSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
Box(modifier = modifier) {

val pullRefreshState = rememberPullRefreshState(refreshing, refresh)

Box(modifier = modifier.pullRefresh(pullRefreshState)) {
LazyColumn(
modifier = Modifier
.padding(horizontal = 16.dp)
Expand Down Expand Up @@ -201,6 +212,13 @@ private fun NFTTracks(
}
}
}
PullRefreshIndicator(
refreshing = refreshing,
state = pullRefreshState,
contentColor = MaterialTheme.colors.primary,
modifier = Modifier.align(Alignment.TopCenter)
)

SongFilterBottomSheet(filterSheetState, filters, onApplyFilters)
}
}
Expand Down Expand Up @@ -320,6 +338,7 @@ fun PreviewNftLibrary() {
sortType = NFTLibrarySortType.None,
showShortTracks = false
),
refreshing = false,
eventSink = {}
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ sealed interface NFTLibraryState : CircuitUiState {
val streamTokenTracks: List<NFTTrack>,
val showZeroResultFound: Boolean,
val filters: NFTLibraryFilters,
val refreshing: Boolean,
val eventSink: (NFTLibraryEvent) -> Unit,
) : NFTLibraryState

Expand All @@ -31,4 +32,4 @@ enum class NFTLibrarySortType {
ByTitle,
ByArtist,
ByLength
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ processPhoenix = "2.1.2"
recaptcha = "18.5.1"
runtime = "1.5.5"
soloader = "0.10.5"
store5 = "5.1.0-alpha04"
testParameterInjector = "1.16"
uiUtil = "1.6.8"

Expand Down Expand Up @@ -125,6 +126,7 @@ recaptcha = { module = "com.google.android.recaptcha:recaptcha", version.ref = "
runtime = { module = "com.squareup.sqldelight:runtime", version.ref = "runtime" }
soloader = { module = "com.facebook.soloader:soloader", version.ref = "soloader" }
sqldelight-gradle-plugin = { module = "com.squareup.sqldelight:gradle-plugin", version.ref = "gradlePlugin" }
store5 = { module = "org.mobilenativefoundation.store:store5", version.ref = "store5" }
test-parameter-injector = { module = "com.google.testparameterinjector:test-parameter-injector", version.ref = "testParameterInjector" }

[plugins]
Expand Down
1 change: 1 addition & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ kotlin {
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.auth)
implementation(libs.androidx.datastore.preferences)
implementation(libs.store5)
}
}
val commonTest by getting {
Expand Down
28 changes: 15 additions & 13 deletions shared/src/commonMain/kotlin/io.newm.shared/di/Koin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,39 @@ import io.newm.shared.NewmAppLogger
import io.newm.shared.config.NewmSharedBuildConfig
import io.newm.shared.config.NewmSharedBuildConfigImpl
import io.newm.shared.internal.TokenManager
import io.newm.shared.internal.api.CardanoWalletAPI
import io.newm.shared.internal.api.GenresAPI
import io.newm.shared.internal.api.LoginAPI
import io.newm.shared.internal.api.NEWMWalletConnectionAPI
import io.newm.shared.internal.api.PlaylistAPI
import io.newm.shared.internal.api.UserAPI
import io.newm.shared.internal.implementations.ChangePasswordUseCaseImpl
import io.newm.shared.internal.implementations.ConnectWalletUseCaseImpl
import io.newm.shared.internal.implementations.DisconnectWalletUseCaseImpl
import io.newm.shared.internal.implementations.ForceAppUpdateUseCaseImpl
import io.newm.shared.internal.implementations.GetGenresUseCaseImpl
import io.newm.shared.internal.implementations.GetWalletConnectionsUseCaseImpl
import io.newm.shared.internal.implementations.HasWalletConnectionsUseCaseImpl
import io.newm.shared.internal.implementations.LoginUseCaseImpl
import io.newm.shared.internal.implementations.ResetPasswordUseCaseImpl
import io.newm.shared.internal.implementations.SignupUseCaseImpl
import io.newm.shared.internal.implementations.SyncWalletConnectionsUseCaseImpl
import io.newm.shared.internal.implementations.UserDetailsUseCaseImpl
import io.newm.shared.internal.implementations.UserSessionUseCaseImpl
import io.newm.shared.internal.implementations.WalletNFTTracksUseCaseImpl
import io.newm.shared.internal.repositories.GenresRepository
import io.newm.shared.internal.repositories.LogInRepository
import io.newm.shared.internal.repositories.RemoteConfigRepositoryImpl
import io.newm.shared.internal.repositories.NFTRepository
import io.newm.shared.internal.repositories.PlaylistRepository
import io.newm.shared.internal.repositories.RemoteConfigRepository
import io.newm.shared.internal.repositories.RemoteConfigRepositoryImpl
import io.newm.shared.internal.repositories.UserRepository
import io.newm.shared.internal.api.CardanoWalletAPI
import io.newm.shared.internal.api.GenresAPI
import io.newm.shared.internal.api.LoginAPI
import io.newm.shared.internal.api.NEWMWalletConnectionAPI
import io.newm.shared.internal.api.PlaylistAPI
import io.newm.shared.internal.api.UserAPI
import io.newm.shared.internal.implementations.DisconnectWalletUseCaseImpl
import io.newm.shared.internal.implementations.GetWalletConnectionsUseCaseImpl
import io.newm.shared.internal.implementations.HasWalletConnectionsUseCaseImpl
import io.newm.shared.internal.implementations.SyncWalletConnectionsUseCaseImpl
import io.newm.shared.internal.repositories.NFTRepository
import io.newm.shared.internal.repositories.WalletRepository
import io.newm.shared.internal.services.cache.NFTCacheService
import io.newm.shared.internal.services.cache.WalletConnectionCacheService
import io.newm.shared.internal.services.network.NFTNetworkService
import io.newm.shared.internal.services.network.WalletConnectionNetworkService
import io.newm.shared.internal.store.NftTrackStore
import io.newm.shared.public.usecases.ChangePasswordUseCase
import io.newm.shared.public.usecases.ConnectWalletUseCase
import io.newm.shared.public.usecases.DisconnectWalletUseCase
Expand Down Expand Up @@ -103,9 +104,10 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
single { WalletConnectionCacheService(get()) }
single { NFTNetworkService(get()) }
single { NFTCacheService(get()) }
single { NftTrackStore(get(), get()) }
// Internal Repositories
single { WalletRepository(get(), get(), get()) }
single { NFTRepository(get(), get(), get()) }
single { NFTRepository(get(), get()) }
single { GenresRepository() }
single { LogInRepository() }
single { PlaylistRepository() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,56 @@
package io.newm.shared.internal.repositories

import io.newm.shared.NewmAppLogger
import io.newm.shared.internal.services.cache.NFTCacheService
import io.newm.shared.internal.services.network.NFTNetworkService
import io.newm.shared.internal.store.NftTrackStore
import io.newm.shared.public.models.NFTTrack
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import org.mobilenativefoundation.store.core5.ExperimentalStoreApi
import org.mobilenativefoundation.store.store5.StoreReadRequest
import org.mobilenativefoundation.store.store5.impl.extensions.fresh

internal class NFTRepository(
private val networkService: NFTNetworkService,
private val cacheService: NFTCacheService,
private val logger: NewmAppLogger
private val nftStore: NftTrackStore,
private val cacheService: NFTCacheService
) {

// TODO remove this when we start returning the state of the store
private val _syncedNftWallet = MutableStateFlow(false)

val isSynced: Flow<Boolean>
get() = _syncedNftWallet.asStateFlow()

suspend fun syncNFTTracksFromNetworkToDevice(): List<NFTTrack>? {
return try {
_syncedNftWallet.update { false }
val nfts = networkService.getWalletNFTs()
cacheService.cacheNFTTracks(nfts)
_syncedNftWallet.update { true }
nfts
} catch (e: Exception) {
logger.error("NFTRepository", "Error fetching NFTs from network ${e.cause}", e)
null
suspend fun syncNFTTracksFromNetworkToDevice(): List<NFTTrack> {
return nftStore.fresh(Unit).also {
_syncedNftWallet.value = true
}
}

fun getAllCollectableTracksFlow(): Flow<List<NFTTrack>> {
return cacheService.getAllTracks()
.map { tracks -> tracks.filter { !it.isStreamToken } }
fun getAllCollectableTracksFlow(): Flow<List<NFTTrack>> = getAll().map { tracks ->
tracks.filter { !it.isStreamToken }
}

fun getAllStreamTokensFlow(): Flow<List<NFTTrack>> {
return cacheService.getAllTracks()
.map { tracks -> tracks.filter { it.isStreamToken } }
fun getAllStreamTokensFlow(): Flow<List<NFTTrack>> = getAll().map { tracks ->
tracks.filter { it.isStreamToken }
}

fun deleteAllTracksNFTsCache() {
cacheService.deleteAllNFTs()
@OptIn(ExperimentalStoreApi::class)
suspend fun deleteAllTracksNFTsCache() {
nftStore.clear()
}

fun getTrack(id: String): NFTTrack? {
return cacheService.getTrack(id)
}

fun getAll(): Flow<List<NFTTrack>> =
cacheService.getAllTracks()
fun getAll(refresh: Boolean = false): Flow<List<NFTTrack>> =
nftStore.stream(StoreReadRequest.cached(Unit, refresh))
.map { result ->
// TODO handle error, loading, etc
result.dataOrNull() ?: emptyList()
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.newm.shared.internal.store

import io.newm.shared.internal.services.cache.NFTCacheService
import io.newm.shared.internal.services.network.NFTNetworkService
import io.newm.shared.public.models.NFTTrack
import org.mobilenativefoundation.store.store5.Fetcher
import org.mobilenativefoundation.store.store5.SourceOfTruth
import org.mobilenativefoundation.store.store5.Store
import org.mobilenativefoundation.store.store5.StoreBuilder
import org.mobilenativefoundation.store.store5.Validator

internal class NftTrackStore(
private val networkService: NFTNetworkService,
private val cacheService: NFTCacheService,
) : Store<Unit, List<NFTTrack>> by StoreBuilder.from(
fetcher = Fetcher.of { _: Unit ->
networkService.getWalletNFTs()
}, sourceOfTruth = SourceOfTruth.of(
reader = { _: Unit ->
cacheService.getAllTracks()
},
writer = { _: Unit, tracks: List<NFTTrack> -> cacheService.cacheNFTTracks(tracks) },
delete = { _: Unit ->
cacheService.deleteAllNFTs()
},
deleteAll = cacheService::deleteAllNFTs
)
).validator(Validator.by { it.isNotEmpty() } // If we have no data, we should always fetch
).build()

0 comments on commit 19a696e

Please sign in to comment.