Add dialog to start chat

This commit is contained in:
Kgothatso Ngako
2026-07-12 23:15:03 +02:00
parent 56dc651b1d
commit 1fb66300f8
16 changed files with 327 additions and 129 deletions

View File

@@ -5,35 +5,34 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Diversity2
import androidx.compose.material.icons.filled.Diversity3
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Button
import androidx.compose.material.icons.filled.Start
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
@@ -46,26 +45,31 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
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.extensions.assureValidNpub
import at.torch.compose.repository.ChatRepository
import at.torch.compose.repository.NostrRepository
import at.torch.compose.ui.composable.navigation.routes.ActiveProfileRoute
import at.torch.compose.ui.composable.navigation.routes.ImplementationPendingRoute
import at.torch.compose.ui.composable.navigation.routes.Route
import at.torch.compose.ui.composable.widgets.LoadingDataIndicator
import at.torch.compose.ui.composable.widgets.dialogs.NewChatBottomSheetDialog
import at.torch.compose.ui.composable.widgets.dialogs.StartDirectMessageToNpubOrNip05Dialog
import at.torch.compose.ui.theme.TorchTheme
import at.torch.compose.ui.view.model.HomeScreenType
import at.torch.compose.ui.view.model.HomeViewModel
import at.torch.compose.ui.view.state.HomeScreenUIState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import fr.acinq.phoenix.utils.EmailLikeAddress
import fr.acinq.phoenix.utils.Parser
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -83,6 +87,7 @@ fun HomeScreen(
val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
var showBottomSheet by remember { mutableStateOf(false) }
val openNpubDialog = remember { mutableStateOf(false) }
val homeScreenViewModel: HomeViewModel = viewModel(
factory = HomeViewModel.factory(
@@ -239,99 +244,29 @@ fun HomeScreen(
}
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
NewChatBottomSheetDialog(
scope = scope,
sheetState = sheetState,
onSetShowBottomSheetUpdate = { toggle ->
showBottomSheet = toggle
},
sheetState = sheetState
) {
// Sheet content
Column(
modifier = Modifier.fillMaxWidth().padding(10.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Create new chat",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge
)
Card(
onClick = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
onNavigateToRoute.invoke(
ImplementationPendingRoute("Direct Message via npub")
)
}
}
) {
ListItem(
leadingContent = {
Icon(
Icons.Default.Key,
contentDescription = "Direct Message via npub"
)
},
headlineContent = {
Text("Direct Message via npub")
},
trailingContent = {
Icon(
Icons.Default.ChevronRight,
contentDescription = "Direct Message via npub"
)
}
)
}
Card(
onClick = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
onNavigateToChatRoomCreation.invoke()
}
}
) {
ListItem(
leadingContent = {
Icon(
Icons.Default.Diversity2,
contentDescription = "Start group chat"
)
},
headlineContent = {
Text(
text = "Start a group chat"
)
},
trailingContent = {
Icon(
Icons.Default.ChevronRight,
contentDescription = "Start group chat"
)
}
)
}
TextButton(
onClick = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
showBottomSheet = false
}
}
}
) {
Icon(
Icons.Default.Close,
contentDescription = "Close new chat"
)
Spacer(
modifier = Modifier.width(10.dp)
)
Text("Close")
}
onNavigateToRoute = onNavigateToRoute,
onNavigateToChatRoomCreation = onNavigateToChatRoomCreation,
onStartDirectMessageChatWithNpub = {
openNpubDialog.value = true
}
)
}
when {
openNpubDialog.value -> {
StartDirectMessageToNpubOrNip05Dialog(
scope = scope,
toggleOpenDialogSetting = {
openNpubDialog.value = false
},
onNavigateToRoute = onNavigateToRoute
)
}
}
}

View File

