From c57fc80082a22c3b2414e53d7616e2cee6aadcf9 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Wed, 6 May 2026 00:58:08 +0200 Subject: [PATCH] Show message lists --- .../compose/ui/composable/HomeScreen.kt | 26 +- .../ui/view/model/FollowersListViewModel.kt | 2 +- .../ui/view/model/FollowingListViewModel.kt | 2 +- .../compose/ui/view/model/HomeViewModel.kt | 4 +- .../ui/view/model/MessageListViewModel.kt | 303 ++++++++++++++++++ .../ui/view/state/MessageListUIState.kt | 13 + 6 files changed, 343 insertions(+), 7 deletions(-) create mode 100755 composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MessageListViewModel.kt create mode 100755 composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/MessageListUIState.kt diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt index ffa8a92b..5a03d26c 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt @@ -12,8 +12,10 @@ 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.HomeScreenType import ac.cord.auxiliary.compose.ui.view.model.HomeViewModel +import ac.cord.auxiliary.compose.ui.view.model.MessageListViewModel import ac.cord.auxiliary.compose.ui.view.state.FeedListUIState import ac.cord.auxiliary.compose.ui.view.state.HomeScreenUIState +import ac.cord.auxiliary.compose.ui.view.state.MessageListUIState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -208,9 +210,29 @@ fun HomeScreen( feedListViewModel.initiate() } } - HomeScreenType.Messages -> { - Text("Messaging coming soon.") + val messageListViewModel: MessageListViewModel = viewModel( + key = homeScreenTypeType.name, + factory = MessageListViewModel.factory( + initialMessageListUIState = MessageListUIState.Loading, + publicKey = homeScreenUIState.profileWithFollowing.profile.publicKey, + synchronizationFilter = homeScreenViewModel.getSynchronizationFilter( + homeScreenTypeType, + profileWithFollowing = homeScreenUIState.profileWithFollowing + ), + nostrRepository = nostrRepository + ), + ) + + key(messageListViewModel.messageListUIState) { + messageListViewModel.RenderFeed( + onNavigateToEvent = onNavigateToEvent + ) + } + + key(true) { + messageListViewModel.initiate() + } } } } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowersListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowersListViewModel.kt index 5b8656ad..e73e4994 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowersListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowersListViewModel.kt @@ -180,7 +180,7 @@ class FollowersListViewModel( } companion object { - const val TAG = "FeedListViewModel" + const val TAG = "FollowersListViewModel" fun factory( initialFeedListUIState: FollowersListUIState = FollowersListUIState.Loading, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowingListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowingListViewModel.kt index 28228af2..b2e038c0 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowingListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowingListViewModel.kt @@ -179,7 +179,7 @@ class FollowingListViewModel( } companion object { - const val TAG = "FeedListViewModel" + const val TAG = "FollowingListViewModel" fun factory( initialFeedListUIState: FollowingListUIState = FollowingListUIState.Loading, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/HomeViewModel.kt index 0ea3c987..8ee29388 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/HomeViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/HomeViewModel.kt @@ -120,10 +120,8 @@ class HomeViewModel( publicKey ), kinds = arrayOf( - LongTextNoteEvent.KIND, + ContactListEvent.KIND, ), - since = twelveHoursAgo, - until = now, limit = 50 ) } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MessageListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MessageListViewModel.kt new file mode 100755 index 00000000..e621f98f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MessageListViewModel.kt @@ -0,0 +1,303 @@ +package ac.cord.auxiliary.compose.ui.view.model + +import ac.cord.auxiliary.compose.database.model.NegentropySynchronizeRequest +import ac.cord.auxiliary.compose.database.model.NostrEvent +import ac.cord.auxiliary.compose.database.model.Profile +import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing +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.HomeScreen +import ac.cord.auxiliary.compose.ui.composable.navigation.routes.Route +import ac.cord.auxiliary.compose.ui.theme.AuxTheme +import ac.cord.auxiliary.compose.ui.view.state.FollowingListUIState +import ac.cord.auxiliary.compose.ui.view.state.HomeScreenUIState +import ac.cord.auxiliary.compose.ui.view.state.MessageListUIState +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.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.Surface +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.tooling.preview.Preview +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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +class MessageListViewModel( + initialMessageListUIState: MessageListUIState, + val publicKey: String, + val synchronizationFilter: SynchronizationFilter, + val nostrRepository: NostrRepository +): ViewModel() { + val logger = Logger.withTag(TAG) + var messageListUIState: MessageListUIState by mutableStateOf(initialMessageListUIState) + private set + + val isSynchronizationPending: MutableState = mutableStateOf(false) + + fun initiate() { + logger.d("init") +// scheduleSynchronization() + observeLocalNostrFeed() +// observeSynchronizeNostrEventRequestByPurposeAndStatusCount() + } + fun observeLocalNostrFeed() { + logger.d("observeLocalNostrFeed") + viewModelScope.launch(Dispatchers.IO) { + nostrRepository.observeProfileWithFollowing( + publicKey + ).collect { + messageListUIState = MessageListUIState.Loaded( + messageList = emptyList() + ) + } + } + } + + fun scheduleSynchronization() { + logger.d("scheduleSynchronization") + viewModelScope.launch(Dispatchers.IO) { + // Sync Notifications... might want to also run this in the background + nostrRepository.queueNegentropySynchronizeRequest( + Relays.negentropicRelaySet.shuffled().map { normalizedRelayUrl -> + NegentropySynchronizeRequest( + id = NegentropySynchronizeRequest.computeId( + relayURL = normalizedRelayUrl.url, + synchronizationFilter = synchronizationFilter + ), + purpose = "following", + synchronizationFilter = synchronizationFilter, + relayURL = normalizedRelayUrl.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 = messageListUIState) { + MessageListUIState.Error -> { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer( + modifier = Modifier.height(50.dp) + ) + Text( + text = "Something went wrong", + ) + } + } + is MessageListUIState.Loaded -> { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = "Messages" + ) + + Card( + modifier = Modifier.fillMaxWidth(), + onClick = { + + } + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Steve Biko" + ) + } + + } + + Card( + modifier = Modifier.fillMaxWidth(), + onClick = { + + } + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Chris Hani" + ) + } + + } + + Card( + modifier = Modifier.fillMaxWidth(), + onClick = { + + } + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Desmond Tutu" + ) + } + + } + + Card( + modifier = Modifier.fillMaxWidth(), + onClick = { + + } + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Hip Hop Pantsula" + ) + } + + } + + Card( + modifier = Modifier.fillMaxWidth(), + onClick = { + + } + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Knee Cap" + ) + } + } + } +// if (feedListUIState.messageList.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.messageList, +// key = { profile -> profile.publicKey } +// ) { profile -> +// profile.RenderAsListItem( +// onNavigateToEvent = onNavigateToEvent +// ) +// } +// } +// } + + } + MessageListUIState.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 = "MessageListViewModel" + + fun factory( + initialMessageListUIState: MessageListUIState = MessageListUIState.Loading, + publicKey: String, + synchronizationFilter: SynchronizationFilter, + nostrRepository: NostrRepository, + ): ViewModelProvider.Factory = viewModelFactory { + initializer { + MessageListViewModel( + initialMessageListUIState = initialMessageListUIState, + publicKey = publicKey, + synchronizationFilter = synchronizationFilter, + nostrRepository = nostrRepository + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/MessageListUIState.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/MessageListUIState.kt new file mode 100755 index 00000000..48812f48 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/MessageListUIState.kt @@ -0,0 +1,13 @@ +package ac.cord.auxiliary.compose.ui.view.state + +import ac.cord.auxiliary.compose.database.model.Profile + +sealed interface MessageListUIState { + data class Loaded( + val messageList: List + ): MessageListUIState + + data object Error: MessageListUIState + data object Loading: MessageListUIState +} +