diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt index f76bf870..7418537d 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt @@ -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( diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt index cfa518b5..ecaf38d1 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt @@ -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 } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt index 174dc55a..66092f06 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt @@ -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 = {} ) } } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/WriteNewNoteScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/WriteNewNoteScreen.kt new file mode 100644 index 00000000..eea0b010 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/WriteNewNoteScreen.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt index 77040ebe..185704b9 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt @@ -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 { backStackEntry -> val route = backStackEntry.toRoute() @@ -247,6 +262,11 @@ fun AuxNavHost( event.content ) ) + }, + onNavigateToWriteNewNote = { + navController.navigate( + route = WriteNewNoteRoute() + ) } ) } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/WriteNewNoteRoute.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/WriteNewNoteRoute.kt new file mode 100755 index 00000000..90184554 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/WriteNewNoteRoute.kt @@ -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() { +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/WriteNewNoteViewModel.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/WriteNewNoteViewModel.kt new file mode 100644 index 00000000..06b742b9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/WriteNewNoteViewModel.kt @@ -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 = mutableStateOf(false) + + var writeNewNoteUIState: MutableState = 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 + } + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/WriteNewNoteUIState.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/WriteNewNoteUIState.kt new file mode 100644 index 00000000..f42cf35a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/WriteNewNoteUIState.kt @@ -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() + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/form/WriteNewNoteFormState.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/form/WriteNewNoteFormState.kt new file mode 100644 index 00000000..640b6ba3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/form/WriteNewNoteFormState.kt @@ -0,0 +1,5 @@ +package ac.aux.compose.ui.view.state.form + +data class WriteNewNoteFormState( + val textField: TextField = TextField(), +) \ No newline at end of file