feat: migrate ThinPaddingTextField to BasicTextField(TextFieldState) for GIF keyboard support
Migrates all new post screens from the old BasicTextField(TextFieldValue) to the new BasicTextField(TextFieldState) API, which properly sets EditorInfo.contentMimeTypes to enable GIF keyboard input. Key changes: - Add TextFieldState-based ThinPaddingTextField overload (keep old for readonly dropdowns) - Create UrlUserTagOutputTransformation for the new OutputTransformation API - Create TextFieldState extension functions (insertUrlAtCursor, replaceCurrentWord, currentWord) - Update IMessageField interface to use TextFieldState + onMessageChanged() - Migrate all IMessageField implementors: ChannelNewMessageViewModel, ChatNewMessageViewModel, NewProductViewModel, NewPublicMessageViewModel, CommentPostViewModel, ShortNotePostViewModel, LongFormPostViewModel - Update PreviewState to accept String instead of TextFieldValue - Add TextFieldState overload to UserSuggestionState.replaceCurrentWord() - Update all post screen call sites https://claude.ai/code/session_01FDGf1Zi1pVvzFi3JY5agnJ
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.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
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.foundation.text.selection.LocalTextSelectionColors
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
@@ -35,8 +39,11 @@ import androidx.compose.material3.TextFieldColors
|
|||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
@@ -50,6 +57,124 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
|||||||
// COPIED FROM TEXT FIELD
|
// COPIED FROM TEXT FIELD
|
||||||
// The only change is the contentPadding below
|
// 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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ThinPaddingTextField(
|
fun ThinPaddingTextField(
|
||||||
@@ -76,7 +201,6 @@ fun ThinPaddingTextField(
|
|||||||
interactionSource: MutableInteractionSource? = null,
|
interactionSource: MutableInteractionSource? = null,
|
||||||
shape: Shape = TextFieldDefaults.shape,
|
shape: Shape = TextFieldDefaults.shape,
|
||||||
colors: TextFieldColors = TextFieldDefaults.colors(),
|
colors: TextFieldColors = TextFieldDefaults.colors(),
|
||||||
// new fields
|
|
||||||
contentPadding: PaddingValues =
|
contentPadding: PaddingValues =
|
||||||
if (label == null) {
|
if (label == null) {
|
||||||
TextFieldDefaults.contentPaddingWithoutLabel(
|
TextFieldDefaults.contentPaddingWithoutLabel(
|
||||||
@@ -113,6 +237,7 @@ fun ThinPaddingTextField(
|
|||||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = value,
|
value = value,
|
||||||
modifier =
|
modifier =
|
||||||
|
|||||||
+3
-3
@@ -20,10 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.note.creators.messagefield
|
package com.vitorpamplona.amethyst.ui.note.creators.messagefield
|
||||||
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
|
||||||
interface IMessageField {
|
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.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.style.TextDirection
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
import androidx.compose.ui.unit.dp
|
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.components.ThinPaddingTextField
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
@@ -65,8 +65,8 @@ fun MessageField(
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThinPaddingTextField(
|
ThinPaddingTextField(
|
||||||
value = viewModel.message,
|
state = viewModel.message,
|
||||||
onValueChange = viewModel::updateMessage,
|
onTextChanged = viewModel::onMessageChanged,
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions.Default.copy(
|
KeyboardOptions.Default.copy(
|
||||||
capitalization = KeyboardCapitalization.Sentences,
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
@@ -91,7 +91,7 @@ fun MessageField(
|
|||||||
focusedBorderColor = Color.Transparent,
|
focusedBorderColor = Color.Transparent,
|
||||||
unfocusedBorderColor = Color.Transparent,
|
unfocusedBorderColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
contentPadding =
|
contentPadding =
|
||||||
TextFieldDefaults.contentPaddingWithoutLabel(
|
TextFieldDefaults.contentPaddingWithoutLabel(
|
||||||
|
|||||||
+4
-5
@@ -21,7 +21,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note.creators.previews
|
package com.vitorpamplona.amethyst.ui.note.creators.previews
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import com.vitorpamplona.amethyst.service.CachedUrlParser
|
import com.vitorpamplona.amethyst.service.CachedUrlParser
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
@@ -33,22 +32,22 @@ import kotlinx.coroutines.flow.map
|
|||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class PreviewState {
|
class PreviewState {
|
||||||
var source = MutableStateFlow(TextFieldValue(""))
|
var source = MutableStateFlow("")
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
val results =
|
val results =
|
||||||
source
|
source
|
||||||
.debounce(500)
|
.debounce(500)
|
||||||
.map {
|
.map {
|
||||||
CachedUrlParser.parseValidUrls(it.text)
|
CachedUrlParser.parseValidUrls(it)
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
source.tryEmit(TextFieldValue(""))
|
source.tryEmit("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(text: TextFieldValue) {
|
fun update(text: String) {
|
||||||
source.tryEmit(text)
|
source.tryEmit(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions
|
package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
@@ -199,4 +200,17 @@ class UserSuggestionState(
|
|||||||
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length),
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-25
@@ -21,12 +21,14 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note.nip22Comments
|
package com.vitorpamplona.amethyst.ui.note.nip22Comments
|
||||||
|
|
||||||
import android.content.Context
|
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.Stable
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
@@ -147,7 +149,7 @@ open class CommentPostViewModel :
|
|||||||
|
|
||||||
var notifying by mutableStateOf<List<User>?>(null)
|
var notifying by mutableStateOf<List<User>?>(null)
|
||||||
|
|
||||||
override var message by mutableStateOf(TextFieldValue(""))
|
override val message = TextFieldState()
|
||||||
|
|
||||||
val urlPreviews = PreviewState()
|
val urlPreviews = PreviewState()
|
||||||
|
|
||||||
@@ -242,7 +244,7 @@ open class CommentPostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
open fun quote(quote: Note) {
|
open fun quote(quote: Note) {
|
||||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||||
|
|
||||||
quote.author?.let { quotedUser ->
|
quote.author?.let { quotedUser ->
|
||||||
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||||
@@ -262,7 +264,7 @@ open class CommentPostViewModel :
|
|||||||
wantsForwardZapTo = true
|
wantsForwardZapTo = true
|
||||||
}
|
}
|
||||||
|
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromDraft(draft: Note) {
|
private fun loadFromDraft(draft: Note) {
|
||||||
@@ -322,11 +324,11 @@ open class CommentPostViewModel :
|
|||||||
wantsForwardZapTo = true
|
wantsForwardZapTo = true
|
||||||
}
|
}
|
||||||
|
|
||||||
message = TextFieldValue(draftEvent.content)
|
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||||
|
|
||||||
iMetaAttachments.addAll(draftEvent.imetas())
|
iMetaAttachments.addAll(draftEvent.imetas())
|
||||||
|
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendPostSync() {
|
suspend fun sendPostSync() {
|
||||||
@@ -359,7 +361,7 @@ open class CommentPostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendDraftSync() {
|
suspend fun sendDraftSync() {
|
||||||
if (message.text.isBlank()) {
|
if (message.text.toString().isBlank()) {
|
||||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||||
} else {
|
} else {
|
||||||
val attachments = mutableSetOf<Event>()
|
val attachments = mutableSetOf<Event>()
|
||||||
@@ -376,7 +378,7 @@ open class CommentPostViewModel :
|
|||||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||||
val tagger =
|
val tagger =
|
||||||
NewMessageTagger(
|
NewMessageTagger(
|
||||||
message = message.text,
|
message = message.text.toString(),
|
||||||
dao = accountViewModel,
|
dao = accountViewModel,
|
||||||
)
|
)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
@@ -525,8 +527,8 @@ open class CommentPostViewModel :
|
|||||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||||
|
|
||||||
note?.let {
|
note?.let {
|
||||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||||
val iMeta =
|
val iMeta =
|
||||||
@@ -549,8 +551,8 @@ open class CommentPostViewModel :
|
|||||||
|
|
||||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||||
|
|
||||||
message = message.insertUrlAtCursor(state.result.url)
|
message.insertUrlAtCursor(state.result.url)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,7 +578,7 @@ open class CommentPostViewModel :
|
|||||||
open fun cancel() {
|
open fun cancel() {
|
||||||
draftTag.rotate()
|
draftTag.rotate()
|
||||||
|
|
||||||
message = TextFieldValue("")
|
message.setTextAndPlaceCursorAtEnd("")
|
||||||
|
|
||||||
replyingTo = null
|
replyingTo = null
|
||||||
externalIdentity = null
|
externalIdentity = null
|
||||||
@@ -618,9 +620,8 @@ open class CommentPostViewModel :
|
|||||||
notifying = notifying?.filter { it != userToRemove }
|
notifying = notifying?.filter { it != userToRemove }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMessage(newMessage: TextFieldValue) {
|
override fun onMessageChanged() {
|
||||||
message = newMessage
|
urlPreviews.update(message.text.toString())
|
||||||
urlPreviews.update(message)
|
|
||||||
|
|
||||||
if (message.selection.collapsed) {
|
if (message.selection.collapsed) {
|
||||||
val lastWord = message.currentWord()
|
val lastWord = message.currentWord()
|
||||||
@@ -651,8 +652,8 @@ open class CommentPostViewModel :
|
|||||||
userSuggestions?.let { userSuggestions ->
|
userSuggestions?.let { userSuggestions ->
|
||||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||||
val lastWord = message.currentWord()
|
val lastWord = message.currentWord()
|
||||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||||
forwardZapTo.value.addItem(item)
|
forwardZapTo.value.addItem(item)
|
||||||
forwardZapToEditting.value = TextFieldValue("")
|
forwardZapToEditting.value = TextFieldValue("")
|
||||||
@@ -668,8 +669,8 @@ open class CommentPostViewModel :
|
|||||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||||
val wordToInsert = ":${item.code}:"
|
val wordToInsert = ":${item.code}:"
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -685,8 +686,8 @@ open class CommentPostViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -694,14 +695,14 @@ open class CommentPostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun canPost(): Boolean =
|
fun canPost(): Boolean =
|
||||||
message.text.isNotBlank() &&
|
message.text.toString().isNotBlank() &&
|
||||||
!mediaUploadTracker.isUploading &&
|
!mediaUploadTracker.isUploading &&
|
||||||
!wantsInvoice &&
|
!wantsInvoice &&
|
||||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
||||||
multiOrchestrator == null
|
multiOrchestrator == null
|
||||||
|
|
||||||
fun insertAtCursor(newElement: String) {
|
fun insertAtCursor(newElement: String) {
|
||||||
message = message.insertUrlAtCursor(newElement)
|
message.insertUrlAtCursor(newElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||||
@@ -718,7 +719,7 @@ open class CommentPostViewModel :
|
|||||||
override fun updateZapFromText() {
|
override fun updateZapFromText() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val tagger =
|
val tagger =
|
||||||
NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
tagger.pTags?.forEach { taggedUser ->
|
tagger.pTags?.forEach { taggedUser ->
|
||||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||||
|
|||||||
+3
-2
@@ -46,7 +46,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -125,7 +125,8 @@ fun ReplyCommentPostScreen(
|
|||||||
postViewModel.quote(it)
|
postViewModel.quote(it)
|
||||||
}
|
}
|
||||||
message?.ifBlank { null }?.let {
|
message?.ifBlank { null }?.let {
|
||||||
postViewModel.updateMessage(TextFieldValue(it))
|
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||||
|
postViewModel.onMessageChanged()
|
||||||
}
|
}
|
||||||
attachment?.ifBlank { null }?.toUri()?.let {
|
attachment?.ifBlank { null }?.toUri()?.let {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|||||||
+3
-2
@@ -29,7 +29,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||||
@@ -109,7 +109,8 @@ fun ChatroomView(
|
|||||||
|
|
||||||
if (draftMessage != null) {
|
if (draftMessage != null) {
|
||||||
LaunchedEffect(key1 = draftMessage) {
|
LaunchedEffect(key1 = draftMessage) {
|
||||||
newPostModel.updateMessage(TextFieldValue(draftMessage))
|
newPostModel.message.setTextAndPlaceCursorAtEnd(draftMessage)
|
||||||
|
newPostModel.onMessageChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
-48
@@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -204,7 +205,7 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
var uploadsWaitingToBeSent by mutableStateOf<List<SuccessfulUploads>>(emptyList())
|
var uploadsWaitingToBeSent by mutableStateOf<List<SuccessfulUploads>>(emptyList())
|
||||||
|
|
||||||
override var message by mutableStateOf(TextFieldValue(""))
|
override val message = TextFieldState()
|
||||||
|
|
||||||
val urlPreviews = PreviewState()
|
val urlPreviews = PreviewState()
|
||||||
|
|
||||||
@@ -216,8 +217,8 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
var emojiSuggestions: EmojiSuggestionState? = null
|
var emojiSuggestions: EmojiSuggestionState? = null
|
||||||
|
|
||||||
var toUsers by mutableStateOf(TextFieldValue(""))
|
val toUsers = TextFieldState()
|
||||||
var subject by mutableStateOf(TextFieldValue(""))
|
val subject = TextFieldState()
|
||||||
|
|
||||||
// Invoices
|
// Invoices
|
||||||
var canAddInvoice by mutableStateOf(false)
|
var canAddInvoice by mutableStateOf(false)
|
||||||
@@ -274,10 +275,9 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
fun load(room: ChatroomKey) {
|
fun load(room: ChatroomKey) {
|
||||||
this.room.tryEmit(room)
|
this.room.tryEmit(room)
|
||||||
this.toUsers =
|
this.toUsers.setTextAndPlaceCursorAtEnd(
|
||||||
TextFieldValue(
|
room.users.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||||
room.users.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reply(replyNote: Note) {
|
fun reply(replyNote: Note) {
|
||||||
@@ -291,8 +291,8 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun quote(quote: Note) {
|
fun quote(quote: Note) {
|
||||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
// creates a split with that author.
|
// creates a split with that author.
|
||||||
val quotedAuthor = quote.author ?: return
|
val quotedAuthor = quote.author ?: return
|
||||||
@@ -372,14 +372,13 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
draftEvent.subject()?.let {
|
draftEvent.subject()?.let {
|
||||||
subject = TextFieldValue()
|
subject.setTextAndPlaceCursorAtEnd("")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (draftEvent is NIP17Group) {
|
if (draftEvent is NIP17Group) {
|
||||||
toUsers =
|
toUsers.setTextAndPlaceCursorAtEnd(
|
||||||
TextFieldValue(
|
draftEvent.groupMembers().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||||
draftEvent.groupMembers().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
)
|
||||||
)
|
|
||||||
|
|
||||||
val replyId =
|
val replyId =
|
||||||
when (draftEvent) {
|
when (draftEvent) {
|
||||||
@@ -393,7 +392,7 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
} else if (draftEvent is PrivateDmEvent) {
|
} else if (draftEvent is PrivateDmEvent) {
|
||||||
val recipientNPub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
val recipientNPub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
||||||
toUsers = TextFieldValue("@$recipientNPub")
|
toUsers.setTextAndPlaceCursorAtEnd("@$recipientNPub")
|
||||||
|
|
||||||
val replyId = draftEvent.replyTo()
|
val replyId = draftEvent.replyTo()
|
||||||
if (replyId != null) {
|
if (replyId != null) {
|
||||||
@@ -401,13 +400,14 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message =
|
val draftText =
|
||||||
if (draftEvent is PrivateDmEvent) {
|
if (draftEvent is PrivateDmEvent) {
|
||||||
TextFieldValue(accountViewModel.account.privateDMDecryptionCache.cachedDM(draftEvent) ?: "")
|
accountViewModel.account.privateDMDecryptionCache.cachedDM(draftEvent) ?: ""
|
||||||
} else {
|
} else {
|
||||||
TextFieldValue(draftEvent.content)
|
draftEvent.content
|
||||||
}
|
}
|
||||||
urlPreviews.update(message)
|
message.setTextAndPlaceCursorAtEnd(draftText)
|
||||||
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
iMetaAttachments.addAll(draftEvent.imetas())
|
iMetaAttachments.addAll(draftEvent.imetas())
|
||||||
}
|
}
|
||||||
@@ -422,7 +422,7 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendDraftSync() {
|
suspend fun sendDraftSync() {
|
||||||
if (message.text.isBlank()) {
|
if (message.text.toString().isBlank()) {
|
||||||
account.deleteDraftIgnoreErrors(draftTag.current)
|
account.deleteDraftIgnoreErrors(draftTag.current)
|
||||||
} else {
|
} else {
|
||||||
innerSendPost(draftTag.current)
|
innerSendPost(draftTag.current)
|
||||||
@@ -564,11 +564,12 @@ class ChatNewMessageViewModel :
|
|||||||
private suspend fun innerSendPost(draftTag: String?) {
|
private suspend fun innerSendPost(draftTag: String?) {
|
||||||
val room = room.value ?: return
|
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 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 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 contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null
|
||||||
val localExpirationDate = if (wantsExpirationDate) expirationDate else null
|
val localExpirationDate = if (wantsExpirationDate) expirationDate else null
|
||||||
@@ -634,8 +635,8 @@ class ChatNewMessageViewModel :
|
|||||||
fun cancel() {
|
fun cancel() {
|
||||||
draftTag.rotate()
|
draftTag.rotate()
|
||||||
|
|
||||||
message = TextFieldValue("")
|
message.setTextAndPlaceCursorAtEnd("")
|
||||||
subject = TextFieldValue("")
|
subject.setTextAndPlaceCursorAtEnd("")
|
||||||
|
|
||||||
replyTo.value = null
|
replyTo.value = null
|
||||||
|
|
||||||
@@ -668,15 +669,15 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addToMessage(it: String) {
|
fun addToMessage(it: String) {
|
||||||
updateMessage(TextFieldValue(message.text + " " + it))
|
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||||
|
onMessageChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMessage(newMessage: TextFieldValue) {
|
override fun onMessageChanged() {
|
||||||
message = newMessage
|
urlPreviews.update(message.text.toString())
|
||||||
urlPreviews.update(newMessage)
|
|
||||||
|
|
||||||
if (message.selection.collapsed) {
|
if (message.selection.collapsed) {
|
||||||
val lastWord = newMessage.currentWord()
|
val lastWord = message.currentWord()
|
||||||
if (lastWord.startsWith("@")) {
|
if (lastWord.startsWith("@")) {
|
||||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||||
userSuggestions?.processCurrentWord(lastWord)
|
userSuggestions?.processCurrentWord(lastWord)
|
||||||
@@ -690,11 +691,9 @@ class ChatNewMessageViewModel :
|
|||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateToUsers(newToUsersValue: TextFieldValue) {
|
fun onToUsersChanged() {
|
||||||
toUsers = newToUsersValue
|
if (toUsers.selection.collapsed) {
|
||||||
|
val lastWord = toUsers.currentWord()
|
||||||
if (newToUsersValue.selection.collapsed) {
|
|
||||||
val lastWord = newToUsersValue.currentWord()
|
|
||||||
userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS
|
userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS
|
||||||
userSuggestions?.processCurrentWord(lastWord)
|
userSuggestions?.processCurrentWord(lastWord)
|
||||||
}
|
}
|
||||||
@@ -707,7 +706,7 @@ class ChatNewMessageViewModel :
|
|||||||
fun updateRoomFromUsersInput() {
|
fun updateRoomFromUsersInput() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
delay(300)
|
delay(300)
|
||||||
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, accountViewModel)
|
val toUsersTagger = NewMessageTagger(toUsers.text.toString(), null, null, accountViewModel)
|
||||||
toUsersTagger.run()
|
toUsersTagger.run()
|
||||||
|
|
||||||
val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex }
|
val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex }
|
||||||
@@ -721,8 +720,7 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSubject(it: TextFieldValue) {
|
fun onSubjectChanged() {
|
||||||
subject = it
|
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,14 +737,14 @@ class ChatNewMessageViewModel :
|
|||||||
userSuggestions?.let { userSuggestions ->
|
userSuggestions?.let { userSuggestions ->
|
||||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||||
val lastWord = message.currentWord()
|
val lastWord = message.currentWord()
|
||||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||||
forwardZapTo.value.addItem(item)
|
forwardZapTo.value.addItem(item)
|
||||||
forwardZapToEditting.value = TextFieldValue("")
|
forwardZapToEditting.value = TextFieldValue("")
|
||||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
||||||
val lastWord = toUsers.currentWord()
|
val lastWord = toUsers.currentWord()
|
||||||
toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||||
updateRoomFromUsersInput()
|
updateRoomFromUsersInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,8 +758,8 @@ class ChatNewMessageViewModel :
|
|||||||
fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||||
val wordToInsert = ":${item.code}:"
|
val wordToInsert = ":${item.code}:"
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -777,8 +775,8 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -795,8 +793,8 @@ class ChatNewMessageViewModel :
|
|||||||
recipientsMissingDmRelays.value.isEmpty()
|
recipientsMissingDmRelays.value.isEmpty()
|
||||||
|
|
||||||
fun insertAtCursor(newElement: String) {
|
fun insertAtCursor(newElement: String) {
|
||||||
message = message.insertUrlAtCursor(newElement)
|
message.insertUrlAtCursor(newElement)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
@@ -817,7 +815,7 @@ class ChatNewMessageViewModel :
|
|||||||
|
|
||||||
override fun updateZapFromText() {
|
override fun updateZapFromText() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
tagger.pTags?.forEach { taggedUser ->
|
tagger.pTags?.forEach { taggedUser ->
|
||||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||||
|
|||||||
+12
-11
@@ -67,7 +67,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.MediaUrlImage
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
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.SelectFromFiles
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
@@ -158,7 +158,8 @@ fun NewGroupDMScreen(
|
|||||||
|
|
||||||
LaunchedEffect(postViewModel, accountViewModel) {
|
LaunchedEffect(postViewModel, accountViewModel) {
|
||||||
message?.ifBlank { null }?.let {
|
message?.ifBlank { null }?.let {
|
||||||
postViewModel.updateMessage(TextFieldValue(it))
|
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||||
|
postViewModel.onMessageChanged()
|
||||||
}
|
}
|
||||||
attachment?.let {
|
attachment?.let {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -530,8 +531,8 @@ fun SendDirectMessageTo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ThinPaddingTextField(
|
ThinPaddingTextField(
|
||||||
value = postViewModel.toUsers,
|
state = postViewModel.toUsers,
|
||||||
onValueChange = postViewModel::updateToUsers,
|
onTextChanged = postViewModel::onToUsersChanged,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
@@ -547,8 +548,8 @@ fun SendDirectMessageTo(
|
|||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
visualTransformation =
|
outputTransformation =
|
||||||
UrlUserTagTransformation(
|
UrlUserTagOutputTransformation(
|
||||||
MaterialTheme.colorScheme.primary,
|
MaterialTheme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
@@ -572,8 +573,8 @@ fun SendDirectMessageTo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ThinPaddingTextField(
|
ThinPaddingTextField(
|
||||||
value = postViewModel.subject,
|
state = postViewModel.subject,
|
||||||
onValueChange = { postViewModel.updateSubject(it) },
|
onTextChanged = { postViewModel.onSubjectChanged() },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
@@ -581,8 +582,8 @@ fun SendDirectMessageTo(
|
|||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
visualTransformation =
|
outputTransformation =
|
||||||
UrlUserTagTransformation(
|
UrlUserTagOutputTransformation(
|
||||||
MaterialTheme.colorScheme.primary,
|
MaterialTheme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
|
|||||||
+4
-4
@@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
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.actions.uploads.SelectFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
@@ -202,8 +202,8 @@ fun EditField(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
ThinPaddingTextField(
|
ThinPaddingTextField(
|
||||||
value = channelScreenModel.message,
|
state = channelScreenModel.message,
|
||||||
onValueChange = { channelScreenModel.updateMessage(it) },
|
onTextChanged = { channelScreenModel.onMessageChanged() },
|
||||||
keyboardOptions = PostKeyboard,
|
keyboardOptions = PostKeyboard,
|
||||||
shape = EditFieldBorder,
|
shape = EditFieldBorder,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -232,7 +232,7 @@ fun EditField(
|
|||||||
focusedIndicatorColor = Color.Transparent,
|
focusedIndicatorColor = Color.Transparent,
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-20
@@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -137,7 +138,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
val iMetaAttachments = IMetaAttachments()
|
val iMetaAttachments = IMetaAttachments()
|
||||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||||
|
|
||||||
var message by mutableStateOf(TextFieldValue(""))
|
val message = TextFieldState()
|
||||||
var urlPreview by mutableStateOf<String?>(null)
|
var urlPreview by mutableStateOf<String?>(null)
|
||||||
val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false
|
val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false
|
||||||
val isUploadingFile: Boolean get() = uploadState?.isUploadingFile ?: 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())
|
iMetaAttachments.addAll(draftEvent.imetas())
|
||||||
|
|
||||||
@@ -302,7 +303,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendDraftSync() {
|
suspend fun sendDraftSync() {
|
||||||
if (message.text.isBlank()) {
|
if (message.text.toString().isBlank()) {
|
||||||
account.deleteDraftIgnoreErrors(draftTag.current)
|
account.deleteDraftIgnoreErrors(draftTag.current)
|
||||||
} else {
|
} else {
|
||||||
val attachments = mutableSetOf<Event>()
|
val attachments = mutableSetOf<Event>()
|
||||||
@@ -365,14 +366,14 @@ open class ChannelNewMessageViewModel :
|
|||||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||||
|
|
||||||
note?.let {
|
note?.let {
|
||||||
message = message.insertUrlAtCursor(it.toNostrUri())
|
message.insertUrlAtCursor(it.toNostrUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
urlPreview = findUrlInMessage()
|
urlPreview = findUrlInMessage()
|
||||||
} else if (upload.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
} else if (upload.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||||
iMetaAttachments.add(upload.result, uploadState.caption, uploadState.contentWarningReason)
|
iMetaAttachments.add(upload.result, uploadState.caption, uploadState.contentWarningReason)
|
||||||
|
|
||||||
message = message.insertUrlAtCursor(upload.result.url)
|
message.insertUrlAtCursor(upload.result.url)
|
||||||
urlPreview = findUrlInMessage()
|
urlPreview = findUrlInMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,18 +393,19 @@ open class ChannelNewMessageViewModel :
|
|||||||
|
|
||||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||||
val channel = channel ?: return null
|
val channel = channel ?: return null
|
||||||
|
val messageText = message.text.toString()
|
||||||
val tagger =
|
val tagger =
|
||||||
NewMessageTagger(
|
NewMessageTagger(
|
||||||
message = message.text,
|
message = messageText,
|
||||||
pTags = listOfNotNull(replyTo.value?.author),
|
pTags = listOfNotNull(replyTo.value?.author),
|
||||||
eTags = listOfNotNull(replyTo.value),
|
eTags = listOfNotNull(replyTo.value),
|
||||||
dao = accountViewModel,
|
dao = accountViewModel,
|
||||||
)
|
)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
|
|
||||||
val urls = findURLs(message.text)
|
val urls = findURLs(messageText)
|
||||||
val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet())
|
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 channelRelays = channel.relays()
|
||||||
val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null
|
val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null
|
||||||
@@ -533,7 +535,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
open fun cancel() {
|
open fun cancel() {
|
||||||
draftTag.rotate()
|
draftTag.rotate()
|
||||||
|
|
||||||
message = TextFieldValue("")
|
message.setTextAndPlaceCursorAtEnd("")
|
||||||
|
|
||||||
replyTo.value = null
|
replyTo.value = null
|
||||||
|
|
||||||
@@ -561,18 +563,18 @@ open class ChannelNewMessageViewModel :
|
|||||||
emojiSuggestions?.reset()
|
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) {
|
open fun addToMessage(it: String) {
|
||||||
updateMessage(TextFieldValue(message.text + " " + it))
|
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||||
|
onMessageChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun updateMessage(newMessage: TextFieldValue) {
|
open fun onMessageChanged() {
|
||||||
message = newMessage
|
|
||||||
urlPreview = findUrlInMessage()
|
urlPreview = findUrlInMessage()
|
||||||
|
|
||||||
if (newMessage.selection.collapsed) {
|
if (message.selection.collapsed) {
|
||||||
val lastWord = newMessage.currentWord()
|
val lastWord = message.currentWord()
|
||||||
if (lastWord.startsWith("@")) {
|
if (lastWord.startsWith("@")) {
|
||||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||||
userSuggestions?.processCurrentWord(lastWord)
|
userSuggestions?.processCurrentWord(lastWord)
|
||||||
@@ -600,7 +602,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
userSuggestions?.let {
|
userSuggestions?.let {
|
||||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||||
val lastWord = message.currentWord()
|
val lastWord = message.currentWord()
|
||||||
message = it.replaceCurrentWord(message, lastWord, item)
|
it.replaceCurrentWord(message, lastWord, item)
|
||||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||||
forwardZapTo.addItem(item)
|
forwardZapTo.addItem(item)
|
||||||
forwardZapToEditting = TextFieldValue("")
|
forwardZapToEditting = TextFieldValue("")
|
||||||
@@ -615,7 +617,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
|
|
||||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||||
val wordToInsert = ":${item.code}:"
|
val wordToInsert = ":${item.code}:"
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -631,7 +633,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -648,7 +650,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
uploadState?.multiOrchestrator == null
|
uploadState?.multiOrchestrator == null
|
||||||
|
|
||||||
fun insertAtCursor(newElement: String) {
|
fun insertAtCursor(newElement: String) {
|
||||||
message = message.insertUrlAtCursor(newElement)
|
message.insertUrlAtCursor(newElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||||
@@ -675,7 +677,7 @@ open class ChannelNewMessageViewModel :
|
|||||||
|
|
||||||
fun updateZapFromText() {
|
fun updateZapFromText() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
tagger.pTags?.forEach { taggedUser ->
|
tagger.pTags?.forEach { taggedUser ->
|
||||||
if (!forwardZapTo.items.any { it.key == 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 androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
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.actions.uploads.SelectFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
@@ -114,8 +114,8 @@ fun EditFieldRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThinPaddingTextField(
|
ThinPaddingTextField(
|
||||||
value = channelScreenModel.message,
|
state = channelScreenModel.message,
|
||||||
onValueChange = { channelScreenModel.updateMessage(it) },
|
onTextChanged = { channelScreenModel.onMessageChanged() },
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions.Default.copy(
|
KeyboardOptions.Default.copy(
|
||||||
capitalization = KeyboardCapitalization.Sentences,
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
@@ -151,7 +151,7 @@ fun EditFieldRow(
|
|||||||
focusedIndicatorColor = Color.Transparent,
|
focusedIndicatorColor = Color.Transparent,
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-16
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm
|
|||||||
import android.R.attr.version
|
import android.R.attr.version
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -148,7 +150,7 @@ class LongFormPostViewModel :
|
|||||||
|
|
||||||
var isUploadingCoverImage by mutableStateOf(false)
|
var isUploadingCoverImage by mutableStateOf(false)
|
||||||
|
|
||||||
override var message by mutableStateOf(TextFieldValue(""))
|
override val message = TextFieldState()
|
||||||
|
|
||||||
var showPreview by mutableStateOf(false)
|
var showPreview by mutableStateOf(false)
|
||||||
|
|
||||||
@@ -242,7 +244,7 @@ class LongFormPostViewModel :
|
|||||||
summary = TextFieldValue(noteEvent.summary() ?: "")
|
summary = TextFieldValue(noteEvent.summary() ?: "")
|
||||||
publishedAt = noteEvent.publishedAt() ?: noteEvent.createdAt
|
publishedAt = noteEvent.publishedAt() ?: noteEvent.createdAt
|
||||||
coverImageUrl = noteEvent.image() ?: ""
|
coverImageUrl = noteEvent.image() ?: ""
|
||||||
message = TextFieldValue(noteEvent.content)
|
message.setTextAndPlaceCursorAtEnd(noteEvent.content)
|
||||||
existingDTag = noteEvent.dTag()
|
existingDTag = noteEvent.dTag()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +268,7 @@ class LongFormPostViewModel :
|
|||||||
summary = TextFieldValue(draftEvent.summary() ?: "")
|
summary = TextFieldValue(draftEvent.summary() ?: "")
|
||||||
publishedAt = draftEvent.publishedAt() ?: draftEvent.createdAt
|
publishedAt = draftEvent.publishedAt() ?: draftEvent.createdAt
|
||||||
coverImageUrl = draftEvent.image() ?: ""
|
coverImageUrl = draftEvent.image() ?: ""
|
||||||
message = TextFieldValue(draftEvent.content)
|
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||||
existingDTag = draftEvent.dTag()
|
existingDTag = draftEvent.dTag()
|
||||||
|
|
||||||
canAddInvoice = accountViewModel.userProfile().lnAddress() != null
|
canAddInvoice = accountViewModel.userProfile().lnAddress() != null
|
||||||
@@ -336,7 +338,7 @@ class LongFormPostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendDraftSync() {
|
suspend fun sendDraftSync() {
|
||||||
if (message.text.isBlank() && title.text.isBlank()) {
|
if (message.text.toString().isBlank() && title.text.isBlank()) {
|
||||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||||
} else {
|
} else {
|
||||||
val template = createTemplate() ?: return
|
val template = createTemplate() ?: return
|
||||||
@@ -350,7 +352,7 @@ class LongFormPostViewModel :
|
|||||||
|
|
||||||
val tagger =
|
val tagger =
|
||||||
NewMessageTagger(
|
NewMessageTagger(
|
||||||
message.text,
|
message.text.toString(),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
@@ -540,7 +542,7 @@ class LongFormPostViewModel :
|
|||||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||||
|
|
||||||
val markdownImage = ""
|
val markdownImage = ""
|
||||||
message = message.insertUrlAtCursor(markdownImage)
|
message.insertUrlAtCursor(markdownImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,7 +562,7 @@ class LongFormPostViewModel :
|
|||||||
title = TextFieldValue("")
|
title = TextFieldValue("")
|
||||||
summary = TextFieldValue("")
|
summary = TextFieldValue("")
|
||||||
coverImageUrl = ""
|
coverImageUrl = ""
|
||||||
message = TextFieldValue("")
|
message.setTextAndPlaceCursorAtEnd("")
|
||||||
publishedAt = TimeUtils.now()
|
publishedAt = TimeUtils.now()
|
||||||
showPreview = false
|
showPreview = false
|
||||||
|
|
||||||
@@ -595,11 +597,9 @@ class LongFormPostViewModel :
|
|||||||
this.multiOrchestrator?.remove(selected)
|
this.multiOrchestrator?.remove(selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMessage(newMessage: TextFieldValue) {
|
override fun onMessageChanged() {
|
||||||
message = newMessage
|
|
||||||
|
|
||||||
if (message.selection.collapsed) {
|
if (message.selection.collapsed) {
|
||||||
val lastWord = newMessage.currentWord()
|
val lastWord = message.currentWord()
|
||||||
if (lastWord.startsWith("@")) {
|
if (lastWord.startsWith("@")) {
|
||||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||||
userSuggestions?.processCurrentWord(lastWord)
|
userSuggestions?.processCurrentWord(lastWord)
|
||||||
@@ -627,7 +627,7 @@ class LongFormPostViewModel :
|
|||||||
userSuggestions?.let { userSuggestions ->
|
userSuggestions?.let { userSuggestions ->
|
||||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||||
val lastWord = message.currentWord()
|
val lastWord = message.currentWord()
|
||||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||||
forwardZapTo.value.addItem(item)
|
forwardZapTo.value.addItem(item)
|
||||||
forwardZapToEditting.value = TextFieldValue("")
|
forwardZapToEditting.value = TextFieldValue("")
|
||||||
@@ -642,7 +642,7 @@ class LongFormPostViewModel :
|
|||||||
|
|
||||||
fun autocompleteWithEmoji(item: EmojiMedia) {
|
fun autocompleteWithEmoji(item: EmojiMedia) {
|
||||||
val wordToInsert = ":${item.code}:"
|
val wordToInsert = ":${item.code}:"
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
@@ -656,21 +656,21 @@ class LongFormPostViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canPost(): Boolean =
|
fun canPost(): Boolean =
|
||||||
title.text.isNotBlank() &&
|
title.text.isNotBlank() &&
|
||||||
message.text.isNotBlank() &&
|
message.text.toString().isNotBlank() &&
|
||||||
!isUploadingImage &&
|
!isUploadingImage &&
|
||||||
!wantsInvoice &&
|
!wantsInvoice &&
|
||||||
(!wantsZapRaiser || zapRaiserAmount.value != null) &&
|
(!wantsZapRaiser || zapRaiserAmount.value != null) &&
|
||||||
multiOrchestrator == null
|
multiOrchestrator == null
|
||||||
|
|
||||||
fun insertAtCursor(newElement: String) {
|
fun insertAtCursor(newElement: String) {
|
||||||
message = message.insertUrlAtCursor(newElement)
|
message.insertUrlAtCursor(newElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||||
|
|||||||
+3
-2
@@ -41,7 +41,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -112,7 +112,8 @@ fun NewProductScreen(
|
|||||||
postViewModel.quote(it)
|
postViewModel.quote(it)
|
||||||
}
|
}
|
||||||
message?.ifBlank { null }?.let {
|
message?.ifBlank { null }?.let {
|
||||||
postViewModel.updateMessage(TextFieldValue(it))
|
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||||
|
postViewModel.onMessageChanged()
|
||||||
}
|
}
|
||||||
attachment?.ifBlank { null }?.toUri()?.let {
|
attachment?.ifBlank { null }?.toUri()?.let {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|||||||
+42
-51
@@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -131,7 +132,7 @@ open class NewProductViewModel :
|
|||||||
var productImages by mutableStateOf<List<ProductImageMeta>>(emptyList())
|
var productImages by mutableStateOf<List<ProductImageMeta>>(emptyList())
|
||||||
val iMetaDescription = IMetaAttachments()
|
val iMetaDescription = IMetaAttachments()
|
||||||
|
|
||||||
override var message by mutableStateOf(TextFieldValue(""))
|
override val message = TextFieldState()
|
||||||
|
|
||||||
val urlPreviews = PreviewState()
|
val urlPreviews = PreviewState()
|
||||||
|
|
||||||
@@ -151,10 +152,10 @@ open class NewProductViewModel :
|
|||||||
val strippingFailureConfirmation = SuspendableConfirmation()
|
val strippingFailureConfirmation = SuspendableConfirmation()
|
||||||
|
|
||||||
// Classifieds
|
// Classifieds
|
||||||
var title by mutableStateOf(TextFieldValue(""))
|
val title = TextFieldState()
|
||||||
var price by mutableStateOf(TextFieldValue(""))
|
val price = TextFieldState()
|
||||||
var locationText by mutableStateOf(TextFieldValue(""))
|
val locationText = TextFieldState()
|
||||||
var category by mutableStateOf(TextFieldValue(""))
|
val category = TextFieldState()
|
||||||
var condition by mutableStateOf(ConditionTag.CONDITION.USED_LIKE_NEW)
|
var condition by mutableStateOf(ConditionTag.CONDITION.USED_LIKE_NEW)
|
||||||
|
|
||||||
// Invoices
|
// Invoices
|
||||||
@@ -226,7 +227,7 @@ open class NewProductViewModel :
|
|||||||
open fun quote(quote: Note) {
|
open fun quote(quote: Note) {
|
||||||
val accountViewModel = accountViewModel
|
val accountViewModel = accountViewModel
|
||||||
|
|
||||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||||
|
|
||||||
quote.author?.let { quotedUser ->
|
quote.author?.let { quotedUser ->
|
||||||
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||||
@@ -246,7 +247,7 @@ open class NewProductViewModel :
|
|||||||
wantsForwardZapTo = true
|
wantsForwardZapTo = true
|
||||||
}
|
}
|
||||||
|
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromDraft(draft: Note) {
|
private fun loadFromDraft(draft: Note) {
|
||||||
@@ -284,10 +285,10 @@ open class NewProductViewModel :
|
|||||||
zapRaiserAmount.value = zapraiser
|
zapRaiserAmount.value = zapraiser
|
||||||
}
|
}
|
||||||
|
|
||||||
title = TextFieldValue(draftEvent.title() ?: "")
|
title.setTextAndPlaceCursorAtEnd(draftEvent.title() ?: "")
|
||||||
price = TextFieldValue(draftEvent.price()?.amount ?: "")
|
price.setTextAndPlaceCursorAtEnd(draftEvent.price()?.amount ?: "")
|
||||||
category = TextFieldValue(draftEvent.categories().firstOrNull() ?: "")
|
category.setTextAndPlaceCursorAtEnd(draftEvent.categories().firstOrNull() ?: "")
|
||||||
locationText = TextFieldValue(draftEvent.location() ?: "")
|
locationText.setTextAndPlaceCursorAtEnd(draftEvent.location() ?: "")
|
||||||
condition = draftEvent.conditionValid() ?: ConditionTag.CONDITION.USED_LIKE_NEW
|
condition = draftEvent.conditionValid() ?: ConditionTag.CONDITION.USED_LIKE_NEW
|
||||||
|
|
||||||
val imageSet = draftEvent.images().toMutableSet()
|
val imageSet = draftEvent.images().toMutableSet()
|
||||||
@@ -305,9 +306,9 @@ open class NewProductViewModel :
|
|||||||
productImages = productImages + ProductImageMeta(it)
|
productImages = productImages + ProductImageMeta(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
message = TextFieldValue(draftEvent.content)
|
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||||
|
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendPostSync() {
|
suspend fun sendPostSync() {
|
||||||
@@ -323,7 +324,7 @@ open class NewProductViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendDraftSync() {
|
suspend fun sendDraftSync() {
|
||||||
if (message.text.isBlank()) {
|
if (message.text.toString().isBlank()) {
|
||||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||||
} else {
|
} else {
|
||||||
val template = createTemplate() ?: return
|
val template = createTemplate() ?: return
|
||||||
@@ -336,7 +337,7 @@ open class NewProductViewModel :
|
|||||||
|
|
||||||
val tagger =
|
val tagger =
|
||||||
NewMessageTagger(
|
NewMessageTagger(
|
||||||
message = message.text,
|
message = message.text.toString(),
|
||||||
dao = accountViewModel,
|
dao = accountViewModel,
|
||||||
)
|
)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
@@ -356,15 +357,15 @@ open class NewProductViewModel :
|
|||||||
|
|
||||||
val template =
|
val template =
|
||||||
ClassifiedsEvent.build(
|
ClassifiedsEvent.build(
|
||||||
title.text,
|
title.text.toString(),
|
||||||
PriceTag(price.text, "SATS", null),
|
PriceTag(price.text.toString(), "SATS", null),
|
||||||
tagger.message,
|
tagger.message,
|
||||||
locationText.text.ifBlank { null },
|
locationText.text.toString().ifBlank { null },
|
||||||
condition,
|
condition,
|
||||||
) {
|
) {
|
||||||
productImages.forEach { image(it.url) }
|
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)
|
quotes(quotes)
|
||||||
|
|
||||||
geoHash?.let { geohash(it) }
|
geoHash?.let { geohash(it) }
|
||||||
@@ -438,8 +439,8 @@ open class NewProductViewModel :
|
|||||||
} else {
|
} else {
|
||||||
iMetaDescription.add(it.result, alt, contentWarningReason)
|
iMetaDescription.add(it.result, alt, contentWarningReason)
|
||||||
|
|
||||||
message = message.insertUrlAtCursor(it.result.url)
|
message.insertUrlAtCursor(it.result.url)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,7 +459,7 @@ open class NewProductViewModel :
|
|||||||
open fun cancel() {
|
open fun cancel() {
|
||||||
draftTag.rotate()
|
draftTag.rotate()
|
||||||
|
|
||||||
message = TextFieldValue("")
|
message.setTextAndPlaceCursorAtEnd("")
|
||||||
|
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
mediaUploadTracker.finishUpload()
|
mediaUploadTracker.finishUpload()
|
||||||
@@ -468,10 +469,10 @@ open class NewProductViewModel :
|
|||||||
zapRaiserAmount.value = null
|
zapRaiserAmount.value = null
|
||||||
|
|
||||||
condition = ConditionTag.CONDITION.USED_LIKE_NEW
|
condition = ConditionTag.CONDITION.USED_LIKE_NEW
|
||||||
locationText = TextFieldValue("")
|
locationText.setTextAndPlaceCursorAtEnd("")
|
||||||
title = TextFieldValue("")
|
title.setTextAndPlaceCursorAtEnd("")
|
||||||
category = TextFieldValue("")
|
category.setTextAndPlaceCursorAtEnd("")
|
||||||
price = TextFieldValue("")
|
price.setTextAndPlaceCursorAtEnd("")
|
||||||
|
|
||||||
wantsForwardZapTo = false
|
wantsForwardZapTo = false
|
||||||
wantsToMarkAsSensitive = false
|
wantsToMarkAsSensitive = false
|
||||||
@@ -505,9 +506,8 @@ open class NewProductViewModel :
|
|||||||
this.multiOrchestrator?.remove(selected)
|
this.multiOrchestrator?.remove(selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMessage(newMessage: TextFieldValue) {
|
override fun onMessageChanged() {
|
||||||
message = newMessage
|
urlPreviews.update(message.text.toString())
|
||||||
urlPreviews.update(message)
|
|
||||||
|
|
||||||
if (message.selection.collapsed) {
|
if (message.selection.collapsed) {
|
||||||
val lastWord = message.currentWord()
|
val lastWord = message.currentWord()
|
||||||
@@ -538,8 +538,8 @@ open class NewProductViewModel :
|
|||||||
userSuggestions?.let { userSuggestions ->
|
userSuggestions?.let { userSuggestions ->
|
||||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||||
val lastWord = message.currentWord()
|
val lastWord = message.currentWord()
|
||||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||||
forwardZapTo.value.addItem(item)
|
forwardZapTo.value.addItem(item)
|
||||||
forwardZapToEditting.value = TextFieldValue("")
|
forwardZapToEditting.value = TextFieldValue("")
|
||||||
@@ -555,8 +555,8 @@ open class NewProductViewModel :
|
|||||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||||
val wordToInsert = ":${item.code}:"
|
val wordToInsert = ":${item.code}:"
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -572,8 +572,8 @@ open class NewProductViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -591,7 +591,7 @@ open class NewProductViewModel :
|
|||||||
multiOrchestrator == null
|
multiOrchestrator == null
|
||||||
|
|
||||||
fun insertAtCursor(newElement: String) {
|
fun insertAtCursor(newElement: String) {
|
||||||
message = message.insertUrlAtCursor(newElement)
|
message.insertUrlAtCursor(newElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||||
@@ -620,7 +620,7 @@ open class NewProductViewModel :
|
|||||||
|
|
||||||
override fun updateZapFromText() {
|
override fun updateZapFromText() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
tagger.pTags?.forEach { taggedUser ->
|
tagger.pTags?.forEach { taggedUser ->
|
||||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||||
@@ -648,19 +648,11 @@ open class NewProductViewModel :
|
|||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTitle(it: TextFieldValue) {
|
fun onTitleChanged() {
|
||||||
title = it
|
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePrice(it: TextFieldValue) {
|
fun onPriceChanged() {
|
||||||
runCatching {
|
|
||||||
if (it.text.isEmpty()) {
|
|
||||||
price = TextFieldValue("")
|
|
||||||
} else if (it.text.toLongOrNull() != null) {
|
|
||||||
price = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,13 +661,12 @@ open class NewProductViewModel :
|
|||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCategory(value: TextFieldValue) {
|
fun updateCategory(text: String) {
|
||||||
category = value
|
category.setTextAndPlaceCursorAtEnd(text)
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLocation(it: TextFieldValue) {
|
fun onLocationChanged() {
|
||||||
locationText = it
|
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-20
@@ -44,12 +44,12 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.vitorpamplona.amethyst.R
|
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.TextSpinner
|
||||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||||
@@ -109,10 +109,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
ThinPaddingTextField(
|
ThinPaddingTextField(
|
||||||
value = postViewModel.title,
|
state = postViewModel.title,
|
||||||
onValueChange = {
|
onTextChanged = { postViewModel.onTitleChanged() },
|
||||||
postViewModel.updateTitle(it)
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
@@ -120,8 +118,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
|||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
visualTransformation =
|
outputTransformation =
|
||||||
UrlUserTagTransformation(
|
UrlUserTagOutputTransformation(
|
||||||
MaterialTheme.colorScheme.primary,
|
MaterialTheme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
@@ -146,10 +144,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
|||||||
|
|
||||||
ThinPaddingTextField(
|
ThinPaddingTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
value = postViewModel.price,
|
state = postViewModel.price,
|
||||||
onValueChange = {
|
onTextChanged = { postViewModel.onPriceChanged() },
|
||||||
postViewModel.updatePrice(it)
|
|
||||||
},
|
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
text = "1000",
|
text = "1000",
|
||||||
@@ -275,10 +271,10 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
|||||||
categoryTypes.map { TitleExplainer(it.second, null) }.toImmutableList()
|
categoryTypes.map { TitleExplainer(it.second, null) }.toImmutableList()
|
||||||
}
|
}
|
||||||
TextSpinner(
|
TextSpinner(
|
||||||
placeholder = categoryTypes.firstOrNull { it.second == postViewModel.category.text }?.second ?: "",
|
placeholder = categoryTypes.firstOrNull { it.second == postViewModel.category.text.toString() }?.second ?: "",
|
||||||
options = categoryOptions,
|
options = categoryOptions,
|
||||||
onSelect = {
|
onSelect = {
|
||||||
postViewModel.updateCategory(TextFieldValue(categoryTypes[it].second))
|
postViewModel.updateCategory(categoryTypes[it].second)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -313,10 +309,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
ThinPaddingTextField(
|
ThinPaddingTextField(
|
||||||
value = postViewModel.locationText,
|
state = postViewModel.locationText,
|
||||||
onValueChange = {
|
onTextChanged = { postViewModel.onLocationChanged() },
|
||||||
postViewModel.updateLocation(it)
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
@@ -324,8 +318,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
|||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
visualTransformation =
|
outputTransformation =
|
||||||
UrlUserTagTransformation(
|
UrlUserTagOutputTransformation(
|
||||||
MaterialTheme.colorScheme.primary,
|
MaterialTheme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
|
|||||||
+33
-32
@@ -28,7 +28,8 @@ import androidx.compose.runtime.mutableStateMapOf
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
@@ -183,7 +184,7 @@ open class ShortNotePostViewModel :
|
|||||||
val iMetaAttachments = IMetaAttachments()
|
val iMetaAttachments = IMetaAttachments()
|
||||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||||
|
|
||||||
override var message by mutableStateOf(TextFieldValue(""))
|
override val message = TextFieldState()
|
||||||
|
|
||||||
val urlPreviews = PreviewState()
|
val urlPreviews = PreviewState()
|
||||||
|
|
||||||
@@ -367,7 +368,7 @@ open class ShortNotePostViewModel :
|
|||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
|
|
||||||
quote?.let { quotedNote ->
|
quote?.let { quotedNote ->
|
||||||
message = TextFieldValue(message.text + "\nnostr:${quotedNote.toNEvent()}")
|
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quotedNote.toNEvent()}")
|
||||||
|
|
||||||
quotedNote.author?.let { quotedUser ->
|
quotedNote.author?.let { quotedUser ->
|
||||||
if (quotedUser.pubkeyHex != user.pubkeyHex) {
|
if (quotedUser.pubkeyHex != user.pubkeyHex) {
|
||||||
@@ -385,7 +386,7 @@ open class ShortNotePostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fork?.let { forkedNoted ->
|
fork?.let { forkedNoted ->
|
||||||
message = TextFieldValue(version?.event?.content ?: forkedNoted.event?.content ?: "")
|
message.setTextAndPlaceCursorAtEnd(version?.event?.content ?: forkedNoted.event?.content ?: "")
|
||||||
|
|
||||||
forkedNoted.event?.isSensitiveOrNSFW()?.let {
|
forkedNoted.event?.isSensitiveOrNSFW()?.let {
|
||||||
if (it) wantsToMarkAsSensitive = true
|
if (it) wantsToMarkAsSensitive = true
|
||||||
@@ -436,7 +437,7 @@ open class ShortNotePostViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromDraft(draft: Note) {
|
private fun loadFromDraft(draft: Note) {
|
||||||
@@ -533,11 +534,11 @@ open class ShortNotePostViewModel :
|
|||||||
wantsPoll = false
|
wantsPoll = false
|
||||||
wantsZapPoll = false
|
wantsZapPoll = false
|
||||||
|
|
||||||
message = TextFieldValue(draftEvent.content)
|
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||||
|
|
||||||
iMetaAttachments.addAll(draftEvent.imetas())
|
iMetaAttachments.addAll(draftEvent.imetas())
|
||||||
|
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromDraft(draftEvent: PollEvent) {
|
private fun loadFromDraft(draftEvent: PollEvent) {
|
||||||
@@ -605,11 +606,11 @@ open class ShortNotePostViewModel :
|
|||||||
pollType = draftEvent.pollType()
|
pollType = draftEvent.pollType()
|
||||||
closedAt = draftEvent.endsAt() ?: TimeUtils.oneDayAhead()
|
closedAt = draftEvent.endsAt() ?: TimeUtils.oneDayAhead()
|
||||||
|
|
||||||
message = TextFieldValue(draftEvent.content)
|
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||||
|
|
||||||
iMetaAttachments.addAll(draftEvent.imetas())
|
iMetaAttachments.addAll(draftEvent.imetas())
|
||||||
|
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromDraft(draftEvent: ZapPollEvent) {
|
private fun loadFromDraft(draftEvent: ZapPollEvent) {
|
||||||
@@ -679,11 +680,11 @@ open class ShortNotePostViewModel :
|
|||||||
zapPollConsensusThreshold = draftEvent.consensusThreshold()?.let { (it * 100).toInt() }
|
zapPollConsensusThreshold = draftEvent.consensusThreshold()?.let { (it * 100).toInt() }
|
||||||
zapPollClosedAt = draftEvent.closedAt() ?: TimeUtils.oneDayAhead()
|
zapPollClosedAt = draftEvent.closedAt() ?: TimeUtils.oneDayAhead()
|
||||||
|
|
||||||
message = TextFieldValue(draftEvent.content)
|
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||||
|
|
||||||
iMetaAttachments.addAll(draftEvent.imetas())
|
iMetaAttachments.addAll(draftEvent.imetas())
|
||||||
|
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendPostSync() {
|
suspend fun sendPostSync() {
|
||||||
@@ -750,7 +751,7 @@ open class ShortNotePostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendDraftSync() {
|
suspend fun sendDraftSync() {
|
||||||
if (message.text.isBlank()) {
|
if (message.text.toString().isBlank()) {
|
||||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||||
} else {
|
} else {
|
||||||
val attachments = mutableSetOf<Event>()
|
val attachments = mutableSetOf<Event>()
|
||||||
@@ -811,7 +812,7 @@ open class ShortNotePostViewModel :
|
|||||||
|
|
||||||
val tagger =
|
val tagger =
|
||||||
NewMessageTagger(
|
NewMessageTagger(
|
||||||
message.text,
|
message.text.toString(),
|
||||||
pTags,
|
pTags,
|
||||||
eTags,
|
eTags,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
@@ -1000,8 +1001,8 @@ open class ShortNotePostViewModel :
|
|||||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||||
|
|
||||||
note?.let {
|
note?.let {
|
||||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||||
val iMeta =
|
val iMeta =
|
||||||
@@ -1024,8 +1025,8 @@ open class ShortNotePostViewModel :
|
|||||||
|
|
||||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||||
|
|
||||||
message = message.insertUrlAtCursor(state.result.url)
|
message.insertUrlAtCursor(state.result.url)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1042,7 +1043,7 @@ open class ShortNotePostViewModel :
|
|||||||
open fun cancel() {
|
open fun cancel() {
|
||||||
draftTag.rotate()
|
draftTag.rotate()
|
||||||
|
|
||||||
message = TextFieldValue("")
|
message.setTextAndPlaceCursorAtEnd("")
|
||||||
|
|
||||||
forkedFromNote = null
|
forkedFromNote = null
|
||||||
|
|
||||||
@@ -1104,15 +1105,15 @@ open class ShortNotePostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
open fun addToMessage(it: String) {
|
open fun addToMessage(it: String) {
|
||||||
updateMessage(TextFieldValue(message.text + " " + it))
|
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||||
|
onMessageChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMessage(newMessage: TextFieldValue) {
|
override fun onMessageChanged() {
|
||||||
message = newMessage
|
urlPreviews.update(message.text.toString())
|
||||||
urlPreviews.update(message)
|
|
||||||
|
|
||||||
if (message.selection.collapsed) {
|
if (message.selection.collapsed) {
|
||||||
val lastWord = newMessage.currentWord()
|
val lastWord = message.currentWord()
|
||||||
if (lastWord.startsWith("@")) {
|
if (lastWord.startsWith("@")) {
|
||||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||||
userSuggestions?.processCurrentWord(lastWord)
|
userSuggestions?.processCurrentWord(lastWord)
|
||||||
@@ -1140,8 +1141,8 @@ open class ShortNotePostViewModel :
|
|||||||
userSuggestions?.let { userSuggestions ->
|
userSuggestions?.let { userSuggestions ->
|
||||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||||
val lastWord = message.currentWord()
|
val lastWord = message.currentWord()
|
||||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||||
forwardZapTo.value.addItem(item)
|
forwardZapTo.value.addItem(item)
|
||||||
forwardZapToEditting.value = TextFieldValue("")
|
forwardZapToEditting.value = TextFieldValue("")
|
||||||
@@ -1157,8 +1158,8 @@ open class ShortNotePostViewModel :
|
|||||||
open fun autocompleteWithEmoji(item: EmojiMedia) {
|
open fun autocompleteWithEmoji(item: EmojiMedia) {
|
||||||
val wordToInsert = ":${item.code}:"
|
val wordToInsert = ":${item.code}:"
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -1174,8 +1175,8 @@ open class ShortNotePostViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -1201,7 +1202,7 @@ open class ShortNotePostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Regular text/media posts require text
|
// Regular text/media posts require text
|
||||||
return message.text.isNotBlank() &&
|
return message.text.toString().isNotBlank() &&
|
||||||
!mediaUploadTracker.isUploading &&
|
!mediaUploadTracker.isUploading &&
|
||||||
!isUploadingVoice &&
|
!isUploadingVoice &&
|
||||||
!wantsInvoice &&
|
!wantsInvoice &&
|
||||||
@@ -1226,7 +1227,7 @@ open class ShortNotePostViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun insertAtCursor(newElement: String) {
|
fun insertAtCursor(newElement: String) {
|
||||||
message = message.insertUrlAtCursor(newElement)
|
message.insertUrlAtCursor(newElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||||
@@ -1378,7 +1379,7 @@ open class ShortNotePostViewModel :
|
|||||||
override fun updateZapFromText() {
|
override fun updateZapFromText() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val tagger =
|
val tagger =
|
||||||
NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
tagger.pTags?.forEach { taggedUser ->
|
tagger.pTags?.forEach { taggedUser ->
|
||||||
if (!forwardZapTo.value.items.any { it.key == 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 androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
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.SelectFromFiles
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||||
@@ -415,8 +415,8 @@ fun SendDirectMessageTo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ThinPaddingTextField(
|
ThinPaddingTextField(
|
||||||
value = postViewModel.toUsers,
|
state = postViewModel.toUsers,
|
||||||
onValueChange = postViewModel::updateToUsers,
|
onTextChanged = postViewModel::onToUsersChanged,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
@@ -432,8 +432,8 @@ fun SendDirectMessageTo(
|
|||||||
color = MaterialTheme.colorScheme.placeholderText,
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
visualTransformation =
|
outputTransformation =
|
||||||
UrlUserTagTransformation(
|
UrlUserTagOutputTransformation(
|
||||||
MaterialTheme.colorScheme.primary,
|
MaterialTheme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
colors =
|
colors =
|
||||||
|
|||||||
+41
-43
@@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -147,7 +148,7 @@ class NewPublicMessageViewModel :
|
|||||||
val iMetaAttachments = IMetaAttachments()
|
val iMetaAttachments = IMetaAttachments()
|
||||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||||
|
|
||||||
override var message by mutableStateOf(TextFieldValue(""))
|
override val message = TextFieldState()
|
||||||
|
|
||||||
val urlPreviews = PreviewState()
|
val urlPreviews = PreviewState()
|
||||||
|
|
||||||
@@ -160,7 +161,7 @@ class NewPublicMessageViewModel :
|
|||||||
|
|
||||||
var emojiSuggestions: EmojiSuggestionState? = null
|
var emojiSuggestions: EmojiSuggestionState? = null
|
||||||
|
|
||||||
var toUsers by mutableStateOf(TextFieldValue(""))
|
val toUsers = TextFieldState()
|
||||||
|
|
||||||
// Images and Videos
|
// Images and Videos
|
||||||
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
||||||
@@ -218,10 +219,9 @@ class NewPublicMessageViewModel :
|
|||||||
fun load(users: Set<HexKey>) {
|
fun load(users: Set<HexKey>) {
|
||||||
val userSet = users - account.userProfile().pubkeyHex
|
val userSet = users - account.userProfile().pubkeyHex
|
||||||
|
|
||||||
toUsers =
|
toUsers.setTextAndPlaceCursorAtEnd(
|
||||||
TextFieldValue(
|
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||||
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reply(post: Note) {
|
fun reply(post: Note) {
|
||||||
@@ -229,8 +229,8 @@ class NewPublicMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun quote(quote: Note) {
|
fun quote(quote: Note) {
|
||||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
// creates a split with that author.
|
// creates a split with that author.
|
||||||
val quotedAuthor = quote.author ?: return
|
val quotedAuthor = quote.author ?: return
|
||||||
@@ -306,13 +306,12 @@ class NewPublicMessageViewModel :
|
|||||||
|
|
||||||
val userSet = draftEvent.groupKeys() - account.userProfile().pubkeyHex
|
val userSet = draftEvent.groupKeys() - account.userProfile().pubkeyHex
|
||||||
|
|
||||||
toUsers =
|
toUsers.setTextAndPlaceCursorAtEnd(
|
||||||
TextFieldValue(
|
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||||
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
)
|
||||||
)
|
|
||||||
|
|
||||||
message = TextFieldValue(draftEvent.content)
|
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
iMetaAttachments.addAll(draftEvent.imetas())
|
iMetaAttachments.addAll(draftEvent.imetas())
|
||||||
}
|
}
|
||||||
@@ -349,7 +348,7 @@ class NewPublicMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendDraftSync() {
|
suspend fun sendDraftSync() {
|
||||||
if (message.text.isBlank()) {
|
if (message.text.toString().isBlank()) {
|
||||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||||
} else {
|
} else {
|
||||||
val broadcast = mutableSetOf<Event>()
|
val broadcast = mutableSetOf<Event>()
|
||||||
@@ -364,13 +363,14 @@ class NewPublicMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createTemplate(): EventTemplate<PublicMessageEvent> {
|
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()
|
toUsersTagger.run()
|
||||||
|
|
||||||
|
val messageText = message.text.toString()
|
||||||
val msg =
|
val msg =
|
||||||
replyingTo?.let {
|
replyingTo?.let {
|
||||||
it.toNostrUri() + "\n" + message.text
|
it.toNostrUri() + "\n" + messageText
|
||||||
} ?: message.text
|
} ?: messageText
|
||||||
|
|
||||||
val tagger = NewMessageTagger(msg, null, null, accountViewModel)
|
val tagger = NewMessageTagger(msg, null, null, accountViewModel)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
@@ -470,8 +470,8 @@ class NewPublicMessageViewModel :
|
|||||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||||
|
|
||||||
note?.let {
|
note?.let {
|
||||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||||
val iMeta =
|
val iMeta =
|
||||||
@@ -494,8 +494,8 @@ class NewPublicMessageViewModel :
|
|||||||
|
|
||||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||||
|
|
||||||
message = message.insertUrlAtCursor(state.result.url)
|
message.insertUrlAtCursor(state.result.url)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,8 +512,8 @@ class NewPublicMessageViewModel :
|
|||||||
fun cancel() {
|
fun cancel() {
|
||||||
draftTag.rotate()
|
draftTag.rotate()
|
||||||
|
|
||||||
toUsers = TextFieldValue("")
|
toUsers.setTextAndPlaceCursorAtEnd("")
|
||||||
message = TextFieldValue("")
|
message.setTextAndPlaceCursorAtEnd("")
|
||||||
multiOrchestrator = null
|
multiOrchestrator = null
|
||||||
|
|
||||||
wantsInvoice = false
|
wantsInvoice = false
|
||||||
@@ -546,15 +546,15 @@ class NewPublicMessageViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addToMessage(it: String) {
|
fun addToMessage(it: String) {
|
||||||
updateMessage(TextFieldValue(message.text + " " + it))
|
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||||
|
onMessageChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMessage(newMessage: TextFieldValue) {
|
override fun onMessageChanged() {
|
||||||
message = newMessage
|
urlPreviews.update(message.text.toString())
|
||||||
urlPreviews.update(newMessage)
|
|
||||||
|
|
||||||
if (message.selection.collapsed) {
|
if (message.selection.collapsed) {
|
||||||
val lastWord = newMessage.currentWord()
|
val lastWord = message.currentWord()
|
||||||
if (lastWord.startsWith("@")) {
|
if (lastWord.startsWith("@")) {
|
||||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||||
userSuggestions?.processCurrentWord(lastWord)
|
userSuggestions?.processCurrentWord(lastWord)
|
||||||
@@ -569,11 +569,9 @@ class NewPublicMessageViewModel :
|
|||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateToUsers(newToUsersValue: TextFieldValue) {
|
fun onToUsersChanged() {
|
||||||
toUsers = newToUsersValue
|
if (toUsers.selection.collapsed) {
|
||||||
|
val lastWord = toUsers.currentWord()
|
||||||
if (newToUsersValue.selection.collapsed) {
|
|
||||||
val lastWord = newToUsersValue.currentWord()
|
|
||||||
userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS
|
userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS
|
||||||
userSuggestions?.processCurrentWord(lastWord)
|
userSuggestions?.processCurrentWord(lastWord)
|
||||||
}
|
}
|
||||||
@@ -594,14 +592,14 @@ class NewPublicMessageViewModel :
|
|||||||
userSuggestions?.let { userSuggestions ->
|
userSuggestions?.let { userSuggestions ->
|
||||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||||
val lastWord = message.currentWord()
|
val lastWord = message.currentWord()
|
||||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||||
forwardZapTo.value.addItem(item)
|
forwardZapTo.value.addItem(item)
|
||||||
forwardZapToEditting.value = TextFieldValue("")
|
forwardZapToEditting.value = TextFieldValue("")
|
||||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
||||||
val lastWord = toUsers.currentWord()
|
val lastWord = toUsers.currentWord()
|
||||||
toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
userSuggestionsMainMessage = null
|
userSuggestionsMainMessage = null
|
||||||
@@ -614,8 +612,8 @@ class NewPublicMessageViewModel :
|
|||||||
fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||||
val wordToInsert = ":${item.code}:"
|
val wordToInsert = ":${item.code}:"
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -631,8 +629,8 @@ class NewPublicMessageViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -648,7 +646,7 @@ class NewPublicMessageViewModel :
|
|||||||
multiOrchestrator == null
|
multiOrchestrator == null
|
||||||
|
|
||||||
fun insertAtCursor(newElement: String) {
|
fun insertAtCursor(newElement: String) {
|
||||||
message = message.insertUrlAtCursor(newElement)
|
message.insertUrlAtCursor(newElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||||
@@ -671,7 +669,7 @@ class NewPublicMessageViewModel :
|
|||||||
|
|
||||||
override fun updateZapFromText() {
|
override fun updateZapFromText() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||||
tagger.run()
|
tagger.run()
|
||||||
tagger.pTags?.forEach { taggedUser ->
|
tagger.pTags?.forEach { taggedUser ->
|
||||||
if (!forwardZapTo.value.items.any { it.key == 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