Setup UI for follow/unfollow logic

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

View File

@@ -5,6 +5,8 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Upsert
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.flow.Flow
@Dao
interface ConnectionDao {
@@ -12,6 +14,9 @@ interface ConnectionDao {
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
suspend fun insertPlaceholderProfile(connection: Connection)

View File

@@ -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.model.BroadcastNostrEventReceipt
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.Profile
import ac.cord.auxiliary.compose.database.model.RecentSearch
@@ -59,6 +60,16 @@ class DatabaseNostrRepository(
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?> {
return database.unsignedNostrEventDao().observeUnsignedNostrEvents(publicKey)
}

View File

@@ -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.BroadcastNostrEventRequest
import ac.cord.auxiliary.compose.database.model.Connection
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.database.model.RecentSearch
@@ -21,6 +22,8 @@ import kotlinx.coroutines.flow.Flow
interface NostrRepository {
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 observePendingBroadcastNostrEventRequests(): Flow<List<LocalBroadcastNostrEventRequest>>
@@ -99,6 +102,13 @@ interface NostrRepository {
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?> {
TODO("Not yet implemented")
}

View File

@@ -39,6 +39,7 @@ fun NostrEventDetailScreen(
nostrRepository: NostrRepository,
onNavigateBack: () -> Unit,
onNavigateToEvent: (Route) -> Unit,
onNavigateToEditProfile: () -> Unit,
onNavigateToWriteAReply: (String) -> Unit
) {
val nostrEventDetailViewModel: NostrEventDetailViewModel = viewModel(
@@ -67,6 +68,7 @@ fun NostrEventDetailScreen(
localNostrEvent = feedListUIState.localNostrEvent,
onNavigateBack = onNavigateBack,
onNavigateToEvent = onNavigateToEvent,
onNavigateToEditProfile = onNavigateToEditProfile,
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,
onNavigateBack = {},
onNavigateToEvent = {},
onNavigateToWriteAReply = {}
onNavigateToWriteAReply = {},
onNavigateToEditProfile = {}
)
}
}

View File

@@ -387,6 +387,11 @@ fun AuxNavHost(
inReplyToEventId = nostrEventId
)
)
},
onNavigateToEditProfile = {
navController.navigate(
route = ImplementationPendingRoute("Edit Profile")
)
}
)
}

View File

@@ -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.FollowersListUIState
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.Column
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.filled.ArrowBackIosNew
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.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Scaffold
@@ -66,6 +68,7 @@ fun MetadataEventDetail(
localNostrEvent: LocalNostrEvent,
onNavigateBack: () -> Unit,
onNavigateToEvent: (Route) -> Unit,
onNavigateToEditProfile: () -> Unit,
nostrRepository: NostrRepository
) {
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"
}
)
}
}
}
}
) {
Text(
"Follow"
)
MetadataEventDetailUIState.Loading -> {
LoadingIndicator()
}
}
}
Text(
@@ -182,7 +235,7 @@ fun MetadataEventDetail(
}
}
item {
stickyHeader {
PrimaryScrollableTabRow(
modifier = Modifier.padding(10.dp),
edgePadding = 10.dp,
@@ -206,7 +259,9 @@ fun MetadataEventDetail(
)
}
}
}
item {
Column(
modifier = Modifier.fillParentMaxSize()
) {
@@ -289,6 +344,10 @@ fun MetadataEventDetail(
}
}
}
key(true) {
metadataEventDetailViewModel.initiate()
}
}
}
}
@@ -321,6 +380,7 @@ private fun MetadataEventEventDetailPreview() {
),
onNavigateBack = {},
onNavigateToEvent = {},
onNavigateToEditProfile = {},
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY
)
}

View File

@@ -73,9 +73,10 @@ class CreateProfileViewModel(
isActionPending.value = true
viewModelScope.launch(Dispatchers.IO) {
val publicKey = SeedManager.activePublicKey().toHexKey()
nostrRepository.createNewProfile(
SeedManager.activePublicKey().toHexKey(),
publicKey,
name = createProfileFormState.nameField.text.value,
biography = createProfileFormState.biographyField.text.value,
onError = {

View File

@@ -1,11 +1,12 @@
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.nostr.Relays
import ac.cord.auxiliary.compose.managers.SeedManager
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.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -16,6 +17,7 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@@ -27,7 +29,6 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
enum class MetadataEventDetailType {
@@ -35,11 +36,41 @@ enum class MetadataEventDetailType {
}
class MetadataEventDetailViewModel(
initialMetadataEventDetailUIState: MetadataEventDetailUIState = MetadataEventDetailUIState.Loading,
val publicKey: HexKey,
val nostrRepository: NostrRepository,
val pagerState: PagerState
): 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(
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 {
const val TAG = "MetadataEventDetailViewModel"

View File

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