diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/FeedScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/FeedScreen.kt index 020dc4da..076c4c3a 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/FeedScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/FeedScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -239,6 +240,10 @@ fun FeedScreen( } } } + + key(true) { + feedListViewModel.initiate() + } } } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt index 597ee599..08a68923 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt @@ -7,10 +7,12 @@ import ac.cord.auxiliary.compose.repository.NostrRepository 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.model.FeedListViewModel +import ac.cord.auxiliary.compose.ui.view.model.FollowersListViewModel import ac.cord.auxiliary.compose.ui.view.model.FollowingListViewModel import ac.cord.auxiliary.compose.ui.view.model.MetadataEventDetailType import ac.cord.auxiliary.compose.ui.view.model.MetadataEventDetailViewModel import ac.cord.auxiliary.compose.ui.view.state.FeedListUIState +import ac.cord.auxiliary.compose.ui.view.state.FollowersListUIState import ac.cord.auxiliary.compose.ui.view.state.FollowingListUIState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -230,6 +232,10 @@ fun MetadataEventDetail( onNavigateToEvent = onNavigateToEvent ) } + + key(true) { + feedListViewModel.initiate() + } } MetadataEventDetailType.Following -> { @@ -249,10 +255,32 @@ fun MetadataEventDetail( onNavigateToEvent = onNavigateToEvent ) } + + key(true) { + followingListViewModel.initiate() + } } MetadataEventDetailType.Followers -> { + val followersListViewModel: FollowersListViewModel = viewModel( + key = metadataEventDetailType.name, + factory = FollowersListViewModel.factory( + initialFeedListUIState = FollowersListUIState.Loading, + publicKey = localNostrEvent.nostrEvent.pubKey, + synchronizationFilter = metadataEventDetailViewModel.getSynchronizationFilter(metadataEventDetailType), + nostrRepository = nostrRepository + ), + ) - Text("Implementing logic to see followers") + + key(followersListViewModel.followersListUIState) { + followersListViewModel.RenderFeed( + onNavigateToEvent = onNavigateToEvent + ) + } + + key(true) { + followersListViewModel.initiate() + } } } } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FeedListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FeedListViewModel.kt index 808096c2..97919e72 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FeedListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FeedListViewModel.kt @@ -60,7 +60,7 @@ class FeedListViewModel( val isSynchronizationPending: MutableState = mutableStateOf(false) - init { + fun initiate() { logger.d("init") scheduleSynchronization() observeLocalNostrFeed() 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 new file mode 100755 index 00000000..0251fb9f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowersListViewModel.kt @@ -0,0 +1,212 @@ +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.view.state.FollowersListUIState +import androidx.compose.foundation.clickable +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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.launch + +class FollowersListViewModel( + initialFollowingListUIState: FollowersListUIState, + val publicKey: String, + val synchronizationFilter: SynchronizationFilter, + val nostrRepository: NostrRepository +): ViewModel() { + val logger = Logger.withTag(TAG) + var followersListUIState: FollowersListUIState by mutableStateOf(initialFollowingListUIState) + private set + + val isSynchronizationPending: MutableState = mutableStateOf(false) + + fun initiate() { + logger.d("initiate") + scheduleSynchronization() + observeLocalNostrFeed() + observeSynchronizeNostrEventRequestByPurposeAndStatusCount() + } + + fun observeLocalNostrFeed() { + logger.d("observeLocalNostrFeed") + viewModelScope.launch(Dispatchers.IO) { + nostrRepository.observeProfileFollowers( + publicKey + ).collect { localFollowers -> + followersListUIState = FollowersListUIState.Loaded( + followers = localFollowers?.followers ?: emptyList() + ) + } + } + } + + 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 + ) + } + ) + } + } + + fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount() { + logger.d("observeSynchronizeNostrEventRequestByPurposeAndStatusCount") + viewModelScope.launch(Dispatchers.IO) { + nostrRepository.observeSynchronizeNostrEventRequestByPurposeAndStatusCount("feed").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 followersListUIState = followersListUIState) { + FollowersListUIState.Error -> { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer( + modifier = Modifier.height(50.dp) + ) + Text( + text = "Something went wrong", + ) + } + } + is FollowersListUIState.Loaded -> { + if (followersListUIState.followers.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 = followersListUIState.followers, + key = { profile -> profile.publicKey } + ) { follower -> + Column( + modifier = Modifier.clickable { + onNavigateToEvent.invoke( + NostrEventDetailRoute( + follower.nostrEventId + ) + ) + } + ) { + Text( + text = follower.displayName ?: follower.userName ?: follower.userName ?: follower.publicKey + ) + + follower.nip05?.let { + Text( + text = follower.nip05 + ) + } + } + } + } + } + + } + FollowersListUIState.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 = "FeedListViewModel" + + fun factory( + initialFeedListUIState: FollowersListUIState = FollowersListUIState.Loading, + publicKey: String, + synchronizationFilter: SynchronizationFilter, + nostrRepository: NostrRepository, + ): ViewModelProvider.Factory = viewModelFactory { + initializer { + FollowersListViewModel( + initialFollowingListUIState = initialFeedListUIState, + 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/model/FollowingListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/FollowingListViewModel.kt index bbe3e610..853c7264 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 @@ -6,7 +6,6 @@ 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.FollowingListUIState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -50,13 +49,12 @@ class FollowingListViewModel( val isSynchronizationPending: MutableState = mutableStateOf(false) - init { + fun initiate() { logger.d("init") scheduleSynchronization() observeLocalNostrFeed() observeSynchronizeNostrEventRequestByPurposeAndStatusCount() } - fun observeLocalNostrFeed() { logger.d("observeLocalNostrFeed") viewModelScope.launch(Dispatchers.IO) { @@ -64,7 +62,7 @@ class FollowingListViewModel( publicKey ).collect { localFollowing -> followingListUIState = FollowingListUIState.Loaded( - profilesBeingFollowed = localFollowing?.following ?: emptyList() + following = localFollowing?.following ?: emptyList() ) } } @@ -122,7 +120,7 @@ class FollowingListViewModel( } } is FollowingListUIState.Loaded -> { - if (feedListUIState.profilesBeingFollowed.isEmpty()) { + if (feedListUIState.following.isEmpty()) { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally @@ -141,7 +139,7 @@ class FollowingListViewModel( verticalArrangement = Arrangement.spacedBy(10.dp) ) { items( - items = feedListUIState.profilesBeingFollowed, + items = feedListUIState.following, key = { profile -> profile.publicKey } ) { profile -> Column( diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/FollowersListUIState.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/FollowersListUIState.kt new file mode 100755 index 00000000..bb181e81 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/FollowersListUIState.kt @@ -0,0 +1,13 @@ +package ac.cord.auxiliary.compose.ui.view.state + +import ac.cord.auxiliary.compose.database.model.Profile + +sealed interface FollowersListUIState { + data class Loaded( + val followers: List + ): FollowersListUIState + + data object Error: FollowersListUIState + data object Loading: FollowersListUIState +} + diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/FollowingListUIState.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/FollowingListUIState.kt index 01032a54..0cf775b0 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/FollowingListUIState.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/FollowingListUIState.kt @@ -4,7 +4,7 @@ import ac.cord.auxiliary.compose.database.model.Profile sealed interface FollowingListUIState { data class Loaded( - val profilesBeingFollowed: List + val following: List ): FollowingListUIState data object Error: FollowingListUIState