Setup NostrEventDetailScreen.kt

This commit is contained in:
Kgothatso Ngako
2026-04-01 00:26:12 +02:00
parent c45bb216a3
commit a910a95b1d
8 changed files with 236 additions and 16 deletions

View File

@@ -20,7 +20,7 @@ interface NostrEventDao {
fun getAllNostrEvents(kinds: Array<Kind>): List<NostrEvent>
@Query("SELECT * FROM NostrEvent WHERE id = :id")
fun observeNostrEventById(id: String): Flow<NostrEvent?>
fun observeNostrEventById(id: String): Flow<LocalNostrEvent?>
@Query("SELECT * FROM NostrEvent WHERE id = :id")
fun getNostrEventById(id: String): NostrEvent?

View File

@@ -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<LocalNostrEvent?> {
return database.nostrEventDao().observeNostrEventById(nostrEventId)
}
}

View File

@@ -67,6 +67,8 @@ interface NostrRepository {
suspend fun observeNostrFeed(): Flow<List<LocalNostrEvent>>
suspend fun observeLocalNostrEventById(nostrEventId: String): Flow<LocalNostrEvent?>
companion object {
val NO_OP_NOSTR_REPOSITORY = object : NostrRepository {
override suspend fun observeProfile(publicKey: HexKey): Flow<LocalProfile?> {
@@ -146,6 +148,10 @@ interface NostrRepository {
override suspend fun observeNostrFeed(): Flow<List<LocalNostrEvent>> {
TODO("Not yet implemented")
}
override suspend fun observeLocalNostrEventById(nostrEventId: String): Flow<LocalNostrEvent?> {
TODO("Not yet implemented")
}
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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<NostrEventDetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<NostrEventDetailRoute>()
NostrEventDetailScreen()
NostrEventDetailScreen(
initialNostrEventDetailUIState = NostrEventDetailUIState.Loading,
nostrEventId = route.nostrEventId,
nostrRepository = nostrRepository,
)
}
composable<ImplementationPendingRoute> { backStackEntry ->
val route: ImplementationPendingRoute = backStackEntry.toRoute()

View File

@@ -7,6 +7,6 @@ import kotlin.time.Clock
@Serializable
data class NostrEventDetailRoute(
val nostrEventId: String? = null,
val nostrEventId: String,
): Route() {
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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
}