diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ConnectionDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ConnectionDao.kt index a84d8d0e..6f0ba565 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ConnectionDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ConnectionDao.kt @@ -2,6 +2,7 @@ package ac.cord.auxiliary.compose.database.dao import ac.cord.auxiliary.compose.database.model.Connection import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.Query import androidx.room.Upsert @@ -23,4 +24,7 @@ interface ConnectionDao { @Upsert suspend fun upsert(connection: Connection) + @Delete + suspend fun delete(connection: Connection) + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrEventDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrEventDao.kt index 3c57c917..829f7cc9 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrEventDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrEventDao.kt @@ -9,6 +9,7 @@ import androidx.room.Query import androidx.room.Upsert import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Kind +import fr.acinq.bitcoin.PublicKey import kotlinx.coroutines.flow.Flow @Dao @@ -43,6 +44,9 @@ interface NostrEventDao { @Query("SELECT * FROM NostrEvent WHERE id = :id") fun getNostrEventById(id: String): NostrEvent? + @Query("SELECT * FROM NostrEvent WHERE pubKey = :publicKey AND kind = :kind") + fun getNostrEventByPublicKeyAndKind(publicKey: HexKey, kind: Kind): NostrEvent? + @Upsert suspend fun upsert(nostrEvent: NostrEvent) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt index 4d1ebb3e..4dec5115 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt @@ -24,6 +24,7 @@ import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.repository.RelayRepository import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.RelaySet @@ -70,6 +71,10 @@ class DatabaseNostrRepository( ) } + override suspend fun deleteConnection(connection: Connection) { + database.connectionDao().delete(connection) + } + override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow { return database.unsignedNostrEventDao().observeUnsignedNostrEvents(publicKey) } @@ -239,6 +244,7 @@ class DatabaseNostrRepository( } override suspend fun saveUnsignedNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): UnsignedNostrEvent? = try { + // TODO: We can actually check the unsignedNostrEvent.pubKey for support... if it's writeable delay(2_100) // Just to make it look like we are doing serious work... unsignedNostrEvent.copy( id = database.unsignedNostrEventDao().upsert(unsignedNostrEvent) @@ -328,6 +334,13 @@ class DatabaseNostrRepository( ) } + override suspend fun getNostrEvent(publicKey: HexKey, kind: Kind): NostrEvent? { + return database.nostrEventDao().getNostrEventByPublicKeyAndKind( + publicKey = publicKey, + kind = kind + ) + } + override suspend fun observeNostrFeed(): Flow> { return database.nostrEventDao().observeNostrEvents( kinds = arrayOf( diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt index 0845286b..984c83bb 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt @@ -17,6 +17,7 @@ import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFoll import ac.cord.auxiliary.compose.database.model.typealiases.SynchronizationFilterArray import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind import kotlinx.coroutines.flow.Flow interface NostrRepository { @@ -24,6 +25,8 @@ interface NostrRepository { suspend fun observeRelation(publicKey: HexKey, activePublicKey: HexKey): Flow> + suspend fun deleteConnection(connection: Connection) + suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow suspend fun observePendingBroadcastNostrEventRequests(): Flow> @@ -76,6 +79,9 @@ interface NostrRepository { suspend fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount(purpose: String): Flow + + suspend fun getNostrEvent(publicKey: HexKey, kind: Kind): NostrEvent? + suspend fun observeNostrFeed(): Flow> suspend fun observeNostrFeed(synchronizationFilter: SynchronizationFilter): Flow> @@ -109,6 +115,10 @@ interface NostrRepository { TODO("Not yet implemented") } + override suspend fun deleteConnection(connection: Connection) { + TODO("Not yet implemented") + } + override suspend fun observeUnsignedNostrEvents(publicKey: HexKey): Flow { TODO("Not yet implemented") } @@ -186,6 +196,13 @@ interface NostrRepository { TODO("Not yet implemented") } + override suspend fun getNostrEvent( + publicKey: HexKey, + kind: Kind + ): NostrEvent? { + TODO("Not yet implemented") + } + override suspend fun observeNostrFeed(): Flow> { TODO("Not yet implemented") } 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 b31d233e..1414be75 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 @@ -206,7 +206,7 @@ fun MetadataEventDetail( } ) { Text( - text = if (metadataEventDetailUIState.followingActiveUserConnection == null) { + text = if (metadataEventDetailUIState.followingActiveUserConnection != null) { "follow back" } else { "follow" diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MetadataEventDetailViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MetadataEventDetailViewModel.kt index c6826859..71c46fe7 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MetadataEventDetailViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/MetadataEventDetailViewModel.kt @@ -1,6 +1,7 @@ package ac.cord.auxiliary.compose.ui.view.model import ac.cord.auxiliary.compose.database.model.Connection +import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter import ac.cord.auxiliary.compose.managers.SeedManager import ac.cord.auxiliary.compose.repository.NostrRepository @@ -17,12 +18,17 @@ 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.TagArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip02FollowList.RelaySet +import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent @@ -63,11 +69,17 @@ class MetadataEventDetailViewModel( 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 } ) + + if (isActionPending.value) { + // This means this new relation was caused by user action... + isActionPending.value = false + } } } } @@ -210,7 +222,45 @@ class MetadataEventDetailViewModel( val activePublicKey = SeedManager.activePublicKey().toHexKey() + viewModelScope.launch(Dispatchers.IO) { + val contactListEvent = nostrRepository.getNostrEvent( + publicKey = activePublicKey, + kind = ContactListEvent.KIND + ) + + val followUsers: TagArray = if (contactListEvent != null) { + if (contactListEvent.tags.isTaggedUser(publicKey)) { + // User already being followed + logger.d("User is already following $publicKey") + return@launch + } + + contactListEvent.tags.plus( + ContactTag( + pubKey = publicKey + ).toTagArray() + ) + } else { + arrayOf( + ContactTag(activePublicKey, null, null).toTagArray(), + ContactTag(publicKey, null, null).toTagArray(), + ) + } + val contactListTagList = listOf(AltTag.assemble(ContactListEvent.ALT)) + + followUsers.map { it } + + nostrRepository.saveUnsignedNostrEvent( + UnsignedNostrEvent( + pubKey = activePublicKey, + kind = ContactListEvent.KIND, + tags = contactListTagList.toTypedArray(), + content = RelaySet.assemble( + emptyMap() + ) + ) + ) + } } fun unfollow(connection: Connection) { @@ -218,6 +268,34 @@ class MetadataEventDetailViewModel( val activePublicKey = SeedManager.activePublicKey().toHexKey() + viewModelScope.launch(Dispatchers.IO) { + val contactListEvent = nostrRepository.getNostrEvent( + publicKey = activePublicKey, + kind = ContactListEvent.KIND + ) + + + if (contactListEvent != null) { + if (!contactListEvent.tags.isTaggedUser(publicKey)) { + logger.d("User is not being followed so no need to unfollow: $publicKey") + } else { + nostrRepository.saveUnsignedNostrEvent( + UnsignedNostrEvent( + pubKey = activePublicKey, + kind = ContactListEvent.KIND, + tags = contactListEvent.tags.filter { it.size > 1 && it[1] != publicKey }.toTypedArray(), + content = RelaySet.assemble( + emptyMap() + ) + ) + ) + } + + nostrRepository.deleteConnection( + connection + ) + } + } } companion object { const val TAG = "MetadataEventDetailViewModel"