Setup UI for follow/unfollow logic

This commit is contained in:
Kgothatso Ngako committed 2026-04-24 00:18:36 +02:00
1 parent be1dbcacb8
commit 5cd9cc09d6
9 files changed
+171 -16

No files matched your search

@@ -5,6 +5,8 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert import androidx.room.Upsert
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface ConnectionDao { interface ConnectionDao {
@@ -12,6 +14,9 @@ interface ConnectionDao {
fun getAllConnections(limit: Int = 21): List<Connection> fun getAllConnections(limit: Int = 21): List<Connection>
@Query("SELECT * FROM Connection WHERE (sourcePublicKey = :alicePublicKey AND destinationPublicKey = :bobPublicKey) OR (sourcePublicKey = :bobPublicKey AND destinationPublicKey = :alicePublicKey)")
fun observeRelation(alicePublicKey: HexKey, bobPublicKey: HexKey): Flow<List<Connection>>
@Insert @Insert
suspend fun insertPlaceholderProfile(connection: Connection) suspend fun insertPlaceholderProfile(connection: Connection)
@@ -4,6 +4,7 @@ import ac.cord.auxiliary.compose.database.AuxDatabase
import ac.cord.auxiliary.compose.database.GENESIS_AT import ac.cord.auxiliary.compose.database.GENESIS_AT
import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventReceipt import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventReceipt
import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest
import ac.cord.auxiliary.compose.database.model.Connection
import ac.cord.auxiliary.compose.database.model.NostrEvent import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.Profile import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.database.model.RecentSearch import ac.cord.auxiliary.compose.database.model.RecentSearch
@@ -59,6 +60,16 @@ class DatabaseNostrRepository(
return database.unsignedNostrEventDao().observeProfile(publicKey) return database.unsignedNostrEventDao().observeProfile(publicKey)
} }
override suspend fun observeRelation(
publicKey: HexKey,
activePublicKey: HexKey
): Flow<List<Connection>> {
return database.connectionDao().observeRelation(
alicePublicKey = publicKey,
bobPublicKey = activePublicKey
)
}
override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<UnsignedNostrEvent?> { override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<UnsignedNostrEvent?> {
return database.unsignedNostrEventDao().observeUnsignedNostrEvents(publicKey) return database.unsignedNostrEventDao().observeUnsignedNostrEvents(publicKey)
} }
@@ -2,6 +2,7 @@ package ac.cord.auxiliary.compose.repository
import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventReceipt import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventReceipt
import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest
import ac.cord.auxiliary.compose.database.model.Connection
import ac.cord.auxiliary.compose.database.model.NostrEvent import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.Profile import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.database.model.RecentSearch import ac.cord.auxiliary.compose.database.model.RecentSearch
@@ -21,6 +22,8 @@ import kotlinx.coroutines.flow.Flow
interface NostrRepository { interface NostrRepository {
suspend fun observeProfile(publicKey: HexKey): Flow<LocalAccount?> suspend fun observeProfile(publicKey: HexKey): Flow<LocalAccount?>
suspend fun observeRelation(publicKey: HexKey, activePublicKey: HexKey): Flow<List<Connection>>
suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<UnsignedNostrEvent?> suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<UnsignedNostrEvent?>
suspend fun observePendingBroadcastNostrEventRequests(): Flow<List<LocalBroadcastNostrEventRequest>> suspend fun observePendingBroadcastNostrEventRequests(): Flow<List<LocalBroadcastNostrEventRequest>>
@@ -99,6 +102,13 @@ interface NostrRepository {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun observeRelation(
publicKey: HexKey,
activePublicKey: HexKey
): Flow<List<Connection>> {
TODO("Not yet implemented")
}
override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<UnsignedNostrEvent?> { override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow<UnsignedNostrEvent?> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@@ -39,6 +39,7 @@ fun NostrEventDetailScreen(
nostrRepository: NostrRepository, nostrRepository: NostrRepository,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToEvent: (Route) -> Unit, onNavigateToEvent: (Route) -> Unit,
onNavigateToEditProfile: () -> Unit,
onNavigateToWriteAReply: (String) -> Unit onNavigateToWriteAReply: (String) -> Unit
) { ) {
val nostrEventDetailViewModel: NostrEventDetailViewModel = viewModel( val nostrEventDetailViewModel: NostrEventDetailViewModel = viewModel(
@@ -67,6 +68,7 @@ fun NostrEventDetailScreen(
localNostrEvent = feedListUIState.localNostrEvent, localNostrEvent = feedListUIState.localNostrEvent,
onNavigateBack = onNavigateBack, onNavigateBack = onNavigateBack,
onNavigateToEvent = onNavigateToEvent, onNavigateToEvent = onNavigateToEvent,
onNavigateToEditProfile = onNavigateToEditProfile,
nostrRepository = nostrRepository nostrRepository = nostrRepository
) )
} }
@@ -160,7 +162,8 @@ It has survived not only five centuries, but also the leap into electronic types
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
onNavigateBack = {}, onNavigateBack = {},
onNavigateToEvent = {}, onNavigateToEvent = {},
onNavigateToWriteAReply = {} onNavigateToWriteAReply = {},
onNavigateToEditProfile = {}
) )
} }
} }
@@ -387,6 +387,11 @@ fun AuxNavHost(
inReplyToEventId = nostrEventId inReplyToEventId = nostrEventId
) )
) )
},
onNavigateToEditProfile = {
navController.navigate(
route = ImplementationPendingRoute("Edit Profile")
)
} }
) )
} }
@@ -15,6 +15,7 @@ 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.FeedListUIState
import ac.cord.auxiliary.compose.ui.view.state.FollowersListUIState import ac.cord.auxiliary.compose.ui.view.state.FollowersListUIState
import ac.cord.auxiliary.compose.ui.view.state.FollowingListUIState import ac.cord.auxiliary.compose.ui.view.state.FollowingListUIState
import ac.cord.auxiliary.compose.ui.view.state.MetadataEventDetailUIState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -30,13 +31,14 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Error
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -66,6 +68,7 @@ fun MetadataEventDetail(
localNostrEvent: LocalNostrEvent, localNostrEvent: LocalNostrEvent,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToEvent: (Route) -> Unit, onNavigateToEvent: (Route) -> Unit,
onNavigateToEditProfile: () -> Unit,
nostrRepository: NostrRepository nostrRepository: NostrRepository
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -160,14 +163,64 @@ fun MetadataEventDetail(
) )
Button(
onClick = { when(val metadataEventDetailUIState = metadataEventDetailViewModel.metadataEventDetailUIState) {
MetadataEventDetailUIState.Error -> {
Icon(
Icons.Default.Error,
contentDescription = "Something went wrong"
)
}
is MetadataEventDetailUIState.Loaded -> {
if (metadataEventDetailViewModel.isActionPending.value) {
LoadingIndicator()
} else {
if (metadataEventDetailUIState.isActiveUser) {
Button(
onClick = onNavigateToEditProfile
) {
Text(
"Edit Profile"
)
}
} else if (metadataEventDetailUIState.followedByActiveUserConnection != null) {
Button(
onClick = {
// Unfollow
metadataEventDetailViewModel.unfollow(
metadataEventDetailUIState.followedByActiveUserConnection
)
}
) {
Text(
"Unfollow"
)
}
} else {
// flow
Button(
onClick = {
metadataEventDetailViewModel.follow()
}
) {
Text(
text = if (metadataEventDetailUIState.followingActiveUserConnection == null) {
"follow back"
} else {
"follow"
}
)
}
}
}
} }
) { MetadataEventDetailUIState.Loading -> {
Text( LoadingIndicator()
"Follow" }
)
} }
} }
Text( Text(
@@ -182,7 +235,7 @@ fun MetadataEventDetail(
} }
} }
item { stickyHeader {
PrimaryScrollableTabRow( PrimaryScrollableTabRow(
modifier = Modifier.padding(10.dp), modifier = Modifier.padding(10.dp),
edgePadding = 10.dp, edgePadding = 10.dp,
@@ -206,7 +259,9 @@ fun MetadataEventDetail(
) )
} }
} }
}
item {
Column( Column(
modifier = Modifier.fillParentMaxSize() modifier = Modifier.fillParentMaxSize()
) { ) {
@@ -289,6 +344,10 @@ fun MetadataEventDetail(
} }
} }
} }
key(true) {
metadataEventDetailViewModel.initiate()
}
} }
} }
} }
@@ -321,6 +380,7 @@ private fun MetadataEventEventDetailPreview() {
), ),
onNavigateBack = {}, onNavigateBack = {},
onNavigateToEvent = {}, onNavigateToEvent = {},
onNavigateToEditProfile = {},
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY
) )
} }
@@ -73,9 +73,10 @@ class CreateProfileViewModel(
isActionPending.value = true isActionPending.value = true
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val publicKey = SeedManager.activePublicKey().toHexKey()
nostrRepository.createNewProfile( nostrRepository.createNewProfile(
SeedManager.activePublicKey().toHexKey(), publicKey,
name = createProfileFormState.nameField.text.value, name = createProfileFormState.nameField.text.value,
biography = createProfileFormState.biographyField.text.value, biography = createProfileFormState.biographyField.text.value,
onError = { onError = {
@@ -1,11 +1,12 @@
package ac.cord.auxiliary.compose.ui.view.model package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.database.model.SynchronizeNostrEventRequest import ac.cord.auxiliary.compose.database.model.Connection
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.nostr.Relays import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.view.state.FeedListUIState import ac.cord.auxiliary.compose.ui.view.state.MetadataEventDetailUIState
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -16,6 +17,7 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@@ -27,7 +29,6 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
enum class MetadataEventDetailType { enum class MetadataEventDetailType {
@@ -35,11 +36,41 @@ enum class MetadataEventDetailType {
} }
class MetadataEventDetailViewModel( class MetadataEventDetailViewModel(
initialMetadataEventDetailUIState: MetadataEventDetailUIState = MetadataEventDetailUIState.Loading,
val publicKey: HexKey, val publicKey: HexKey,
val nostrRepository: NostrRepository, val nostrRepository: NostrRepository,
val pagerState: PagerState val pagerState: PagerState
): ViewModel() { ): ViewModel() {
val logger = Logger.withTag(TAG)
val isActionPending: MutableState<Boolean> = mutableStateOf(false)
var metadataEventDetailUIState: MetadataEventDetailUIState by mutableStateOf(initialMetadataEventDetailUIState)
private set
val logger = Logger.withTag("MetadataEventDetailViewModel")
fun initiate() {
observeMetadataEventRelationWithActiveUser()
}
fun observeMetadataEventRelationWithActiveUser() {
logger.d("observeLocalNostrFeed")
viewModelScope.launch(Dispatchers.IO) {
val activePublicKey = SeedManager.activePublicKey().toHexKey()
nostrRepository.observeRelation(
publicKey,
activePublicKey = activePublicKey
).collect { relations ->
logger.d("We found relations: $relations")
metadataEventDetailUIState = MetadataEventDetailUIState.Loaded(
isActiveUser = activePublicKey == publicKey,
followingActiveUserConnection = relations.find { connection -> connection.sourcePublicKey == publicKey && connection.destinationPublicKey == activePublicKey },
followedByActiveUserConnection = relations.find { connection -> connection.sourcePublicKey == activePublicKey && connection.destinationPublicKey == publicKey }
)
}
}
}
fun getSynchronizationFilter( fun getSynchronizationFilter(
metadataEventDetailType: MetadataEventDetailType metadataEventDetailType: MetadataEventDetailType
@@ -174,7 +205,20 @@ class MetadataEventDetailViewModel(
} }
} }
// TODO: Queue profile synchronization request... fun follow() {
isActionPending.value = true
val activePublicKey = SeedManager.activePublicKey().toHexKey()
}
fun unfollow(connection: Connection) {
isActionPending.value = true
val activePublicKey = SeedManager.activePublicKey().toHexKey()
}
companion object { companion object {
const val TAG = "MetadataEventDetailViewModel" const val TAG = "MetadataEventDetailViewModel"
@@ -0,0 +1,16 @@
package ac.cord.auxiliary.compose.ui.view.state
import ac.cord.auxiliary.compose.database.model.Connection
sealed interface MetadataEventDetailUIState {
data class Loaded(
val isActiveUser: Boolean,
val followingActiveUserConnection: Connection?,
val followedByActiveUserConnection: Connection?,
): MetadataEventDetailUIState
data object Error: MetadataEventDetailUIState
data object Loading: MetadataEventDetailUIState
}