Merge pull request #3022 from vitorpamplona/claude/fix-illegal-argument-exception-H4omh

Migrate text input to new Compose TextFieldState API
This commit is contained in:
Vitor Pamplona
2026-05-20 17:39:06 -04:00
committed by GitHub
14 changed files with 108 additions and 597 deletions
@@ -1,225 +0,0 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.actions.buildAnnotatedStringWithUrlHighlighting
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class UrlUserTagTransformationTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.vitorpamplona.amethyst", appContext.packageName.removeSuffix(".debug"))
}
@Test
fun testKeepTransformedIndexFullyInsideTransformedText() {
val user =
LocalCache.getOrCreateUser(
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
.toHexKey(),
)
user.metadata().newMetadata(
UserMetadata().also {
it.displayName = "Vitor Pamplona"
},
MetadataEvent(
id = "",
pubKey = "",
createdAt = 0,
tags = emptyArray(),
content = "",
sig = "",
),
)
val original = "@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"
val transformedText =
buildAnnotatedStringWithUrlHighlighting(
AnnotatedString(original),
Color.Red,
)
val expected = "@Vitor Pamplona"
assertEquals(expected, transformedText.text.text)
// The mention is treated as an atomic wedge: any cursor strictly inside the
// underlying npub snaps to the trailing edge of the displayed "@Vitor Pamplona"
// (and vice versa). This prevents an IME from placing the cursor in the middle
// of the bech32 and corrupting it on backspace.
assertEquals(0, transformedText.offsetMapping.originalToTransformed(0))
for (i in 1..63) {
assertEquals("originalToTransformed($i)", 15, transformedText.offsetMapping.originalToTransformed(i))
}
assertEquals(15, transformedText.offsetMapping.originalToTransformed(64))
assertEquals(0, transformedText.offsetMapping.transformedToOriginal(0))
for (i in 1..14) {
assertEquals("transformedToOriginal($i)", 64, transformedText.offsetMapping.transformedToOriginal(i))
}
assertEquals(64, transformedText.offsetMapping.transformedToOriginal(15))
}
@Test
fun transformationText() {
val user =
LocalCache.getOrCreateUser(
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
.toHexKey(),
)
user.metadata().newMetadata(
UserMetadata().also {
it.displayName = "Vitor Pamplona"
},
MetadataEvent(
id = "",
pubKey = "",
createdAt = 0,
tags = emptyArray(),
content = "",
sig = "",
),
)
val transformedText =
buildAnnotatedStringWithUrlHighlighting(
AnnotatedString("New Hey @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"),
Color.Red,
)
assertEquals("New Hey @Vitor Pamplona", transformedText.text.text)
// Outside the wedge: identity mapping.
assertEquals(0, transformedText.offsetMapping.originalToTransformed(0)) // Before N
assertEquals(4, transformedText.offsetMapping.originalToTransformed(4)) // Before H
assertEquals(8, transformedText.offsetMapping.originalToTransformed(8)) // Before @ (boundary)
// Strictly inside the underlying npub: snaps to the end of "@Vitor Pamplona" (offset 23).
assertEquals(23, transformedText.offsetMapping.originalToTransformed(9)) // Before n
assertEquals(23, transformedText.offsetMapping.originalToTransformed(12)) // Before b
assertEquals(23, transformedText.offsetMapping.originalToTransformed(13)) // Before 1
assertEquals(23, transformedText.offsetMapping.originalToTransformed(71)) // Before z
// End-of-wedge boundary maps to end of displayed mention.
assertEquals(23, transformedText.offsetMapping.originalToTransformed(72))
// Outside the wedge in displayed: identity.
assertEquals(0, transformedText.offsetMapping.transformedToOriginal(0))
assertEquals(4, transformedText.offsetMapping.transformedToOriginal(4))
assertEquals(8, transformedText.offsetMapping.transformedToOriginal(8)) // Before @ (boundary)
// Strictly inside displayed "@Vitor Pamplona": snaps to end of underlying npub (offset 72).
assertEquals(72, transformedText.offsetMapping.transformedToOriginal(9))
assertEquals(72, transformedText.offsetMapping.transformedToOriginal(22))
// End-of-wedge boundary maps to end of underlying mention; past it shifts by deltas.
assertEquals(72, transformedText.offsetMapping.transformedToOriginal(23))
assertEquals(73, transformedText.offsetMapping.transformedToOriginal(24))
}
@Test
fun transformationTextTwoKeys() {
val user =
LocalCache.getOrCreateUser(
decodePublicKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
.toHexKey(),
)
user.metadata().newMetadata(
UserMetadata().also {
it.displayName = "Vitor Pamplona"
},
MetadataEvent(
id = "",
pubKey = "",
createdAt = 0,
tags = emptyArray(),
content = "",
sig = "",
),
)
val transformedText =
buildAnnotatedStringWithUrlHighlighting(
AnnotatedString(
"New Hey @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z and @npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",
),
Color.Red,
)
assertEquals("New Hey @Vitor Pamplona and @Vitor Pamplona", transformedText.text.text)
// Strictly inside the first underlying npub [8, 72): snap to end of first
// displayed "@Vitor Pamplona" (offset 23).
assertEquals(23, transformedText.offsetMapping.originalToTransformed(11))
assertEquals(23, transformedText.offsetMapping.originalToTransformed(12))
assertEquals(23, transformedText.offsetMapping.originalToTransformed(13))
assertEquals(23, transformedText.offsetMapping.originalToTransformed(70))
assertEquals(23, transformedText.offsetMapping.originalToTransformed(71))
// Boundary at end of first wedge: end of first displayed mention.
assertEquals(23, transformedText.offsetMapping.originalToTransformed(72)) // Before <space>
assertEquals(24, transformedText.offsetMapping.originalToTransformed(73)) // Before a
assertEquals(25, transformedText.offsetMapping.originalToTransformed(74)) // Before n
assertEquals(26, transformedText.offsetMapping.originalToTransformed(75)) // Before d
assertEquals(27, transformedText.offsetMapping.originalToTransformed(76)) // Before <space>
assertEquals(28, transformedText.offsetMapping.originalToTransformed(77)) // Before @ (boundary, second wedge)
// Strictly inside the second underlying npub [77, 141): snap to end of second
// displayed "@Vitor Pamplona" (offset 43).
assertEquals(43, transformedText.offsetMapping.originalToTransformed(78)) // Before n
assertEquals(43, transformedText.offsetMapping.originalToTransformed(140))
// Strictly inside first displayed "@Vitor Pamplona" [8, 23): snap to end of
// first underlying npub (offset 72).
assertEquals(72, transformedText.offsetMapping.transformedToOriginal(22)) // Before a (display)
assertEquals(72, transformedText.offsetMapping.transformedToOriginal(23)) // Before <space>
assertEquals(73, transformedText.offsetMapping.transformedToOriginal(24)) // Before a
assertEquals(74, transformedText.offsetMapping.transformedToOriginal(25)) // Before n
assertEquals(75, transformedText.offsetMapping.transformedToOriginal(26)) // Before d
assertEquals(76, transformedText.offsetMapping.transformedToOriginal(27)) // Before <space>
assertEquals(77, transformedText.offsetMapping.transformedToOriginal(28)) // Before @ (boundary, second wedge)
// Strictly inside second displayed "@Vitor Pamplona" [28, 43): snap to end of
// second underlying npub (offset 141).
assertEquals(141, transformedText.offsetMapping.transformedToOriginal(29))
assertEquals(141, transformedText.offsetMapping.transformedToOriginal(42))
}
}
@@ -36,16 +36,16 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -63,7 +63,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -81,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.components.BechLink
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.ui.note.NoteCompose
@@ -300,8 +300,9 @@ fun EditPostView(
stringRes(id = R.string.lightning_invoice),
stringRes(id = R.string.lightning_create_and_add_invoice),
onNewInvoice = {
postViewModel.message =
TextFieldValue(postViewModel.message.text + "\n\n" + it)
postViewModel.message.setTextAndPlaceCursorAtEnd(
postViewModel.message.text.toString() + "\n\n" + it,
)
postViewModel.wantsInvoice = false
},
onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
@@ -309,43 +310,6 @@ fun EditPostView(
}
}
}
/*
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = Size5dp, horizontal = Size10dp),
) {
Column {
Text(
text = stringRes(R.string.message_to_author),
fontSize = 18.sp,
fontWeight = FontWeight.W500,
)
HorizontalDivider(thickness = DividerThickness)
MyTextField(
value = postViewModel.subject,
onValueChange = { postViewModel.updateSubject(it) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
text = stringRes(R.string.message_to_author_placeholder),
color = MaterialTheme.colorScheme.placeholderText,
)
},
visualTransformation =
UrlUserTagTransformation(
MaterialTheme.colorScheme.primary,
),
colors =
OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
),
)
}
}*/
}
}
@@ -416,9 +380,11 @@ private fun MessageField(postViewModel: EditPostViewModel) {
}
}
OutlinedTextField(
value = postViewModel.message,
onValueChange = { postViewModel.updateMessage(it) },
ThinPaddingTextField(
state = postViewModel.message,
onTextChanged = { postViewModel.onMessageChanged() },
inputTransformation = MentionPreservingInputTransformation,
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
@@ -443,11 +409,10 @@ private fun MessageField(postViewModel: EditPostViewModel) {
)
},
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
)
}
@@ -21,6 +21,8 @@
package com.vitorpamplona.amethyst.ui.actions
import android.content.Context
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -77,7 +79,7 @@ open class EditPostViewModel : ViewModel() {
var nip95attachments by
mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
var message by mutableStateOf(TextFieldValue(""))
val message = TextFieldState()
var urlPreview by mutableStateOf<String?>(null)
val mediaUploadTracker = MediaUploadTracker()
val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage
@@ -111,7 +113,7 @@ open class EditPostViewModel : ViewModel() {
canAddInvoice = accountViewModel.userProfile().lnAddress() != null
multiOrchestrator = null
message = TextFieldValue(versionLookingAt?.event?.content ?: edit.event?.content ?: "")
message.setTextAndPlaceCursorAtEnd(versionLookingAt?.event?.content ?: edit.event?.content ?: "")
urlPreview = findUrlInMessage()
this.editedFromNote = edit
@@ -141,7 +143,7 @@ open class EditPostViewModel : ViewModel() {
}
account.sendEdit(
message = message.text,
message = message.text.toString(),
originalNote = editedFromNote!!,
notify = notify,
summary = subject.text.ifBlank { null },
@@ -248,7 +250,7 @@ open class EditPostViewModel : ViewModel() {
}
}
message = message.insertUrlAtCursor(urls.joinToString(" "))
message.insertUrlAtCursor(urls.joinToString(" "))
urlPreview = findUrlInMessage()
this@EditPostViewModel.multiOrchestrator = null
@@ -264,7 +266,7 @@ open class EditPostViewModel : ViewModel() {
}
open fun cancel() {
message = TextFieldValue("")
message.setTextAndPlaceCursorAtEnd("")
subject = TextFieldValue("")
editedFromNote = null
@@ -280,17 +282,16 @@ open class EditPostViewModel : ViewModel() {
}
open fun findUrlInMessage(): String? =
message.text.split('\n').firstNotNullOfOrNull { paragraph ->
message.text.toString().split('\n').firstNotNullOfOrNull { paragraph ->
paragraph.split(' ').firstOrNull { word: String ->
RichTextParser.isValidURL(word) || RichTextParser.isUrlWithoutScheme(word)
}
}
open fun updateMessage(it: TextFieldValue) {
message = it
open fun onMessageChanged() {
urlPreview = findUrlInMessage()
if (it.selection.collapsed) {
if (message.selection.collapsed) {
val lastWord = message.currentWord()
if (lastWord.startsWith("@")) {
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
@@ -305,7 +306,7 @@ open class EditPostViewModel : ViewModel() {
open fun autocompleteWithUser(item: User) {
userSuggestions?.let { userSuggestions ->
val lastWord = message.currentWord()
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
userSuggestions.replaceCurrentWord(message, lastWord, item)
userSuggestionsMainMessage = null
userSuggestions.reset()
@@ -1,229 +0,0 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions
import android.util.Patterns
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextDecoration
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import kotlin.coroutines.cancellation.CancellationException
data class RangesChanges(
val original: TextRange,
val modified: TextRange,
)
class UrlUserTagTransformation(
val color: Color,
) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText = buildAnnotatedStringWithUrlHighlighting(text, color)
}
fun buildAnnotatedStringWithUrlHighlighting(
text: AnnotatedString,
color: Color,
): TransformedText {
val substitutions = mutableListOf<RangesChanges>()
val newText =
buildAnnotatedString {
val builderBefore = StringBuilder() // important to correctly measure Tag start and end
val builderAfter = StringBuilder() // important to correctly measure Tag start and end
append(
text.text.split('\n').joinToString("\n") { paragraph: String ->
paragraph.split(' ').joinToString(" ") { word: String ->
try {
val mention = parseUserMention(word)
if (mention != null) {
val (keyPortion, restOfWord, hexKey) = mention
val startIndex = builderBefore.toString().length
builderBefore.append(
"$keyPortion$restOfWord ",
) // accounts for the \n at the end of each paragraph
val endIndex = startIndex + keyPortion.length
val user = LocalCache.getOrCreateUser(hexKey)
val newWord = "@${user.toBestDisplayName()}"
val startNew = builderAfter.toString().length
builderAfter.append(
"$newWord$restOfWord ",
) // accounts for the \n at the end of each paragraph
substitutions.add(
RangesChanges(
TextRange(startIndex, endIndex),
TextRange(startNew, startNew + newWord.length),
),
)
newWord + restOfWord
} else if (Patterns.WEB_URL.matcher(word).matches()) {
val startIndex = builderBefore.toString().length
val endIndex = startIndex + word.length
val startNew = builderAfter.toString().length
val endNew = startNew + word.length
substitutions.add(
RangesChanges(
TextRange(startIndex, endIndex),
TextRange(startNew, endNew),
),
)
builderBefore.append("$word ")
builderAfter.append("$word ")
word
} else {
builderBefore.append("$word ")
builderAfter.append("$word ")
word
}
} catch (e: Exception) {
if (e is CancellationException) throw e
// if it can't parse the key, don't try to change.
builderBefore.append("$word ")
builderAfter.append("$word ")
word
}
}
},
)
substitutions.forEach {
addStyle(
style =
SpanStyle(
color = color,
textDecoration = TextDecoration.None,
),
start = it.modified.start,
end = it.modified.end,
)
}
}
val numberOffsetTranslator =
object : OffsetMapping {
// Treat each substitution as an atomic wedge: any cursor position that falls
// strictly inside a substituted range snaps to the wedge's trailing edge.
// Without this, an IME (e.g. SwiftKey in extracted-text mode) can place the
// cursor in the middle of an "@npub1..." mention, and a subsequent backspace
// deletes a char from inside the bech32, breaking the npub and "expanding"
// the collapsed mention.
override fun originalToTransformed(offset: Int): Int {
val inInsideRange =
substitutions.firstOrNull { offset > it.original.start && offset < it.original.end }
if (inInsideRange != null) {
return inInsideRange.modified.end
}
val lastRangeThrough = substitutions.lastOrNull { offset >= it.original.end }
return if (lastRangeThrough != null) {
lastRangeThrough.modified.end + (offset - lastRangeThrough.original.end)
} else {
offset
}
}
override fun transformedToOriginal(offset: Int): Int {
val inInsideRange =
substitutions.firstOrNull { offset > it.modified.start && offset < it.modified.end }
if (inInsideRange != null) {
return inInsideRange.original.end
}
val lastRangeThrough = substitutions.lastOrNull { offset >= it.modified.end }
return if (lastRangeThrough != null) {
lastRangeThrough.original.end + (offset - lastRangeThrough.modified.end)
} else {
offset
}
}
}
return TransformedText(
newText,
numberOffsetTranslator,
)
}
private data class UserMention(
val keyPortion: String,
val restOfWord: String,
val hexKey: String,
)
private fun parseUserMention(word: String): UserMention? {
var key = word
val prefix: String
if (key.startsWith("nostr:", true)) {
prefix = key.substring(0, 6)
key = key.substring(6)
} else if (key.startsWith("@")) {
prefix = "@"
key = key.substring(1)
} else {
return null
}
if (key.startsWith("npub1", true) && key.length >= 63) {
val keyB32 = key.substring(0, 63)
val restOfWord = key.substring(63)
val hex = decodePublicKeyAsHexOrNull(keyB32) ?: return null
return UserMention("$prefix$keyB32", restOfWord, hex)
} else if (key.startsWith("nprofile1", true)) {
val parsed = Nip19Parser.uriToRoute(key) ?: return null
val entity = parsed.entity
if (entity !is NProfile && entity !is NPub) return null
val hex =
when (entity) {
is NProfile -> entity.hex
is NPub -> entity.hex
else -> return null
}
val bech32Len = parsed.nip19raw.length
val restOfWord = key.substring(bech32Len)
return UserMention("$prefix${parsed.nip19raw}", restOfWord, hex)
}
return null
}
@@ -29,19 +29,22 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
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.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -123,9 +126,11 @@ fun ForwardZapTo(
}
}
OutlinedTextField(
value = postViewModel.forwardZapToEditting.value,
onValueChange = { postViewModel.updateZapForwardTo(it) },
ThinPaddingTextField(
state = postViewModel.forwardZapToEditting,
onTextChanged = postViewModel::onForwardZapTextChanged,
inputTransformation = MentionPreservingInputTransformation,
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
label = { Text(text = stringRes(R.string.zap_split_search_and_add_user)) },
modifier = Modifier.fillMaxWidth(),
placeholder = {
@@ -135,9 +140,10 @@ fun ForwardZapTo(
)
},
singleLine = true,
visualTransformation =
UrlUserTagTransformation(
MaterialTheme.colorScheme.primary,
colors =
TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
)
@@ -20,15 +20,15 @@
*/
package com.vitorpamplona.amethyst.ui.note.creators.zapsplits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.input.TextFieldValue
import com.vitorpamplona.amethyst.model.User
@Stable
interface IZapField {
val forwardZapTo: MutableState<SplitBuilder<User>>
val forwardZapToEditting: MutableState<TextFieldValue>
val forwardZapToEditting: TextFieldState
fun updateZapFromText()
@@ -37,5 +37,5 @@ interface IZapField {
sliderValue: Float,
)
fun updateZapForwardTo(newZapForwardTo: TextFieldValue)
fun onForwardZapTextChanged()
}
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments
import android.content.Context
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
@@ -194,7 +194,7 @@ open class CommentPostViewModel :
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
override var forwardZapTo = mutableStateOf<SplitBuilder<User>>(SplitBuilder())
override var forwardZapToEditting = mutableStateOf(TextFieldValue(""))
override val forwardZapToEditting = TextFieldState()
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
@@ -389,7 +389,7 @@ open class CommentPostViewModel :
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value)
}
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -713,7 +713,7 @@ open class CommentPostViewModel :
wantsAnonymousPost = false
forwardZapTo.value = SplitBuilder()
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
urlPreviews.reset()
@@ -753,10 +753,9 @@ open class CommentPostViewModel :
draftTag.newVersion()
}
override fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
forwardZapToEditting.value = newZapForwardTo
if (newZapForwardTo.selection.collapsed) {
val lastWord = newZapForwardTo.text
override fun onForwardZapTextChanged() {
if (forwardZapToEditting.selection.collapsed) {
val lastWord = forwardZapToEditting.text.toString()
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
userSuggestions?.processCurrentWord(lastWord)
}
@@ -770,7 +769,7 @@ open class CommentPostViewModel :
urlPreviews.update(message.text.toString())
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo.value.addItem(item)
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
}
userSuggestionsMainMessage = null
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
import android.content.Context
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
@@ -229,7 +229,7 @@ class ChatNewMessageViewModel :
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
override val forwardZapTo = mutableStateOf<SplitBuilder<User>>(SplitBuilder())
override val forwardZapToEditting = mutableStateOf(TextFieldValue(""))
override val forwardZapToEditting = TextFieldState()
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
@@ -347,7 +347,7 @@ class ChatNewMessageViewModel :
}
// don't support editing old-style splits.
}
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -651,7 +651,7 @@ class ChatNewMessageViewModel :
wantsSecretEmoji = false
forwardZapTo.value = SplitBuilder()
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
urlPreviews.reset()
@@ -724,10 +724,9 @@ class ChatNewMessageViewModel :
draftTag.newVersion()
}
override fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
forwardZapToEditting.value = newZapForwardTo
if (newZapForwardTo.selection.collapsed) {
val lastWord = newZapForwardTo.text
override fun onForwardZapTextChanged() {
if (forwardZapToEditting.selection.collapsed) {
val lastWord = forwardZapToEditting.text.toString()
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
userSuggestions?.processCurrentWord(lastWord)
}
@@ -744,7 +743,7 @@ class ChatNewMessageViewModel :
UserSuggestionAnchor.FORWARD_ZAPS -> {
forwardZapTo.value.addItem(item)
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
}
UserSuggestionAnchor.TO_USERS -> {
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
import android.content.Context
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
@@ -156,7 +156,7 @@ open class ChannelNewMessageViewModel :
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
var forwardZapTo by mutableStateOf<SplitBuilder<User>>(SplitBuilder())
var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
val forwardZapToEditting = TextFieldState()
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
@@ -240,7 +240,7 @@ open class ChannelNewMessageViewModel :
}
// don't support edditing old-style splits.
}
forwardZapToEditting = TextFieldValue("")
forwardZapToEditting.clearText()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -561,7 +561,7 @@ open class ChannelNewMessageViewModel :
wantsToAddGeoHash = false
forwardZapTo = SplitBuilder()
forwardZapToEditting = TextFieldValue("")
forwardZapToEditting.clearText()
userSuggestions?.reset()
userSuggestionsMainMessage = null
@@ -599,10 +599,9 @@ open class ChannelNewMessageViewModel :
draftTag.newVersion()
}
open fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
forwardZapToEditting = newZapForwardTo
if (newZapForwardTo.selection.collapsed) {
val lastWord = newZapForwardTo.text
open fun onForwardZapTextChanged() {
if (forwardZapToEditting.selection.collapsed) {
val lastWord = forwardZapToEditting.text.toString()
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
userSuggestions?.processCurrentWord(lastWord)
}
@@ -615,7 +614,7 @@ open class ChannelNewMessageViewModel :
it.replaceCurrentWord(message, lastWord, item)
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo.addItem(item)
forwardZapToEditting = TextFieldValue("")
forwardZapToEditting.clearText()
}
userSuggestionsMainMessage = null
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm
import android.content.Context
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
@@ -181,7 +182,7 @@ class LongFormPostViewModel :
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
override var forwardZapTo = mutableStateOf<SplitBuilder<User>>(SplitBuilder())
override var forwardZapToEditting = mutableStateOf(TextFieldValue(""))
override val forwardZapToEditting = TextFieldState()
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
@@ -290,7 +291,7 @@ class LongFormPostViewModel :
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value)
}
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -603,7 +604,7 @@ class LongFormPostViewModel :
wantsSecretEmoji = false
forwardZapTo.value = SplitBuilder()
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
userSuggestions?.reset()
userSuggestionsMainMessage = null
@@ -634,10 +635,9 @@ class LongFormPostViewModel :
draftTag.newVersion()
}
override fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
forwardZapToEditting.value = newZapForwardTo
if (newZapForwardTo.selection.collapsed) {
val lastWord = newZapForwardTo.text
override fun onForwardZapTextChanged() {
if (forwardZapToEditting.selection.collapsed) {
val lastWord = forwardZapToEditting.text.toString()
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
userSuggestions?.processCurrentWord(lastWord)
}
@@ -650,7 +650,7 @@ class LongFormPostViewModel :
userSuggestions.replaceCurrentWord(message, lastWord, item)
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo.value.addItem(item)
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
}
userSuggestionsMainMessage = null
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
import android.content.Context
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
@@ -168,7 +168,7 @@ open class NewProductViewModel :
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
override var forwardZapTo = mutableStateOf<SplitBuilder<User>>(SplitBuilder())
override var forwardZapToEditting = mutableStateOf(TextFieldValue(""))
override val forwardZapToEditting = TextFieldState()
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
@@ -266,7 +266,7 @@ open class NewProductViewModel :
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value)
}
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
wantsForwardZapTo = localfowardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -491,7 +491,7 @@ open class NewProductViewModel :
wantsSecretEmoji = false
forwardZapTo.value = SplitBuilder()
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
urlPreviews.reset()
@@ -535,10 +535,9 @@ open class NewProductViewModel :
draftTag.newVersion()
}
override fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
forwardZapToEditting.value = newZapForwardTo
if (newZapForwardTo.selection.collapsed) {
val lastWord = newZapForwardTo.text
override fun onForwardZapTextChanged() {
if (forwardZapToEditting.selection.collapsed) {
val lastWord = forwardZapToEditting.text.toString()
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
userSuggestions?.processCurrentWord(lastWord)
}
@@ -552,7 +551,7 @@ open class NewProductViewModel :
urlPreviews.update(message.text.toString())
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo.value.addItem(item)
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
}
userSuggestionsMainMessage = null
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
import android.content.Context
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
@@ -30,7 +31,6 @@ import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
@@ -283,7 +283,7 @@ open class ShortNotePostViewModel :
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
override var forwardZapTo = mutableStateOf<SplitBuilder<User>>(SplitBuilder())
override var forwardZapToEditting = mutableStateOf(TextFieldValue(""))
override val forwardZapToEditting = TextFieldState()
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
@@ -578,7 +578,7 @@ open class ShortNotePostViewModel :
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value)
}
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -664,7 +664,7 @@ open class ShortNotePostViewModel :
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value)
}
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -736,7 +736,7 @@ open class ShortNotePostViewModel :
val value = it.last().toFloatOrNull() ?: 0f
forwardZapTo.value.addItem(user, value)
}
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -1238,7 +1238,7 @@ open class ShortNotePostViewModel :
scheduledForSec = null
forwardZapTo.value = SplitBuilder()
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
urlPreviews.reset()
@@ -1283,10 +1283,9 @@ open class ShortNotePostViewModel :
draftTag.newVersion()
}
override fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
forwardZapToEditting.value = newZapForwardTo
if (newZapForwardTo.selection.collapsed) {
val lastWord = newZapForwardTo.text
override fun onForwardZapTextChanged() {
if (forwardZapToEditting.selection.collapsed) {
val lastWord = forwardZapToEditting.text.toString()
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
userSuggestions?.processCurrentWord(lastWord)
}
@@ -1300,7 +1299,7 @@ open class ShortNotePostViewModel :
urlPreviews.update(message.text.toString())
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo.value.addItem(item)
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
}
userSuggestionsMainMessage = null
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat
import android.content.Context
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
@@ -162,7 +162,7 @@ open class NestNewMessageViewModel :
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
var forwardZapTo by mutableStateOf<SplitBuilder<User>>(SplitBuilder())
var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
val forwardZapToEditting = TextFieldState()
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
@@ -246,7 +246,7 @@ open class NestNewMessageViewModel :
}
// don't support edditing old-style splits.
}
forwardZapToEditting = TextFieldValue("")
forwardZapToEditting.clearText()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -480,7 +480,7 @@ open class NestNewMessageViewModel :
wantsToAddGeoHash = false
forwardZapTo = SplitBuilder()
forwardZapToEditting = TextFieldValue("")
forwardZapToEditting.clearText()
userSuggestions?.reset()
userSuggestionsMainMessage = null
@@ -518,10 +518,9 @@ open class NestNewMessageViewModel :
draftTag.newVersion()
}
open fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
forwardZapToEditting = newZapForwardTo
if (newZapForwardTo.selection.collapsed) {
val lastWord = newZapForwardTo.text
open fun onForwardZapTextChanged() {
if (forwardZapToEditting.selection.collapsed) {
val lastWord = forwardZapToEditting.text.toString()
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
userSuggestions?.processCurrentWord(lastWord)
}
@@ -534,7 +533,7 @@ open class NestNewMessageViewModel :
it.replaceCurrentWord(message, lastWord, item)
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo.addItem(item)
forwardZapToEditting = TextFieldValue("")
forwardZapToEditting.clearText()
}
userSuggestionsMainMessage = null
@@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessag
import android.content.Context
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
@@ -180,7 +180,7 @@ class NewPublicMessageViewModel :
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
override val forwardZapTo = mutableStateOf<SplitBuilder<User>>(SplitBuilder())
override val forwardZapToEditting = mutableStateOf(TextFieldValue(""))
override val forwardZapToEditting = TextFieldState()
// NSFW, Sensitive
var wantsToMarkAsSensitive by mutableStateOf(false)
@@ -282,7 +282,7 @@ class NewPublicMessageViewModel :
}
// don't support editing old-style splits.
}
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
wantsForwardZapTo = localForwardZapTo.isNotEmpty()
wantsToMarkAsSensitive = draftEvent.isSensitive()
@@ -535,7 +535,7 @@ class NewPublicMessageViewModel :
wantsSecretEmoji = false
forwardZapTo.value = SplitBuilder()
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
urlPreviews.reset()
@@ -587,10 +587,9 @@ class NewPublicMessageViewModel :
draftTag.newVersion()
}
override fun updateZapForwardTo(newZapForwardTo: TextFieldValue) {
forwardZapToEditting.value = newZapForwardTo
if (newZapForwardTo.selection.collapsed) {
val lastWord = newZapForwardTo.text
override fun onForwardZapTextChanged() {
if (forwardZapToEditting.selection.collapsed) {
val lastWord = forwardZapToEditting.text.toString()
userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS
userSuggestions?.processCurrentWord(lastWord)
}
@@ -607,7 +606,7 @@ class NewPublicMessageViewModel :
UserSuggestionAnchor.FORWARD_ZAPS -> {
forwardZapTo.value.addItem(item)
forwardZapToEditting.value = TextFieldValue("")
forwardZapToEditting.clearText()
}
UserSuggestionAnchor.TO_USERS -> {