Show replies to notes.
This commit is contained in:
@@ -14,6 +14,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import fr.acinq.bitcoin.PublicKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Dao
|
||||
interface NostrEventDao {
|
||||
@@ -49,6 +50,14 @@ interface NostrEventDao {
|
||||
publicKey: HexKey
|
||||
): Flow<List<LocalNostrEvent>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM NostrEvent WHERE tags LIKE '%' || :eventId || '%reply%' AND kind in (:kinds) AND createdAt > :since ORDER BY createdAt DESC")
|
||||
fun observeNostrEventReplies(
|
||||
kinds: Array<Kind>,
|
||||
eventId: HexKey,
|
||||
since: Instant
|
||||
): Flow<List<LocalNostrEvent>>
|
||||
|
||||
@Transaction
|
||||
@RawQuery
|
||||
fun observePublicKeyFollowingNostrEvents(
|
||||
|
||||
@@ -491,12 +491,18 @@ class DatabaseNostrRepository(
|
||||
authors = synchronizationFilter.authors
|
||||
)
|
||||
}
|
||||
synchronizationFilter.kinds != null && synchronizationFilter.since != null && synchronizationFilter.tags?.contains("e") == true && synchronizationFilter.tags["e"]?.first()?.isNotEmpty() == true -> {
|
||||
logger.d("observePublicKeyMentionedNostrEvents: $synchronizationFilter")
|
||||
database.nostrEventDao().observeNostrEventReplies(
|
||||
kinds = synchronizationFilter.kinds,
|
||||
eventId = synchronizationFilter.tags["e"]?.first()!!,
|
||||
since = synchronizationFilter.since
|
||||
)
|
||||
}
|
||||
synchronizationFilter.kinds != null && synchronizationFilter.tags?.contains("p") == true && synchronizationFilter.tags["p"]?.first()?.isNotEmpty() == true -> {
|
||||
logger.d("observePublicKeyMentionedNostrEvents: $synchronizationFilter")
|
||||
database.nostrEventDao().observePublicKeyMentionedNostrEvents(
|
||||
kinds = arrayOf(
|
||||
TextNoteEvent.KIND,
|
||||
),
|
||||
kinds = synchronizationFilter.kinds,
|
||||
publicKey = synchronizationFilter.tags["p"]?.first()!!
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import ac.cord.auxiliary.compose.ui.composable.widgets.profile.ProfileAvatar
|
||||
import ac.cord.auxiliary.compose.ui.composable.widgets.profile.ProfileColor
|
||||
import ac.cord.auxiliary.compose.ui.theme.AuxTheme
|
||||
import ac.cord.auxiliary.compose.ui.view.model.FeedListViewModel
|
||||
import ac.cord.auxiliary.compose.ui.view.model.InReplyToViewModel
|
||||
import ac.cord.auxiliary.compose.ui.view.state.FeedListUIState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -23,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddReaction
|
||||
@@ -33,26 +35,35 @@ import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Repeat
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.BottomAppBarDefaults
|
||||
import androidx.compose.material3.BottomAppBarScrollBehavior
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FlexibleBottomAppBar
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LoadingIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
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.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
@@ -63,20 +74,20 @@ fun TextNoteEventDetail(
|
||||
onNavigateToWriteAReply: (String) -> Unit,
|
||||
onNavigateToNostrEvent: (HexKey) -> Unit,
|
||||
) {
|
||||
val inReplyToNostrEventFeedListViewModel: FeedListViewModel = viewModel(
|
||||
key = "replies",
|
||||
factory = FeedListViewModel.factory(
|
||||
val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
|
||||
|
||||
val inReplyToNostrEventFeedListViewModel: InReplyToViewModel = viewModel(
|
||||
key = "replies-${localNostrEvent.nostrEvent.id}",
|
||||
factory = InReplyToViewModel.factory(
|
||||
nostrEventId = localNostrEvent.nostrEvent.id,
|
||||
initialFeedListUIState = FeedListUIState.Loading,
|
||||
synchronizationFilter = SynchronizationFilter(
|
||||
ids = arrayOf(localNostrEvent.nostrEvent.id)
|
||||
// TODO: Marked Root In Reply...
|
||||
),
|
||||
nostrRepository = nostrRepository
|
||||
nostrRepository = nostrRepository,
|
||||
createdAt = localNostrEvent.nostrEvent.createdAt
|
||||
),
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
@@ -100,7 +111,8 @@ fun TextNoteEventDetail(
|
||||
contentPadding = PaddingValues(
|
||||
start = 10.dp,
|
||||
end = 10.dp
|
||||
)
|
||||
),
|
||||
scrollBehavior = scrollBehavior
|
||||
) {
|
||||
|
||||
Surface(
|
||||
@@ -189,7 +201,8 @@ fun TextNoteEventDetail(
|
||||
) {
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// TODO: get all the notes inReplyToRoot before this note
|
||||
item {
|
||||
@@ -338,15 +351,41 @@ fun TextNoteEventDetail(
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
inReplyToNostrEventFeedListViewModel.RenderFeed(
|
||||
onNavigateToEvent = {
|
||||
// TODO: NavigateToEvent...
|
||||
when (val inReplyToFeedListUIState = inReplyToNostrEventFeedListViewModel.inReplyToFeedListUIState) {
|
||||
is FeedListUIState.Loaded -> {
|
||||
if (inReplyToFeedListUIState.localNostrEvents.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
"Be the first to comment."
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(
|
||||
items = inReplyToFeedListUIState.localNostrEvents,
|
||||
key = { e -> "reply-${e.nostrEvent.id}" }
|
||||
) { localNostrEvent ->
|
||||
localNostrEvent.RenderNotePreview()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
FeedListUIState.Error -> {
|
||||
item {
|
||||
Text(
|
||||
text = "Something went wrong"
|
||||
)
|
||||
}
|
||||
}
|
||||
FeedListUIState.Loading -> {
|
||||
item {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(true) {
|
||||
inReplyToNostrEventFeedListViewModel.initiate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package ac.cord.auxiliary.compose.ui.view.model
|
||||
|
||||
import ac.cord.auxiliary.compose.database.model.SynchronizeNostrEventRequest
|
||||
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
|
||||
import ac.cord.auxiliary.compose.nostr.Relays
|
||||
import ac.cord.auxiliary.compose.repository.NostrRepository
|
||||
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.NostrEventDetailRoute
|
||||
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.Route
|
||||
import ac.cord.auxiliary.compose.ui.composable.widgets.feed.ListView
|
||||
import ac.cord.auxiliary.compose.ui.view.state.FeedListUIState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.LoadingIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Instant
|
||||
|
||||
class InReplyToViewModel(
|
||||
nostrEventId: String,
|
||||
initialFeedListUIState: FeedListUIState,
|
||||
val synchronizationFilter: SynchronizationFilter,
|
||||
val nostrRepository: NostrRepository
|
||||
): ViewModel() {
|
||||
val logger = Logger.withTag(TAG)
|
||||
var inReplyToFeedListUIState: FeedListUIState by mutableStateOf(initialFeedListUIState)
|
||||
private set
|
||||
|
||||
val isSynchronizationPending: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
fun initiate() {
|
||||
logger.d("init")
|
||||
scheduleSynchronization()
|
||||
observeLocalNostrFeed()
|
||||
observeSynchronizeNostrEventRequestByPurposeAndStatusCount()
|
||||
}
|
||||
|
||||
fun observeLocalNostrFeed() {
|
||||
logger.d("observeLocalNostrFeed")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
nostrRepository.observeNostrFeed(
|
||||
synchronizationFilter
|
||||
).distinctUntilChanged().collect { localNostrEvents ->
|
||||
inReplyToFeedListUIState = FeedListUIState.Loaded(
|
||||
localNostrEvents = localNostrEvents
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun scheduleSynchronization() {
|
||||
logger.d("scheduleSynchronization")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Sync Notifications... might want to also run this in the background
|
||||
nostrRepository.queueSynchronizeNostrEvent(
|
||||
Relays.eventPublishRelaySet.take(1).map { normalizedRelay -> // TODO: Sync from all the publish relays...
|
||||
SynchronizeNostrEventRequest(
|
||||
purpose = "feed",
|
||||
synchronizationFilters = arrayOf(
|
||||
synchronizationFilter
|
||||
),
|
||||
relayURL = normalizedRelay.url,
|
||||
level = 0,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount() {
|
||||
logger.d("observeSynchronizeNostrEventRequestByPurposeAndStatusCount")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
nostrRepository.observeSynchronizeNostrEventRequestByPurposeAndStatusCount("feed").distinctUntilChanged().collect { count ->
|
||||
logger.d("Pending/Processing Synchronization Request: $count")
|
||||
isSynchronizationPending.value = count > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun RenderFeed(
|
||||
onNavigateToEvent: (Route) -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
when (val feedListUIState = inReplyToFeedListUIState) {
|
||||
FeedListUIState.Error -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier.height(50.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Something went wrong",
|
||||
)
|
||||
}
|
||||
}
|
||||
is FeedListUIState.Loaded -> {
|
||||
if (feedListUIState.localNostrEvents.isEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier.height(50.dp)
|
||||
)
|
||||
Text(
|
||||
text = "No events were found",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
items(
|
||||
items = feedListUIState.localNostrEvents,
|
||||
key = { localNostrEvent -> localNostrEvent.nostrEvent.id}
|
||||
) { localNostrEvent ->
|
||||
localNostrEvent.ListView(
|
||||
onNavigateToEvent = { nostrEventId ->
|
||||
onNavigateToEvent.invoke(
|
||||
NostrEventDetailRoute(
|
||||
nostrEventId = nostrEventId
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
FeedListUIState.Loading -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier.height(50.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Loading",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.height(50.dp)
|
||||
)
|
||||
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "InReplyToViewModel"
|
||||
|
||||
fun factory(
|
||||
nostrEventId: String,
|
||||
createdAt: Instant,
|
||||
initialFeedListUIState: FeedListUIState = FeedListUIState.Loading,
|
||||
synchronizationFilter: SynchronizationFilter = SynchronizationFilter(
|
||||
kinds = arrayOf(
|
||||
TextNoteEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
),
|
||||
tags = mapOf(
|
||||
"e" to listOf(
|
||||
nostrEventId
|
||||
)
|
||||
),
|
||||
since = createdAt
|
||||
),
|
||||
nostrRepository: NostrRepository,
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
InReplyToViewModel(
|
||||
initialFeedListUIState = initialFeedListUIState,
|
||||
nostrEventId = nostrEventId,
|
||||
synchronizationFilter = synchronizationFilter,
|
||||
nostrRepository = nostrRepository
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user