This commit is contained in:
2026-07-15 17:57:55 +07:00
parent fcc30157bb
commit 2d830b778f
8 changed files with 50 additions and 25 deletions

View File

@@ -98,9 +98,9 @@ import su.reya.coop.RoomKind
import su.reya.coop.RoomUiState import su.reya.coop.RoomUiState
import su.reya.coop.Screen import su.reya.coop.Screen
import su.reya.coop.ago import su.reya.coop.ago
import su.reya.coop.flow
import su.reya.coop.shared.Avatar import su.reya.coop.shared.Avatar
import su.reya.coop.shared.getExpressiveFontFamily import su.reya.coop.shared.getExpressiveFontFamily
import su.reya.coop.uiStateFlow
import su.reya.coop.viewmodel.AccountViewModel import su.reya.coop.viewmodel.AccountViewModel
import su.reya.coop.viewmodel.ChatViewModel import su.reya.coop.viewmodel.ChatViewModel
@@ -620,9 +620,9 @@ fun NewRequests(requests: List<Room>) {
val firstRoom = requests.getOrNull(0) val firstRoom = requests.getOrNull(0)
val secondRoom = requests.getOrNull(1) val secondRoom = requests.getOrNull(1)
val firstRoomState by (firstRoom as Room).flow(profileCache) val firstRoomState by (firstRoom as Room).uiStateFlow(profileCache)
.collectAsStateWithLifecycle(RoomUiState()) .collectAsStateWithLifecycle(RoomUiState())
val secondRoomState by (secondRoom ?: firstRoom).flow(profileCache) val secondRoomState by (secondRoom ?: firstRoom).uiStateFlow(profileCache)
.collectAsStateWithLifecycle(RoomUiState()) .collectAsStateWithLifecycle(RoomUiState())
val supportingText = when { val supportingText = when {
@@ -695,7 +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.flow(profileCache).collectAsStateWithLifecycle(RoomUiState()) val roomState by room.uiStateFlow(profileCache).collectAsStateWithLifecycle(RoomUiState())
ListItem( ListItem(
modifier = Modifier.clickable(onClick = onClick), modifier = Modifier.clickable(onClick = onClick),

View File

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

View File

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

View File

@@ -73,6 +73,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 +227,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 +252,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)
} }
@@ -340,6 +346,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)
} }

View File

@@ -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()

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ import kotlin.time.Duration.Companion.milliseconds
class ProfileCache( class ProfileCache(
private val nostr: Nostr, private val nostr: Nostr,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default, defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
) : ErrorHost by createErrorHost() { ) : ErrorHost by createErrorHost() {
private val scope = CoroutineScope(SupervisorJob() + defaultDispatcher) private val scope = CoroutineScope(SupervisorJob() + defaultDispatcher)
private val profiles = MutableStateFlow<Map<PublicKey, MutableStateFlow<Profile?>>>(emptyMap()) private val profiles = MutableStateFlow<Map<PublicKey, MutableStateFlow<Profile?>>>(emptyMap())