Add write note functionality
This commit is contained in:
@@ -12,6 +12,7 @@ import ac.aux.compose.repository.NostrRepository
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Instant
|
||||
@@ -59,8 +60,6 @@ class DatabaseNostrRepository(
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
if (unsignedNostrEvent == null) {
|
||||
onError.invoke()
|
||||
} else if (unsignedNostrEvent.kind == 0 ) {
|
||||
@@ -69,6 +68,33 @@ class DatabaseNostrRepository(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createNewTextNote(
|
||||
publicKey: HexKey,
|
||||
text: String,
|
||||
onError: () -> Unit,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
val textNote = TextNoteEvent.build(
|
||||
note = text,
|
||||
)
|
||||
|
||||
val unsignedNostrEvent = saveUnsignedNostrEvent(
|
||||
UnsignedNostrEvent(
|
||||
pubKey = publicKey,
|
||||
kind = textNote.kind,
|
||||
createdAt = Instant.fromEpochMilliseconds(textNote.createdAt),
|
||||
tags = textNote.tags,
|
||||
content = textNote.content
|
||||
)
|
||||
)
|
||||
|
||||
if (unsignedNostrEvent == null) {
|
||||
onError.invoke()
|
||||
} else {
|
||||
onSuccess.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveUnsignedNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): UnsignedNostrEvent? = try {
|
||||
delay(2_100) // Just to make it look like we are doing serious work...
|
||||
unsignedNostrEvent.copy(
|
||||
|
||||
@@ -27,6 +27,13 @@ interface NostrRepository {
|
||||
onProfileReady: (Profile) -> Unit
|
||||
)
|
||||
|
||||
suspend fun createNewTextNote(
|
||||
publicKey: HexKey,
|
||||
text: String,
|
||||
onError: () -> Unit,
|
||||
onSuccess: () -> Unit
|
||||
)
|
||||
|
||||
suspend fun saveUnsignedNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): UnsignedNostrEvent?
|
||||
|
||||
suspend fun wipeDatabase()
|
||||
@@ -64,6 +71,15 @@ interface NostrRepository {
|
||||
) {
|
||||
}
|
||||
|
||||
override suspend fun createNewTextNote(
|
||||
publicKey: HexKey,
|
||||
text: String,
|
||||
onError: () -> Unit,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun saveUnsignedNostrEvent(unsignedNostrEvent: UnsignedNostrEvent): UnsignedNostrEvent? {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -14,8 +14,12 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Adb
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -40,7 +44,8 @@ import kotlin.time.Clock
|
||||
@Composable
|
||||
fun FeedScreen(
|
||||
initialFeedListUIState: FeedListUIState,
|
||||
onNavigateToEvent: (Event) -> Unit
|
||||
onNavigateToEvent: (Event) -> Unit,
|
||||
onNavigateToWriteNewNote: () -> Unit,
|
||||
) {
|
||||
val feedListViewModel: FeedListViewModel = viewModel(
|
||||
factory = FeedListViewModel.factory(
|
||||
@@ -80,6 +85,20 @@ fun FeedScreen(
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = onNavigateToWriteNewNote,
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Default.Create,
|
||||
contentDescription = "Write"
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text("Write")
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
@@ -125,7 +144,8 @@ private fun FeedScreenPreview() {
|
||||
) {
|
||||
FeedScreen(
|
||||
initialFeedListUIState = FeedListUIState.Loading,
|
||||
onNavigateToEvent = {}
|
||||
onNavigateToEvent = {},
|
||||
onNavigateToWriteNewNote = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package ac.aux.compose.ui.composable
|
||||
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.repository.NostrRepository
|
||||
import ac.aux.compose.ui.theme.AuxTheme
|
||||
import ac.aux.compose.ui.theme.BluePill
|
||||
import ac.aux.compose.ui.theme.RedPill
|
||||
import ac.aux.compose.ui.view.model.WriteNewNoteViewModel
|
||||
import ac.aux.compose.ui.view.state.CreateProfileUIState
|
||||
import ac.aux.compose.ui.view.state.WriteNewNoteUIState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.NavigateNext
|
||||
import androidx.compose.material.icons.filled.Badge
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.Description
|
||||
import androidx.compose.material.icons.filled.NavigateNext
|
||||
import androidx.compose.material.icons.filled.NextPlan
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LoadingIndicator
|
||||
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.graphics.Color
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun WriteNewNoteScreen(
|
||||
initialWriteNewNoteUIState: WriteNewNoteUIState = WriteNewNoteUIState.InputPrompt,
|
||||
onNostrEventPublished: () -> Unit,
|
||||
nostrRepository: NostrRepository
|
||||
) {
|
||||
val writeNewNoteViewModel: WriteNewNoteViewModel = viewModel (
|
||||
factory = WriteNewNoteViewModel.factory(
|
||||
initialWriteNewNoteUIState,
|
||||
nostrRepository
|
||||
)
|
||||
)
|
||||
Scaffold { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(
|
||||
10.dp
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
|
||||
when (val writeNewNoteUIState = writeNewNoteViewModel.writeNewNoteUIState.value) {
|
||||
is WriteNewNoteUIState.InputPrompt -> {
|
||||
Text(
|
||||
"Write new note",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = writeNewNoteViewModel.writeNewNoteFormState.textField.text.value,
|
||||
onValueChange = {
|
||||
writeNewNoteViewModel.writeNewNoteFormState.textField.text.value = it
|
||||
},
|
||||
isError = writeNewNoteViewModel.writeNewNoteFormState.textField.errorMessage.value != null,
|
||||
supportingText = writeNewNoteViewModel.writeNewNoteFormState.textField.errorMessage.value?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.padding(
|
||||
bottom = 8.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
label = {
|
||||
Text(
|
||||
text = "What vibrations do you want to send out?",
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "Enter the name you want to use for your profile",
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Create,
|
||||
contentDescription = "Write note"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
|
||||
writeNewNoteViewModel.validateInput().let { isInputValid ->
|
||||
if (isInputValid) {
|
||||
writeNewNoteViewModel.writeNewNoteUIState.value =
|
||||
WriteNewNoteUIState.ConfirmNote(
|
||||
text = writeNewNoteViewModel.writeNewNoteFormState.textField.text.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
"Next"
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.NavigateNext,
|
||||
contentDescription = "Next"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
is WriteNewNoteUIState.ConfirmNote-> {
|
||||
|
||||
// TODO: Get profile for active pubKey
|
||||
Text(
|
||||
text = "Display Name",
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
|
||||
Text(
|
||||
text = writeNewNoteUIState.text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
|
||||
|
||||
Text(
|
||||
text = "The above will be your new note.",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
|
||||
if (writeNewNoteViewModel.isActionPending.value) {
|
||||
LoadingIndicator(
|
||||
modifier = Modifier
|
||||
)
|
||||
} else {
|
||||
Button(
|
||||
onClick = {
|
||||
writeNewNoteViewModel.createNewNote(
|
||||
onNostrEventPublished = onNostrEventPublished
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "Transmit Note"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
is WriteNewNoteUIState.Error -> {
|
||||
Spacer(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "Something went wrong and we were unable to write a new note. Please try again later.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WriteNewNoteScreenPreview() {
|
||||
AuxTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
WriteNewNoteScreen(
|
||||
initialWriteNewNoteUIState =
|
||||
WriteNewNoteUIState.InputPrompt,
|
||||
// CreateProfileUIState.Error,
|
||||
// CreateProfileUIState.ConfirmInput(
|
||||
// name = "Alan Turing",
|
||||
// bio = "Polyglot: Math... is my middle name, computer scientist. cryptographer, and gold hider."
|
||||
// ),
|
||||
// CreateProfileUIState.UnsignedProfile(
|
||||
// unsignedNostrEvent = UnsignedNostrEvent(
|
||||
// kind = 0,
|
||||
// content = "Something here",
|
||||
// tags = emptyArray(),
|
||||
// ),
|
||||
// name = "Alan Turing",
|
||||
// bio = "Polyglot: Math... is my middle name, computer scientist. cryptographer, and gold hider."
|
||||
// ),
|
||||
// CreateProfileUIState.UnbroadcastedProfile(
|
||||
// nostrEvent = NostrEvent(
|
||||
// id = "",
|
||||
// pubKey = "",
|
||||
// kind = 0,
|
||||
// content = "Something here",
|
||||
// tags = emptyArray(),
|
||||
// sig = ""
|
||||
// ),
|
||||
// name = "Alan Turing",
|
||||
// bio = "Polyglot: Math... is my middle name, computer scientist. cryptographer, and gold hider."
|
||||
// ),
|
||||
onNostrEventPublished = {},
|
||||
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import ac.aux.compose.ui.composable.UnannouncedProfileScreen
|
||||
import ac.aux.compose.ui.composable.UnindexedProfileScreen
|
||||
import ac.aux.compose.ui.composable.UnqueuedProfileScreen
|
||||
import ac.aux.compose.ui.composable.UnsignedProfileScreen
|
||||
import ac.aux.compose.ui.composable.WriteNewNoteScreen
|
||||
import ac.aux.compose.ui.composable.navigation.routes.BlankRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.CreateProfileRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.FeedRoute
|
||||
@@ -29,6 +30,7 @@ import ac.aux.compose.ui.composable.navigation.routes.UnannouncedProfileRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.UnindexedProfileRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.UnqueuedProfileRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.UnsignedProfileRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.WriteNewNoteRoute
|
||||
import ac.aux.compose.ui.composable.widgets.feed.DUMMY_EVENTS
|
||||
import ac.aux.compose.ui.view.model.NavigationViewModel
|
||||
import ac.aux.compose.ui.view.state.FeedListUIState
|
||||
@@ -47,6 +49,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun AuxNavHost(
|
||||
@@ -68,6 +72,7 @@ fun AuxNavHost(
|
||||
}
|
||||
|
||||
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)
|
||||
val applicationMainScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
|
||||
val navigationViewModel: NavigationViewModel = viewModel (
|
||||
@@ -197,6 +202,16 @@ fun AuxNavHost(
|
||||
nostrRepository = nostrRepository
|
||||
)
|
||||
}
|
||||
composable< WriteNewNoteRoute> {
|
||||
WriteNewNoteScreen(
|
||||
onNostrEventPublished = {
|
||||
applicationMainScope.launch {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
nostrRepository = nostrRepository
|
||||
)
|
||||
}
|
||||
composable<UnsignedProfileRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<UnsignedProfileRoute>()
|
||||
|
||||
@@ -247,6 +262,11 @@ fun AuxNavHost(
|
||||
event.content
|
||||
)
|
||||
)
|
||||
},
|
||||
onNavigateToWriteNewNote = {
|
||||
navController.navigate(
|
||||
route = WriteNewNoteRoute()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package ac.aux.compose.ui.composable.navigation.routes
|
||||
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
|
||||
@Serializable
|
||||
data class WriteNewNoteRoute(
|
||||
val inReplyToEventId: String? = null,
|
||||
): Route() {
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package ac.aux.compose.ui.view.model
|
||||
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.managers.SeedManager
|
||||
import ac.aux.compose.repository.NostrRepository
|
||||
import ac.aux.compose.ui.view.state.WriteNewNoteUIState
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import ac.aux.compose.ui.view.state.form.WriteNewNoteFormState
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WriteNewNoteViewModel(
|
||||
val initialWriteNewNoteUIState: WriteNewNoteUIState,
|
||||
val writeNewNoteFormState: WriteNewNoteFormState = WriteNewNoteFormState(),
|
||||
val nostrRepository: NostrRepository
|
||||
): ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "WriteNewNoteViewModel"
|
||||
|
||||
fun factory(
|
||||
initialWriteNewNoteUIState: WriteNewNoteUIState,
|
||||
nostrRepository: NostrRepository
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
WriteNewNoteViewModel(
|
||||
initialWriteNewNoteUIState,
|
||||
nostrRepository = nostrRepository
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val logger = Logger.withTag(TAG)
|
||||
|
||||
val isActionPending: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
var writeNewNoteUIState: MutableState<WriteNewNoteUIState> = mutableStateOf(
|
||||
initialWriteNewNoteUIState
|
||||
)
|
||||
|
||||
fun validateInput(): Boolean {
|
||||
if (writeNewNoteFormState.textField.text.value.isBlank()) {
|
||||
writeNewNoteFormState.textField.errorMessage.value = "Please input a note"
|
||||
return false
|
||||
} else {
|
||||
writeNewNoteFormState.textField.errorMessage.value = null
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun createNewNote(
|
||||
onNostrEventPublished: () -> Unit
|
||||
) {
|
||||
logger.d { "createAccount" }
|
||||
isActionPending.value = true
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
||||
|
||||
nostrRepository.createNewTextNote(
|
||||
publicKey = SeedManager.activePublicKey().toHexKey(),
|
||||
text = writeNewNoteFormState.textField.text.value,
|
||||
onSuccess = onNostrEventPublished,
|
||||
onError = {
|
||||
writeNewNoteUIState.value = WriteNewNoteUIState.Error
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ac.aux.compose.ui.view.state
|
||||
|
||||
import ac.aux.compose.database.model.NostrEvent
|
||||
import ac.aux.compose.database.model.Profile
|
||||
import ac.aux.compose.database.model.UnsignedNostrEvent
|
||||
|
||||
abstract class WriteNewNoteUIState {
|
||||
data object Error: WriteNewNoteUIState()
|
||||
|
||||
data object InputPrompt: WriteNewNoteUIState()
|
||||
|
||||
data class ConfirmNote(
|
||||
val text: String,
|
||||
) : WriteNewNoteUIState()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ac.aux.compose.ui.view.state.form
|
||||
|
||||
data class WriteNewNoteFormState(
|
||||
val textField: TextField = TextField(),
|
||||
)
|
||||
Reference in New Issue
Block a user