diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt index 13970301..f1027910 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt @@ -20,7 +20,7 @@ interface NostrEventDao { fun getAllNostrEvents(kinds: Array): List @Query("SELECT * FROM NostrEvent WHERE id = :id") - fun observeNostrEventById(id: String): Flow + fun observeNostrEventById(id: String): Flow @Query("SELECT * FROM NostrEvent WHERE id = :id") fun getNostrEventById(id: String): NostrEvent? diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt index 999da8ac..c6ff8123 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt @@ -35,7 +35,6 @@ import kotlinx.coroutines.flow.Flow import kotlin.collections.emptyMap import kotlin.collections.plus import kotlin.time.Clock -import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant class DatabaseNostrRepository( @@ -308,4 +307,8 @@ class DatabaseNostrRepository( ) } + override suspend fun observeLocalNostrEventById(nostrEventId: String): Flow { + return database.nostrEventDao().observeNostrEventById(nostrEventId) + } + } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt index 8945696b..4bef588b 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt @@ -67,6 +67,8 @@ interface NostrRepository { suspend fun observeNostrFeed(): Flow> + suspend fun observeLocalNostrEventById(nostrEventId: String): Flow + companion object { val NO_OP_NOSTR_REPOSITORY = object : NostrRepository { override suspend fun observeProfile(publicKey: HexKey): Flow { @@ -146,6 +148,10 @@ interface NostrRepository { override suspend fun observeNostrFeed(): Flow> { TODO("Not yet implemented") } + + override suspend fun observeLocalNostrEventById(nostrEventId: String): Flow { + TODO("Not yet implemented") + } } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/NostrEventDetailScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/NostrEventDetailScreen.kt index d3e76e6f..bbfa7db2 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/NostrEventDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/NostrEventDetailScreen.kt @@ -1,6 +1,10 @@ package ac.aux.compose.ui.composable +import ac.aux.compose.repository.NostrRepository +import ac.aux.compose.ui.composable.widgets.LoadingDataIndicator import ac.aux.compose.ui.theme.AuxTheme +import ac.aux.compose.ui.view.model.NostrEventDetailViewModel +import ac.aux.compose.ui.view.state.NostrEventDetailUIState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -9,27 +13,113 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @Composable -fun NostrEventDetailScreen() { +fun NostrEventDetailScreen( + initialNostrEventDetailUIState: NostrEventDetailUIState, + nostrEventId: HexKey, + nostrRepository: NostrRepository +) { + val nostrEventDetailViewModel: NostrEventDetailViewModel = viewModel( + factory = NostrEventDetailViewModel.factory( + nostrEventId = nostrEventId, + initialNostrEventDetailUIState = initialNostrEventDetailUIState, + nostrRepository = nostrRepository + ), + ) - Scaffold { innerPadding -> - Column( - modifier = Modifier.padding(innerPadding).fillMaxSize() - ) { + when(val feedListUIState = nostrEventDetailViewModel.nostrEventDetailUIState) { + NostrEventDetailUIState.Error -> { Column( - modifier = Modifier.padding(20.dp).fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - "\"Nostr Event Detail\" functionality coming soon", + Text("Something went wrong") + } + } + is NostrEventDetailUIState.Loaded -> { - ) + when(feedListUIState.localNostrEvent.nostrEvent.kind) { + MetadataEvent.KIND -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(feedListUIState.localNostrEvent.nostrEvent.content) + + Text("profile functionality coming soon.") + } + } + TextNoteEvent.KIND -> { + // TODO: Post... + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(feedListUIState.localNostrEvent.nostrEvent.content) + + Text("post functionality coming soon.") + } + } + RepostEvent.KIND -> { + // TODO: RePost... + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(feedListUIState.localNostrEvent.nostrEvent.content) + + Text("post functionality coming soon.") + } + } + else -> { + Scaffold { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding).fillMaxSize() + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(feedListUIState.localNostrEvent.nostrEvent.content) + + Text("functionality coming soon.") + } + } + } + + } + } + } + NostrEventDetailUIState.Loading -> { + LoadingDataIndicator() + + LaunchedEffect(true) { + nostrEventDetailViewModel.loadNostrFeed() + } + } + NostrEventDetailUIState.NotFound -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("We couldn't find your nostr event. Please try again later.") } } } @@ -42,7 +132,11 @@ private fun NostrEventDetailScreenPreview() { Surface( modifier = Modifier.padding(20.dp) ) { - NostrEventDetailScreen() + NostrEventDetailScreen( + initialNostrEventDetailUIState = NostrEventDetailUIState.Loading, + nostrEventId = "", + nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY + ) } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt index ec66baec..e499fdf6 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt @@ -44,6 +44,7 @@ import ac.aux.compose.ui.composable.navigation.routes.WriteNewNoteRoute import ac.aux.compose.ui.view.model.NavigationViewModel import ac.aux.compose.ui.view.state.FeedListUIState import ac.aux.compose.ui.view.state.NavigationUIState +import ac.aux.compose.ui.view.state.NostrEventDetailUIState import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.runtime.LaunchedEffect @@ -334,7 +335,11 @@ fun AuxNavHost( composable { backStackEntry -> val route = backStackEntry.toRoute() - NostrEventDetailScreen() + NostrEventDetailScreen( + initialNostrEventDetailUIState = NostrEventDetailUIState.Loading, + nostrEventId = route.nostrEventId, + nostrRepository = nostrRepository, + ) } composable { backStackEntry -> val route: ImplementationPendingRoute = backStackEntry.toRoute() diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/NostrEventDetailRoute.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/NostrEventDetailRoute.kt index 9ca044aa..1d9a6e6c 100755 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/NostrEventDetailRoute.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/NostrEventDetailRoute.kt @@ -7,6 +7,6 @@ import kotlin.time.Clock @Serializable data class NostrEventDetailRoute( - val nostrEventId: String? = null, + val nostrEventId: String, ): Route() { } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NostrEventDetailViewModel.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NostrEventDetailViewModel.kt new file mode 100755 index 00000000..3ba40b01 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/NostrEventDetailViewModel.kt @@ -0,0 +1,97 @@ +package ac.aux.compose.ui.view.model + +import ac.aux.compose.database.model.SynchronizeNostrEventRequest +import ac.aux.compose.database.model.types.SynchronizationFilter +import ac.aux.compose.managers.SeedManager +import ac.aux.compose.nostr.Relays +import ac.aux.compose.repository.NostrRepository +import ac.aux.compose.ui.view.state.NostrEventDetailUIState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.launch + +class NostrEventDetailViewModel( + val nostrEventId: HexKey, + initialNostrEventDetailUIState: NostrEventDetailUIState, + val nostrRepository: NostrRepository +): ViewModel() { + var nostrEventDetailUIState: NostrEventDetailUIState by mutableStateOf(initialNostrEventDetailUIState) + private set + + fun retryLoad() { + nostrEventDetailUIState = NostrEventDetailUIState.Loading + loadNostrFeed() + } + + fun loadNostrFeed() { + viewModelScope.launch(Dispatchers.IO) { + // Sync Notifications... might want to also run this in the background + nostrRepository.queueSynchronizeNostrEvent( + Relays.eventPublishRelaySet.map { normalizedRelay -> + SynchronizeNostrEventRequest( + synchronizationFilters = arrayOf( + SynchronizationFilter( + kinds = arrayOf( + TextNoteEvent.KIND, + RepostEvent.KIND, + ReactionEvent.KIND, + LnZapEvent.KIND, + ), + tags = mapOf( + Pair("p", listOf(SeedManager.activePublicKey().toHexKey())) + ), + limit = 50 + ) + ), + relayURL = normalizedRelay.url + ) + } + ) + + // Get contact list and sync feed... + nostrRepository.observeLocalNostrEventById( + nostrEventId + ).collect { localNostrEvent -> + nostrEventDetailUIState = if (localNostrEvent != null) { + NostrEventDetailUIState.Loaded( + localNostrEvent = localNostrEvent + ) + } else { + NostrEventDetailUIState.Error + } + } + } + } + + companion object { + const val TAG = "NostrEventDetailViewModel" + + fun factory( + nostrEventId: HexKey, + initialNostrEventDetailUIState: NostrEventDetailUIState = NostrEventDetailUIState.Loading, + nostrRepository: NostrRepository, + ): ViewModelProvider.Factory = viewModelFactory { + initializer { + NostrEventDetailViewModel( + nostrEventId = nostrEventId, + initialNostrEventDetailUIState = initialNostrEventDetailUIState, + nostrRepository = nostrRepository + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/NostrEventDetailUIState.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/NostrEventDetailUIState.kt new file mode 100755 index 00000000..41034b4a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/NostrEventDetailUIState.kt @@ -0,0 +1,15 @@ +package ac.aux.compose.ui.view.state + +import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.intermdiate.LocalNostrEvent + +sealed interface NostrEventDetailUIState { + data class Loaded( + val localNostrEvent: LocalNostrEvent + ): NostrEventDetailUIState + + data object Error: NostrEventDetailUIState + data object NotFound: NostrEventDetailUIState + data object Loading: NostrEventDetailUIState +} +