List actual profiles excluding those that are participants in the chat.
This commit is contained in:
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user