3 Commits

Author SHA1 Message Date
8ea66d1769 refactor 2026-07-09 17:28:37 +07:00
38b704fe18 improve initial loading 2026-07-09 16:44:11 +07:00
e0701504aa remove unnecessary query call 2026-07-09 16:12:38 +07:00
5 changed files with 51 additions and 71 deletions

View File

@@ -155,7 +155,7 @@ fun HomeScreen() {
onPauseOrDispose { } onPauseOrDispose { }
} }
LaunchedEffect(Unit) { LaunchedEffect(authState.signerRequired) {
chatViewModel.refreshChatRooms() chatViewModel.refreshChatRooms()
} }

View File

@@ -41,8 +41,7 @@ data class Room(
} }
companion object { companion object {
fun new(rumor: UnsignedEvent, userPubkey: PublicKey): Room { fun new(rumor: UnsignedEvent, userPubkey: PublicKey, id: Long = rumor.roomId()): Room {
val id = rumor.roomId()
val createdAt = rumor.createdAt() val createdAt = rumor.createdAt()
val subject = rumor.tags().toVec().find { it.kind() == "subject" }?.content() val subject = rumor.tags().toVec().find { it.kind() == "subject" }?.content()

View File

@@ -164,38 +164,35 @@ class MessageManager(private val nostr: Nostr) {
try { try {
val userPubkey = val userPubkey =
signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in") signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")
val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA) val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA)
val kTag = SingleLetterTag.lowercase(Alphabet.K) val kTag = SingleLetterTag.lowercase(Alphabet.K)
// Get all DM events // Get all DM events
val filter = Filter().kind(kind).customTags(kTag, listOf("14", "dm")) val filter = Filter().kind(kind).customTags(kTag, listOf("14", "dm"))
val events = client?.database()?.query(filter) val events = client?.database()?.query(filter)?.toVec() ?: return null
// Collect rooms // Collect rooms
val roomsMap: MutableMap<Long, Room> = mutableMapOf() val roomsMap: MutableMap<Long, Room> = mutableMapOf()
events events
?.toVec() .map { UnsignedEvent.fromJson(it.content()) }
?.map { UnsignedEvent.fromJson(it.content()) } .filter { it.tags().publicKeys().isNotEmpty() }
?.filter { it.tags().publicKeys().isNotEmpty() } .forEach { rumor ->
?.forEach { event -> val id = rumor.roomId()
val newRoom = Room.new(rumor = event, userPubkey = userPubkey) val isFromMe = rumor.author() == userPubkey
val existingRoom = roomsMap[newRoom.id] val existing = roomsMap[id]
val createdAt = rumor.createdAt()
// Check if the room already exists // If the room is new or the current rumor is newer than the existing one
if (existingRoom == null || newRoom.createdAt.asSecs() > existingRoom.createdAt.asSecs()) { if (existing == null || createdAt.asSecs() > existing.createdAt.asSecs()) {
val rTag = SingleLetterTag.lowercase(Alphabet.R) // A room is "Ongoing" if it was already marked as such or if the current rumor is from the user
val filter = Filter().kind(kind).pubkey(userPubkey) val isOngoing = (existing?.kind == RoomKind.Ongoing) || isFromMe
.customTag(rTag, newRoom.id.toString()) val room = Room.new(rumor = rumor, userPubkey = userPubkey, id = id)
roomsMap[id] = if (isOngoing) room.copy(kind = RoomKind.Ongoing) else room
// Determine if it's an ongoing room } else if (isFromMe && existing.kind != RoomKind.Ongoing) {
val isOngoing = // If it's an older rumor but sent by the user, mark the room as Ongoing
client?.database()?.query(filter)?.toVec()?.isNotEmpty() ?: false roomsMap[id] = existing.copy(kind = RoomKind.Ongoing)
// Append room to map
roomsMap[newRoom.id] =
if (isOngoing) newRoom.copy(kind = RoomKind.Ongoing) else newRoom
} }
} }

View File

@@ -24,7 +24,7 @@ import su.reya.coop.repository.MediaRepository
import su.reya.coop.roomId import su.reya.coop.roomId
data class ChatState( data class ChatState(
val rooms: Set<Room> = emptySet(), val rooms: Map<Long, Room> = emptyMap(),
val isSyncing: Boolean = false, val isSyncing: Boolean = false,
val isPartialProcessedGiftWrap: Boolean = false, val isPartialProcessedGiftWrap: Boolean = false,
) )
@@ -48,8 +48,8 @@ class ChatViewModel(private val nostr: Nostr) : BaseViewModel() {
private val _sentReports = MutableSharedFlow<Map<EventId, List<RelayUrl>>>() private val _sentReports = MutableSharedFlow<Map<EventId, List<RelayUrl>>>()
val sentReport = _sentReports.asSharedFlow() val sentReport = _sentReports.asSharedFlow()
val chatRooms = state.map { it.rooms } val chatRooms = state.map { it.rooms.values.sortedByDescending { it.createdAt.asSecs() } }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptySet()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
val isSyncing = state.map { it.isSyncing } val isSyncing = state.map { it.isSyncing }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
@@ -69,8 +69,8 @@ class ChatViewModel(private val nostr: Nostr) : BaseViewModel() {
_state.update { it.copy(isPartialProcessedGiftWrap = true) } _state.update { it.copy(isPartialProcessedGiftWrap = true) }
} }
// Refresh UI every 10 messages OR when sync is fully done // Refresh UI every 100 messages OR when sync is fully done
if (syncState.processedCount % 10 == 0 || !syncState.isSyncing) { if (syncState.processedCount % 100 == 0 || !syncState.isSyncing) {
refreshChatRooms() refreshChatRooms()
} }
} }
@@ -80,16 +80,12 @@ class ChatViewModel(private val nostr: Nostr) : BaseViewModel() {
launch { launch {
nostr.newEvents.collect { event -> nostr.newEvents.collect { event ->
val roomId = event.roomId() val roomId = event.roomId()
val existingRoom = _state.value.rooms.firstOrNull { it.id == roomId } val existingRoom = _state.value.rooms[roomId]
if (existingRoom == null) { if (existingRoom == null) {
val currentUser = nostr.signer.getPublicKeyAsync() ?: return@collect val currentUser = nostr.signer.getPublicKeyAsync() ?: return@collect
val newRoom = Room.new(event, currentUser) val newRoom = Room.new(event, currentUser)
_state.update { _state.update { it.copy(rooms = it.rooms + (newRoom.id to newRoom)) }
it.copy(
rooms = (it.rooms + newRoom).sortedDescending().toSet()
)
}
} else { } else {
updateRoomList(roomId, event) updateRoomList(roomId, event)
} }
@@ -97,9 +93,6 @@ class ChatViewModel(private val nostr: Nostr) : BaseViewModel() {
_newEvents.emit(event) _newEvents.emit(event)
} }
} }
// Initial load of rooms
refreshChatRooms()
} }
} }
@@ -120,7 +113,7 @@ class ChatViewModel(private val nostr: Nostr) : BaseViewModel() {
// Check if the room already exists // Check if the room already exists
val id = rumor.roomId() val id = rumor.roomId()
val existingRoom = _state.value.rooms.firstOrNull { it.id == id } val existingRoom = _state.value.rooms[id]
// If the room already exists, return its ID // If the room already exists, return its ID
if (existingRoom != null) { if (existingRoom != null) {
@@ -131,7 +124,7 @@ class ChatViewModel(private val nostr: Nostr) : BaseViewModel() {
val room = Room.new(rumor, currentUser) val room = Room.new(rumor, currentUser)
// Update the chat rooms state // Update the chat rooms state
_state.update { it.copy(rooms = (it.rooms + room).sortedDescending().toSet()) } _state.update { it.copy(rooms = it.rooms + (room.id to room)) }
return room.id return room.id
} catch (e: Exception) { } catch (e: Exception) {
@@ -140,20 +133,16 @@ class ChatViewModel(private val nostr: Nostr) : BaseViewModel() {
} }
fun getChatRoom(id: Long): Room? { fun getChatRoom(id: Long): Room? {
return _state.value.rooms.firstOrNull { it.id == id } return _state.value.rooms[id]
} }
suspend fun refreshChatRooms() { suspend fun refreshChatRooms() {
try { try {
val rooms = nostr.messages.getChatRooms() ?: emptySet() val rooms = nostr.messages.getChatRooms() ?: emptySet()
_state.update { currentState -> _state.update { currentState ->
val merged = currentState.rooms.associateBy { it.id }.toMutableMap() val newMap = currentState.rooms.toMutableMap()
// Add or update rooms from the database rooms.forEach { room -> newMap[room.id] = room }
rooms.forEach { room -> currentState.copy(rooms = newMap)
merged[room.id] = room
}
// Return as a sorted set to maintain UI consistency
currentState.copy(rooms = merged.values.sortedDescending().toSet())
} }
} catch (e: Exception) { } catch (e: Exception) {
showError("Error: ${e.message}") showError("Error: ${e.message}")
@@ -235,24 +224,19 @@ class ChatViewModel(private val nostr: Nostr) : BaseViewModel() {
private fun updateRoomList(roomId: Long, newMessage: UnsignedEvent) { private fun updateRoomList(roomId: Long, newMessage: UnsignedEvent) {
_state.update { currentState -> _state.update { currentState ->
val updatedRooms = currentState.rooms.map { room -> val room = currentState.rooms[roomId] ?: return@update currentState
if (room.id == roomId) { val updatedRoom = room.copy(
room.copy( lastMessage = newMessage.content(),
lastMessage = newMessage.content(), createdAt = newMessage.createdAt()
createdAt = newMessage.createdAt() )
) currentState.copy(rooms = currentState.rooms + (roomId to updatedRoom))
} else {
room
}
}.sortedDescending().toSet()
currentState.copy(rooms = updatedRooms)
} }
} }
fun resetInternalState() { fun resetInternalState() {
_state.update { _state.update {
it.copy( it.copy(
rooms = emptySet(), rooms = emptyMap(),
isPartialProcessedGiftWrap = false, isPartialProcessedGiftWrap = false,
) )
} }

View File

@@ -71,7 +71,7 @@ class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>() private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>()
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED) private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
private val seenPublicKeys = mutableSetOf<PublicKey>() private val seenPublicKeys = mutableSetOf<PublicKey>()
val isRelayListEmpty = appState.map { it.isRelayListEmpty } val isRelayListEmpty = appState.map { it.isRelayListEmpty }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
@@ -158,12 +158,14 @@ class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
viewModelScope.launch { viewModelScope.launch {
// Wait until the client is ready // Wait until the client is ready
nostr.waitUntilInitialized() nostr.waitUntilInitialized()
val cache = nostr.profiles.getAllCacheMetadata()
nostr.profiles.getAllCacheMetadata().forEach { (pubkey, metadata) -> profilesMutex.withLock {
// Update the metadata state cache.forEach { (pubkey, metadata) ->
updateMetadata(pubkey, Profile(pubkey, metadata)) val profile = Profile(pubkey, metadata)
// Update seenPublicKeys to avoid duplicate requests profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
seenPublicKeys.add(pubkey) seenPublicKeys.add(pubkey)
}
} }
} }
} }
@@ -196,11 +198,9 @@ class NostrViewModel(private val nostr: Nostr) : BaseViewModel() {
} }
} }
private fun updateMetadata(pubkey: PublicKey, profile: Profile) { private suspend fun updateMetadata(pubkey: PublicKey, profile: Profile) {
viewModelScope.launch { profilesMutex.withLock {
profilesMutex.withLock { profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
}
} }
} }