Merge pull request #2040 from vitorpamplona/claude/migrate-textfield-state-6ezEZ
Migrate text fields from TextFieldValue to TextFieldState
This commit is contained in:
+80
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.actions
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.OutputTransformation
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldBuffer
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||||
|
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
class UrlUserTagOutputTransformation(
|
||||||
|
val color: Color,
|
||||||
|
) : OutputTransformation {
|
||||||
|
override fun TextFieldBuffer.transformOutput() {
|
||||||
|
val text = asCharSequence().toString()
|
||||||
|
|
||||||
|
// Find all @npub mentions using regex and replace in reverse order
|
||||||
|
// so that earlier indices remain valid after replacements.
|
||||||
|
val npubRegex = Regex("@npub1[a-z0-9]{58}")
|
||||||
|
val matches = npubRegex.findAll(text).toList().reversed()
|
||||||
|
|
||||||
|
for (match in matches) {
|
||||||
|
try {
|
||||||
|
val key = decodePublicKey(match.value.removePrefix("@"))
|
||||||
|
val user = LocalCache.getOrCreateUser(key.toHexKey())
|
||||||
|
val displayName = "@${user.toBestDisplayName()}"
|
||||||
|
|
||||||
|
replace(match.range.first, match.range.last + 1, displayName)
|
||||||
|
|
||||||
|
// Apply color styling to the replaced display name
|
||||||
|
addAnnotation(
|
||||||
|
SpanStyle(color = color, textDecoration = TextDecoration.None),
|
||||||
|
match.range.first,
|
||||||
|
match.range.first + displayName.length,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight URLs in remaining text
|
||||||
|
highlightUrls(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TextFieldBuffer.highlightUrls(color: Color) {
|
||||||
|
val text = asCharSequence().toString()
|
||||||
|
val urlPattern = android.util.Patterns.WEB_URL
|
||||||
|
|
||||||
|
val matcher = urlPattern.matcher(text)
|
||||||
|
while (matcher.find()) {
|
||||||
|
addAnnotation(
|
||||||
|
SpanStyle(color = color, textDecoration = TextDecoration.None),
|
||||||
|
matcher.start(),
|
||||||
|
matcher.end(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
-1
@@ -27,6 +27,10 @@ import androidx.compose.foundation.layout.defaultMinSize
|
|||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-24
@@ -21,6 +21,8 @@
|
|||||||
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.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
@@ -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
@@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -46,7 +47,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.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.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
@@ -24,11 +24,11 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
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.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
|
||||||
@@ -108,7 +108,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+42
-44
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
@@ -203,7 +204,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()
|
||||||
|
|
||||||
@@ -215,8 +216,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)
|
||||||
@@ -273,8 +274,7 @@ 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" },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -290,8 +290,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
|
||||||
@@ -371,12 +371,11 @@ 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" },
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -392,7 +391,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) {
|
||||||
@@ -400,13 +399,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())
|
||||||
}
|
}
|
||||||
@@ -421,7 +421,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)
|
||||||
@@ -563,11 +563,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
|
||||||
@@ -633,8 +634,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
|
||||||
|
|
||||||
@@ -667,15 +668,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)
|
||||||
@@ -689,11 +690,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)
|
||||||
}
|
}
|
||||||
@@ -706,7 +705,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 }
|
||||||
@@ -720,8 +719,7 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSubject(it: TextFieldValue) {
|
fun onSubjectChanged() {
|
||||||
subject = it
|
|
||||||
draftTag.newVersion()
|
draftTag.newVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,14 +736,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,8 +757,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()
|
||||||
|
|
||||||
@@ -776,8 +774,8 @@ class ChatNewMessageViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message = message.replaceCurrentWord(wordToInsert)
|
message.replaceCurrentWord(wordToInsert)
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
|
|
||||||
emojiSuggestions?.reset()
|
emojiSuggestions?.reset()
|
||||||
|
|
||||||
@@ -794,8 +792,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() {
|
||||||
@@ -816,7 +814,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
@@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AddPhotoAlternate
|
import androidx.compose.material.icons.filled.AddPhotoAlternate
|
||||||
@@ -67,7 +68,6 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.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.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
@@ -54,7 +54,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
|
||||||
@@ -201,8 +201,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(),
|
||||||
@@ -231,7 +231,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
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
@@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-6
@@ -62,12 +62,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||||
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.SelectSingleFromGallery
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
|
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||||
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||||
@@ -293,7 +294,7 @@ private fun MarkdownPostScreenBody(
|
|||||||
.padding(Size10dp),
|
.padding(Size10dp),
|
||||||
) {
|
) {
|
||||||
RenderContentAsMarkdown(
|
RenderContentAsMarkdown(
|
||||||
content = postViewModel.message.text,
|
content = postViewModel.message.text.toString(),
|
||||||
tags = EmptyTagList,
|
tags = EmptyTagList,
|
||||||
canPreview = true,
|
canPreview = true,
|
||||||
quotesLeft = 1,
|
quotesLeft = 1,
|
||||||
@@ -304,9 +305,9 @@ private fun MarkdownPostScreenBody(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Markdown editor
|
// Markdown editor
|
||||||
OutlinedTextField(
|
ThinPaddingTextField(
|
||||||
value = postViewModel.message,
|
state = postViewModel.message,
|
||||||
onValueChange = postViewModel::updateMessage,
|
onTextChanged = postViewModel::onMessageChanged,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -328,7 +329,7 @@ private fun MarkdownPostScreenBody(
|
|||||||
focusedBorderColor = Color.Transparent,
|
focusedBorderColor = Color.Transparent,
|
||||||
unfocusedBorderColor = Color.Transparent,
|
unfocusedBorderColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions.Default.copy(
|
KeyboardOptions.Default.copy(
|
||||||
capitalization = KeyboardCapitalization.Sentences,
|
capitalization = KeyboardCapitalization.Sentences,
|
||||||
|
|||||||
+17
-17
@@ -21,6 +21,8 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm
|
||||||
|
|
||||||
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.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
@@ -147,7 +149,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)
|
||||||
|
|
||||||
@@ -241,7 +243,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +267,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
|
||||||
@@ -335,7 +337,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
|
||||||
@@ -349,7 +351,7 @@ class LongFormPostViewModel :
|
|||||||
|
|
||||||
val tagger =
|
val tagger =
|
||||||
NewMessageTagger(
|
NewMessageTagger(
|
||||||
message.text,
|
message.text.toString(),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
@@ -539,7 +541,7 @@ class LongFormPostViewModel :
|
|||||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||||
|
|
||||||
val markdownImage = ""
|
val markdownImage = ""
|
||||||
message = message.insertUrlAtCursor(markdownImage)
|
message.insertUrlAtCursor(markdownImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,7 +561,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
|
||||||
|
|
||||||
@@ -594,11 +596,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)
|
||||||
@@ -626,7 +626,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("")
|
||||||
@@ -641,7 +641,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()
|
||||||
}
|
}
|
||||||
@@ -655,21 +655,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>) {
|
||||||
@@ -699,7 +699,7 @@ class LongFormPostViewModel :
|
|||||||
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
@@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -41,7 +42,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.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.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
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
@@ -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 =
|
||||||
|
|||||||
+3
-2
@@ -20,10 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
@@ -67,7 +67,8 @@ fun GeoHashPostScreen(
|
|||||||
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
@@ -20,10 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||||
@@ -67,7 +67,8 @@ fun HashtagPostScreen(
|
|||||||
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
@@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Poll
|
import androidx.compose.material.icons.outlined.Poll
|
||||||
@@ -54,7 +55,6 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.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.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.IntentCompat
|
import androidx.core.content.IntentCompat
|
||||||
@@ -150,7 +150,8 @@ fun ShortNotePostScreen(
|
|||||||
val draft = draftId?.let { accountViewModel.getNoteIfExists(it) }
|
val draft = draftId?.let { accountViewModel.getNoteIfExists(it) }
|
||||||
postViewModel.load(baseReplyTo, quote, fork, version, draft)
|
postViewModel.load(baseReplyTo, quote, fork, version, draft)
|
||||||
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) {
|
||||||
|
|||||||
+33
-31
@@ -21,6 +21,8 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
|
||||||
|
|
||||||
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.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
@@ -183,7 +185,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 +369,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 +387,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 +438,7 @@ open class ShortNotePostViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
urlPreviews.update(message)
|
urlPreviews.update(message.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFromDraft(draft: Note) {
|
private fun loadFromDraft(draft: Note) {
|
||||||
@@ -533,11 +535,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 +607,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 +681,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 +752,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 +813,7 @@ open class ShortNotePostViewModel :
|
|||||||
|
|
||||||
val tagger =
|
val tagger =
|
||||||
NewMessageTagger(
|
NewMessageTagger(
|
||||||
message.text,
|
message.text.toString(),
|
||||||
pTags,
|
pTags,
|
||||||
eTags,
|
eTags,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
@@ -1000,8 +1002,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 +1026,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 +1044,7 @@ open class ShortNotePostViewModel :
|
|||||||
open fun cancel() {
|
open fun cancel() {
|
||||||
draftTag.rotate()
|
draftTag.rotate()
|
||||||
|
|
||||||
message = TextFieldValue("")
|
message.setTextAndPlaceCursorAtEnd("")
|
||||||
|
|
||||||
forkedFromNote = null
|
forkedFromNote = null
|
||||||
|
|
||||||
@@ -1104,15 +1106,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 +1142,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 +1159,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 +1176,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 +1203,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 +1228,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 +1380,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 =
|
||||||
|
|||||||
+37
-39
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
@@ -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,8 +219,7 @@ 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" },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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