Compare commits
2 Commits
2d830b778f
...
v0.2.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 47196dbea4 | |||
| 30191170f1 |
@@ -69,7 +69,7 @@ android {
|
|||||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.2.4"
|
versionName = "0.2.5"
|
||||||
}
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
|
|||||||
@@ -695,8 +695,7 @@ fun NewRequests(requests: List<Room>) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
||||||
val profileCache = LocalProfileCache.current
|
val profileCache = LocalProfileCache.current
|
||||||
val roomState by room.uiStateFlow(profileCache)
|
val roomState by room.uiStateFlow(profileCache).collectAsStateWithLifecycle(RoomUiState())
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(onClick = onClick),
|
modifier = Modifier.clickable(onClick = onClick),
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ fun ContactListItem(
|
|||||||
supportingContent = { Text(text = pubkey.short()) },
|
supportingContent = { Text(text = pubkey.short()) },
|
||||||
content = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = profile?.name ?: "",
|
text = profile?.name ?: "Unknown",
|
||||||
style = MaterialTheme.typography.titleMediumEmphasized,
|
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,12 +113,7 @@ fun UnsignedEvent.roomId(): Long {
|
|||||||
pubkeys.add(this.author())
|
pubkeys.add(this.author())
|
||||||
pubkeys.addAll(this.tags().publicKeys())
|
pubkeys.addAll(this.tags().publicKeys())
|
||||||
|
|
||||||
// Sort and hash the list of public keys
|
return pubkeys.map { it.toBech32() }.distinct().sorted().hashCode().toLong()
|
||||||
val sortedUniqueKeys = pubkeys
|
|
||||||
.distinctBy { it.toBech32() }
|
|
||||||
.sortedBy { it.toBech32() }
|
|
||||||
|
|
||||||
return sortedUniqueKeys.hashCode().toLong()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Timestamp.formatAsTime(): String {
|
fun Timestamp.formatAsTime(): String {
|
||||||
|
|||||||
@@ -39,12 +39,11 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
private val client: Client? get() = nostr.client
|
private val client: Client? get() = nostr.client
|
||||||
private val signer: UniversalSigner get() = nostr.signer
|
private val signer: UniversalSigner get() = nostr.signer
|
||||||
|
|
||||||
val sentEvents: MutableMap<EventId, List<RelayUrl>> = mutableMapOf()
|
|
||||||
val rumorMap: MutableMap<EventId, EventId> = mutableMapOf()
|
|
||||||
|
|
||||||
private val _messageSyncState = MutableStateFlow(MessageSyncState())
|
private val _messageSyncState = MutableStateFlow(MessageSyncState())
|
||||||
val messageSyncState = _messageSyncState.asStateFlow()
|
val messageSyncState = _messageSyncState.asStateFlow()
|
||||||
|
|
||||||
|
val rumorMap: MutableMap<EventId, EventId> = mutableMapOf()
|
||||||
|
|
||||||
fun updateSyncState(update: (MessageSyncState) -> MessageSyncState) {
|
fun updateSyncState(update: (MessageSyncState) -> MessageSyncState) {
|
||||||
_messageSyncState.update(update)
|
_messageSyncState.update(update)
|
||||||
}
|
}
|
||||||
@@ -73,6 +72,8 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
target = ReqTarget.manual(target),
|
target = ReqTarget.manual(target),
|
||||||
id = "gift-wraps"
|
id = "gift-wraps"
|
||||||
)
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to fetch user messages: ${e.message}", e)
|
throw IllegalStateException("Failed to fetch user messages: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -225,6 +226,8 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
// Filter out events without public keys (receivers)
|
// Filter out events without public keys (receivers)
|
||||||
?.filter { it.tags().publicKeys().isNotEmpty() }
|
?.filter { it.tags().publicKeys().isNotEmpty() }
|
||||||
?.sortedByDescending { it.createdAt().asSecs() } ?: emptyList()
|
?.sortedByDescending { it.createdAt().asSecs() } ?: emptyList()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to get chat room messages: ${e.message}", e)
|
throw IllegalStateException("Failed to get chat room messages: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -248,6 +251,8 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
connectMsgRelays(event)
|
connectMsgRelays(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to fetch relays: ${e.message}", e)
|
throw IllegalStateException("Failed to fetch relays: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -327,9 +332,6 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (output != null) {
|
if (output != null) {
|
||||||
// Keep track of sent events
|
|
||||||
sentEvents[output.id] = emptyList()
|
|
||||||
|
|
||||||
// Keep track of rumor IDs
|
// Keep track of rumor IDs
|
||||||
val id = rumor.id() ?: throw IllegalStateException("Rumor ID is null")
|
val id = rumor.id() ?: throw IllegalStateException("Rumor ID is null")
|
||||||
rumorMap[id] = output.id
|
rumorMap[id] = output.id
|
||||||
@@ -340,6 +342,8 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to send message: ${e.message}", e)
|
throw IllegalStateException("Failed to send message: ${e.message}", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import rust.nostr.sdk.AsyncNostrSigner
|
import rust.nostr.sdk.AsyncNostrSigner
|
||||||
import rust.nostr.sdk.Client
|
import rust.nostr.sdk.Client
|
||||||
import rust.nostr.sdk.ClientBuilder
|
import rust.nostr.sdk.ClientBuilder
|
||||||
@@ -108,14 +108,6 @@ class Nostr(
|
|||||||
relays.connectBootstrapRelays()
|
relays.connectBootstrapRelays()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reconnect() {
|
|
||||||
relays.reconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun disconnect() {
|
|
||||||
relays.disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun prune() {
|
suspend fun prune() {
|
||||||
try {
|
try {
|
||||||
client?.database()?.wipe()
|
client?.database()?.wipe()
|
||||||
@@ -182,8 +174,6 @@ class Nostr(
|
|||||||
|
|
||||||
when (notification) {
|
when (notification) {
|
||||||
is ClientNotification.Message -> {
|
is ClientNotification.Message -> {
|
||||||
val relayUrl = notification.relayUrl
|
|
||||||
|
|
||||||
when (val message = notification.message.asEnum()) {
|
when (val message = notification.message.asEnum()) {
|
||||||
is RelayMessageEnum.EventMsg -> {
|
is RelayMessageEnum.EventMsg -> {
|
||||||
val event = message.event
|
val event = message.event
|
||||||
@@ -237,14 +227,6 @@ class Nostr(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is RelayMessageEnum.Ok -> {
|
|
||||||
if (messages.sentEvents.containsKey(message.eventId)) {
|
|
||||||
val currentRelays =
|
|
||||||
messages.sentEvents[message.eventId] ?: emptyList()
|
|
||||||
messages.sentEvents[message.eventId] = currentRelays + relayUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
/* Ignore other message types */
|
/* Ignore other message types */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import rust.nostr.sdk.ReqTarget
|
|||||||
import rust.nostr.sdk.SendEventTarget
|
import rust.nostr.sdk.SendEventTarget
|
||||||
import rust.nostr.sdk.SubscribeAutoCloseOptions
|
import rust.nostr.sdk.SubscribeAutoCloseOptions
|
||||||
import rust.nostr.sdk.Timestamp
|
import rust.nostr.sdk.Timestamp
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
class ProfileManager(private val nostr: Nostr) {
|
class ProfileManager(private val nostr: Nostr) {
|
||||||
@@ -80,6 +81,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
|
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
|
||||||
|
|
||||||
client?.subscribe(target = target, id = "user-metadata", closeOn = opts)
|
client?.subscribe(target = target, id = "user-metadata", closeOn = opts)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to fetch user metadata: ${e.message}", e)
|
throw IllegalStateException("Failed to fetch user metadata: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -92,6 +95,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val relays = RelayManager.BOOTSTRAP_RELAYS.map { RelayUrl.parse(it) }
|
val relays = RelayManager.BOOTSTRAP_RELAYS.map { RelayUrl.parse(it) }
|
||||||
|
|
||||||
client?.sync(filter, relays)
|
client?.sync(filter, relays)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to sync mutual contacts: ${e.message}")
|
println("Failed to sync mutual contacts: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -174,6 +179,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return newMetadata
|
return newMetadata
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to update identity: ${e.message}", e)
|
throw IllegalStateException("Failed to update identity: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -186,6 +193,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val event = client?.database()?.query(filter)?.first() ?: return null
|
val event = client?.database()?.query(filter)?.first() ?: return null
|
||||||
|
|
||||||
Metadata.fromJson(event.content())
|
Metadata.fromJson(event.content())
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to get latest metadata: ${e.message}")
|
println("Failed to get latest metadata: ${e.message}")
|
||||||
null
|
null
|
||||||
@@ -208,6 +217,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to get all cache metadata: ${e.message}")
|
println("Failed to get all cache metadata: ${e.message}")
|
||||||
return emptyMap()
|
return emptyMap()
|
||||||
@@ -232,6 +243,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client?.subscribe(target = ReqTarget.manual(target), closeOn = opts)
|
client?.subscribe(target = ReqTarget.manual(target), closeOn = opts)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to fetch metadata batch: ${e.message}", e)
|
throw IllegalStateException("Failed to fetch metadata batch: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -247,6 +260,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
target = SendEventTarget.broadcast(),
|
target = SendEventTarget.broadcast(),
|
||||||
ackPolicy = AckPolicy.none(),
|
ackPolicy = AckPolicy.none(),
|
||||||
)
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to set contact list: ${e.message}", e)
|
throw IllegalStateException("Failed to set contact list: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -258,6 +273,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val bodyString: String = response.body()
|
val bodyString: String = response.body()
|
||||||
|
|
||||||
return Nip05Profile.fromJson(address, bodyString)
|
return Nip05Profile.fromJson(address, bodyString)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to fetch profile from address: ${e.message}", e)
|
throw IllegalStateException("Failed to fetch profile from address: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -269,6 +286,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val profile = profileFromAddress(httpClient, address)
|
val profile = profileFromAddress(httpClient, address)
|
||||||
|
|
||||||
return profile.publicKey()
|
return profile.publicKey()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to search address: ${e.message}", e)
|
throw IllegalStateException("Failed to search address: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -304,6 +323,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to search nostr: ${e.message}", e)
|
throw IllegalStateException("Failed to search nostr: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -323,6 +344,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return events?.first()?.createdAt()
|
return events?.first()?.createdAt()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to get latest activity: ${e.message}", e)
|
throw IllegalStateException("Failed to get latest activity: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -340,6 +363,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val pubkeys = events?.first()?.tags()?.publicKeys() ?: listOf()
|
val pubkeys = events?.first()?.tags()?.publicKeys() ?: listOf()
|
||||||
|
|
||||||
return pubkeys.contains(pubkey)
|
return pubkeys.contains(pubkey)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to get mutual contacts: ${e.message}", e)
|
throw IllegalStateException("Failed to get mutual contacts: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -361,6 +386,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return contacts.toSet()
|
return contacts.toSet()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to get mutual contacts: ${e.message}", e)
|
throw IllegalStateException("Failed to get mutual contacts: ${e.message}", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class RelayManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setRelaylist(relays: Map<RelayUrl, RelayMetadata?>) {
|
suspend fun setRelayList(relays: Map<RelayUrl, RelayMetadata?>) {
|
||||||
try {
|
try {
|
||||||
val event = EventBuilder.relayList(relays).finalizeAsync(signer)
|
val event = EventBuilder.relayList(relays).finalizeAsync(signer)
|
||||||
|
|
||||||
|
|||||||
@@ -511,7 +511,7 @@ class AccountRepository(
|
|||||||
val relays = currentUserRelayListInternal().toMutableMap()
|
val relays = currentUserRelayListInternal().toMutableMap()
|
||||||
relays[relayUrl] = RelayMetadata.WRITE
|
relays[relayUrl] = RelayMetadata.WRITE
|
||||||
|
|
||||||
nostr.relays.setRelaylist(relays)
|
nostr.relays.setRelayList(relays)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -525,7 +525,7 @@ class AccountRepository(
|
|||||||
val relays = currentUserRelayListInternal().toMutableMap()
|
val relays = currentUserRelayListInternal().toMutableMap()
|
||||||
relays[relayUrl] = RelayMetadata.READ
|
relays[relayUrl] = RelayMetadata.READ
|
||||||
|
|
||||||
nostr.relays.setRelaylist(relays)
|
nostr.relays.setRelayList(relays)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -539,7 +539,7 @@ class AccountRepository(
|
|||||||
val relays = currentUserRelayListInternal().toMutableMap()
|
val relays = currentUserRelayListInternal().toMutableMap()
|
||||||
relays.remove(relayUrl)
|
relays.remove(relayUrl)
|
||||||
|
|
||||||
nostr.relays.setRelaylist(relays)
|
nostr.relays.setRelayList(relays)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,13 +51,16 @@ class ChatRepository(
|
|||||||
)
|
)
|
||||||
val newEvents = _newEvents.asSharedFlow()
|
val newEvents = _newEvents.asSharedFlow()
|
||||||
|
|
||||||
val chatRooms = state.map { it.rooms.values.sortedByDescending { it.createdAt.asSecs() } }
|
val chatRooms = state
|
||||||
|
.map { it.rooms.values.sortedByDescending { it.createdAt.asSecs() } }
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(5000), emptyList())
|
.stateIn(scope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||||
|
|
||||||
val isSyncing = nostr.messages.messageSyncState.map { it.isSyncing }
|
val isSyncing = nostr.messages.messageSyncState
|
||||||
|
.map { it.isSyncing }
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(5000), false)
|
.stateIn(scope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
val isPartialProcessedGiftWrap = state.map { it.isPartialProcessedGiftWrap }
|
val isPartialProcessedGiftWrap = state
|
||||||
|
.map { it.isPartialProcessedGiftWrap }
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(5000), false)
|
.stateIn(scope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -82,18 +85,7 @@ class ChatRepository(
|
|||||||
// Observe new messages
|
// Observe new messages
|
||||||
launch {
|
launch {
|
||||||
nostr.newEvents.collect { event ->
|
nostr.newEvents.collect { event ->
|
||||||
val roomId = event.roomId()
|
updateRoomState(event)
|
||||||
val existingRoom = _state.value.rooms[roomId]
|
|
||||||
|
|
||||||
if (existingRoom == null) {
|
|
||||||
val currentUser = nostr.signer.getPublicKeyAsync() ?: return@collect
|
|
||||||
val newRoom = Room.new(event, currentUser)
|
|
||||||
_state.update { it.copy(rooms = it.rooms + (newRoom.id to newRoom)) }
|
|
||||||
} else {
|
|
||||||
updateRoomList(roomId, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
_newEvents.tryEmit(event)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,9 +186,10 @@ class ChatRepository(
|
|||||||
content = message,
|
content = message,
|
||||||
subject = room.subject,
|
subject = room.subject,
|
||||||
replies = replies,
|
replies = replies,
|
||||||
onRumorCreated = { event ->
|
onRumorCreated = {
|
||||||
updateRoomList(roomId, event)
|
scope.launch(defaultDispatcher) {
|
||||||
scope.launch(defaultDispatcher) { _newEvents.tryEmit(event) }
|
updateRoomState(it, roomId)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -223,26 +216,32 @@ class ChatRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isMessageSent(id: EventId): Boolean {
|
private suspend fun updateRoomState(event: UnsignedEvent, roomId: Long = event.roomId()) {
|
||||||
val giftWrapId = nostr.messages.rumorMap[id]
|
val currentUser = nostr.signer.getPublicKeyAsync() ?: return
|
||||||
|
|
||||||
if (giftWrapId != null) {
|
|
||||||
val isSent = nostr.messages.sentEvents[giftWrapId]?.isNotEmpty() ?: false
|
|
||||||
return isSent
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateRoomList(roomId: Long, newMessage: UnsignedEvent) {
|
|
||||||
_state.update { currentState ->
|
_state.update { currentState ->
|
||||||
val room = currentState.rooms[roomId] ?: return@update currentState
|
val rooms = currentState.rooms.toMutableMap()
|
||||||
val updatedRoom = room.copy(
|
val existingRoom = rooms[roomId]
|
||||||
lastMessage = newMessage.content(),
|
|
||||||
createdAt = newMessage.createdAt()
|
if (existingRoom == null) {
|
||||||
|
// New room discovery
|
||||||
|
val newRoom = Room.new(event, currentUser, roomId)
|
||||||
|
rooms[newRoom.id] = newRoom
|
||||||
|
} else if (event.createdAt().asSecs() >= existingRoom.createdAt.asSecs()) {
|
||||||
|
// Only update preview if message is newer (handles sync/late arrivals)
|
||||||
|
rooms[roomId] = existingRoom.copy(
|
||||||
|
lastMessage = event.content(),
|
||||||
|
createdAt = event.createdAt()
|
||||||
)
|
)
|
||||||
currentState.copy(rooms = currentState.rooms + (roomId to updatedRoom))
|
} else {
|
||||||
|
// Don't update the room list state for older messages
|
||||||
|
return@update currentState
|
||||||
}
|
}
|
||||||
|
currentState.copy(rooms = rooms)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify subscribers about the new event (for the active chat screen)
|
||||||
|
_newEvents.tryEmit(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetInternalState() {
|
fun resetInternalState() {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import su.reya.coop.repository.AccountState
|
|||||||
class AccountViewModel(
|
class AccountViewModel(
|
||||||
private val repository: AccountRepository,
|
private val repository: AccountRepository,
|
||||||
) : ViewModel(), ErrorHost by repository {
|
) : ViewModel(), ErrorHost by repository {
|
||||||
|
|
||||||
val state: StateFlow<AccountState> = repository.state
|
val state: StateFlow<AccountState> = repository.state
|
||||||
val isUpdatingProfile: StateFlow<Boolean> = repository.isUpdatingProfile
|
val isUpdatingProfile: StateFlow<Boolean> = repository.isUpdatingProfile
|
||||||
val currentUserProfile: StateFlow<Profile?> = repository.currentUserProfile
|
val currentUserProfile: StateFlow<Profile?> = repository.currentUserProfile
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
package su.reya.coop.viewmodel
|
package su.reya.coop.viewmodel
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import su.reya.coop.Profile
|
import su.reya.coop.Profile
|
||||||
@@ -19,29 +18,43 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
|
|
||||||
class ProfileCache(
|
class ProfileCache(
|
||||||
private val nostr: Nostr,
|
private val nostr: Nostr,
|
||||||
|
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||||
) : ErrorHost by createErrorHost() {
|
) : ErrorHost by createErrorHost() {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + defaultDispatcher)
|
||||||
private val profilesMutex = Mutex()
|
private val profiles = MutableStateFlow<Map<PublicKey, MutableStateFlow<Profile?>>>(emptyMap())
|
||||||
private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>()
|
private val seenPublicKeys = MutableStateFlow<Set<PublicKey>>(emptySet())
|
||||||
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
||||||
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch { runObserver() }
|
|
||||||
scope.launch { runMetadataBatching() }
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
try {
|
||||||
|
runObserver()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Metadata observer failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
runMetadataBatching()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Metadata batching failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
nostr.waitUntilInitialized()
|
nostr.waitUntilInitialized()
|
||||||
loadCacheMetadata()
|
getCachedMetadata()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Failed to load initial cache: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun runObserver() = coroutineScope {
|
private suspend fun runObserver() {
|
||||||
launch {
|
|
||||||
nostr.profiles.metadataUpdates.collect { (pubkey, metadata) ->
|
nostr.profiles.metadataUpdates.collect { (pubkey, metadata) ->
|
||||||
updateMetadata(pubkey, Profile(pubkey, metadata))
|
updateMetadata(pubkey, Profile(pubkey, metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun runMetadataBatching() {
|
private suspend fun runMetadataBatching() {
|
||||||
nostr.waitUntilInitialized()
|
nostr.waitUntilInitialized()
|
||||||
@@ -57,37 +70,69 @@ class ProfileCache(
|
|||||||
batch.add(nextKey)
|
batch.add(nextKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
nostr.profiles.fetchMetadataBatch(batch.toList())
|
nostr.profiles.fetchMetadataBatch(batch.toList())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Allow these keys to be requested again since the fetch failed
|
||||||
|
seenPublicKeys.update { it - batch }
|
||||||
|
println("Failed to fetch metadata batch: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadCacheMetadata() {
|
private suspend fun getCachedMetadata() {
|
||||||
val cache = nostr.profiles.getAllCacheMetadata()
|
val cache = nostr.profiles.getAllCacheMetadata()
|
||||||
|
|
||||||
profilesMutex.withLock {
|
|
||||||
cache.forEach { (pubkey, metadata) ->
|
cache.forEach { (pubkey, metadata) ->
|
||||||
val profile = Profile(pubkey, metadata)
|
updateMetadata(pubkey, Profile(pubkey, metadata))
|
||||||
profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
|
|
||||||
seenPublicKeys.add(pubkey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestMetadata(pubkey: PublicKey) {
|
private fun requestMetadata(pubkey: PublicKey) {
|
||||||
if (seenPublicKeys.add(pubkey)) {
|
var added = false
|
||||||
metadataRequestChannel.trySend(pubkey)
|
seenPublicKeys.update { current ->
|
||||||
|
if (current.contains(pubkey)) {
|
||||||
|
added = false
|
||||||
|
current
|
||||||
|
} else {
|
||||||
|
added = true
|
||||||
|
current + pubkey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (added) metadataRequestChannel.trySend(pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun updateMetadata(pubkey: PublicKey, profile: Profile) {
|
private fun updateMetadata(pubkey: PublicKey, profile: Profile) {
|
||||||
profilesMutex.withLock {
|
profiles.update { current ->
|
||||||
profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
|
val flow = current[pubkey] ?: MutableStateFlow<Profile?>(null)
|
||||||
|
flow.value = profile
|
||||||
|
if (current.containsKey(pubkey)) current else current + (pubkey to flow)
|
||||||
}
|
}
|
||||||
|
seenPublicKeys.update { it + pubkey }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMetadata(pubkey: PublicKey): StateFlow<Profile?> {
|
fun getMetadata(pubkey: PublicKey): StateFlow<Profile?> {
|
||||||
val flow = profiles.getOrPut(pubkey) { MutableStateFlow(null) }
|
val currentMap = profiles.value
|
||||||
if (flow.value == null) requestMetadata(pubkey)
|
val existingFlow = currentMap[pubkey]
|
||||||
return flow.asStateFlow()
|
|
||||||
|
if (existingFlow != null) {
|
||||||
|
if (existingFlow.value == null) requestMetadata(pubkey)
|
||||||
|
return existingFlow.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
val newFlow = MutableStateFlow<Profile?>(null)
|
||||||
|
var resultFlow = newFlow
|
||||||
|
|
||||||
|
profiles.update { prev ->
|
||||||
|
if (prev.containsKey(pubkey)) {
|
||||||
|
resultFlow = prev[pubkey]!!
|
||||||
|
prev
|
||||||
|
} else {
|
||||||
|
prev + (pubkey to newFlow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultFlow.value == null) requestMetadata(pubkey)
|
||||||
|
|
||||||
|
return resultFlow.asStateFlow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user