Merge pull request #2040 from vitorpamplona/claude/migrate-textfield-state-6ezEZ
Migrate text fields from TextFieldValue to TextFieldState
This commit is contained in:
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 androidx.compose.foundation.text.input.OutputTransformation
|
||||
import androidx.compose.foundation.text.input.TextFieldBuffer
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class UrlUserTagOutputTransformation(
|
||||
val color: Color,
|
||||
) : OutputTransformation {
|
||||
override fun TextFieldBuffer.transformOutput() {
|
||||
val text = asCharSequence().toString()
|
||||
|
||||
// Find all @npub mentions using regex and replace in reverse order
|
||||
// so that earlier indices remain valid after replacements.
|
||||
val npubRegex = Regex("@npub1[a-z0-9]{58}")
|
||||
val matches = npubRegex.findAll(text).toList().reversed()
|
||||
|
||||
for (match in matches) {
|
||||
try {
|
||||
val key = decodePublicKey(match.value.removePrefix("@"))
|
||||
val user = LocalCache.getOrCreateUser(key.toHexKey())
|
||||
val displayName = "@${user.toBestDisplayName()}"
|
||||
|
||||
replace(match.range.first, match.range.last + 1, displayName)
|
||||
|
||||
// Apply color styling to the replaced display name
|
||||
addAnnotation(
|
||||
SpanStyle(color = color, textDecoration = TextDecoration.None),
|
||||
match.range.first,
|
||||
match.range.first + displayName.length,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight URLs in remaining text
|
||||
highlightUrls(color)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TextFieldBuffer.highlightUrls(color: Color) {
|
||||
val text = asCharSequence().toString()
|
||||
val urlPattern = android.util.Patterns.WEB_URL
|
||||
|
||||
val matcher = urlPattern.matcher(text)
|
||||
while (matcher.find()) {
|
||||
addAnnotation(
|
||||
SpanStyle(color = color, textDecoration = TextDecoration.None),
|
||||
matcher.start(),
|
||||
matcher.end(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+126
-1
@@ -27,6 +27,10 @@ import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.InputTransformation
|
||||
import androidx.compose.foundation.text.input.OutputTransformation
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
@@ -35,8 +39,11 @@ import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
@@ -50,6 +57,124 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
// COPIED FROM TEXT FIELD
|
||||
// The only change is the contentPadding below
|
||||
|
||||
// New TextFieldState-based overload for GIF keyboard support
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ThinPaddingTextField(
|
||||
state: TextFieldState,
|
||||
modifier: Modifier = Modifier,
|
||||
onTextChanged: (() -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
inputTransformation: InputTransformation? = null,
|
||||
outputTransformation: OutputTransformation? = null,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
prefix: @Composable (() -> Unit)? = null,
|
||||
suffix: @Composable (() -> Unit)? = null,
|
||||
supportingText: @Composable (() -> Unit)? = null,
|
||||
isError: Boolean = false,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
singleLine: Boolean = false,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
minLines: Int = 1,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors(),
|
||||
contentPadding: PaddingValues =
|
||||
if (label == null) {
|
||||
TextFieldDefaults.contentPaddingWithoutLabel(
|
||||
start = 10.dp,
|
||||
top = 12.dp,
|
||||
end = 10.dp,
|
||||
bottom = 12.dp,
|
||||
)
|
||||
} else {
|
||||
TextFieldDefaults.contentPaddingWithLabel(
|
||||
start = 10.dp,
|
||||
top = 12.dp,
|
||||
end = 10.dp,
|
||||
bottom = 12.dp,
|
||||
)
|
||||
},
|
||||
) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
|
||||
|
||||
if (onTextChanged != null) {
|
||||
val callback by rememberUpdatedState(onTextChanged)
|
||||
LaunchedEffect(state) {
|
||||
snapshotFlow { state.text }
|
||||
.collect { callback() }
|
||||
}
|
||||
}
|
||||
|
||||
val textColor =
|
||||
textStyle.color.takeOrElse {
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
when {
|
||||
!enabled -> MaterialTheme.colorScheme.placeholderText
|
||||
isError -> MaterialTheme.colorScheme.onSurface
|
||||
focused -> MaterialTheme.colorScheme.onSurface
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
}
|
||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||
|
||||
val lineLimits =
|
||||
if (singleLine) {
|
||||
TextFieldLineLimits.SingleLine
|
||||
} else {
|
||||
TextFieldLineLimits.MultiLine(minLines, maxLines)
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
||||
BasicTextField(
|
||||
state = state,
|
||||
modifier =
|
||||
modifier
|
||||
.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = 36.dp,
|
||||
),
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = mergedTextStyle,
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
keyboardOptions = keyboardOptions,
|
||||
lineLimits = lineLimits,
|
||||
interactionSource = interactionSource,
|
||||
inputTransformation = inputTransformation,
|
||||
outputTransformation = outputTransformation,
|
||||
decorator = { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.text.toString(),
|
||||
visualTransformation = VisualTransformation.None,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = placeholder,
|
||||
label = label,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
prefix = prefix,
|
||||
suffix = suffix,
|
||||
supportingText = supportingText,
|
||||
shape = shape,
|
||||
singleLine = singleLine,
|
||||
enabled = enabled,
|
||||
isError = isError,
|
||||
interactionSource = interactionSource,
|
||||
colors = colors,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy TextFieldValue-based overload (kept for readonly dropdown fields)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ThinPaddingTextField(
|
||||
@@ -76,7 +201,6 @@ fun ThinPaddingTextField(
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors(),
|
||||
// new fields
|
||||
contentPadding: PaddingValues =
|
||||
if (label == null) {
|
||||
TextFieldDefaults.contentPaddingWithoutLabel(
|
||||
@@ -113,6 +237,7 @@ fun ThinPaddingTextField(
|
||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
||||
@Suppress("DEPRECATION")
|
||||
BasicTextField(
|
||||
value = value,
|
||||
modifier =
|
||||
|
||||
+3
-3
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.messagefield
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
|
||||
interface IMessageField {
|
||||
var message: TextFieldValue
|
||||
val message: TextFieldState
|
||||
|
||||
fun updateMessage(newMessage: TextFieldValue)
|
||||
fun onMessageChanged()
|
||||
}
|
||||
|
||||
+4
-4
@@ -39,7 +39,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -65,8 +65,8 @@ fun MessageField(
|
||||
}
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = viewModel.message,
|
||||
onValueChange = viewModel::updateMessage,
|
||||
state = viewModel.message,
|
||||
onTextChanged = viewModel::onMessageChanged,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
@@ -91,7 +91,7 @@ fun MessageField(
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
contentPadding =
|
||||
TextFieldDefaults.contentPaddingWithoutLabel(
|
||||
|
||||
+4
-5
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.previews
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.vitorpamplona.amethyst.service.CachedUrlParser
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -33,22 +32,22 @@ import kotlinx.coroutines.flow.map
|
||||
|
||||
@Stable
|
||||
class PreviewState {
|
||||
var source = MutableStateFlow(TextFieldValue(""))
|
||||
var source = MutableStateFlow("")
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val results =
|
||||
source
|
||||
.debounce(500)
|
||||
.map {
|
||||
CachedUrlParser.parseValidUrls(it.text)
|
||||
CachedUrlParser.parseValidUrls(it)
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
fun reset() {
|
||||
source.tryEmit(TextFieldValue(""))
|
||||
source.tryEmit("")
|
||||
}
|
||||
|
||||
fun update(text: TextFieldValue) {
|
||||
fun update(text: String) {
|
||||
source.tryEmit(text)
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
@@ -199,4 +200,17 @@ class UserSuggestionState(
|
||||
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length),
|
||||
)
|
||||
}
|
||||
|
||||
fun replaceCurrentWord(
|
||||
state: TextFieldState,
|
||||
word: String,
|
||||
item: User,
|
||||
) {
|
||||
val wordToInsert = "@${item.pubkeyNpub()}"
|
||||
state.edit {
|
||||
val lastWordStart = selection.end - word.length
|
||||
replace(lastWordStart, selection.end, wordToInsert)
|
||||
selection = TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-24
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.nip22Comments
|
||||
|
||||
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.mutableLongStateOf
|
||||
@@ -147,7 +149,7 @@ open class CommentPostViewModel :
|
||||
|
||||
var notifying by mutableStateOf<List<User>?>(null)
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
@@ -242,7 +244,7 @@ open class CommentPostViewModel :
|
||||
}
|
||||
|
||||
open fun quote(quote: Note) {
|
||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||
|
||||
quote.author?.let { quotedUser ->
|
||||
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||
@@ -262,7 +264,7 @@ open class CommentPostViewModel :
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
@@ -322,11 +324,11 @@ open class CommentPostViewModel :
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
@@ -359,7 +361,7 @@ open class CommentPostViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val attachments = mutableSetOf<Event>()
|
||||
@@ -376,7 +378,7 @@ open class CommentPostViewModel :
|
||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message = message.text,
|
||||
message = message.text.toString(),
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
@@ -525,8 +527,8 @@ open class CommentPostViewModel :
|
||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
val iMeta =
|
||||
@@ -549,8 +551,8 @@ open class CommentPostViewModel :
|
||||
|
||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||
|
||||
message = message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +578,7 @@ open class CommentPostViewModel :
|
||||
open fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
replyingTo = null
|
||||
externalIdentity = null
|
||||
@@ -618,9 +620,8 @@ open class CommentPostViewModel :
|
||||
notifying = notifying?.filter { it != userToRemove }
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreviews.update(message)
|
||||
override fun onMessageChanged() {
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = message.currentWord()
|
||||
@@ -651,8 +652,8 @@ open class CommentPostViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message.text.toString())
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
@@ -668,8 +669,8 @@ open class CommentPostViewModel :
|
||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -685,8 +686,8 @@ open class CommentPostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -694,14 +695,14 @@ open class CommentPostViewModel :
|
||||
}
|
||||
|
||||
fun canPost(): Boolean =
|
||||
message.text.isNotBlank() &&
|
||||
message.text.toString().isNotBlank() &&
|
||||
!mediaUploadTracker.isUploading &&
|
||||
!wantsInvoice &&
|
||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
||||
multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
@@ -718,7 +719,7 @@ open class CommentPostViewModel :
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger =
|
||||
NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
+3
-2
@@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -46,7 +47,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -125,7 +125,8 @@ fun ReplyCommentPostScreen(
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
+3
-2
@@ -24,11 +24,11 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
@@ -108,7 +108,8 @@ fun ChatroomView(
|
||||
|
||||
if (draftMessage != null) {
|
||||
LaunchedEffect(key1 = draftMessage) {
|
||||
newPostModel.updateMessage(TextFieldValue(draftMessage))
|
||||
newPostModel.message.setTextAndPlaceCursorAtEnd(draftMessage)
|
||||
newPostModel.onMessageChanged()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
-48
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -203,7 +204,7 @@ class ChatNewMessageViewModel :
|
||||
|
||||
var uploadsWaitingToBeSent by mutableStateOf<List<SuccessfulUploads>>(emptyList())
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
@@ -215,8 +216,8 @@ class ChatNewMessageViewModel :
|
||||
|
||||
var emojiSuggestions: EmojiSuggestionState? = null
|
||||
|
||||
var toUsers by mutableStateOf(TextFieldValue(""))
|
||||
var subject by mutableStateOf(TextFieldValue(""))
|
||||
val toUsers = TextFieldState()
|
||||
val subject = TextFieldState()
|
||||
|
||||
// Invoices
|
||||
var canAddInvoice by mutableStateOf(false)
|
||||
@@ -273,10 +274,9 @@ class ChatNewMessageViewModel :
|
||||
|
||||
fun load(room: ChatroomKey) {
|
||||
this.room.tryEmit(room)
|
||||
this.toUsers =
|
||||
TextFieldValue(
|
||||
room.users.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
this.toUsers.setTextAndPlaceCursorAtEnd(
|
||||
room.users.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
}
|
||||
|
||||
fun reply(replyNote: Note) {
|
||||
@@ -290,8 +290,8 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
|
||||
fun quote(quote: Note) {
|
||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||
urlPreviews.update(message)
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
// creates a split with that author.
|
||||
val quotedAuthor = quote.author ?: return
|
||||
@@ -371,14 +371,13 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
|
||||
draftEvent.subject()?.let {
|
||||
subject = TextFieldValue()
|
||||
subject.setTextAndPlaceCursorAtEnd("")
|
||||
}
|
||||
|
||||
if (draftEvent is NIP17Group) {
|
||||
toUsers =
|
||||
TextFieldValue(
|
||||
draftEvent.groupMembers().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
toUsers.setTextAndPlaceCursorAtEnd(
|
||||
draftEvent.groupMembers().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
|
||||
val replyId =
|
||||
when (draftEvent) {
|
||||
@@ -392,7 +391,7 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
} else if (draftEvent is PrivateDmEvent) {
|
||||
val recipientNPub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
||||
toUsers = TextFieldValue("@$recipientNPub")
|
||||
toUsers.setTextAndPlaceCursorAtEnd("@$recipientNPub")
|
||||
|
||||
val replyId = draftEvent.replyTo()
|
||||
if (replyId != null) {
|
||||
@@ -400,13 +399,14 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message =
|
||||
val draftText =
|
||||
if (draftEvent is PrivateDmEvent) {
|
||||
TextFieldValue(accountViewModel.account.privateDMDecryptionCache.cachedDM(draftEvent) ?: "")
|
||||
accountViewModel.account.privateDMDecryptionCache.cachedDM(draftEvent) ?: ""
|
||||
} else {
|
||||
TextFieldValue(draftEvent.content)
|
||||
draftEvent.content
|
||||
}
|
||||
urlPreviews.update(message)
|
||||
message.setTextAndPlaceCursorAtEnd(draftText)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
}
|
||||
@@ -421,7 +421,7 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
innerSendPost(draftTag.current)
|
||||
@@ -563,11 +563,12 @@ class ChatNewMessageViewModel :
|
||||
private suspend fun innerSendPost(draftTag: String?) {
|
||||
val room = room.value ?: return
|
||||
|
||||
val urls = findURLs(message.text)
|
||||
val messageText = message.text.toString()
|
||||
val urls = findURLs(messageText)
|
||||
val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet())
|
||||
val emojis = findEmoji(message.text, accountViewModel.account.emoji.myEmojis.value)
|
||||
val emojis = findEmoji(messageText, accountViewModel.account.emoji.myEmojis.value)
|
||||
val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null
|
||||
val message = message.text
|
||||
val message = messageText
|
||||
|
||||
val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null
|
||||
val localExpirationDate = if (wantsExpirationDate) expirationDate else null
|
||||
@@ -633,8 +634,8 @@ class ChatNewMessageViewModel :
|
||||
fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message = TextFieldValue("")
|
||||
subject = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
subject.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
replyTo.value = null
|
||||
|
||||
@@ -667,15 +668,15 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
|
||||
fun addToMessage(it: String) {
|
||||
updateMessage(TextFieldValue(message.text + " " + it))
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||
onMessageChanged()
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreviews.update(newMessage)
|
||||
override fun onMessageChanged() {
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
@@ -689,11 +690,9 @@ class ChatNewMessageViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateToUsers(newToUsersValue: TextFieldValue) {
|
||||
toUsers = newToUsersValue
|
||||
|
||||
if (newToUsersValue.selection.collapsed) {
|
||||
val lastWord = newToUsersValue.currentWord()
|
||||
fun onToUsersChanged() {
|
||||
if (toUsers.selection.collapsed) {
|
||||
val lastWord = toUsers.currentWord()
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
@@ -706,7 +705,7 @@ class ChatNewMessageViewModel :
|
||||
fun updateRoomFromUsersInput() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(300)
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, accountViewModel)
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text.toString(), null, null, accountViewModel)
|
||||
toUsersTagger.run()
|
||||
|
||||
val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex }
|
||||
@@ -720,8 +719,7 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSubject(it: TextFieldValue) {
|
||||
subject = it
|
||||
fun onSubjectChanged() {
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
@@ -738,14 +736,14 @@ class ChatNewMessageViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message.text.toString())
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
||||
val lastWord = toUsers.currentWord()
|
||||
toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||
userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||
updateRoomFromUsersInput()
|
||||
}
|
||||
|
||||
@@ -759,8 +757,8 @@ class ChatNewMessageViewModel :
|
||||
fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -776,8 +774,8 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -794,8 +792,8 @@ class ChatNewMessageViewModel :
|
||||
recipientsMissingDmRelays.value.isEmpty()
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -816,7 +814,7 @@ class ChatNewMessageViewModel :
|
||||
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
+12
-11
@@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddPhotoAlternate
|
||||
@@ -67,7 +68,6 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -80,7 +80,7 @@ import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
@@ -158,7 +158,8 @@ fun NewGroupDMScreen(
|
||||
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -530,8 +531,8 @@ fun SendDirectMessageTo(
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = postViewModel.toUsers,
|
||||
onValueChange = postViewModel::updateToUsers,
|
||||
state = postViewModel.toUsers,
|
||||
onTextChanged = postViewModel::onToUsersChanged,
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
@@ -547,8 +548,8 @@ fun SendDirectMessageTo(
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
visualTransformation =
|
||||
UrlUserTagTransformation(
|
||||
outputTransformation =
|
||||
UrlUserTagOutputTransformation(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
colors =
|
||||
@@ -572,8 +573,8 @@ fun SendDirectMessageTo(
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = postViewModel.subject,
|
||||
onValueChange = { postViewModel.updateSubject(it) },
|
||||
state = postViewModel.subject,
|
||||
onTextChanged = { postViewModel.onSubjectChanged() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
@@ -581,8 +582,8 @@ fun SendDirectMessageTo(
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
visualTransformation =
|
||||
UrlUserTagTransformation(
|
||||
outputTransformation =
|
||||
UrlUserTagOutputTransformation(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
colors =
|
||||
|
||||
+4
-4
@@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
@@ -201,8 +201,8 @@ fun EditField(
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
ThinPaddingTextField(
|
||||
value = channelScreenModel.message,
|
||||
onValueChange = { channelScreenModel.updateMessage(it) },
|
||||
state = channelScreenModel.message,
|
||||
onTextChanged = { channelScreenModel.onMessageChanged() },
|
||||
keyboardOptions = PostKeyboard,
|
||||
shape = EditFieldBorder,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -231,7 +231,7 @@ fun EditField(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+22
-20
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -137,7 +138,7 @@ open class ChannelNewMessageViewModel :
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||
|
||||
var message by mutableStateOf(TextFieldValue(""))
|
||||
val message = TextFieldState()
|
||||
var urlPreview by mutableStateOf<String?>(null)
|
||||
val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false
|
||||
val isUploadingFile: Boolean get() = uploadState?.isUploadingFile ?: false
|
||||
@@ -274,7 +275,7 @@ open class ChannelNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
@@ -302,7 +303,7 @@ open class ChannelNewMessageViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val attachments = mutableSetOf<Event>()
|
||||
@@ -365,14 +366,14 @@ open class ChannelNewMessageViewModel :
|
||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor(it.toNostrUri())
|
||||
message.insertUrlAtCursor(it.toNostrUri())
|
||||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
} else if (upload.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
iMetaAttachments.add(upload.result, uploadState.caption, uploadState.contentWarningReason)
|
||||
|
||||
message = message.insertUrlAtCursor(upload.result.url)
|
||||
message.insertUrlAtCursor(upload.result.url)
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
}
|
||||
@@ -392,18 +393,19 @@ open class ChannelNewMessageViewModel :
|
||||
|
||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||
val channel = channel ?: return null
|
||||
val messageText = message.text.toString()
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message = message.text,
|
||||
message = messageText,
|
||||
pTags = listOfNotNull(replyTo.value?.author),
|
||||
eTags = listOfNotNull(replyTo.value),
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
|
||||
val urls = findURLs(message.text)
|
||||
val urls = findURLs(messageText)
|
||||
val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet())
|
||||
val emojis = findEmoji(message.text, accountViewModel.account.emoji.myEmojis.value)
|
||||
val emojis = findEmoji(messageText, accountViewModel.account.emoji.myEmojis.value)
|
||||
|
||||
val channelRelays = channel.relays()
|
||||
val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null
|
||||
@@ -533,7 +535,7 @@ open class ChannelNewMessageViewModel :
|
||||
open fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
replyTo.value = null
|
||||
|
||||
@@ -561,18 +563,18 @@ open class ChannelNewMessageViewModel :
|
||||
emojiSuggestions?.reset()
|
||||
}
|
||||
|
||||
open fun findUrlInMessage(): String? = UrlParser().parseValidUrls(message.text).withScheme.firstOrNull()
|
||||
open fun findUrlInMessage(): String? = UrlParser().parseValidUrls(message.text.toString()).withScheme.firstOrNull()
|
||||
|
||||
open fun addToMessage(it: String) {
|
||||
updateMessage(TextFieldValue(message.text + " " + it))
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||
onMessageChanged()
|
||||
}
|
||||
|
||||
open fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
open fun onMessageChanged() {
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
if (newMessage.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
@@ -600,7 +602,7 @@ open class ChannelNewMessageViewModel :
|
||||
userSuggestions?.let {
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = it.replaceCurrentWord(message, lastWord, item)
|
||||
it.replaceCurrentWord(message, lastWord, item)
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.addItem(item)
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
@@ -615,7 +617,7 @@ open class ChannelNewMessageViewModel :
|
||||
|
||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -631,7 +633,7 @@ open class ChannelNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -648,7 +650,7 @@ open class ChannelNewMessageViewModel :
|
||||
uploadState?.multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
@@ -675,7 +677,7 @@ open class ChannelNewMessageViewModel :
|
||||
|
||||
fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.items.any { it.key == taggedUser }) {
|
||||
|
||||
+4
-4
@@ -38,7 +38,7 @@ import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
@@ -114,8 +114,8 @@ fun EditFieldRow(
|
||||
}
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = channelScreenModel.message,
|
||||
onValueChange = { channelScreenModel.updateMessage(it) },
|
||||
state = channelScreenModel.message,
|
||||
onTextChanged = { channelScreenModel.onMessageChanged() },
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
@@ -151,7 +151,7 @@ fun EditFieldRow(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -62,12 +62,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||
@@ -293,7 +294,7 @@ private fun MarkdownPostScreenBody(
|
||||
.padding(Size10dp),
|
||||
) {
|
||||
RenderContentAsMarkdown(
|
||||
content = postViewModel.message.text,
|
||||
content = postViewModel.message.text.toString(),
|
||||
tags = EmptyTagList,
|
||||
canPreview = true,
|
||||
quotesLeft = 1,
|
||||
@@ -304,9 +305,9 @@ private fun MarkdownPostScreenBody(
|
||||
}
|
||||
} else {
|
||||
// Markdown editor
|
||||
OutlinedTextField(
|
||||
value = postViewModel.message,
|
||||
onValueChange = postViewModel::updateMessage,
|
||||
ThinPaddingTextField(
|
||||
state = postViewModel.message,
|
||||
onTextChanged = postViewModel::onMessageChanged,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -328,7 +329,7 @@ private fun MarkdownPostScreenBody(
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
|
||||
+17
-17
@@ -21,6 +21,8 @@
|
||||
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.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -147,7 +149,7 @@ class LongFormPostViewModel :
|
||||
|
||||
var isUploadingCoverImage by mutableStateOf(false)
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
var showPreview by mutableStateOf(false)
|
||||
|
||||
@@ -241,7 +243,7 @@ class LongFormPostViewModel :
|
||||
summary = TextFieldValue(noteEvent.summary() ?: "")
|
||||
publishedAt = noteEvent.publishedAt() ?: noteEvent.createdAt
|
||||
coverImageUrl = noteEvent.image() ?: ""
|
||||
message = TextFieldValue(noteEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(noteEvent.content)
|
||||
existingDTag = noteEvent.dTag()
|
||||
}
|
||||
|
||||
@@ -265,7 +267,7 @@ class LongFormPostViewModel :
|
||||
summary = TextFieldValue(draftEvent.summary() ?: "")
|
||||
publishedAt = draftEvent.publishedAt() ?: draftEvent.createdAt
|
||||
coverImageUrl = draftEvent.image() ?: ""
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
existingDTag = draftEvent.dTag()
|
||||
|
||||
canAddInvoice = accountViewModel.userProfile().lnAddress() != null
|
||||
@@ -335,7 +337,7 @@ class LongFormPostViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank() && title.text.isBlank()) {
|
||||
if (message.text.toString().isBlank() && title.text.isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val template = createTemplate() ?: return
|
||||
@@ -349,7 +351,7 @@ class LongFormPostViewModel :
|
||||
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message.text,
|
||||
message.text.toString(),
|
||||
null,
|
||||
null,
|
||||
accountViewModel,
|
||||
@@ -539,7 +541,7 @@ class LongFormPostViewModel :
|
||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||
|
||||
val markdownImage = ""
|
||||
message = message.insertUrlAtCursor(markdownImage)
|
||||
message.insertUrlAtCursor(markdownImage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,7 +561,7 @@ class LongFormPostViewModel :
|
||||
title = TextFieldValue("")
|
||||
summary = TextFieldValue("")
|
||||
coverImageUrl = ""
|
||||
message = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
publishedAt = TimeUtils.now()
|
||||
showPreview = false
|
||||
|
||||
@@ -594,11 +596,9 @@ class LongFormPostViewModel :
|
||||
this.multiOrchestrator?.remove(selected)
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
|
||||
override fun onMessageChanged() {
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
@@ -626,7 +626,7 @@ class LongFormPostViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
@@ -641,7 +641,7 @@ class LongFormPostViewModel :
|
||||
|
||||
fun autocompleteWithEmoji(item: EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
emojiSuggestions?.reset()
|
||||
draftTag.newVersion()
|
||||
}
|
||||
@@ -655,21 +655,21 @@ class LongFormPostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
emojiSuggestions?.reset()
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun canPost(): Boolean =
|
||||
title.text.isNotBlank() &&
|
||||
message.text.isNotBlank() &&
|
||||
message.text.toString().isNotBlank() &&
|
||||
!isUploadingImage &&
|
||||
!wantsInvoice &&
|
||||
(!wantsZapRaiser || zapRaiserAmount.value != null) &&
|
||||
multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
@@ -699,7 +699,7 @@ class LongFormPostViewModel :
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger =
|
||||
NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
+3
-2
@@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -41,7 +42,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -112,7 +112,8 @@ fun NewProductScreen(
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
+42
-51
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -131,7 +132,7 @@ open class NewProductViewModel :
|
||||
var productImages by mutableStateOf<List<ProductImageMeta>>(emptyList())
|
||||
val iMetaDescription = IMetaAttachments()
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
@@ -151,10 +152,10 @@ open class NewProductViewModel :
|
||||
val strippingFailureConfirmation = SuspendableConfirmation()
|
||||
|
||||
// Classifieds
|
||||
var title by mutableStateOf(TextFieldValue(""))
|
||||
var price by mutableStateOf(TextFieldValue(""))
|
||||
var locationText by mutableStateOf(TextFieldValue(""))
|
||||
var category by mutableStateOf(TextFieldValue(""))
|
||||
val title = TextFieldState()
|
||||
val price = TextFieldState()
|
||||
val locationText = TextFieldState()
|
||||
val category = TextFieldState()
|
||||
var condition by mutableStateOf(ConditionTag.CONDITION.USED_LIKE_NEW)
|
||||
|
||||
// Invoices
|
||||
@@ -226,7 +227,7 @@ open class NewProductViewModel :
|
||||
open fun quote(quote: Note) {
|
||||
val accountViewModel = accountViewModel
|
||||
|
||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||
|
||||
quote.author?.let { quotedUser ->
|
||||
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||
@@ -246,7 +247,7 @@ open class NewProductViewModel :
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
@@ -284,10 +285,10 @@ open class NewProductViewModel :
|
||||
zapRaiserAmount.value = zapraiser
|
||||
}
|
||||
|
||||
title = TextFieldValue(draftEvent.title() ?: "")
|
||||
price = TextFieldValue(draftEvent.price()?.amount ?: "")
|
||||
category = TextFieldValue(draftEvent.categories().firstOrNull() ?: "")
|
||||
locationText = TextFieldValue(draftEvent.location() ?: "")
|
||||
title.setTextAndPlaceCursorAtEnd(draftEvent.title() ?: "")
|
||||
price.setTextAndPlaceCursorAtEnd(draftEvent.price()?.amount ?: "")
|
||||
category.setTextAndPlaceCursorAtEnd(draftEvent.categories().firstOrNull() ?: "")
|
||||
locationText.setTextAndPlaceCursorAtEnd(draftEvent.location() ?: "")
|
||||
condition = draftEvent.conditionValid() ?: ConditionTag.CONDITION.USED_LIKE_NEW
|
||||
|
||||
val imageSet = draftEvent.images().toMutableSet()
|
||||
@@ -305,9 +306,9 @@ open class NewProductViewModel :
|
||||
productImages = productImages + ProductImageMeta(it)
|
||||
}
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
@@ -323,7 +324,7 @@ open class NewProductViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val template = createTemplate() ?: return
|
||||
@@ -336,7 +337,7 @@ open class NewProductViewModel :
|
||||
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message = message.text,
|
||||
message = message.text.toString(),
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
@@ -356,15 +357,15 @@ open class NewProductViewModel :
|
||||
|
||||
val template =
|
||||
ClassifiedsEvent.build(
|
||||
title.text,
|
||||
PriceTag(price.text, "SATS", null),
|
||||
title.text.toString(),
|
||||
PriceTag(price.text.toString(), "SATS", null),
|
||||
tagger.message,
|
||||
locationText.text.ifBlank { null },
|
||||
locationText.text.toString().ifBlank { null },
|
||||
condition,
|
||||
) {
|
||||
productImages.forEach { image(it.url) }
|
||||
|
||||
hashtags(listOfNotNull(category.text.ifBlank { null }) + findHashtags(tagger.message))
|
||||
hashtags(listOfNotNull(category.text.toString().ifBlank { null }) + findHashtags(tagger.message))
|
||||
quotes(quotes)
|
||||
|
||||
geoHash?.let { geohash(it) }
|
||||
@@ -438,8 +439,8 @@ open class NewProductViewModel :
|
||||
} else {
|
||||
iMetaDescription.add(it.result, alt, contentWarningReason)
|
||||
|
||||
message = message.insertUrlAtCursor(it.result.url)
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor(it.result.url)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,7 +459,7 @@ open class NewProductViewModel :
|
||||
open fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
multiOrchestrator = null
|
||||
mediaUploadTracker.finishUpload()
|
||||
@@ -468,10 +469,10 @@ open class NewProductViewModel :
|
||||
zapRaiserAmount.value = null
|
||||
|
||||
condition = ConditionTag.CONDITION.USED_LIKE_NEW
|
||||
locationText = TextFieldValue("")
|
||||
title = TextFieldValue("")
|
||||
category = TextFieldValue("")
|
||||
price = TextFieldValue("")
|
||||
locationText.setTextAndPlaceCursorAtEnd("")
|
||||
title.setTextAndPlaceCursorAtEnd("")
|
||||
category.setTextAndPlaceCursorAtEnd("")
|
||||
price.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
@@ -505,9 +506,8 @@ open class NewProductViewModel :
|
||||
this.multiOrchestrator?.remove(selected)
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreviews.update(message)
|
||||
override fun onMessageChanged() {
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = message.currentWord()
|
||||
@@ -538,8 +538,8 @@ open class NewProductViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message.text.toString())
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
@@ -555,8 +555,8 @@ open class NewProductViewModel :
|
||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -572,8 +572,8 @@ open class NewProductViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -591,7 +591,7 @@ open class NewProductViewModel :
|
||||
multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
@@ -620,7 +620,7 @@ open class NewProductViewModel :
|
||||
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
@@ -648,19 +648,11 @@ open class NewProductViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateTitle(it: TextFieldValue) {
|
||||
title = it
|
||||
fun onTitleChanged() {
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updatePrice(it: TextFieldValue) {
|
||||
runCatching {
|
||||
if (it.text.isEmpty()) {
|
||||
price = TextFieldValue("")
|
||||
} else if (it.text.toLongOrNull() != null) {
|
||||
price = it
|
||||
}
|
||||
}
|
||||
fun onPriceChanged() {
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
@@ -669,13 +661,12 @@ open class NewProductViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateCategory(value: TextFieldValue) {
|
||||
category = value
|
||||
fun updateCategory(text: String) {
|
||||
category.setTextAndPlaceCursorAtEnd(text)
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateLocation(it: TextFieldValue) {
|
||||
locationText = it
|
||||
fun onLocationChanged() {
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
|
||||
+14
-20
@@ -44,12 +44,12 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue // kept for readonly dropdown ThinPaddingTextField
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
@@ -109,10 +109,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = postViewModel.title,
|
||||
onValueChange = {
|
||||
postViewModel.updateTitle(it)
|
||||
},
|
||||
state = postViewModel.title,
|
||||
onTextChanged = { postViewModel.onTitleChanged() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
@@ -120,8 +118,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
visualTransformation =
|
||||
UrlUserTagTransformation(
|
||||
outputTransformation =
|
||||
UrlUserTagOutputTransformation(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
colors =
|
||||
@@ -146,10 +144,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
|
||||
ThinPaddingTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.price,
|
||||
onValueChange = {
|
||||
postViewModel.updatePrice(it)
|
||||
},
|
||||
state = postViewModel.price,
|
||||
onTextChanged = { postViewModel.onPriceChanged() },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "1000",
|
||||
@@ -275,10 +271,10 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
categoryTypes.map { TitleExplainer(it.second, null) }.toImmutableList()
|
||||
}
|
||||
TextSpinner(
|
||||
placeholder = categoryTypes.firstOrNull { it.second == postViewModel.category.text }?.second ?: "",
|
||||
placeholder = categoryTypes.firstOrNull { it.second == postViewModel.category.text.toString() }?.second ?: "",
|
||||
options = categoryOptions,
|
||||
onSelect = {
|
||||
postViewModel.updateCategory(TextFieldValue(categoryTypes[it].second))
|
||||
postViewModel.updateCategory(categoryTypes[it].second)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -313,10 +309,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = postViewModel.locationText,
|
||||
onValueChange = {
|
||||
postViewModel.updateLocation(it)
|
||||
},
|
||||
state = postViewModel.locationText,
|
||||
onTextChanged = { postViewModel.onLocationChanged() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
@@ -324,8 +318,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
visualTransformation =
|
||||
UrlUserTagTransformation(
|
||||
outputTransformation =
|
||||
UrlUserTagOutputTransformation(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
colors =
|
||||
|
||||
+3
-2
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash
|
||||
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
@@ -67,7 +67,8 @@ fun GeoHashPostScreen(
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
+3
-2
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag
|
||||
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
@@ -67,7 +67,8 @@ fun HashtagPostScreen(
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
+3
-2
@@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Poll
|
||||
@@ -54,7 +55,6 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.IntentCompat
|
||||
@@ -150,7 +150,8 @@ fun ShortNotePostScreen(
|
||||
val draft = draftId?.let { accountViewModel.getNoteIfExists(it) }
|
||||
postViewModel.load(baseReplyTo, quote, fork, version, draft)
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
+33
-31
@@ -21,6 +21,8 @@
|
||||
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.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -183,7 +185,7 @@ open class ShortNotePostViewModel :
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
@@ -367,7 +369,7 @@ open class ShortNotePostViewModel :
|
||||
multiOrchestrator = null
|
||||
|
||||
quote?.let { quotedNote ->
|
||||
message = TextFieldValue(message.text + "\nnostr:${quotedNote.toNEvent()}")
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quotedNote.toNEvent()}")
|
||||
|
||||
quotedNote.author?.let { quotedUser ->
|
||||
if (quotedUser.pubkeyHex != user.pubkeyHex) {
|
||||
@@ -385,7 +387,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
fork?.let { forkedNoted ->
|
||||
message = TextFieldValue(version?.event?.content ?: forkedNoted.event?.content ?: "")
|
||||
message.setTextAndPlaceCursorAtEnd(version?.event?.content ?: forkedNoted.event?.content ?: "")
|
||||
|
||||
forkedNoted.event?.isSensitiveOrNSFW()?.let {
|
||||
if (it) wantsToMarkAsSensitive = true
|
||||
@@ -436,7 +438,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
@@ -533,11 +535,11 @@ open class ShortNotePostViewModel :
|
||||
wantsPoll = false
|
||||
wantsZapPoll = false
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draftEvent: PollEvent) {
|
||||
@@ -605,11 +607,11 @@ open class ShortNotePostViewModel :
|
||||
pollType = draftEvent.pollType()
|
||||
closedAt = draftEvent.endsAt() ?: TimeUtils.oneDayAhead()
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draftEvent: ZapPollEvent) {
|
||||
@@ -679,11 +681,11 @@ open class ShortNotePostViewModel :
|
||||
zapPollConsensusThreshold = draftEvent.consensusThreshold()?.let { (it * 100).toInt() }
|
||||
zapPollClosedAt = draftEvent.closedAt() ?: TimeUtils.oneDayAhead()
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
@@ -750,7 +752,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val attachments = mutableSetOf<Event>()
|
||||
@@ -811,7 +813,7 @@ open class ShortNotePostViewModel :
|
||||
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message.text,
|
||||
message.text.toString(),
|
||||
pTags,
|
||||
eTags,
|
||||
accountViewModel,
|
||||
@@ -1000,8 +1002,8 @@ open class ShortNotePostViewModel :
|
||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
val iMeta =
|
||||
@@ -1024,8 +1026,8 @@ open class ShortNotePostViewModel :
|
||||
|
||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||
|
||||
message = message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1042,7 +1044,7 @@ open class ShortNotePostViewModel :
|
||||
open fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
forkedFromNote = null
|
||||
|
||||
@@ -1104,15 +1106,15 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
open fun addToMessage(it: String) {
|
||||
updateMessage(TextFieldValue(message.text + " " + it))
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||
onMessageChanged()
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreviews.update(message)
|
||||
override fun onMessageChanged() {
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
@@ -1140,8 +1142,8 @@ open class ShortNotePostViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message.text.toString())
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
@@ -1157,8 +1159,8 @@ open class ShortNotePostViewModel :
|
||||
open fun autocompleteWithEmoji(item: EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -1174,8 +1176,8 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -1201,7 +1203,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
// Regular text/media posts require text
|
||||
return message.text.isNotBlank() &&
|
||||
return message.text.toString().isNotBlank() &&
|
||||
!mediaUploadTracker.isUploading &&
|
||||
!isUploadingVoice &&
|
||||
!wantsInvoice &&
|
||||
@@ -1226,7 +1228,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
@@ -1378,7 +1380,7 @@ open class ShortNotePostViewModel :
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger =
|
||||
NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
+5
-5
@@ -56,7 +56,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||
@@ -415,8 +415,8 @@ fun SendDirectMessageTo(
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = postViewModel.toUsers,
|
||||
onValueChange = postViewModel::updateToUsers,
|
||||
state = postViewModel.toUsers,
|
||||
onTextChanged = postViewModel::onToUsersChanged,
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
@@ -432,8 +432,8 @@ fun SendDirectMessageTo(
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
visualTransformation =
|
||||
UrlUserTagTransformation(
|
||||
outputTransformation =
|
||||
UrlUserTagOutputTransformation(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
colors =
|
||||
|
||||
+41
-43
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -147,7 +148,7 @@ class NewPublicMessageViewModel :
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
@@ -160,7 +161,7 @@ class NewPublicMessageViewModel :
|
||||
|
||||
var emojiSuggestions: EmojiSuggestionState? = null
|
||||
|
||||
var toUsers by mutableStateOf(TextFieldValue(""))
|
||||
val toUsers = TextFieldState()
|
||||
|
||||
// Images and Videos
|
||||
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
||||
@@ -218,10 +219,9 @@ class NewPublicMessageViewModel :
|
||||
fun load(users: Set<HexKey>) {
|
||||
val userSet = users - account.userProfile().pubkeyHex
|
||||
|
||||
toUsers =
|
||||
TextFieldValue(
|
||||
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
toUsers.setTextAndPlaceCursorAtEnd(
|
||||
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
}
|
||||
|
||||
fun reply(post: Note) {
|
||||
@@ -229,8 +229,8 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
|
||||
fun quote(quote: Note) {
|
||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||
urlPreviews.update(message)
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
// creates a split with that author.
|
||||
val quotedAuthor = quote.author ?: return
|
||||
@@ -306,13 +306,12 @@ class NewPublicMessageViewModel :
|
||||
|
||||
val userSet = draftEvent.groupKeys() - account.userProfile().pubkeyHex
|
||||
|
||||
toUsers =
|
||||
TextFieldValue(
|
||||
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
toUsers.setTextAndPlaceCursorAtEnd(
|
||||
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
urlPreviews.update(message)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
}
|
||||
@@ -349,7 +348,7 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val broadcast = mutableSetOf<Event>()
|
||||
@@ -364,13 +363,14 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
|
||||
private suspend fun createTemplate(): EventTemplate<PublicMessageEvent> {
|
||||
val toUsersTagger = NewMessageTagger(this@NewPublicMessageViewModel.toUsers.text, null, null, accountViewModel)
|
||||
val toUsersTagger = NewMessageTagger(this@NewPublicMessageViewModel.toUsers.text.toString(), null, null, accountViewModel)
|
||||
toUsersTagger.run()
|
||||
|
||||
val messageText = message.text.toString()
|
||||
val msg =
|
||||
replyingTo?.let {
|
||||
it.toNostrUri() + "\n" + message.text
|
||||
} ?: message.text
|
||||
it.toNostrUri() + "\n" + messageText
|
||||
} ?: messageText
|
||||
|
||||
val tagger = NewMessageTagger(msg, null, null, accountViewModel)
|
||||
tagger.run()
|
||||
@@ -470,8 +470,8 @@ class NewPublicMessageViewModel :
|
||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
val iMeta =
|
||||
@@ -494,8 +494,8 @@ class NewPublicMessageViewModel :
|
||||
|
||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||
|
||||
message = message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,8 +512,8 @@ class NewPublicMessageViewModel :
|
||||
fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
toUsers = TextFieldValue("")
|
||||
message = TextFieldValue("")
|
||||
toUsers.setTextAndPlaceCursorAtEnd("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
multiOrchestrator = null
|
||||
|
||||
wantsInvoice = false
|
||||
@@ -546,15 +546,15 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
|
||||
fun addToMessage(it: String) {
|
||||
updateMessage(TextFieldValue(message.text + " " + it))
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||
onMessageChanged()
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreviews.update(newMessage)
|
||||
override fun onMessageChanged() {
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
@@ -569,11 +569,9 @@ class NewPublicMessageViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateToUsers(newToUsersValue: TextFieldValue) {
|
||||
toUsers = newToUsersValue
|
||||
|
||||
if (newToUsersValue.selection.collapsed) {
|
||||
val lastWord = newToUsersValue.currentWord()
|
||||
fun onToUsersChanged() {
|
||||
if (toUsers.selection.collapsed) {
|
||||
val lastWord = toUsers.currentWord()
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
@@ -594,14 +592,14 @@ class NewPublicMessageViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message.text.toString())
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
||||
val lastWord = toUsers.currentWord()
|
||||
toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||
userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||
}
|
||||
|
||||
userSuggestionsMainMessage = null
|
||||
@@ -614,8 +612,8 @@ class NewPublicMessageViewModel :
|
||||
fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -631,8 +629,8 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -648,7 +646,7 @@ class NewPublicMessageViewModel :
|
||||
multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
@@ -671,7 +669,7 @@ class NewPublicMessageViewModel :
|
||||
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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.commons.compose
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
fun TextFieldState.insertUrlAtCursor(url: String) {
|
||||
edit {
|
||||
var toInsert = url.trim()
|
||||
val selStart = selection.start
|
||||
val selEnd = selection.end
|
||||
val text = asCharSequence()
|
||||
|
||||
if (selStart > 0 && text[selStart - 1] != ' ' && text[selStart - 1] != '\n') {
|
||||
toInsert = " $toInsert"
|
||||
}
|
||||
|
||||
// takes the position before adding an empty char after the url
|
||||
val endOfUrlIndex = selStart + toInsert.length
|
||||
|
||||
if (selEnd < length && text[selEnd] != ' ' && text[selEnd] != '\n') {
|
||||
toInsert = "$toInsert "
|
||||
}
|
||||
|
||||
replace(selStart, selEnd, toInsert)
|
||||
selection = TextRange(endOfUrlIndex, endOfUrlIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun TextFieldState.replaceCurrentWord(wordToInsert: String) {
|
||||
edit {
|
||||
val text = asCharSequence()
|
||||
val selStart = selection.start
|
||||
val selEnd = selection.end
|
||||
val lastWordStart = currentWordStartIdx(text, selStart)
|
||||
val lastWordEnd = currentWordEndIdx(text, selEnd, length)
|
||||
val cursor = lastWordStart + wordToInsert.length
|
||||
replace(lastWordStart, lastWordEnd, wordToInsert)
|
||||
selection = TextRange(cursor, cursor)
|
||||
}
|
||||
}
|
||||
|
||||
fun TextFieldState.currentWord(): String {
|
||||
val text = this.text
|
||||
val selection = this.selection
|
||||
if (selection.end != selection.start) return ""
|
||||
|
||||
val start = currentWordStartIdx(text, selection.start)
|
||||
val end = currentWordEndIdx(text, selection.end, text.length)
|
||||
|
||||
return if (start < end) {
|
||||
text.subSequence(start, end).toString()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentWordStartIdx(
|
||||
text: CharSequence,
|
||||
selectionStart: Int,
|
||||
): Int {
|
||||
val previousNewLine = text.lastIndexOf('\n', selectionStart - 1)
|
||||
val previousSpace = text.lastIndexOf(' ', selectionStart - 1)
|
||||
|
||||
return max(
|
||||
previousNewLine,
|
||||
previousSpace,
|
||||
) + 1
|
||||
}
|
||||
|
||||
private fun currentWordEndIdx(
|
||||
text: CharSequence,
|
||||
selectionEnd: Int,
|
||||
textLength: Int,
|
||||
): Int {
|
||||
val nextNewLine = text.indexOf('\n', selectionEnd)
|
||||
val nextSpace = text.indexOf(' ', selectionEnd)
|
||||
|
||||
if (nextSpace < 0 && nextNewLine < 0) return selectionEnd
|
||||
if (nextSpace > 0 && nextNewLine > 0) {
|
||||
return min(
|
||||
nextNewLine,
|
||||
nextSpace,
|
||||
)
|
||||
}
|
||||
if (nextSpace > 0) {
|
||||
return nextSpace
|
||||
}
|
||||
return nextNewLine
|
||||
}
|
||||
|
||||
private fun CharSequence.lastIndexOf(
|
||||
char: Char,
|
||||
startIndex: Int,
|
||||
): Int {
|
||||
for (i in startIndex downTo 0) {
|
||||
if (this[i] == char) return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun CharSequence.indexOf(
|
||||
char: Char,
|
||||
startIndex: Int,
|
||||
): Int {
|
||||
for (i in startIndex until length) {
|
||||
if (this[i] == char) return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
Reference in New Issue
Block a user