refactor
This commit is contained in:
@@ -3,6 +3,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
kotlin("plugin.serialization") version libs.versions.kotlin.get()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
@@ -17,6 +18,8 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -31,7 +34,6 @@ import rust.nostr.sdk.EventId
|
||||
import rust.nostr.sdk.Keys
|
||||
import rust.nostr.sdk.Kind
|
||||
import rust.nostr.sdk.KindStandard
|
||||
import rust.nostr.sdk.Metadata
|
||||
import rust.nostr.sdk.NostrConnect
|
||||
import rust.nostr.sdk.NostrConnectUri
|
||||
import rust.nostr.sdk.PublicKey
|
||||
@@ -44,16 +46,14 @@ import su.reya.coop.blossom.BlossomClient
|
||||
import su.reya.coop.nostr.ExternalSignerHandler
|
||||
import su.reya.coop.nostr.ExternalSignerProxy
|
||||
import su.reya.coop.nostr.Nostr
|
||||
import su.reya.coop.nostr.Room
|
||||
import su.reya.coop.nostr.SignerPermissions
|
||||
import su.reya.coop.nostr.roomId
|
||||
import su.reya.coop.storage.SecretStorage
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class NostrViewModel(
|
||||
val nostr: Nostr,
|
||||
private val nostr: Nostr,
|
||||
private val secretStore: SecretStorage,
|
||||
private val externalSignerHandler: ExternalSignerHandler? = null,
|
||||
) : ViewModel() {
|
||||
@@ -87,21 +87,25 @@ class NostrViewModel(
|
||||
private val _errorEvents = Channel<String>(Channel.BUFFERED)
|
||||
val errorEvents = _errorEvents.receiveAsFlow()
|
||||
|
||||
private val _metadataStore = mutableMapOf<PublicKey, MutableStateFlow<Metadata?>>()
|
||||
private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>()
|
||||
|
||||
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
||||
|
||||
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
||||
|
||||
val isSyncing = nostr.messages.messageSyncState.map { it.isSyncing }
|
||||
val isSyncing = nostr.messages.messageSyncState
|
||||
.map { it.isSyncing }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||
|
||||
init {
|
||||
// Skip the splash screen if a user is already logged in
|
||||
if (nostr.signer.currentUser != null) {
|
||||
_signerRequired.value = false
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val currentUserProfile = nostr.signer.publicKeyFlow
|
||||
.flatMapLatest { pubkey ->
|
||||
if (pubkey != null) getMetadata(pubkey) else flowOf(null)
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
|
||||
|
||||
init {
|
||||
// Check if the notification banner has been dismissed
|
||||
checkNotificationBannerDismissedStatus()
|
||||
|
||||
@@ -183,11 +187,9 @@ class NostrViewModel(
|
||||
val existingRoom = _chatRooms.value.firstOrNull { it.id == roomId }
|
||||
|
||||
if (existingRoom == null) {
|
||||
val currentUser = nostr.signer.currentUser
|
||||
if (currentUser != null) {
|
||||
val newRoom = Room.new(event, currentUser)
|
||||
_chatRooms.update { (it + newRoom).sortedDescending().toSet() }
|
||||
}
|
||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: return@collect
|
||||
val newRoom = Room.new(event, currentUser)
|
||||
_chatRooms.update { (it + newRoom).sortedDescending().toSet() }
|
||||
} else {
|
||||
updateRoomList(roomId, event)
|
||||
}
|
||||
@@ -206,7 +208,7 @@ class NostrViewModel(
|
||||
// Observe metadata updates
|
||||
launch {
|
||||
nostr.profiles.metadataUpdates.collect { (pubkey, metadata) ->
|
||||
updateMetadata(pubkey, metadata)
|
||||
updateMetadata(pubkey, Profile(pubkey, metadata))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,7 +259,7 @@ class NostrViewModel(
|
||||
val results = nostr.profiles.getAllCacheMetadata()
|
||||
results.forEach { (pubkey, metadata) ->
|
||||
// Update the metadata state
|
||||
updateMetadata(pubkey, metadata)
|
||||
updateMetadata(pubkey, Profile(pubkey, metadata))
|
||||
// Update seenPublicKeys to avoid duplicate requests
|
||||
seenPublicKeys.add(pubkey)
|
||||
}
|
||||
@@ -295,9 +297,9 @@ class NostrViewModel(
|
||||
private fun observeSignerAndCheckRelays() {
|
||||
viewModelScope.launch {
|
||||
while (true) {
|
||||
val pubkey = nostr.signer.currentUser
|
||||
val currentUser = nostr.signer.getPublicKeyAsync()
|
||||
|
||||
if (pubkey != null) {
|
||||
if (currentUser != null) {
|
||||
// Get chat rooms
|
||||
val rooms = nostr.messages.getChatRooms() ?: emptySet()
|
||||
if (rooms.isNotEmpty()) {
|
||||
@@ -312,7 +314,7 @@ class NostrViewModel(
|
||||
delay(2.seconds)
|
||||
|
||||
// Check if the relay list is empty
|
||||
val relays = nostr.relays.getMsgRelays(pubkey)
|
||||
val relays = nostr.relays.getMsgRelays(currentUser)
|
||||
if (relays.isEmpty()) _isRelayListEmpty.value = true
|
||||
|
||||
break
|
||||
@@ -331,22 +333,17 @@ class NostrViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMetadata(pubkey: PublicKey, metadata: Metadata) {
|
||||
_metadataStore.getOrPut(pubkey) { MutableStateFlow(null) }.value = metadata
|
||||
private fun updateMetadata(pubkey: PublicKey, profile: Profile) {
|
||||
profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
|
||||
}
|
||||
|
||||
fun getMetadata(pubkey: PublicKey): StateFlow<Metadata?> {
|
||||
val flow = _metadataStore.getOrPut(pubkey) { MutableStateFlow(null) }
|
||||
if (flow.value == null) {
|
||||
requestMetadata(pubkey)
|
||||
}
|
||||
fun getMetadata(pubkey: PublicKey): StateFlow<Profile?> {
|
||||
val flow = profiles.getOrPut(pubkey) { MutableStateFlow(null) }
|
||||
if (flow.value == null) requestMetadata(pubkey)
|
||||
|
||||
return flow.asStateFlow()
|
||||
}
|
||||
|
||||
fun currentUser(): PublicKey? {
|
||||
return nostr.signer.currentUser
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
@@ -445,7 +442,7 @@ class NostrViewModel(
|
||||
val newMetadata = nostr.profiles.updateProfile(name, bio, avatarUrl)
|
||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
||||
// Update the metadata state after successfully published
|
||||
updateMetadata(currentUser, newMetadata)
|
||||
updateMetadata(currentUser, Profile(currentUser, newMetadata))
|
||||
// Update local state
|
||||
_isBusy.value = false
|
||||
} catch (e: Exception) {
|
||||
@@ -573,8 +570,10 @@ class NostrViewModel(
|
||||
return externalSignerHandler?.isAvailable() == true
|
||||
}
|
||||
|
||||
suspend fun refetchMsgRelays(pubkey: PublicKey) {
|
||||
val relays = nostr.relays.fetchMsgRelays(pubkey)
|
||||
suspend fun refetchMsgRelays() {
|
||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: return
|
||||
val relays = nostr.relays.fetchMsgRelays(currentUser)
|
||||
|
||||
if (relays.isNotEmpty()) dismissRelayWarning()
|
||||
}
|
||||
|
||||
@@ -717,10 +716,13 @@ class NostrViewModel(
|
||||
|
||||
fun createChatRoom(to: List<PublicKey>): Long {
|
||||
try {
|
||||
if (nostr.signer.currentUser == null) throw IllegalStateException("User not signed in")
|
||||
if (to.isEmpty()) throw IllegalArgumentException("At least one recipient is required")
|
||||
if (to.isEmpty()) {
|
||||
throw IllegalArgumentException("At least one recipient is required")
|
||||
}
|
||||
|
||||
val currentUser = nostr.signer.currentUser ?: throw Exception("User not found")
|
||||
// Get current user
|
||||
val currentUser = nostr.signer.publicKeyFlow.value
|
||||
?: throw IllegalStateException("User not signed in")
|
||||
|
||||
// Construct the rumor event
|
||||
val rumor = EventBuilder(Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE), "")
|
||||
|
||||
20
shared/src/commonMain/kotlin/su/reya/coop/Profile.kt
Normal file
20
shared/src/commonMain/kotlin/su/reya/coop/Profile.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package su.reya.coop
|
||||
|
||||
import rust.nostr.sdk.Metadata
|
||||
import rust.nostr.sdk.PublicKey
|
||||
|
||||
data class Profile(
|
||||
val publicKey: PublicKey,
|
||||
val metadata: Metadata
|
||||
) {
|
||||
private val record by lazy { metadata.asRecord() }
|
||||
|
||||
val name: String
|
||||
get() = record.displayName ?: record.name ?: publicKey.short()
|
||||
|
||||
val picture: String?
|
||||
get() = record.picture
|
||||
|
||||
val shortPublicKey: String
|
||||
get() = publicKey.short()
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
package su.reya.coop.nostr
|
||||
package su.reya.coop
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.minus
|
||||
@@ -75,9 +82,60 @@ data class Room(
|
||||
return this.copy(lastMessage = message)
|
||||
}
|
||||
|
||||
fun isGroup(): Boolean {
|
||||
return members.size > 1
|
||||
fun isGroup(): Boolean = members.size > 1
|
||||
}
|
||||
|
||||
data class RoomUiState(
|
||||
val name: String = "Loading...",
|
||||
val picture: String? = null,
|
||||
val isGroup: Boolean = false
|
||||
)
|
||||
|
||||
fun Room.uiStateFlow(
|
||||
nostrViewModel: NostrViewModel,
|
||||
currentUser: PublicKey? = null
|
||||
): Flow<RoomUiState> {
|
||||
val displayMembers = if (isGroup()) members.take(2) else members.take(1)
|
||||
|
||||
if (!subject.isNullOrBlank()) {
|
||||
return flowOf(RoomUiState(name = subject, isGroup = isGroup()))
|
||||
}
|
||||
|
||||
return combine(displayMembers.map { nostrViewModel.getMetadata(it) }) { profiles ->
|
||||
val names = profiles.mapIndexed { i, profile -> profile?.name ?: displayMembers[i].short() }
|
||||
|
||||
val name = when {
|
||||
isGroup() -> {
|
||||
val combined = names.joinToString(", ")
|
||||
val extra = members.size - names.size
|
||||
if (extra > 0) "$combined, +$extra" else combined
|
||||
}
|
||||
|
||||
else -> {
|
||||
val first = names.firstOrNull() ?: "Unknown"
|
||||
if (displayMembers.firstOrNull() == currentUser) "$first (you)" else first
|
||||
}
|
||||
}
|
||||
|
||||
RoomUiState(
|
||||
name = name,
|
||||
picture = profiles.firstOrNull()?.picture,
|
||||
isGroup = isGroup()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Room.rememberUiState(
|
||||
viewModel: NostrViewModel,
|
||||
currentUser: PublicKey? = null
|
||||
): State<RoomUiState> {
|
||||
return remember(this, currentUser) {
|
||||
uiStateFlow(
|
||||
viewModel,
|
||||
currentUser
|
||||
)
|
||||
}.collectAsStateWithLifecycle(RoomUiState())
|
||||
}
|
||||
|
||||
fun UnsignedEvent.roomId(): Long {
|
||||
@@ -24,6 +24,9 @@ import rust.nostr.sdk.Tag
|
||||
import rust.nostr.sdk.UnsignedEvent
|
||||
import rust.nostr.sdk.nip17ExtractRelayList
|
||||
import rust.nostr.sdk.nip59MakeGiftWrapAsync
|
||||
import su.reya.coop.Room
|
||||
import su.reya.coop.RoomKind
|
||||
import su.reya.coop.roomId
|
||||
import kotlin.time.Duration
|
||||
|
||||
data class MessageSyncState(
|
||||
@@ -47,7 +50,9 @@ class MessageManager(private val nostr: Nostr) {
|
||||
|
||||
suspend fun getUserMessages(msgRelayList: Event) {
|
||||
try {
|
||||
val author = signer.currentUser ?: throw IllegalStateException("User not signed in")
|
||||
val author =
|
||||
signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")
|
||||
|
||||
val relays = nip17ExtractRelayList(msgRelayList)
|
||||
|
||||
// Ensure relay connections
|
||||
|
||||
@@ -136,7 +136,7 @@ class Nostr {
|
||||
|
||||
fun isSignedByUser(event: Event): Boolean {
|
||||
return try {
|
||||
signer.currentUser == event.author()
|
||||
signer.publicKeyFlow.value == event.author()
|
||||
} catch (e: Exception) {
|
||||
println("Failed to check if event is signed by user: ${e.message}")
|
||||
false
|
||||
|
||||
@@ -49,7 +49,8 @@ class ProfileManager(private val nostr: Nostr) {
|
||||
|
||||
suspend fun getUserMetadata() {
|
||||
try {
|
||||
val author = signer.currentUser ?: throw IllegalStateException("User not signed in")
|
||||
val author =
|
||||
signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")
|
||||
|
||||
// Get the latest metadata event
|
||||
val metadataFilter =
|
||||
@@ -146,7 +147,8 @@ class ProfileManager(private val nostr: Nostr) {
|
||||
bio: String? = null,
|
||||
picture: String? = null
|
||||
): Metadata {
|
||||
val currentUser = signer.currentUser ?: throw IllegalStateException("User not signed in")
|
||||
val currentUser =
|
||||
signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")
|
||||
|
||||
try {
|
||||
val record = getLatestMetadata(currentUser)?.asRecord() ?: MetadataRecord()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package su.reya.coop.nostr
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
@@ -16,9 +18,8 @@ class UniversalSigner(initialSigner: AsyncNostrSigner) : AsyncNostrSigner {
|
||||
@Volatile
|
||||
private var signer: AsyncNostrSigner = initialSigner
|
||||
|
||||
@Volatile
|
||||
var currentUser: PublicKey? = null
|
||||
private set
|
||||
private val _publicKeyFlow = MutableStateFlow<PublicKey?>(null)
|
||||
val publicKeyFlow = _publicKeyFlow.asStateFlow()
|
||||
|
||||
/**
|
||||
* Get the current signer.
|
||||
@@ -37,7 +38,7 @@ class UniversalSigner(initialSigner: AsyncNostrSigner) : AsyncNostrSigner {
|
||||
throw IllegalStateException("Failed to get public key from signer", e)
|
||||
}
|
||||
signer = newSigner
|
||||
currentUser = pubkey
|
||||
_publicKeyFlow.value = pubkey
|
||||
}
|
||||
|
||||
override suspend fun getPublicKeyAsync(): PublicKey? {
|
||||
|
||||
Reference in New Issue
Block a user