List actual profiles excluding those that are participants in the chat.

This commit is contained in:
Kgothatso Ngako
2026-07-10 18:43:14 +02:00
parent 23d9e92675
commit 6c5c6bd607
8 changed files with 109 additions and 116 deletions

View File

@@ -4,22 +4,32 @@ import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.Query
import androidx.room3.Upsert
import at.torch.compose.database.GENESIS_AT
import at.torch.compose.database.model.Profile
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.flow.Flow
import kotlin.time.Instant
@Dao
interface ProfileDao {
@Query("SELECT * FROM Profile WHERE createdAt > :createdAt LIMIT :limit")
fun getAllProfiles(limit: Int = 21, createdAt: Instant = _root_ide_package_.at.torch.compose.database.GENESIS_AT): List<at.torch.compose.database.model.Profile>
fun getAllProfiles(limit: Int = 21, createdAt: Instant = GENESIS_AT): List<Profile>
@Query("SELECT * FROM Profile WHERE publicKey NOT IN (:excludedPublicKeys) AND createdAt > :createdAt LIMIT :limit")
fun getAllProfilesExcludingOnesWithThesePublicKeys(
excludedPublicKeys: Array<HexKey>,
limit: Int = 21, createdAt: Instant = GENESIS_AT
): List<Profile>
@Query("SELECT * FROM NostrEvent WHERE kind = 0 AND content LIKE '%' || :searchTerm || '%' LIMIT :limit")
fun filterProfiles(searchTerm: String, limit: Int = 21): List<at.torch.compose.database.model.intermdiate.LocalProfile>
@Query("SELECT * FROM Profile WHERE publicKey = :publicKey")
fun getProfileByPublicKey(publicKey: String): at.torch.compose.database.model.Profile?
fun getProfileByPublicKey(publicKey: String): Profile?
@Query("SELECT * FROM Profile WHERE publicKey IN (:publicKeys)")
fun getProfileByPublicKeys(publicKeys: List<String>): List<at.torch.compose.database.model.Profile>
fun getProfileByPublicKeys(publicKeys: List<String>): List<Profile>
@Query("SELECT * FROM NostrEvent WHERE kind = 0 AND pubKey = :publicKey")
fun observeProfileWithFollowersByPublicKey(publicKey: String): Flow<at.torch.compose.database.model.intermdiate.LocalProfileWithFollowers?>
@@ -28,9 +38,9 @@ interface ProfileDao {
fun observeProfileWithFollowingByPublicKey(publicKey: String): Flow<at.torch.compose.database.model.intermdiate.LocalProfileWithFollowing?>
@Insert
suspend fun insertPlaceholderProfile(profile: at.torch.compose.database.model.Profile)
suspend fun insertPlaceholderProfile(profile: Profile)
@Upsert
suspend fun upsert(profile: at.torch.compose.database.model.Profile)
suspend fun upsert(profile: Profile)
}

View File

@@ -2,7 +2,6 @@ package at.torch.compose.database.repository
import at.torch.compose.database.GENESIS_AT
import at.torch.compose.database.model.BroadcastNostrEventRequest
import at.torch.compose.database.model.ChatMessageBroadcastNostrEventRequestRelation
import at.torch.compose.database.model.NostrEvent
import at.torch.compose.database.model.UnsignedNostrEvent
import at.torch.compose.database.model.intermdiate.LocalAccount
@@ -734,8 +733,10 @@ class DatabaseNostrRepository(
return database.nostrEventDao().observeNostrEventById(nostrEventId)
}
override suspend fun searchableProfiles(): List<at.torch.compose.database.model.Profile> {
return database.profileDao().getAllProfiles()
override suspend fun searchableProfiles(excludingPublicKeys: Array<HexKey>): List<at.torch.compose.database.model.Profile> {
return database.profileDao().getAllProfilesExcludingOnesWithThesePublicKeys(
excludedPublicKeys = excludingPublicKeys
)
}
override suspend fun filterProfiles(searchTerm: String): List<at.torch.compose.database.model.intermdiate.LocalProfile> {

View File

@@ -120,7 +120,9 @@ interface NostrRepository {
suspend fun observeLocalNostrEventById(nostrEventId: String): Flow<LocalNostrEvent?>
suspend fun searchableProfiles(): List<Profile>
suspend fun searchableProfiles(
excludingPublicKeys: Array<HexKey> = emptyArray()
): List<Profile>
suspend fun filterProfiles(searchTerm: String): List<LocalProfile>
@@ -292,7 +294,7 @@ interface NostrRepository {
TODO("Not yet implemented")
}
override suspend fun searchableProfiles(): List<Profile> {
override suspend fun searchableProfiles(excludingPublicKeys: Array<HexKey>): List<Profile> {
TODO("Not yet implemented")
}

View File

@@ -10,16 +10,20 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
@@ -34,16 +38,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import at.torch.compose.database.model.ChatRoom
import at.torch.compose.database.model.Profile
import at.torch.compose.database.model.intermdiate.LocalChatRoom
import at.torch.compose.repository.ChatRepository
import at.torch.compose.repository.NostrRepository
import at.torch.compose.ui.composable.navigation.routes.AddMemberToChatRoomConfirmationRoute
import at.torch.compose.ui.composable.navigation.routes.Route
import at.torch.compose.ui.composable.widgets.LoadingDataIndicator
import at.torch.compose.ui.composable.widgets.profile.ProfileAvatar
import at.torch.compose.ui.theme.TorchTheme
import at.torch.compose.ui.view.model.ChatRoomDetailMessageListViewModel
import at.torch.compose.ui.view.model.SearchMemberToAddToChatRoomViewModel
@@ -118,21 +125,70 @@ fun SearchMemberToAddToChatRoomScreen(
modifier = Modifier.padding(innerPadding).fillMaxSize()
) {
Column(
modifier = Modifier.weight(1f).fillMaxWidth()
modifier = Modifier.weight(1f).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
key(true) {
Button(
onClick = {
onNavigateToRoute.invoke(
AddMemberToChatRoomConfirmationRoute(
activeUserPublicKey = activeUserPublicKey,
chatRoomId = chatRoomId,
relayHint = relayHint
)
)
}
if (searchMemberToAddToChatRoomUIState.contacts.isEmpty()) {
Spacer(
modifier = Modifier.height(20.dp)
)
Text(
modifier = Modifier.padding(20.dp),
text = "Currently no contacts. Please search and chat with a few people.",
textAlign = TextAlign.Center
)
Spacer(
modifier = Modifier.height(20.dp)
)
} else {
LazyColumn(
modifier = Modifier.fillMaxWidth().padding(5.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
reverseLayout = true
) {
Text("Contact List TODO")
items(
items = searchMemberToAddToChatRoomUIState.contacts,
key = { it.publicKey }
) { profile ->
Card(
onClick = {
onNavigateToRoute.invoke(
AddMemberToChatRoomConfirmationRoute(
activeUserPublicKey = activeUserPublicKey,
chatRoomId = chatRoomId,
profilePublicKey = profile.publicKey,
relayHint = relayHint,
)
)
}
) {
ListItem(
leadingContent = {
ProfileAvatar(
profile = profile
)
},
headlineContent = {
Text(
profile.humanReadableNameOrPubkey()
)
},
supportingContent = {
profile.about?.let {
Text(
text = it,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
}
)
}
}
}
}
}
@@ -140,8 +196,6 @@ fun SearchMemberToAddToChatRoomScreen(
key(true) {
chatRoomDetailMessageListViewModel.initiate()
}
// TODO: Check that we have direct message relays for this user...
}
}
}
@@ -207,6 +261,14 @@ private fun ChatRoomDetailScreenPreview() {
initialGiftWrapPayloadId = "sdfaer",
mlsGroupState = null
),
),
contacts = listOf(
Profile(
publicKey = "hex",
displayName = "Man With A Plan",
about = "Making plans and getting things done...",
nostrEventId = "nostrEventId"
)
)
),
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,

View File

@@ -1,10 +1,12 @@
package at.torch.compose.ui.composable.navigation.routes
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.serialization.Serializable
@Serializable
data class AddMemberToChatRoomConfirmationRoute(
val activeUserPublicKey: String,
val chatRoomId: String, // TODO: have this as a publicKey
val profilePublicKey: HexKey,
val relayHint: String?
): Route()

View File

@@ -32,8 +32,6 @@ class AddMemberToChatRoomConfirmationViewModel(
var addMemberToChatRoomConfirmationUIState: AddMemberToChatRoomConfirmationUIState by mutableStateOf(initialAddMemberToChatRoomConfirmationUIState)
private set
private val logger = Logger.withTag(TAG)
val isActionPending: MutableState<Boolean> = mutableStateOf(false)

View File

@@ -11,8 +11,6 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import at.torch.compose.repository.ChatRepository
import at.torch.compose.repository.NostrRepository
import at.torch.compose.ui.composable.navigation.routes.ChatRoomDetailRoute
import at.torch.compose.ui.composable.navigation.routes.Route
import at.torch.compose.ui.view.state.SearchMemberToAddToChatRoomUIState
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -49,97 +47,15 @@ class SearchMemberToAddToChatRoomViewModel(
SearchMemberToAddToChatRoomUIState.Error("Failed to load chat")
} else {
SearchMemberToAddToChatRoomUIState.Loaded(
localChatRoom = localChatRoom
localChatRoom = localChatRoom,
contacts = nostrRepository.searchableProfiles(
excludingPublicKeys = localChatRoom.localParticipants.map { it.participant.participantPublicKey }.toTypedArray()
)
)
}
}
}
fun initiateNewChat(
onNavigateToChat: (Route) -> Unit
) {
logger.d("initiateNewChat: $chatRoomId")
// Get ChatRoomById
viewModelScope.launch(Dispatchers.IO) {
// Check if we have a chat with activeUserPublicKey and chatRoomId (pubkey) as participants
val participants = chatRepository.getAllParticipantsWithPubKey(chatRoomId)
participants.forEach { participant ->
chatRepository.getChatRoomByIdentifier(participant.chatRoomId)?.let { localChatRoom ->
// TODO: Check that chatRoom is not a group chat...
if (localChatRoom.localParticipants.size == 1) {
// This is a chat with self...
// TODO: navigateToTheChat...
viewModelScope.launch(Dispatchers.Main) {
onNavigateToChat.invoke(
ChatRoomDetailRoute(
activeUserPublicKey = activeUserPublicKey,
chatRoomId = localChatRoom.chatRoom.id,
relayHint = relayHint
)
)
}
return@launch
} else if (localChatRoom.localParticipants.size == 2) {
val activeUserParticipant = localChatRoom.localParticipants.find { localParticipant -> localParticipant.participant.participantPublicKey == activeUserPublicKey }
if (activeUserParticipant != null) {
// We have found the chat we are looking for...
viewModelScope.launch(Dispatchers.Main) {
onNavigateToChat.invoke(
ChatRoomDetailRoute(
activeUserPublicKey = activeUserPublicKey,
chatRoomId = localChatRoom.chatRoom.id,
relayHint = relayHint
)
)
}
return@launch
}
}
}
}
val profile = chatRepository.getProfileWithPublicKey(chatRoomId)
if (profile != null) {
logger.d("Create chat with: $profile")
// Check if we have a keyPackageEvent...
val keyPackageEvent = chatRepository.getMarmotKeyPackageForPublicKey(chatRoomId)
if (keyPackageEvent == null) {
searchMemberToAddToChatRoomUIState = SearchMemberToAddToChatRoomUIState.Error("Missing Key Package") // TODO: Introduce missing key package UI State...
} else {
// Create new chat and invite local
try {
val chatRoomId = chatRepository.createMlsDirectMessage(
userPublicKey = activeUserPublicKey,
peerPublicKey = chatRoomId,
peerKeyPackage = keyPackageEvent,
)
onNavigateToChat.invoke(
ChatRoomDetailRoute(
activeUserPublicKey = activeUserPublicKey,
chatRoomId = chatRoomId,
relayHint = relayHint
)
)
} catch (e: Throwable) {
searchMemberToAddToChatRoomUIState = SearchMemberToAddToChatRoomUIState.Error("Failed to create chat")
}
}
} else {
searchMemberToAddToChatRoomUIState = SearchMemberToAddToChatRoomUIState.Error("Couldn't find profile...")
}
}
}
companion object {
private const val TAG = "SearchMemberToAddToChatRoomViewModel"

View File

@@ -1,10 +1,12 @@
package at.torch.compose.ui.view.state
import at.torch.compose.database.model.Profile
import at.torch.compose.database.model.intermdiate.LocalChatRoom
sealed interface SearchMemberToAddToChatRoomUIState {
data class Loaded(
val localChatRoom: LocalChatRoom
val localChatRoom: LocalChatRoom,
val contacts: List<Profile>
): SearchMemberToAddToChatRoomUIState
data class Error(