forked from reya/coop-mobile
217 lines
6.8 KiB
Kotlin
217 lines
6.8 KiB
Kotlin
package su.reya.coop.viewmodels
|
|
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import kotlinx.coroutines.channels.Channel
|
|
import kotlinx.coroutines.coroutineScope
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
import kotlinx.coroutines.flow.update
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withTimeoutOrNull
|
|
import rust.nostr.sdk.Metadata
|
|
import rust.nostr.sdk.PublicKey
|
|
import rust.nostr.sdk.Timestamp
|
|
import su.reya.coop.nostr.Nostr
|
|
import kotlin.time.Clock
|
|
import kotlin.time.Duration.Companion.milliseconds
|
|
|
|
class ProfileViewModel(
|
|
private val nostr: Nostr,
|
|
private val appViewModel: AppViewModel
|
|
) : ViewModel() {
|
|
private val _contactList = MutableStateFlow<Set<PublicKey>>(emptySet())
|
|
val contactList = _contactList.asStateFlow()
|
|
|
|
private val _metadataStore = mutableMapOf<PublicKey, MutableStateFlow<Metadata?>>()
|
|
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
|
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
|
|
|
init {
|
|
getCacheMetadata()
|
|
}
|
|
|
|
suspend fun bindObservers() = coroutineScope {
|
|
launch {
|
|
nostr.profiles.contactListUpdates.collect { contacts ->
|
|
_contactList.value = contacts.toSet()
|
|
}
|
|
}
|
|
|
|
launch {
|
|
nostr.profiles.metadataUpdates.collect { (pubkey, metadata) ->
|
|
updateMetadata(pubkey, metadata)
|
|
}
|
|
}
|
|
|
|
launch { runMetadataBatching() }
|
|
}
|
|
|
|
private fun updateMetadata(pubkey: PublicKey, metadata: Metadata) {
|
|
_metadataStore.getOrPut(pubkey) { MutableStateFlow(null) }.value = metadata
|
|
}
|
|
|
|
fun getMetadata(pubkey: PublicKey): StateFlow<Metadata?> {
|
|
val flow = _metadataStore.getOrPut(pubkey) { MutableStateFlow(null) }
|
|
if (flow.value == null) {
|
|
requestMetadata(pubkey)
|
|
}
|
|
return flow.asStateFlow()
|
|
}
|
|
|
|
private fun requestMetadata(pubkey: PublicKey) {
|
|
if (seenPublicKeys.add(pubkey)) {
|
|
viewModelScope.launch {
|
|
metadataRequestChannel.send(pubkey)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getCacheMetadata() {
|
|
viewModelScope.launch {
|
|
nostr.waitUntilInitialized()
|
|
val results = nostr.profiles.getAllCacheMetadata()
|
|
results.forEach { (pubkey, metadata) ->
|
|
updateMetadata(pubkey, metadata)
|
|
seenPublicKeys.add(pubkey)
|
|
}
|
|
}
|
|
}
|
|
|
|
private suspend fun runMetadataBatching() = coroutineScope {
|
|
nostr.waitUntilInitialized()
|
|
val batch = mutableSetOf<PublicKey>()
|
|
val timeout = 500L
|
|
|
|
while (true) {
|
|
val firstKey = metadataRequestChannel.receive()
|
|
batch.add(firstKey)
|
|
val lastFlushTime = Clock.System.now().toEpochMilliseconds()
|
|
|
|
while (batch.isNotEmpty()) {
|
|
val nextKey = withTimeoutOrNull(timeout.milliseconds) {
|
|
metadataRequestChannel.receive()
|
|
}
|
|
if (nextKey != null) batch.add(nextKey)
|
|
val now = Clock.System.now().toEpochMilliseconds()
|
|
|
|
if (batch.size >= 10 || (now - lastFlushTime) >= timeout || nextKey == null) {
|
|
val keysToRequest = batch.toList()
|
|
batch.clear()
|
|
nostr.profiles.fetchMetadataBatch(keysToRequest)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun updateProfile(
|
|
name: String? = null,
|
|
bio: String? = null,
|
|
picture: ByteArray? = null,
|
|
contentType: String? = "image/jpeg"
|
|
) {
|
|
appViewModel.setBusy(true)
|
|
try {
|
|
val avatarUrl = picture?.let { appViewModel.blossomUpload(it, contentType) }
|
|
val newMetadata = nostr.profiles.updateProfile(name, bio, avatarUrl)
|
|
val currentUser = nostr.signer.getPublicKeyAsync() ?: throw Exception("User not found")
|
|
|
|
updateMetadata(currentUser, newMetadata)
|
|
} catch (e: Exception) {
|
|
appViewModel.showError("Error: ${e.message}")
|
|
} finally {
|
|
appViewModel.setBusy(false)
|
|
}
|
|
}
|
|
|
|
suspend fun addContact(address: String): Boolean {
|
|
val pubkey = try {
|
|
if (address.contains("@")) {
|
|
nostr.profiles.searchByAddress(address)
|
|
} else {
|
|
PublicKey.parse(address)
|
|
}
|
|
} catch (e: Exception) {
|
|
appViewModel.showError("Invalid contact address: ${e.message}")
|
|
return false
|
|
}
|
|
|
|
if (pubkey in contactList.value) return true
|
|
|
|
return try {
|
|
val updated = contactList.value + pubkey
|
|
nostr.profiles.setContactList(updated.toList())
|
|
_contactList.update { it + pubkey }
|
|
true
|
|
} catch (e: Exception) {
|
|
appViewModel.showError("Error: ${e.message}")
|
|
false
|
|
}
|
|
}
|
|
|
|
fun removeContact(publicKey: PublicKey) {
|
|
viewModelScope.launch {
|
|
if (publicKey !in contactList.value) return@launch
|
|
try {
|
|
val updated = contactList.value - publicKey
|
|
nostr.profiles.setContactList(updated.toList())
|
|
_contactList.update { it - publicKey }
|
|
} catch (e: Exception) {
|
|
appViewModel.showError("Error: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun searchByAddress(query: String): PublicKey? {
|
|
try {
|
|
return nostr.profiles.searchByAddress(query)
|
|
} catch (e: Exception) {
|
|
appViewModel.showError("Error: ${e.message}")
|
|
}
|
|
return null
|
|
}
|
|
|
|
suspend fun searchByNostr(query: String): List<PublicKey> {
|
|
try {
|
|
return nostr.profiles.searchByNostr(query)
|
|
} catch (e: Exception) {
|
|
appViewModel.showError("Error: ${e.message}")
|
|
}
|
|
return emptyList()
|
|
}
|
|
|
|
suspend fun verifyActivity(pubkey: PublicKey): Timestamp? {
|
|
return try {
|
|
nostr.profiles.verifyActivity(pubkey)
|
|
} catch (e: Exception) {
|
|
appViewModel.showError("Error: ${e.message}")
|
|
null
|
|
}
|
|
}
|
|
|
|
suspend fun verifyContact(pubkey: PublicKey): Boolean {
|
|
return try {
|
|
nostr.profiles.verifyContact(pubkey)
|
|
} catch (e: Exception) {
|
|
appViewModel.showError("Error: ${e.message}")
|
|
false
|
|
}
|
|
}
|
|
|
|
suspend fun mutualContacts(pubkey: PublicKey): Set<PublicKey> {
|
|
return try {
|
|
nostr.profiles.mutualContacts(pubkey)
|
|
} catch (e: Exception) {
|
|
appViewModel.showError("Error: ${e.message}")
|
|
setOf()
|
|
}
|
|
}
|
|
|
|
fun getUserMetadata() {
|
|
viewModelScope.launch {
|
|
nostr.profiles.getUserMetadata()
|
|
}
|
|
}
|
|
}
|