diff --git a/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryEvent.kt b/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryEvent.kt index 8ae00e8b..e9eb1c44 100644 --- a/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryEvent.kt +++ b/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryEvent.kt @@ -4,6 +4,8 @@ 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 @@ -11,4 +13,4 @@ sealed interface NFTLibraryEvent : CircuitUiEvent { data class OnDownloadTrack(val tackId: String) : NFTLibraryEvent data class OnApplyFilters(val filters: NFTLibraryFilters) : NFTLibraryEvent -} \ No newline at end of file +} diff --git a/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryPresenter.kt b/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryPresenter.kt index fac0e57a..56511e9b 100644 --- a/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryPresenter.kt +++ b/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryPresenter.kt @@ -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() @@ -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 -> @@ -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") @@ -145,6 +154,7 @@ class NFTLibraryPresenter( } is NFTLibraryEvent.OnApplyFilters -> filters = event.filters + NFTLibraryEvent.OnRefresh -> refresh() } } ) diff --git a/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryScreenUi.kt b/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryScreenUi.kt index f0d6787c..5cb5a03e 100644 --- a/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryScreenUi.kt +++ b/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryScreenUi.kt @@ -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 @@ -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)) } ) } @@ -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) @@ -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) } } @@ -320,6 +338,7 @@ fun PreviewNftLibrary() { sortType = NFTLibrarySortType.None, showShortTracks = false ), + refreshing = false, eventSink = {} ) ) diff --git a/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryState.kt b/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryState.kt index 969bfbd2..2f8dbafd 100644 --- a/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryState.kt +++ b/android/app-newm/src/main/java/io/newm/screens/library/NFTLibraryState.kt @@ -15,6 +15,7 @@ sealed interface NFTLibraryState : CircuitUiState { val streamTokenTracks: List, val showZeroResultFound: Boolean, val filters: NFTLibraryFilters, + val refreshing: Boolean, val eventSink: (NFTLibraryEvent) -> Unit, ) : NFTLibraryState @@ -31,4 +32,4 @@ enum class NFTLibrarySortType { ByTitle, ByArtist, ByLength -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 386d722c..1da69632 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" @@ -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] diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index b0c08397..e67af43a 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -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 { diff --git a/shared/src/commonMain/kotlin/io.newm.shared/di/Koin.kt b/shared/src/commonMain/kotlin/io.newm.shared/di/Koin.kt index 2ce16cb0..aa0f6d95 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/di/Koin.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/di/Koin.kt @@ -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 @@ -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() } diff --git a/shared/src/commonMain/kotlin/io.newm.shared/internal/repositories/NFTRepository.kt b/shared/src/commonMain/kotlin/io.newm.shared/internal/repositories/NFTRepository.kt index 6d4b6452..cab231c4 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/internal/repositories/NFTRepository.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/internal/repositories/NFTRepository.kt @@ -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 get() = _syncedNftWallet.asStateFlow() - suspend fun syncNFTTracksFromNetworkToDevice(): List? { - 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 { + return nftStore.fresh(Unit).also { + _syncedNftWallet.value = true } } - fun getAllCollectableTracksFlow(): Flow> { - return cacheService.getAllTracks() - .map { tracks -> tracks.filter { !it.isStreamToken } } + fun getAllCollectableTracksFlow(): Flow> = getAll().map { tracks -> + tracks.filter { !it.isStreamToken } } - fun getAllStreamTokensFlow(): Flow> { - return cacheService.getAllTracks() - .map { tracks -> tracks.filter { it.isStreamToken } } + fun getAllStreamTokensFlow(): Flow> = 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> = - cacheService.getAllTracks() + fun getAll(refresh: Boolean = false): Flow> = + nftStore.stream(StoreReadRequest.cached(Unit, refresh)) + .map { result -> + // TODO handle error, loading, etc + result.dataOrNull() ?: emptyList() + } } + + diff --git a/shared/src/commonMain/kotlin/io.newm.shared/internal/store/NftTrackStore.kt b/shared/src/commonMain/kotlin/io.newm.shared/internal/store/NftTrackStore.kt new file mode 100644 index 00000000..81470042 --- /dev/null +++ b/shared/src/commonMain/kotlin/io.newm.shared/internal/store/NftTrackStore.kt @@ -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> by StoreBuilder.from( + fetcher = Fetcher.of { _: Unit -> + networkService.getWalletNFTs() + }, sourceOfTruth = SourceOfTruth.of( + reader = { _: Unit -> + cacheService.getAllTracks() + }, + writer = { _: Unit, tracks: List -> 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() +