Chat room creation
This commit is contained in:
@@ -28,36 +28,36 @@ abstract class NostrNip17Dao(
|
||||
val logger = Logger.withTag(TAG)
|
||||
|
||||
@Transaction
|
||||
open suspend fun getOrCreateChatRoom(activeUserPublicKey: HexKey, publicKey: String, relayHint: String?, defaultSubject: String? = null): at.torch.compose.database.model.intermdiate.LocalChatRoom? {
|
||||
logger.d("getOrCreateChatRoom: $publicKey")
|
||||
open suspend fun getOrCreateChatRoom(chatRoomId: String, activeUserPublicKey: HexKey, relayHint: String?, defaultSubject: String? = null, mlsGroupState: String? = null): LocalChatRoom? {
|
||||
logger.d("getOrCreateChatRoom: $chatRoomId")
|
||||
|
||||
|
||||
val hexKeys = setOf(
|
||||
activeUserPublicKey,
|
||||
publicKey
|
||||
// publicKey
|
||||
)
|
||||
|
||||
val chatRoomId = _root_ide_package_.at.torch.compose.database.model.ChatRoom.Companion.deriveChatRoomId(hexKeys)
|
||||
logger.d("chatRoomId: $chatRoomId")
|
||||
val localChatRoom = database.chatRoomDao().findChatRoomById(chatRoomId)
|
||||
|
||||
if (localChatRoom != null) {
|
||||
// TODO: might want to update the mlsGroupState?
|
||||
return localChatRoom
|
||||
} else {
|
||||
val profiles = database.profileDao().getProfileByPublicKeys(hexKeys.toList())
|
||||
|
||||
if (profiles.isNotEmpty()) {
|
||||
// Create new chatRoom
|
||||
val chatRoom = _root_ide_package_.at.torch.compose.database.model.ChatRoom(
|
||||
val chatRoom = ChatRoom(
|
||||
id = chatRoomId,
|
||||
userPublicKey = activeUserPublicKey,
|
||||
subject = defaultSubject,
|
||||
mlsGroupState = mlsGroupState
|
||||
)
|
||||
database.chatRoomDao().upsert(chatRoom)
|
||||
|
||||
database.participantDao().upsert(
|
||||
profiles.map {
|
||||
_root_ide_package_.at.torch.compose.database.model.Participant(
|
||||
Participant(
|
||||
participantPublicKey = it.publicKey,
|
||||
chatRoomId = chatRoomId,
|
||||
relayHint = relayHint
|
||||
@@ -65,7 +65,7 @@ abstract class NostrNip17Dao(
|
||||
}
|
||||
)
|
||||
|
||||
return _root_ide_package_.at.torch.compose.database.model.intermdiate.LocalChatRoom(
|
||||
return LocalChatRoom(
|
||||
chatRoom = chatRoom
|
||||
)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ abstract class NostrNip17Dao(
|
||||
giftWrapEvent: GiftWrapEvent,
|
||||
receiverPublicKey: PTag
|
||||
) {
|
||||
val nostrEvent = _root_ide_package_.at.torch.compose.database.model.NostrEvent(
|
||||
val nostrEvent = NostrEvent(
|
||||
id = giftWrapEvent.id,
|
||||
content = giftWrapEvent.content,
|
||||
createdAt = Instant.fromEpochSeconds(giftWrapEvent.createdAt),
|
||||
@@ -99,7 +99,7 @@ abstract class NostrNip17Dao(
|
||||
|
||||
chatMessage?.let {
|
||||
database.chatMessageNostrEventRelationDao().upsert(
|
||||
_root_ide_package_.at.torch.compose.database.model.ChatMessageNostrEventRelation(
|
||||
ChatMessageNostrEventRelation(
|
||||
chatMessageId = it.id,
|
||||
nostrEventId = nostrEvent.id
|
||||
)
|
||||
@@ -112,7 +112,7 @@ abstract class NostrNip17Dao(
|
||||
authors = arrayOf(
|
||||
receiverPublicKey.pubKey
|
||||
),
|
||||
since = _root_ide_package_.at.torch.compose.database.GENESIS_AT,
|
||||
since = GENESIS_AT,
|
||||
limit = 5
|
||||
)
|
||||
logger.d("Found messageRelayLists: $chatMessageRelayListEvents")
|
||||
@@ -134,7 +134,7 @@ abstract class NostrNip17Dao(
|
||||
logger.d("Submit BroadcastNostrEventRequest to $relayUrl")
|
||||
val broadcastNostrEventRequestIds = database.broadcastNostrEventRequestDao().insert(
|
||||
listOf(
|
||||
_root_ide_package_.at.torch.compose.database.model.BroadcastNostrEventRequest(
|
||||
BroadcastNostrEventRequest(
|
||||
nostrEventId = nostrEvent.id,
|
||||
relayURL = relayUrl.url
|
||||
)
|
||||
@@ -144,7 +144,7 @@ abstract class NostrNip17Dao(
|
||||
chatMessage?.let {
|
||||
broadcastNostrEventRequestIds.forEach {
|
||||
database.chatMessageBroadcastNostrEventRequestRelationDao().upsert(
|
||||
_root_ide_package_.at.torch.compose.database.model.ChatMessageBroadcastNostrEventRequestRelation(
|
||||
ChatMessageBroadcastNostrEventRequestRelation(
|
||||
chatMessageId = chatMessage.id,
|
||||
broadcastNostrEventRequestId = it
|
||||
)
|
||||
@@ -159,7 +159,7 @@ abstract class NostrNip17Dao(
|
||||
|
||||
|
||||
database.giftWrapMessageDao().upsert(
|
||||
_root_ide_package_.at.torch.compose.database.model.GiftWrapMessage(
|
||||
GiftWrapMessage(
|
||||
id = giftWrapEvent.id,
|
||||
nostrEventId = nostrEvent.id,
|
||||
content = giftWrapEvent.content,
|
||||
@@ -173,7 +173,7 @@ abstract class NostrNip17Dao(
|
||||
)
|
||||
|
||||
database.giftWrapSealDao().upsert(
|
||||
_root_ide_package_.at.torch.compose.database.model.GiftWrapSeal(
|
||||
GiftWrapSeal(
|
||||
id = sealedRumorEvent.id,
|
||||
publicKey = sealedRumorEvent.pubKey,
|
||||
content = sealedRumorEvent.content,
|
||||
|
||||
@@ -52,6 +52,8 @@ data class ChatRoom(
|
||||
*/
|
||||
val subject: String?,
|
||||
|
||||
val mlsGroupState: String?, // TODO: Can this be nullable???
|
||||
|
||||
/**
|
||||
* Derived from kind14/15 tags
|
||||
* [
|
||||
|
||||
@@ -4,6 +4,7 @@ import at.torch.compose.database.GENESIS_AT
|
||||
import at.torch.compose.database.model.ChatMessage
|
||||
import at.torch.compose.database.model.GiftWrapPayload
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
@@ -45,16 +46,18 @@ class DatabaseChatRepository(
|
||||
|
||||
|
||||
override suspend fun getOrCreateChatRoom(
|
||||
chatRoomId: String,
|
||||
activeUserPublicKey: HexKey,
|
||||
publicKey: String,
|
||||
relayHint: String?,
|
||||
defaultSubject: String?
|
||||
defaultSubject: String?,
|
||||
mlsGroupState: String?
|
||||
): at.torch.compose.database.model.intermdiate.LocalChatRoom? = try {
|
||||
return database.nostrNip17Dao().getOrCreateChatRoom(
|
||||
chatRoomId = chatRoomId,
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
publicKey = publicKey,
|
||||
relayHint = relayHint,
|
||||
defaultSubject = defaultSubject
|
||||
defaultSubject = defaultSubject,
|
||||
mlsGroupState = mlsGroupState
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
logger.e("Error getting or creating chat", e)
|
||||
|
||||
@@ -8,6 +8,7 @@ import at.torch.compose.database.model.UnsignedNostrEvent
|
||||
import at.torch.compose.nostr.Relays
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRotationManager.Companion.KEY_PACKAGE_LIFETIME_SECONDS
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils
|
||||
import com.vitorpamplona.quartz.marmot.mls.crypto.Ed25519
|
||||
@@ -161,10 +162,11 @@ class DatabaseNostrRepository(
|
||||
)
|
||||
|
||||
unsignedNostrEvents.add(
|
||||
_root_ide_package_.at.torch.compose.database.model.UnsignedNostrEvent(
|
||||
UnsignedNostrEvent(
|
||||
pubKey = publicKey,
|
||||
kind = AdvertisedRelayListEvent.KIND,
|
||||
tags = _root_ide_package_.at.torch.compose.nostr.Relays.DefaultNIP65List.map { it.toTagArray() }
|
||||
kind = KeyPackageRelayListEvent.KIND,
|
||||
tags = Relays.DefaultDMRelayList.map { arrayOf("relay", it.url) }
|
||||
.plusElement(AltTag.assemble(KeyPackageRelayListEvent.ALT_DESCRIPTION))
|
||||
.toTypedArray(),
|
||||
content = ""
|
||||
)
|
||||
@@ -173,10 +175,10 @@ class DatabaseNostrRepository(
|
||||
|
||||
|
||||
unsignedNostrEvents.add(
|
||||
_root_ide_package_.at.torch.compose.database.model.UnsignedNostrEvent(
|
||||
UnsignedNostrEvent(
|
||||
pubKey = publicKey,
|
||||
kind = SearchRelayListEvent.KIND,
|
||||
tags = _root_ide_package_.at.torch.compose.nostr.Relays.DefaultSearchRelayList.map {
|
||||
tags = Relays.DefaultSearchRelayList.map {
|
||||
RelayTag.assemble(
|
||||
it
|
||||
)
|
||||
@@ -189,35 +191,6 @@ class DatabaseNostrRepository(
|
||||
)
|
||||
)
|
||||
|
||||
unsignedNostrEvents.add(
|
||||
_root_ide_package_.at.torch.compose.database.model.UnsignedNostrEvent(
|
||||
pubKey = publicKey,
|
||||
kind = IndexerRelayListEvent.KIND,
|
||||
tags = arrayOf(AltTag.assemble(IndexerRelayListEvent.ALT)),
|
||||
privateTags = _root_ide_package_.at.torch.compose.nostr.Relays.DefaultIndexerRelayList.map {
|
||||
RelayTag.assemble(
|
||||
it
|
||||
)
|
||||
}.toTypedArray(),
|
||||
content = ""
|
||||
)
|
||||
)
|
||||
|
||||
unsignedNostrEvents.add(
|
||||
_root_ide_package_.at.torch.compose.database.model.UnsignedNostrEvent(
|
||||
pubKey = publicKey,
|
||||
kind = ChannelListEvent.KIND,
|
||||
tags = arrayOf(
|
||||
AltTag.assemble(
|
||||
ChannelListEvent.ALT
|
||||
)
|
||||
),
|
||||
privateTags = _root_ide_package_.at.torch.compose.nostr.Relays.DefaultChannels.map { it.toTagArray() }
|
||||
.toTypedArray(),
|
||||
content = ""
|
||||
)
|
||||
)
|
||||
|
||||
unsignedNostrEvents.add(
|
||||
UnsignedNostrEvent(
|
||||
pubKey = publicKey,
|
||||
|
||||
@@ -20,9 +20,11 @@ interface ChatRepository {
|
||||
suspend fun observeChatMessageListByChatRoomId(chatRoomId: String): Flow<List<at.torch.compose.database.model.intermdiate.LocalChatMessage>>
|
||||
|
||||
suspend fun getOrCreateChatRoom(
|
||||
chatRoomId: String,
|
||||
activeUserPublicKey: HexKey,
|
||||
publicKey: String, relayHint: String?,
|
||||
defaultSubject: String? = null
|
||||
relayHint: String?,
|
||||
defaultSubject: String?,
|
||||
mlsGroupState: String?
|
||||
): at.torch.compose.database.model.intermdiate.LocalChatRoom?
|
||||
|
||||
suspend fun getChatMessageRelayForPublicKey(publicKey: HexKey): ChatMessageRelayListEvent?
|
||||
@@ -63,10 +65,11 @@ interface ChatRepository {
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateChatRoom(
|
||||
chatRoomId: String,
|
||||
activeUserPublicKey: HexKey,
|
||||
publicKey: String,
|
||||
relayHint: String?,
|
||||
defaultSubject: String?
|
||||
defaultSubject: String?,
|
||||
mlsGroupState: String?
|
||||
): at.torch.compose.database.model.intermdiate.LocalChatRoom? {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package at.torch.compose.ui.composable
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import at.torch.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Badge
|
||||
import androidx.compose.material.icons.filled.Description
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.torch.compose.repository.ChatRepository
|
||||
import at.torch.compose.repository.NostrRepository
|
||||
import at.torch.compose.ui.composable.navigation.routes.Route
|
||||
import at.torch.compose.ui.view.model.ChatRoomCreationViewModel
|
||||
import at.torch.compose.ui.view.state.form.TextField
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
fun ChatRoomCreationScreen(
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
nostrRepository: NostrRepository,
|
||||
chatRepository: ChatRepository,
|
||||
onNavigateToChatRoom: (Route) -> Unit
|
||||
) {
|
||||
val chatRoomCreationViewModel: ChatRoomCreationViewModel = viewModel (
|
||||
factory = ChatRoomCreationViewModel.factory(
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
nostrRepository = nostrRepository,
|
||||
chatRepository = chatRepository
|
||||
)
|
||||
)
|
||||
|
||||
Scaffold { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(innerPadding).fillMaxWidth()
|
||||
) {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(10.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = chatRoomCreationViewModel.chatRoomCreationFormState.nameField.textFieldState,
|
||||
isError = chatRoomCreationViewModel.chatRoomCreationFormState.nameField.errorMessage.value != null,
|
||||
supportingText = chatRoomCreationViewModel.chatRoomCreationFormState.nameField.errorMessage.value?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.padding(
|
||||
bottom = 8.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
label = {
|
||||
Text(
|
||||
text = "Name (eg. Alan Turin)",
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "Enter the name you want to use for your profile",
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Badge,
|
||||
contentDescription = "Name"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "This will be the display name for your profile and also important for search.",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = chatRoomCreationViewModel.chatRoomCreationFormState.descriptionField.textFieldState,
|
||||
isError = chatRoomCreationViewModel.chatRoomCreationFormState.descriptionField.errorMessage.value != null,
|
||||
supportingText = chatRoomCreationViewModel.chatRoomCreationFormState.descriptionField.errorMessage.value?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.padding(
|
||||
bottom = 8.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
lineLimits = TextFieldLineLimits.MultiLine(4),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
label = {
|
||||
Text(
|
||||
text = "Introduce yourself",
|
||||
maxLines = 1,
|
||||
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "What should people know about you?",
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Description,
|
||||
contentDescription = "Name"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "This will be shown when people open your profile.",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
chatRoomCreationViewModel.createChatRoom(
|
||||
onNavigateToChatRoom = {
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
"Create chat"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun LoadingScreenPreview() {
|
||||
_root_ide_package_.at.torch.compose.ui.theme.TorchTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
ChatRoomCreationScreen(
|
||||
activeWalletStateFlow = MutableStateFlow(null),
|
||||
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
|
||||
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
|
||||
onNavigateToChatRoom = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,7 +276,8 @@ private fun ChatRoomDetailScreenPreview() {
|
||||
id = "",
|
||||
userPublicKey = "",
|
||||
subject = "Message title",
|
||||
initialGiftWrapPayloadId = 0
|
||||
initialGiftWrapPayloadId = 0,
|
||||
mlsGroupState = null
|
||||
),
|
||||
)
|
||||
),
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.torch.compose.ui.view.state.CreateProfileUIState
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
@@ -358,7 +359,7 @@ fun CreateAccountScreenPreview() {
|
||||
) {
|
||||
CreateProfileScreen(
|
||||
initialCreateProfileUIState =
|
||||
// CreateProfileUIState.InputPrompt,
|
||||
CreateProfileUIState.InputPrompt,
|
||||
// CreateProfileUIState.Error,
|
||||
// CreateProfileUIState.ConfirmInput(
|
||||
// name = "Alan Turing",
|
||||
@@ -385,14 +386,14 @@ fun CreateAccountScreenPreview() {
|
||||
// name = "Alan Turing",
|
||||
// bio = "Polyglot: Math... is my middle name, computer scientist. cryptographer, and gold hider."
|
||||
// ),
|
||||
_root_ide_package_.at.torch.compose.ui.view.state.CreateProfileUIState.ProfileReady(
|
||||
profile = _root_ide_package_.at.torch.compose.database.model.Profile(
|
||||
publicKey = "",
|
||||
nostrEventId = "",
|
||||
displayName = "Alan Turing",
|
||||
about = "Polyglot: Math... is my middle name, computer scientist. cryptographer, and gold hider."
|
||||
)
|
||||
),
|
||||
// _root_ide_package_.at.torch.compose.ui.view.state.CreateProfileUIState.ProfileReady(
|
||||
// profile = _root_ide_package_.at.torch.compose.database.model.Profile(
|
||||
// publicKey = "",
|
||||
// nostrEventId = "",
|
||||
// displayName = "Alan Turing",
|
||||
// about = "Polyglot: Math... is my middle name, computer scientist. cryptographer, and gold hider."
|
||||
// )
|
||||
// ),
|
||||
onNavigateToEndThis = {},
|
||||
writeSeed = {},
|
||||
nostrRepository = _root_ide_package_.at.torch.compose.repository.NostrRepository.Companion.NO_OP_NOSTR_REPOSITORY
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package at.torch.compose.ui.composable
|
||||
|
||||
import at.torch.compose.database.model.NostrEvent
|
||||
import at.torch.compose.database.model.Profile
|
||||
import at.torch.compose.database.model.intermdiate.LocalProfileWithFollowing
|
||||
import at.torch.compose.ui.composable.navigation.routes.NostrEventDetailRoute
|
||||
import at.torch.compose.ui.composable.widgets.LoadingDataIndicator
|
||||
import at.torch.compose.ui.composable.widgets.profile.ProfileAvatar
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -14,7 +8,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
@@ -50,7 +44,7 @@ fun HomeScreen(
|
||||
initialHomeScreenUIState: at.torch.compose.ui.view.state.HomeScreenUIState = _root_ide_package_.at.torch.compose.ui.view.state.HomeScreenUIState.Loading,
|
||||
onNavigateToEvent: (at.torch.compose.ui.composable.navigation.routes.Route) -> Unit,
|
||||
onNavigateToDirectMessageDetail: (at.torch.compose.ui.composable.navigation.routes.Route) -> Unit,
|
||||
onNavigateToWriteNewNote: () -> Unit,
|
||||
onNavigateToChatRoomCreation: () -> Unit,
|
||||
onNavigateToSearch: () -> Unit,
|
||||
nostrRepository: at.torch.compose.repository.NostrRepository,
|
||||
chatRepository: at.torch.compose.repository.ChatRepository
|
||||
@@ -136,15 +130,15 @@ fun HomeScreen(
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = onNavigateToWriteNewNote,
|
||||
onClick = onNavigateToChatRoomCreation,
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Default.Create,
|
||||
contentDescription = "Write"
|
||||
Icons.Default.Add,
|
||||
contentDescription = "New Chat"
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text("Write")
|
||||
Text("New Chat")
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -182,8 +176,8 @@ fun HomeScreen(
|
||||
HorizontalPager(
|
||||
state = homeScreenViewModel.pagerState,
|
||||
) { pageIndex ->
|
||||
when (val homeScreenTypeType = _root_ide_package_.at.torch.compose.ui.view.model.HomeScreenType.entries[pageIndex]) {
|
||||
_root_ide_package_.at.torch.compose.ui.view.model.HomeScreenType.Messages -> {
|
||||
when (val homeScreenTypeType = HomeScreenType.entries[pageIndex]) {
|
||||
HomeScreenType.Messages -> {
|
||||
val chatRoomListViewModel: at.torch.compose.ui.view.model.ChatRoomListViewModel = viewModel(
|
||||
key = homeScreenTypeType.name,
|
||||
factory = _root_ide_package_.at.torch.compose.ui.view.model.ChatRoomListViewModel.Companion.factory(
|
||||
@@ -256,7 +250,7 @@ It has survived not only five centuries, but also the leap into electronic types
|
||||
)
|
||||
),
|
||||
onNavigateToEvent = {},
|
||||
onNavigateToWriteNewNote = {},
|
||||
onNavigateToChatRoomCreation = {},
|
||||
onNavigateToSearch = {},
|
||||
onNavigateToDirectMessageDetail = {},
|
||||
nostrRepository = _root_ide_package_.at.torch.compose.repository.NostrRepository.Companion.NO_OP_NOSTR_REPOSITORY,
|
||||
|
||||
@@ -14,6 +14,9 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.compose.composable
|
||||
import at.torch.compose.ui.composable.ChatRoomCreationScreen
|
||||
import at.torch.compose.ui.composable.HomeScreen
|
||||
import at.torch.compose.ui.composable.navigation.routes.ChatRoomCreationRoute
|
||||
import co.touchlab.kermit.Logger
|
||||
import fr.acinq.phoenix.PhoenixGlobal
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
@@ -290,6 +293,15 @@ fun TorchNavHost(
|
||||
)
|
||||
|
||||
}
|
||||
composable<ChatRoomCreationRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<ChatRoomCreationRoute>()
|
||||
|
||||
ChatRoomCreationScreen(
|
||||
activeUserPublicKey = route.activeUserPublicKey,
|
||||
nostrRepository = databaseNostrRepository,
|
||||
chatRepository = databaseChatRepository
|
||||
)
|
||||
}
|
||||
composable<WriteNewNoteRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<WriteNewNoteRoute>()
|
||||
_root_ide_package_.at.torch.compose.ui.composable.WriteNewNoteScreen(
|
||||
@@ -375,16 +387,16 @@ fun TorchNavHost(
|
||||
}
|
||||
composable<at.torch.compose.ui.composable.navigation.routes.FeedRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<at.torch.compose.ui.composable.navigation.routes.FeedRoute>()
|
||||
_root_ide_package_.at.torch.compose.ui.composable.HomeScreen(
|
||||
HomeScreen(
|
||||
activeUserPublicKey = route.activeUserPublicKey,
|
||||
onNavigateToEvent = { eventRoute ->
|
||||
navController.navigate(
|
||||
route = eventRoute
|
||||
)
|
||||
},
|
||||
onNavigateToWriteNewNote = {
|
||||
onNavigateToChatRoomCreation = {
|
||||
navController.navigate(
|
||||
route = WriteNewNoteRoute(
|
||||
route = ChatRoomCreationRoute(
|
||||
activeUserPublicKey = route.activeUserPublicKey
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package at.torch.compose.ui.composable.navigation.routes
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ChatRoomCreationRoute(
|
||||
val activeUserPublicKey: String,
|
||||
): Route() {
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package at.torch.compose.ui.view.model
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import at.torch.compose.extensions.toHex
|
||||
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.ImplementationPendingRoute
|
||||
import at.torch.compose.ui.composable.navigation.routes.Route
|
||||
import at.torch.compose.ui.view.state.ChatRoomDetailUIState
|
||||
import at.torch.compose.ui.view.state.form.ChatRoomCreationFormState
|
||||
import at.torch.compose.ui.view.state.form.CreateProfileFormState
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import fr.acinq.bitcoin.Crypto
|
||||
import fr.acinq.phoenix.data.ActiveWallet
|
||||
import fr.acinq.phoenix.managers.nostrPrivateKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChatRoomCreationViewModel(
|
||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
val nostrRepository: NostrRepository,
|
||||
val chatRepository: ChatRepository,
|
||||
val chatRoomCreationFormState: ChatRoomCreationFormState = ChatRoomCreationFormState(),
|
||||
): ViewModel() {
|
||||
|
||||
private val logger = Logger.withTag(TAG)
|
||||
|
||||
val isActionPending: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
|
||||
fun createChatRoom(
|
||||
onNavigateToChatRoom: (Route) -> Unit
|
||||
) {
|
||||
// TODO: Load keypair...
|
||||
activeWalletStateFlow.value?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey ->
|
||||
val keyPair = KeyPair(
|
||||
privKey = nostrPrivateKey.value.toByteArray()
|
||||
)
|
||||
|
||||
val name = chatRoomCreationFormState.nameField.textFieldState.text
|
||||
val description = chatRoomCreationFormState.descriptionField.textFieldState.text
|
||||
|
||||
val gid = RandomInstance.bytes(32).toHexKey() // TODO: Generate GID through frost...
|
||||
|
||||
// Stamp initial metadata via the shared factory so UI + CLI stay
|
||||
// byte-identical. Bake the MarmotGroupData extension into the
|
||||
// epoch-0 GroupContext directly (see `MarmotManager.createGroup`)
|
||||
// so later invitees receive a pre-populated group from the
|
||||
// welcome and never have to chase an undecryptable bootstrap
|
||||
// commit that predates their membership.
|
||||
val metadata = MarmotGroupData.bootstrap(
|
||||
nostrGroupId = gid,
|
||||
creatorPubKey = keyPair.pubKey.toHexKey(),
|
||||
outboxRelays = Relays.DefaultDMRelayList.map { it.url },
|
||||
name = name.toString(),
|
||||
description = description.toString()
|
||||
)
|
||||
|
||||
val extras = listOf(metadata.toExtension())
|
||||
|
||||
val group = MlsGroup.create(keyPair.pubKey, keyPair.privKey, extras)
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val localChatRoom = chatRepository.getOrCreateChatRoom(
|
||||
chatRoomId = group.groupId.toHex(),
|
||||
activeUserPublicKey = keyPair.pubKey.toHexKey(),
|
||||
relayHint = null,
|
||||
defaultSubject = null,
|
||||
mlsGroupState = group.saveState().encodeTls().toHex()
|
||||
)
|
||||
|
||||
if (localChatRoom == null) {
|
||||
onNavigateToChatRoom.invoke(
|
||||
ImplementationPendingRoute("Something went wrong")
|
||||
)
|
||||
} else {
|
||||
onNavigateToChatRoom.invoke(
|
||||
ImplementationPendingRoute("Chat Room view ${localChatRoom.chatRoom.id}")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ChatRoomCreationViewModel"
|
||||
|
||||
fun factory(
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
nostrRepository: NostrRepository,
|
||||
chatRepository: ChatRepository
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
ChatRoomCreationViewModel(
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
nostrRepository = nostrRepository,
|
||||
chatRepository = chatRepository
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import at.torch.compose.ui.view.state.ChatRoomDetailUIState
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.bitcoin.Crypto
|
||||
@@ -20,12 +21,12 @@ class ChatRoomDetailViewModel(
|
||||
val directMessageId: String,
|
||||
val activeUserPublicKey: HexKey,
|
||||
val relayHint: String?,
|
||||
initialChatRoomDetailUIState: at.torch.compose.ui.view.state.ChatRoomDetailUIState,
|
||||
initialChatRoomDetailUIState: ChatRoomDetailUIState,
|
||||
val nostrRepository: at.torch.compose.repository.NostrRepository,
|
||||
val chatRepository: at.torch.compose.repository.ChatRepository
|
||||
): ViewModel() {
|
||||
|
||||
var chatRoomDetailUIState: at.torch.compose.ui.view.state.ChatRoomDetailUIState by mutableStateOf(initialChatRoomDetailUIState)
|
||||
var chatRoomDetailUIState: ChatRoomDetailUIState by mutableStateOf(initialChatRoomDetailUIState)
|
||||
private set
|
||||
|
||||
|
||||
@@ -35,38 +36,17 @@ class ChatRoomDetailViewModel(
|
||||
val isActionPending: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
fun initiateChatRoomDetail() {
|
||||
if (Crypto.isPubKeyCompressed(directMessageId.hexToByteArray())) {
|
||||
logger.d("compressed (most likely chat room): $directMessageId")
|
||||
// Get ChatRoomById
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val localChatRoom = chatRepository.getChatRoomByIdentifier(directMessageId)
|
||||
logger.d("compressed (most likely chat room): $directMessageId")
|
||||
// Get ChatRoomById
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val localChatRoom = chatRepository.getChatRoomByIdentifier(directMessageId)
|
||||
|
||||
chatRoomDetailUIState = if (localChatRoom == null) {
|
||||
_root_ide_package_.at.torch.compose.ui.view.state.ChatRoomDetailUIState.Error
|
||||
} else {
|
||||
_root_ide_package_.at.torch.compose.ui.view.state.ChatRoomDetailUIState.Loaded(
|
||||
localChatRoom = localChatRoom
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
logger.d("Uncompressed publicKey (most likely npub): $directMessageId")
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// We need to get or create a chat with the user on this publicKey...
|
||||
val localChatRoom = chatRepository.getOrCreateChatRoom(
|
||||
activeUserPublicKey = activeUserPublicKey,
|
||||
publicKey = directMessageId,
|
||||
relayHint = relayHint
|
||||
chatRoomDetailUIState = if (localChatRoom == null) {
|
||||
ChatRoomDetailUIState.Error
|
||||
} else {
|
||||
ChatRoomDetailUIState.Loaded(
|
||||
localChatRoom = localChatRoom
|
||||
)
|
||||
|
||||
chatRoomDetailUIState = if (localChatRoom == null) {
|
||||
_root_ide_package_.at.torch.compose.ui.view.state.ChatRoomDetailUIState.Error
|
||||
} else {
|
||||
_root_ide_package_.at.torch.compose.ui.view.state.ChatRoomDetailUIState.Loaded(
|
||||
localChatRoom = localChatRoom
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +59,7 @@ class ChatRoomDetailViewModel(
|
||||
activeUserPublicKey: HexKey,
|
||||
publicKey: String,
|
||||
relayHint: String?,
|
||||
initialChatRoomDetailUIState: at.torch.compose.ui.view.state.ChatRoomDetailUIState = _root_ide_package_.at.torch.compose.ui.view.state.ChatRoomDetailUIState.Loading,
|
||||
initialChatRoomDetailUIState: ChatRoomDetailUIState = ChatRoomDetailUIState.Loading,
|
||||
nostrRepository: at.torch.compose.repository.NostrRepository,
|
||||
chatRepository: at.torch.compose.repository.ChatRepository
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package at.torch.compose.ui.view.state.form
|
||||
|
||||
data class ChatRoomCreationFormState(
|
||||
val nameField: TextField = TextField(),
|
||||
val descriptionField: TextField = TextField()
|
||||
)
|
||||
Reference in New Issue
Block a user