@@ -65,9 +65,9 @@ fun SignInToProfileScreen(
TextField(
modifier = Modifier.fillMaxWidth(),
state = signInToProfileViewModel.signInToProfileFormState.textField.textFieldState,
isError = signInToProfileViewModel.signInToProfileFormState.textField.errorMessage.value != null,
supportingText = signInToProfileViewModel.signInToProfileFormState.textField.errorMessage.value?.let {
state = signInToProfileViewModel.signInToProfileFormState.textFieldForm.textFieldState,
isError = signInToProfileViewModel.signInToProfileFormState.textFieldForm.errorMessage.value != null,
supportingText = signInToProfileViewModel.signInToProfileFormState.textFieldForm.errorMessage.value?.let {
{
Text(
text = it,
@@ -107,13 +107,13 @@ fun SignInToProfileScreen(
when (credentialEntity) {
is NSec -> {
at.torch.compose.ui.view.state.SignInToProfileUIState.ConfirmNsecSignIn(
nsec = signInToProfileViewModel.signInToProfileFormState.textField.textFieldState.text.toString(),
nsec = signInToProfileViewModel.signInToProfileFormState.textFieldForm.textFieldState.text.toString(),
hexKey = credentialEntity.hex
)
}
is NPub -> {
at.torch.compose.ui.view.state.SignInToProfileUIState.ConfirmNpubSignIn(
npub = signInToProfileViewModel.signInToProfileFormState.textField.textFieldState.text.toString(),
npub = signInToProfileViewModel.signInToProfileFormState.textFieldForm.textFieldState.text.toString(),
hexKey = credentialEntity.hex
)
}

View File

@@ -124,9 +124,9 @@ fun WriteNewNoteScreen(
TextField(
modifier = Modifier.fillMaxWidth(),
state = writeNewNoteViewModel.writeNewNoteFormState.textField.textFieldState,
isError = writeNewNoteViewModel.writeNewNoteFormState.textField.errorMessage.value != null,
supportingText = writeNewNoteViewModel.writeNewNoteFormState.textField.errorMessage.value?.let {
state = writeNewNoteViewModel.writeNewNoteFormState.textFieldForm.textFieldState,
isError = writeNewNoteViewModel.writeNewNoteFormState.textFieldForm.errorMessage.value != null,
supportingText = writeNewNoteViewModel.writeNewNoteFormState.textFieldForm.errorMessage.value?.let {
{
Text(
text = it,

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.machankura.compose.ui.composable.widgets.dialogs
package at.torch.compose.ui.composable.widgets.dialogs
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope

View File

@@ -0,0 +1,137 @@
package at.torch.compose.ui.composable.widgets.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Diversity2
import androidx.compose.material.icons.filled.Key
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import at.torch.compose.ui.composable.navigation.routes.ImplementationPendingRoute
import at.torch.compose.ui.composable.navigation.routes.Route
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewChatBottomSheetDialog(
scope: CoroutineScope,
sheetState: SheetState,
onSetShowBottomSheetUpdate: (Boolean) -> Unit,
onNavigateToRoute: (Route) -> Unit,
onNavigateToChatRoomCreation: () -> Unit,
onStartDirectMessageChatWithNpub: () -> Unit
) {
ModalBottomSheet(
onDismissRequest = {
onSetShowBottomSheetUpdate.invoke(false)
},
sheetState = sheetState
) {
// Sheet content
Column(
modifier = Modifier.fillMaxWidth().padding(10.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Create new chat",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge
)
Card(
onClick = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
onStartDirectMessageChatWithNpub.invoke()
}
}
) {
ListItem(
leadingContent = {
Icon(
Icons.Default.Key,
contentDescription = "Direct Message via npub"
)
},
headlineContent = {
Text("Direct Message via npub")
},
trailingContent = {
Icon(
Icons.Default.ChevronRight,
contentDescription = "Direct Message via npub"
)
}
)
}
Card(
onClick = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
onNavigateToChatRoomCreation.invoke()
}
}
) {
ListItem(
leadingContent = {
Icon(
Icons.Default.Diversity2,
contentDescription = "Start group chat"
)
},
headlineContent = {
Text(
text = "Start a group chat"
)
},
trailingContent = {
Icon(
Icons.Default.ChevronRight,
contentDescription = "Start group chat"
)
}
)
}
TextButton(
onClick = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
onSetShowBottomSheetUpdate.invoke(false)
}
}
}
) {
Icon(
Icons.Default.Close,
contentDescription = "Close new chat"
)
Spacer(
modifier = Modifier.width(10.dp)
)
Text("Close")
}
}
}
}

View File

@@ -0,0 +1,126 @@
package at.torch.compose.ui.composable.widgets.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Start
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import at.torch.compose.ui.composable.navigation.routes.ImplementationPendingRoute
import at.torch.compose.ui.composable.navigation.routes.Route
import fr.acinq.phoenix.utils.Parser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable
fun StartDirectMessageToNpubOrNip05Dialog(
scope: CoroutineScope,
toggleOpenDialogSetting: (Boolean) -> Unit,
onNavigateToRoute: (Route) -> Unit
) {
Dialog(
onDismissRequest = {
}
) {
val startChatButtonEnabled = remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
val textFieldState = rememberTextFieldState()
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
TextField(
state = textFieldState,
placeholder = {
Text("Input npub... or nip05")
},
trailingIcon = {
IconButton(
onClick = {
scope.launch {
toggleOpenDialogSetting.invoke(false)
}
}
) {
Icon(
Icons.Default.Close,
contentDescription = "Close Start Npub Direct Message"
)
}
},
lineLimits = TextFieldLineLimits.SingleLine,
inputTransformation = {
val input = asCharSequence().toString().lowercase()
if (input.startsWith("npub1") || input.startsWith("nostr:npub1")) {
// Check for valid npub
startChatButtonEnabled.value = true
} else if (Parser.parseEmailLikeAddress(input) != null) {
// Check for valid npub
startChatButtonEnabled.value = true
} else {
startChatButtonEnabled.value = false
}
}
)
TextButton(
enabled = startChatButtonEnabled.value,
onClick = {
scope.launch {
toggleOpenDialogSetting.invoke(false)
}.invokeOnCompletion {
onNavigateToRoute.invoke(
ImplementationPendingRoute(
"Direct Message to ${textFieldState.text}"
)
)
}
}
) {
Text("Start Chat")
Spacer(
modifier = Modifier.width(10.dp)
)
Icon(
Icons.Default.Start,
contentDescription = "Start chat"
)
}
}
}
}
}

View File

@@ -45,7 +45,7 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.machankura.compose.ui.composable.widgets.buttons.Clickable
import com.machankura.compose.ui.composable.widgets.dialogs.ModalBottomSheet
import at.torch.compose.ui.composable.widgets.dialogs.ModalBottomSheet
object WalletAvatars {
val list = listOf(

View File

@@ -46,15 +46,15 @@ class SignInToProfileViewModel(
)
fun validateCredential(): Entity? {
return signInToProfileFormState.textField.textFieldState.text.toString().let { credentialText ->
return signInToProfileFormState.textFieldForm.textFieldState.text.toString().let { credentialText ->
if (credentialText.isBlank()) {
signInToProfileFormState.textField.errorMessage.value = "Please input a credential"
signInToProfileFormState.textFieldForm.errorMessage.value = "Please input a credential"
return null
} else {
signInToProfileFormState.textField.errorMessage.value = null
signInToProfileFormState.textFieldForm.errorMessage.value = null
}
Nip19Parser.uriToRoute(signInToProfileFormState.textField.textFieldState.text.toString())?.entity
Nip19Parser.uriToRoute(signInToProfileFormState.textFieldForm.textFieldState.text.toString())?.entity
}
}

View File

@@ -81,7 +81,7 @@ class WriteNewNoteViewModel(
fun addProfileToMentionedProfiles(profile: at.torch.compose.database.model.Profile) {
atSymbolRange.value?.let { range ->
mentionedProfiles.add(profile)
writeNewNoteFormState.textField.textFieldState.edit {
writeNewNoteFormState.textFieldForm.textFieldState.edit {
val mentionProfile = "nostr:${
NProfile.create(
profile.publicKey,
@@ -145,7 +145,7 @@ class WriteNewNoteViewModel(
localProfile = localProfile,
inReplyToNostrEvent = inReplyToNostrEvent,
quotedNostrEvent = quotedNostrEvent,
text = writeNewNoteFormState.textField.textFieldState.text.toString(),
text = writeNewNoteFormState.textFieldForm.textFieldState.text.toString(),
)
}
}
@@ -153,11 +153,11 @@ class WriteNewNoteViewModel(
}
fun validateInput(): Boolean {
if (writeNewNoteFormState.textField.textFieldState.text.isBlank()) {
writeNewNoteFormState.textField.errorMessage.value = "Please input a note"
if (writeNewNoteFormState.textFieldForm.textFieldState.text.isBlank()) {
writeNewNoteFormState.textFieldForm.errorMessage.value = "Please input a note"
return false
} else {
writeNewNoteFormState.textField.errorMessage.value = null
writeNewNoteFormState.textFieldForm.errorMessage.value = null
}
return true
}
@@ -173,13 +173,13 @@ class WriteNewNoteViewModel(
viewModelScope.launch(Dispatchers.IO) {
val mentionedPublicKeys = at.torch.compose.ui.composable.widgets.content.ContentParser.parse(
writeNewNoteFormState.textField.textFieldState.text.toString(),
writeNewNoteFormState.textFieldForm.textFieldState.text.toString(),
).filterIsInstance<at.torch.compose.ui.composable.widgets.content.ContentSegment.NostrProfileSegment>().map { it.pubkey }
nostrRepository.createNewTextNote(
publicKey = activeUserPublicKey,
mentionedPublicKeys = mentionedPublicKeys,
textInput = writeNewNoteFormState.textField.textFieldState.text.toString(),
textInput = writeNewNoteFormState.textFieldForm.textFieldState.text.toString(),
inReplyToNostrEvent = inReplyToNostrEvent,
quotedNostrEvent = quotedNostrEvent,
onError = {

View File

@@ -1,6 +1,6 @@
package at.torch.compose.ui.view.state.form
data class ChatRoomCreationFormState(
val nameField: TextField = TextField(),
val descriptionField: TextField = TextField()
val nameField: TextFieldForm = TextFieldForm(),
val descriptionField: TextFieldForm = TextFieldForm()
)

View File

@@ -1,6 +1,6 @@
package at.torch.compose.ui.view.state.form
data class CreateProfileFormState(
val nameField: TextField = TextField(),
val biographyField: TextField = TextField()
val nameField: TextFieldForm = TextFieldForm(),
val biographyField: TextFieldForm = TextFieldForm()
)

View File

@@ -1,5 +1,5 @@
package at.torch.compose.ui.view.state.form
data class SignInToProfileFormState(
val textField: TextField = TextField(),
val textFieldForm: TextFieldForm = TextFieldForm(),
)

View File

@@ -4,7 +4,7 @@ import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
data class TextField(
data class TextFieldForm(
val textFieldState: TextFieldState = TextFieldState(),
val errorMessage: MutableState<String?> = mutableStateOf(null)
)

View File

@@ -1,5 +1,5 @@
package at.torch.compose.ui.view.state.form
data class WriteNewNoteFormState(
val textField: TextField = TextField(),
val textFieldForm: TextFieldForm = TextFieldForm(),
)