Observe profile and sync metadata/keypackage event
This commit is contained in:
@@ -28,6 +28,9 @@ interface ProfileDao {
|
||||
@Query("SELECT * FROM Profile WHERE publicKey = :publicKey")
|
||||
fun getProfileByPublicKey(publicKey: String): Profile?
|
||||
|
||||
@Query("SELECT * FROM Profile WHERE publicKey = :publicKey")
|
||||
fun observeProfileByPublicKey(publicKey: String): Flow<Profile?>
|
||||
|
||||
@Query("SELECT * FROM Profile WHERE publicKey IN (:publicKeys)")
|
||||
fun getProfileByPublicKeys(publicKeys: List<String>): List<Profile>
|
||||
|
||||
|
||||
@@ -117,10 +117,6 @@ class DatabaseChatRepository(
|
||||
return database.marmotKeyPackageDao().getMarmotKeyPackageForPublicKey(publicKey)
|
||||
}
|
||||
|
||||
override suspend fun getProfileWithPublicKey(publicKey: HexKey): Profile? {
|
||||
return database.profileDao().getProfileByPublicKey(publicKey)
|
||||
}
|
||||
|
||||
override suspend fun createMlsDirectMessage(
|
||||
name: String?,
|
||||
description: String?,
|
||||
|
||||
@@ -3,6 +3,7 @@ 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.NostrEvent
|
||||
import at.torch.compose.database.model.Profile
|
||||
import at.torch.compose.database.model.UnsignedNostrEvent
|
||||
import at.torch.compose.database.model.intermdiate.LocalAccount
|
||||
import at.torch.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest
|
||||
@@ -65,10 +66,18 @@ class DatabaseNostrRepository(
|
||||
return database.unsignedNostrEventDao().getLocalAccounts()
|
||||
}
|
||||
|
||||
override suspend fun observeProfile(publicKey: HexKey): Flow<at.torch.compose.database.model.intermdiate.LocalAccount?> {
|
||||
override suspend fun observeLocalAccount(publicKey: HexKey): Flow<LocalAccount?> {
|
||||
return database.unsignedNostrEventDao().observeProfile(publicKey)
|
||||
}
|
||||
|
||||
override suspend fun getProfileWithPublicKey(publicKey: HexKey): Profile? {
|
||||
return database.profileDao().getProfileByPublicKey(publicKey)
|
||||
}
|
||||
|
||||
override suspend fun observeProfileWithPublicKey(publicKey: HexKey): Flow<Profile?> {
|
||||
return database.profileDao().observeProfileByPublicKey(publicKey)
|
||||
}
|
||||
|
||||
override suspend fun observeRelation(
|
||||
publicKey: HexKey,
|
||||
activePublicKey: HexKey
|
||||
|
||||
@@ -41,8 +41,6 @@ interface ChatRepository {
|
||||
|
||||
suspend fun getMarmotKeyPackageForPublicKey(publicKey: HexKey): MarmotKeyPackage?
|
||||
|
||||
suspend fun getProfileWithPublicKey(publicKey: HexKey): Profile?
|
||||
|
||||
suspend fun createMlsDirectMessage(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
@@ -121,10 +119,6 @@ interface ChatRepository {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getProfileWithPublicKey(publicKey: HexKey): Profile? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun createMlsDirectMessage(
|
||||
name: String?,
|
||||
description: String?,
|
||||
|
||||
@@ -24,7 +24,11 @@ import kotlinx.coroutines.flow.Flow
|
||||
interface NostrRepository {
|
||||
suspend fun getLocalAccounts(): List<LocalAccount>
|
||||
|
||||
suspend fun observeProfile(publicKey: HexKey): Flow<LocalAccount?>
|
||||
suspend fun observeLocalAccount(publicKey: HexKey): Flow<LocalAccount?>
|
||||
|
||||
suspend fun getProfileWithPublicKey(publicKey: HexKey): Profile?
|
||||
|
||||
suspend fun observeProfileWithPublicKey(publicKey: HexKey): Flow<Profile?>
|
||||
|
||||
suspend fun observeRelation(publicKey: HexKey, activePublicKey: HexKey): Flow<List<Connection>>
|
||||
|
||||
@@ -144,7 +148,15 @@ interface NostrRepository {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun observeProfile(publicKey: HexKey): Flow<LocalAccount?> {
|
||||
override suspend fun observeLocalAccount(publicKey: HexKey): Flow<LocalAccount?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getProfileWithPublicKey(publicKey: HexKey): Profile? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun observeProfileWithPublicKey(publicKey: HexKey): Flow<Profile?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
@@ -185,7 +186,7 @@ fun ChatRoomMessagingScreen(
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Send,
|
||||
Icons.AutoMirrored.Filled.Send,
|
||||
contentDescription = "Send"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.torch.compose.ui.composable.widgets.feed.ListView
|
||||
import at.torch.compose.ui.view.model.SearchResultType
|
||||
import at.torch.compose.ui.view.model.SearchResultViewModel
|
||||
import at.torch.compose.ui.view.state.SearchResultListUIState
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@@ -64,9 +67,9 @@ fun SearchResultScreen(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val searchViewModel: at.torch.compose.ui.view.model.SearchResultViewModel = viewModel(
|
||||
factory = at.torch.compose.ui.view.model.SearchResultViewModel.factory(
|
||||
initialSearchResultType = at.torch.compose.ui.view.model.SearchResultType.Profiles,
|
||||
val searchViewModel: SearchResultViewModel = viewModel(
|
||||
factory = SearchResultViewModel.factory(
|
||||
initialSearchResultType = SearchResultType.Profiles,
|
||||
searchQuery = searchQuery,
|
||||
nostrRepository = nostrRepository,
|
||||
searchRepository = searchRepository
|
||||
@@ -154,14 +157,14 @@ fun SearchResultScreen(
|
||||
},
|
||||
windowInsets = TopAppBarDefaults.windowInsets
|
||||
) {
|
||||
val pagerState = rememberPagerState { at.torch.compose.ui.view.model.SearchResultType.entries.size }
|
||||
val pagerState = rememberPagerState { SearchResultType.entries.size }
|
||||
|
||||
PrimaryScrollableTabRow(
|
||||
modifier = Modifier,
|
||||
edgePadding = 10.dp,
|
||||
selectedTabIndex = searchViewModel.searchResultType.ordinal,
|
||||
) {
|
||||
at.torch.compose.ui.view.model.SearchResultType.entries.forEach { searchResultType ->
|
||||
SearchResultType.entries.forEach { searchResultType ->
|
||||
Tab(
|
||||
selected = searchViewModel.searchResultType == searchResultType,
|
||||
onClick = {
|
||||
@@ -194,14 +197,14 @@ fun SearchResultScreen(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
when(at.torch.compose.ui.view.model.SearchResultType.entries[pageIndex]) {
|
||||
at.torch.compose.ui.view.model.SearchResultType.Profiles -> {
|
||||
when(SearchResultType.entries[pageIndex]) {
|
||||
SearchResultType.Profiles -> {
|
||||
Text("Profiles")
|
||||
}
|
||||
}
|
||||
|
||||
when (val searchResultListUIState = searchViewModel.searchResultListUIState) {
|
||||
at.torch.compose.ui.view.state.SearchResultListUIState.Error -> {
|
||||
SearchResultListUIState.Error -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
@@ -214,7 +217,7 @@ fun SearchResultScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
is at.torch.compose.ui.view.state.SearchResultListUIState.Loaded -> {
|
||||
is SearchResultListUIState.Loaded -> {
|
||||
// TODO: Handle gallery UI
|
||||
if (searchResultListUIState.localNostrEvents.isEmpty()) {
|
||||
Column(
|
||||
@@ -254,7 +257,7 @@ fun SearchResultScreen(
|
||||
}
|
||||
|
||||
}
|
||||
at.torch.compose.ui.view.state.SearchResultListUIState.Loading -> {
|
||||
SearchResultListUIState.Loading -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
|
||||
@@ -18,6 +18,7 @@ import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AddMemberToChatRoomConfirmationViewModel(
|
||||
@@ -44,7 +45,7 @@ class AddMemberToChatRoomConfirmationViewModel(
|
||||
val localChatRoom = chatRepository.getChatRoomByIdentifier(chatRoomId)
|
||||
|
||||
addMemberToChatRoomConfirmationUIState = if (localChatRoom != null) {
|
||||
val profile = chatRepository.getProfileWithPublicKey(profilePublicKey)
|
||||
val profile = nostrRepository.getProfileWithPublicKey(profilePublicKey)
|
||||
|
||||
if (profile != null) {
|
||||
val marmotKeyPackage = chatRepository.getMarmotKeyPackageForPublicKey(profilePublicKey)
|
||||
|
||||
@@ -9,13 +9,21 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import at.torch.compose.database.GENESIS_AT
|
||||
import at.torch.compose.database.builder.getRoomDatabase
|
||||
import at.torch.compose.database.model.NegentropySynchronizeRequest
|
||||
import at.torch.compose.database.model.types.SynchronizationFilter
|
||||
import at.torch.compose.nostr.Relays
|
||||
import at.torch.compose.repository.ChatRepository
|
||||
import at.torch.compose.repository.NostrRepository
|
||||
import at.torch.compose.ui.composable.navigation.routes.ChatRoomMessagingRoute
|
||||
import at.torch.compose.ui.composable.navigation.routes.Route
|
||||
import at.torch.compose.ui.view.state.ChatRoomMessagingUIState
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -102,39 +110,67 @@ class ChatRoomMessagingViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val profile = chatRepository.getProfileWithPublicKey(chatRoomId)
|
||||
nostrRepository.observeProfileWithPublicKey(chatRoomId).collect { profile ->
|
||||
if (profile != null) {
|
||||
logger.d("Create chat with: $profile")
|
||||
|
||||
if (profile != null) {
|
||||
logger.d("Create chat with: $profile")
|
||||
// Check if we have a keyPackageEvent...
|
||||
val keyPackageEvent = chatRepository.getMarmotKeyPackageForPublicKey(chatRoomId)
|
||||
if (profile.createdAt > GENESIS_AT) {
|
||||
// Check if we have a keyPackageEvent...
|
||||
val keyPackageEvent = chatRepository.getMarmotKeyPackageForPublicKey(chatRoomId) // TODO: Observe
|
||||
|
||||
if (keyPackageEvent == null) {
|
||||
chatRoomMessagingUIState = ChatRoomMessagingUIState.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(
|
||||
ChatRoomMessagingRoute(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
chatRoomId = chatRoomId,
|
||||
relayHint = relayHint
|
||||
)
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
chatRoomMessagingUIState = ChatRoomMessagingUIState.Error("Failed to create chat")
|
||||
if (keyPackageEvent == null) {
|
||||
chatRoomMessagingUIState = ChatRoomMessagingUIState.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(
|
||||
ChatRoomMessagingRoute(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
chatRoomId = chatRoomId,
|
||||
relayHint = relayHint
|
||||
)
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
chatRoomMessagingUIState = ChatRoomMessagingUIState.Error("Failed to create chat")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chatRoomMessagingUIState = ChatRoomMessagingUIState.Error("Couldn't find profile... searching relays.") // Looking for profile...
|
||||
}
|
||||
} else {
|
||||
val synchronizationFilter = SynchronizationFilter(
|
||||
kinds = arrayOf(
|
||||
MetadataEvent.KIND,
|
||||
KeyPackageEvent.KIND,
|
||||
ChatMessageRelayListEvent.KIND
|
||||
),
|
||||
authors = arrayOf(
|
||||
chatRoomId
|
||||
),
|
||||
limit = 1
|
||||
)
|
||||
nostrRepository.queueNegentropySynchronizeRequest(
|
||||
Relays.DefaultDMRelayList.map { relay ->
|
||||
NegentropySynchronizeRequest(
|
||||
id = NegentropySynchronizeRequest.computeId(
|
||||
relay.url,
|
||||
synchronizationFilter = synchronizationFilter
|
||||
),
|
||||
purpose = "initiate-chat",
|
||||
synchronizationFilter = synchronizationFilter,
|
||||
relayURL = relay.url,
|
||||
level = 0
|
||||
)
|
||||
}
|
||||
)
|
||||
chatRoomMessagingUIState = ChatRoomMessagingUIState.Error("Couldn't find profile... queued to search for relay.") // Looking for profile...
|
||||
}
|
||||
|
||||
} else {
|
||||
chatRoomMessagingUIState = ChatRoomMessagingUIState.Error("Couldn't find profile...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import at.torch.compose.database.model.NegentropySynchronizeRequest
|
||||
import at.torch.compose.ui.composable.widgets.feed.ListView
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,8 +45,8 @@ class FeedListViewModel(
|
||||
val isSynchronizationPending: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
val negentropySynchronizeRequests = at.torch.compose.nostr.Relays.DefaultDMRelayList.shuffled().map { normalizedRelayUrl ->
|
||||
_root_ide_package_.at.torch.compose.database.model.NegentropySynchronizeRequest(
|
||||
id = at.torch.compose.database.model.NegentropySynchronizeRequest.computeId(
|
||||
NegentropySynchronizeRequest(
|
||||
id = NegentropySynchronizeRequest.computeId(
|
||||
normalizedRelayUrl.url,
|
||||
synchronizationFilter = synchronizationFilter
|
||||
),
|
||||
|
||||
@@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -147,7 +146,7 @@ class NavigationViewModel(
|
||||
|
||||
|
||||
private suspend fun loadNostrProfile(activeUserPublicKey: String) {
|
||||
nostrRepository.observeProfile(
|
||||
nostrRepository.observeLocalAccount(
|
||||
publicKey = activeUserPublicKey
|
||||
).distinctUntilChanged().collect { localProfile ->
|
||||
processLocalAccount(localProfile)
|
||||
|
||||
@@ -12,6 +12,7 @@ import at.torch.compose.database.model.types.SynchronizationFilter
|
||||
import at.torch.compose.extensions.assureValidNpub
|
||||
import at.torch.compose.extensions.bech32ToHexOrNull
|
||||
import at.torch.compose.extensions.bech32ToHexOrThrow
|
||||
import at.torch.compose.ui.view.state.SearchResultListUIState
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
|
||||
@@ -35,12 +36,12 @@ class SearchResultViewModel(
|
||||
var searchResultType: SearchResultType by mutableStateOf(initialSearchResultType)
|
||||
private set
|
||||
|
||||
var searchResultListUIState: at.torch.compose.ui.view.state.SearchResultListUIState by mutableStateOf(
|
||||
at.torch.compose.ui.view.state.SearchResultListUIState.Loading)
|
||||
var searchResultListUIState: SearchResultListUIState by mutableStateOf(
|
||||
SearchResultListUIState.Loading)
|
||||
private set
|
||||
|
||||
fun updateSearchResultType (searchResultType: SearchResultType) {
|
||||
searchResultListUIState = at.torch.compose.ui.view.state.SearchResultListUIState.Loading
|
||||
searchResultListUIState = SearchResultListUIState.Loading
|
||||
|
||||
this.searchResultType = searchResultType
|
||||
|
||||
@@ -122,7 +123,7 @@ class SearchResultViewModel(
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// This is something not taking into consideration the results we are actually looking for...
|
||||
nostrRepository.observeNostrFeed(synchronizationFilter).distinctUntilChanged().collect { localNostrEvents ->
|
||||
searchResultListUIState = at.torch.compose.ui.view.state.SearchResultListUIState.Loaded(
|
||||
searchResultListUIState = SearchResultListUIState.Loaded(
|
||||
localNostrEvents = localNostrEvents
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user