mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e4ee7a618 | ||
|
|
1872a5879e | ||
|
|
a293976501 | ||
|
|
a1095b4c17 | ||
|
|
95d678f62f | ||
|
|
b625ac6d2d | ||
|
|
f848ad20d8 | ||
|
|
198e049873 | ||
|
|
f2d007f9ba | ||
|
|
4b9c3f239c | ||
|
|
3ac04cdc63 | ||
|
|
f9d231063d | ||
|
|
4bca74888d | ||
|
|
17ff27bac9 | ||
|
|
3913384fcf | ||
|
|
b02b8c4ed9 | ||
|
|
ed31b12e3f | ||
|
|
3b63264927 | ||
|
|
bc491b154b | ||
|
|
4d8583eed9 | ||
|
|
72bc7e0ed5 | ||
|
|
f997586e4b | ||
|
|
1d208edd33 |
@@ -745,8 +745,8 @@ android {
|
||||
applicationId = "com.vitorpamplona.amethyst"
|
||||
minSdk = 26 // Android 8.0 (Oreo)
|
||||
targetSdk = 36 // Android 15
|
||||
versionCode = 435
|
||||
versionName = "1.07.1"
|
||||
versionCode = 438
|
||||
versionName = "1.07.2"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
|
||||
@@ -7,7 +7,7 @@ description: Integration guide for using the Quartz Nostr KMP library in externa
|
||||
|
||||
Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr KMP projects.
|
||||
|
||||
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.07.1` (Maven Central)
|
||||
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.07.2` (Maven Central)
|
||||
**Targets**: JVM 21+, Android (minSdk 21+), iOS (XCFramework `quartz-kmpKit`)
|
||||
**License**: MIT
|
||||
|
||||
@@ -19,7 +19,7 @@ Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
quartz = "1.07.1"
|
||||
quartz = "1.07.2"
|
||||
|
||||
[libraries]
|
||||
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
|
||||
@@ -41,7 +41,7 @@ kotlin {
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.07.1")
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.07.2")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Current version
|
||||
|
||||
```
|
||||
com.vitorpamplona.quartz:quartz:1.07.1
|
||||
com.vitorpamplona.quartz:quartz:1.07.2
|
||||
```
|
||||
|
||||
Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/quartz
|
||||
@@ -16,7 +16,7 @@ Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/qua
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
quartz = "1.07.1"
|
||||
quartz = "1.07.2"
|
||||
|
||||
[libraries]
|
||||
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
|
||||
@@ -55,7 +55,7 @@ kotlin {
|
||||
```kotlin
|
||||
// build.gradle.kts (app module)
|
||||
dependencies {
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.07.1")
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.07.2")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -70,7 +70,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.07.1")
|
||||
implementation("com.vitorpamplona.quartz:quartz:1.07.2")
|
||||
// JNA needed for libsodium (NIP-44) on JVM
|
||||
implementation("net.java.dev.jna:jna:5.18.1")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ The Quartz library was successfully converted from Android-only to full KMP supp
|
||||
## Current artifact
|
||||
|
||||
```
|
||||
com.vitorpamplona.quartz:quartz:1.07.1
|
||||
com.vitorpamplona.quartz:quartz:1.07.2
|
||||
```
|
||||
|
||||
See `.claude/skills/quartz-integration/SKILL.md` for full integration guide.
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,3 +1,18 @@
|
||||
<a id="v1.07.2"></a>
|
||||
# [Release v1.07.2: GIF Keyboard](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.2) - 2026-03-30
|
||||
|
||||
- Adds GIF uploads support from Keyboard
|
||||
- Migrates text fields and @ modifiers to the new Jetpack Compose states
|
||||
- Fixes Bug that wasn't openning Amber to sign
|
||||
- Fixes Bug on rejections using old Ambers
|
||||
- Replaces hex input with user search dialog in relay management (allow/ban user)
|
||||
|
||||
<a id="v1.07.1"></a>
|
||||
# [Release v1.07.1: Fixes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.1) - 2026-03-30
|
||||
|
||||
- New translations
|
||||
- Fixes BLE's module need for synchronized events.
|
||||
|
||||
<a id="v1.07.0"></a>
|
||||
# [Release v1.07.0: Vanish, Relay Management, Pin Notes](https://github.com/vitorpamplona/amethyst/releases/tag/v1.07.0) - 2026-03-30
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@ android {
|
||||
applicationId = "com.vitorpamplona.amethyst"
|
||||
minSdk = libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode = 437
|
||||
versionName = generateVersionName("1.07.1")
|
||||
versionCode = 438
|
||||
versionName = generateVersionName("1.07.2")
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"12cd4bce977ed53502cf121ecba89a190ab02685333c8f230bac35b04f920eeb\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -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
|
||||
addStyle(
|
||||
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()) {
|
||||
addStyle(
|
||||
SpanStyle(color = color, textDecoration = TextDecoration.None),
|
||||
matcher.start(),
|
||||
matcher.end(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.content.MediaType
|
||||
import androidx.compose.foundation.content.ReceiveContentListener
|
||||
import androidx.compose.foundation.content.TransferableContent
|
||||
import androidx.compose.foundation.content.consume
|
||||
import androidx.compose.foundation.content.contentReceiver
|
||||
import androidx.compose.foundation.content.hasMediaType
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -27,6 +35,10 @@ import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.InputTransformation
|
||||
import androidx.compose.foundation.text.input.OutputTransformation
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
@@ -35,8 +47,11 @@ import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
@@ -50,6 +65,155 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
// COPIED FROM TEXT FIELD
|
||||
// The only change is the contentPadding below
|
||||
|
||||
// New TextFieldState-based overload for GIF keyboard support
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ThinPaddingTextField(
|
||||
state: TextFieldState,
|
||||
modifier: Modifier = Modifier,
|
||||
onTextChanged: (() -> Unit)? = null,
|
||||
onContentReceived: ((Uri, String?) -> 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)
|
||||
}
|
||||
|
||||
val contentModifier =
|
||||
if (onContentReceived != null) {
|
||||
modifier.contentReceiver(
|
||||
object : ReceiveContentListener {
|
||||
override fun onReceive(transferableContent: TransferableContent): TransferableContent? {
|
||||
if (!transferableContent.hasMediaType(MediaType.Image)) {
|
||||
return transferableContent
|
||||
}
|
||||
val remaining =
|
||||
transferableContent.consume { item ->
|
||||
val uri = item.uri
|
||||
if (uri != null) {
|
||||
onContentReceived(
|
||||
uri,
|
||||
transferableContent.clipEntry.clipData.description
|
||||
.getMimeType(0),
|
||||
)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
return remaining
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
modifier
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
||||
BasicTextField(
|
||||
state = state,
|
||||
modifier =
|
||||
contentModifier
|
||||
.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = 36.dp,
|
||||
),
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = mergedTextStyle,
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
keyboardOptions = keyboardOptions,
|
||||
lineLimits = lineLimits,
|
||||
interactionSource = interactionSource,
|
||||
inputTransformation = inputTransformation,
|
||||
outputTransformation = outputTransformation,
|
||||
decorator = { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.text.toString(),
|
||||
visualTransformation = VisualTransformation.None,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = placeholder,
|
||||
label = label,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
prefix = prefix,
|
||||
suffix = suffix,
|
||||
supportingText = supportingText,
|
||||
shape = shape,
|
||||
singleLine = singleLine,
|
||||
enabled = enabled,
|
||||
isError = isError,
|
||||
interactionSource = interactionSource,
|
||||
colors = colors,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy TextFieldValue-based overload (kept for readonly dropdown fields)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ThinPaddingTextField(
|
||||
@@ -76,7 +240,6 @@ fun ThinPaddingTextField(
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors(),
|
||||
// new fields
|
||||
contentPadding: PaddingValues =
|
||||
if (label == null) {
|
||||
TextFieldDefaults.contentPaddingWithoutLabel(
|
||||
@@ -113,6 +276,7 @@ fun ThinPaddingTextField(
|
||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
|
||||
@Suppress("DEPRECATION")
|
||||
BasicTextField(
|
||||
value = value,
|
||||
modifier =
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.messagefield
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
|
||||
interface IMessageField {
|
||||
var message: TextFieldValue
|
||||
val message: TextFieldState
|
||||
|
||||
fun updateMessage(newMessage: TextFieldValue)
|
||||
fun onMessageChanged()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.messagefield
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
@@ -39,7 +40,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -51,6 +52,7 @@ fun MessageField(
|
||||
placeholder: Int,
|
||||
viewModel: IMessageField,
|
||||
requestFocus: Boolean = true,
|
||||
onContentReceived: ((Uri, String?) -> Unit)? = null,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
@@ -65,8 +67,9 @@ fun MessageField(
|
||||
}
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = viewModel.message,
|
||||
onValueChange = viewModel::updateMessage,
|
||||
state = viewModel.message,
|
||||
onTextChanged = viewModel::onMessageChanged,
|
||||
onContentReceived = onContentReceived,
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
@@ -91,7 +94,7 @@ fun MessageField(
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
contentPadding =
|
||||
TextFieldDefaults.contentPaddingWithoutLabel(
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.previews
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.vitorpamplona.amethyst.service.CachedUrlParser
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -33,22 +32,22 @@ import kotlinx.coroutines.flow.map
|
||||
|
||||
@Stable
|
||||
class PreviewState {
|
||||
var source = MutableStateFlow(TextFieldValue(""))
|
||||
var source = MutableStateFlow("")
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val results =
|
||||
source
|
||||
.debounce(500)
|
||||
.map {
|
||||
CachedUrlParser.parseValidUrls(it.text)
|
||||
CachedUrlParser.parseValidUrls(it)
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
fun reset() {
|
||||
source.tryEmit(TextFieldValue(""))
|
||||
source.tryEmit("")
|
||||
}
|
||||
|
||||
fun update(text: TextFieldValue) {
|
||||
fun update(text: String) {
|
||||
source.tryEmit(text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.userSuggestions
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
@@ -199,4 +200,17 @@ class UserSuggestionState(
|
||||
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length),
|
||||
)
|
||||
}
|
||||
|
||||
fun replaceCurrentWord(
|
||||
state: TextFieldState,
|
||||
word: String,
|
||||
item: User,
|
||||
) {
|
||||
val wordToInsert = "@${item.pubkeyNpub()}"
|
||||
state.edit {
|
||||
val lastWordStart = selection.end - word.length
|
||||
replace(lastWordStart, selection.end, wordToInsert)
|
||||
selection = TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.nip22Comments
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -147,7 +149,7 @@ open class CommentPostViewModel :
|
||||
|
||||
var notifying by mutableStateOf<List<User>?>(null)
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
@@ -242,7 +244,7 @@ open class CommentPostViewModel :
|
||||
}
|
||||
|
||||
open fun quote(quote: Note) {
|
||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||
|
||||
quote.author?.let { quotedUser ->
|
||||
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||
@@ -262,7 +264,7 @@ open class CommentPostViewModel :
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
@@ -322,11 +324,11 @@ open class CommentPostViewModel :
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
@@ -359,7 +361,7 @@ open class CommentPostViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val attachments = mutableSetOf<Event>()
|
||||
@@ -376,7 +378,7 @@ open class CommentPostViewModel :
|
||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message = message.text,
|
||||
message = message.text.toString(),
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
@@ -525,8 +527,8 @@ open class CommentPostViewModel :
|
||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
val iMeta =
|
||||
@@ -549,8 +551,8 @@ open class CommentPostViewModel :
|
||||
|
||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||
|
||||
message = message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +578,7 @@ open class CommentPostViewModel :
|
||||
open fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
replyingTo = null
|
||||
externalIdentity = null
|
||||
@@ -618,9 +620,8 @@ open class CommentPostViewModel :
|
||||
notifying = notifying?.filter { it != userToRemove }
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreviews.update(message)
|
||||
override fun onMessageChanged() {
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = message.currentWord()
|
||||
@@ -651,8 +652,8 @@ open class CommentPostViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message.text.toString())
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
@@ -668,8 +669,8 @@ open class CommentPostViewModel :
|
||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -685,8 +686,8 @@ open class CommentPostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -694,14 +695,14 @@ open class CommentPostViewModel :
|
||||
}
|
||||
|
||||
fun canPost(): Boolean =
|
||||
message.text.isNotBlank() &&
|
||||
message.text.toString().isNotBlank() &&
|
||||
!mediaUploadTracker.isUploading &&
|
||||
!wantsInvoice &&
|
||||
(!wantsZapraiser || zapRaiserAmount.value != null) &&
|
||||
multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
@@ -718,7 +719,7 @@ open class CommentPostViewModel :
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger =
|
||||
NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -46,7 +47,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -125,7 +125,8 @@ fun ReplyCommentPostScreen(
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.Discove
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.IActivityLauncher
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.NostrSignerWithClientTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -165,7 +166,22 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
||||
|
||||
@Composable
|
||||
private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
|
||||
if (accountViewModel.account.signer is IActivityLauncher) {
|
||||
val externalSignerLauncher =
|
||||
when (val signer = accountViewModel.account.signer) {
|
||||
is IActivityLauncher -> {
|
||||
signer
|
||||
}
|
||||
|
||||
is NostrSignerWithClientTag if signer.inner is IActivityLauncher -> {
|
||||
signer.inner as IActivityLauncher
|
||||
}
|
||||
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (externalSignerLauncher != null) {
|
||||
val launcher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
@@ -173,7 +189,7 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
result.data?.let {
|
||||
accountViewModel.runOnIO {
|
||||
accountViewModel.account.signer.newResponse(it)
|
||||
externalSignerLauncher.newResponse(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +197,7 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
|
||||
)
|
||||
|
||||
DisposableEffect(accountViewModel, accountViewModel.account, launcher) {
|
||||
val launcher: (Intent) -> Unit = { intent ->
|
||||
val intentLauncher: (Intent) -> Unit = { intent ->
|
||||
try {
|
||||
launcher.launch(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
@@ -193,9 +209,9 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
accountViewModel.account.signer.registerForegroundLauncher(launcher)
|
||||
externalSignerLauncher.registerForegroundLauncher(intentLauncher)
|
||||
onDispose {
|
||||
accountViewModel.account.signer.unregisterForegroundLauncher(launcher)
|
||||
externalSignerLauncher.unregisterForegroundLauncher(intentLauncher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
|
||||
@@ -108,7 +108,8 @@ fun ChatroomView(
|
||||
|
||||
if (draftMessage != null) {
|
||||
LaunchedEffect(key1 = draftMessage) {
|
||||
newPostModel.updateMessage(TextFieldValue(draftMessage))
|
||||
newPostModel.message.setTextAndPlaceCursorAtEnd(draftMessage)
|
||||
newPostModel.onMessageChanged()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -203,7 +205,7 @@ class ChatNewMessageViewModel :
|
||||
|
||||
var uploadsWaitingToBeSent by mutableStateOf<List<SuccessfulUploads>>(emptyList())
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
@@ -215,8 +217,8 @@ class ChatNewMessageViewModel :
|
||||
|
||||
var emojiSuggestions: EmojiSuggestionState? = null
|
||||
|
||||
var toUsers by mutableStateOf(TextFieldValue(""))
|
||||
var subject by mutableStateOf(TextFieldValue(""))
|
||||
val toUsers = TextFieldState()
|
||||
val subject = TextFieldState()
|
||||
|
||||
// Invoices
|
||||
var canAddInvoice by mutableStateOf(false)
|
||||
@@ -273,10 +275,9 @@ class ChatNewMessageViewModel :
|
||||
|
||||
fun load(room: ChatroomKey) {
|
||||
this.room.tryEmit(room)
|
||||
this.toUsers =
|
||||
TextFieldValue(
|
||||
room.users.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
this.toUsers.setTextAndPlaceCursorAtEnd(
|
||||
room.users.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
}
|
||||
|
||||
fun reply(replyNote: Note) {
|
||||
@@ -290,8 +291,8 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
|
||||
fun quote(quote: Note) {
|
||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||
urlPreviews.update(message)
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
// creates a split with that author.
|
||||
val quotedAuthor = quote.author ?: return
|
||||
@@ -371,14 +372,13 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
|
||||
draftEvent.subject()?.let {
|
||||
subject = TextFieldValue()
|
||||
subject.setTextAndPlaceCursorAtEnd("")
|
||||
}
|
||||
|
||||
if (draftEvent is NIP17Group) {
|
||||
toUsers =
|
||||
TextFieldValue(
|
||||
draftEvent.groupMembers().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
toUsers.setTextAndPlaceCursorAtEnd(
|
||||
draftEvent.groupMembers().mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
|
||||
val replyId =
|
||||
when (draftEvent) {
|
||||
@@ -392,7 +392,7 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
} else if (draftEvent is PrivateDmEvent) {
|
||||
val recipientNPub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() }
|
||||
toUsers = TextFieldValue("@$recipientNPub")
|
||||
toUsers.setTextAndPlaceCursorAtEnd("@$recipientNPub")
|
||||
|
||||
val replyId = draftEvent.replyTo()
|
||||
if (replyId != null) {
|
||||
@@ -400,13 +400,14 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message =
|
||||
val draftText =
|
||||
if (draftEvent is PrivateDmEvent) {
|
||||
TextFieldValue(accountViewModel.account.privateDMDecryptionCache.cachedDM(draftEvent) ?: "")
|
||||
accountViewModel.account.privateDMDecryptionCache.cachedDM(draftEvent) ?: ""
|
||||
} else {
|
||||
TextFieldValue(draftEvent.content)
|
||||
draftEvent.content
|
||||
}
|
||||
urlPreviews.update(message)
|
||||
message.setTextAndPlaceCursorAtEnd(draftText)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
}
|
||||
@@ -421,7 +422,7 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
innerSendPost(draftTag.current)
|
||||
@@ -563,11 +564,12 @@ class ChatNewMessageViewModel :
|
||||
private suspend fun innerSendPost(draftTag: String?) {
|
||||
val room = room.value ?: return
|
||||
|
||||
val urls = findURLs(message.text)
|
||||
val messageText = message.text.toString()
|
||||
val urls = findURLs(messageText)
|
||||
val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet())
|
||||
val emojis = findEmoji(message.text, accountViewModel.account.emoji.myEmojis.value)
|
||||
val emojis = findEmoji(messageText, accountViewModel.account.emoji.myEmojis.value)
|
||||
val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null
|
||||
val message = message.text
|
||||
val message = messageText
|
||||
|
||||
val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null
|
||||
val localExpirationDate = if (wantsExpirationDate) expirationDate else null
|
||||
@@ -633,8 +635,8 @@ class ChatNewMessageViewModel :
|
||||
fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message = TextFieldValue("")
|
||||
subject = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
subject.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
replyTo.value = null
|
||||
|
||||
@@ -667,15 +669,15 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
|
||||
fun addToMessage(it: String) {
|
||||
updateMessage(TextFieldValue(message.text + " " + it))
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||
onMessageChanged()
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreviews.update(newMessage)
|
||||
override fun onMessageChanged() {
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
@@ -689,11 +691,9 @@ class ChatNewMessageViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateToUsers(newToUsersValue: TextFieldValue) {
|
||||
toUsers = newToUsersValue
|
||||
|
||||
if (newToUsersValue.selection.collapsed) {
|
||||
val lastWord = newToUsersValue.currentWord()
|
||||
fun onToUsersChanged() {
|
||||
if (toUsers.selection.collapsed) {
|
||||
val lastWord = toUsers.currentWord()
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
@@ -706,7 +706,7 @@ class ChatNewMessageViewModel :
|
||||
fun updateRoomFromUsersInput() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(300)
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text, null, null, accountViewModel)
|
||||
val toUsersTagger = NewMessageTagger(toUsers.text.toString(), null, null, accountViewModel)
|
||||
toUsersTagger.run()
|
||||
|
||||
val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex }
|
||||
@@ -720,8 +720,7 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSubject(it: TextFieldValue) {
|
||||
subject = it
|
||||
fun onSubjectChanged() {
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
@@ -738,14 +737,14 @@ class ChatNewMessageViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message.text.toString())
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
||||
val lastWord = toUsers.currentWord()
|
||||
toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||
userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||
updateRoomFromUsersInput()
|
||||
}
|
||||
|
||||
@@ -759,8 +758,8 @@ class ChatNewMessageViewModel :
|
||||
fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -776,8 +775,8 @@ class ChatNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -794,8 +793,8 @@ class ChatNewMessageViewModel :
|
||||
recipientsMissingDmRelays.value.isEmpty()
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -816,7 +815,7 @@ class ChatNewMessageViewModel :
|
||||
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddPhotoAlternate
|
||||
@@ -67,7 +68,6 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -80,7 +80,7 @@ import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
@@ -158,7 +158,8 @@ fun NewGroupDMScreen(
|
||||
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -530,8 +531,8 @@ fun SendDirectMessageTo(
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = postViewModel.toUsers,
|
||||
onValueChange = postViewModel::updateToUsers,
|
||||
state = postViewModel.toUsers,
|
||||
onTextChanged = postViewModel::onToUsersChanged,
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
@@ -547,8 +548,8 @@ fun SendDirectMessageTo(
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
visualTransformation =
|
||||
UrlUserTagTransformation(
|
||||
outputTransformation =
|
||||
UrlUserTagOutputTransformation(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
colors =
|
||||
@@ -572,8 +573,8 @@ fun SendDirectMessageTo(
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = postViewModel.subject,
|
||||
onValueChange = { postViewModel.updateSubject(it) },
|
||||
state = postViewModel.subject,
|
||||
onTextChanged = { postViewModel.onSubjectChanged() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
@@ -581,8 +582,8 @@ fun SendDirectMessageTo(
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
visualTransformation =
|
||||
UrlUserTagTransformation(
|
||||
outputTransformation =
|
||||
UrlUserTagOutputTransformation(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
colors =
|
||||
|
||||
@@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
@@ -201,8 +201,8 @@ fun EditField(
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
ThinPaddingTextField(
|
||||
value = channelScreenModel.message,
|
||||
onValueChange = { channelScreenModel.updateMessage(it) },
|
||||
state = channelScreenModel.message,
|
||||
onTextChanged = { channelScreenModel.onMessageChanged() },
|
||||
keyboardOptions = PostKeyboard,
|
||||
shape = EditFieldBorder,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -231,7 +231,7 @@ fun EditField(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -137,7 +139,7 @@ open class ChannelNewMessageViewModel :
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||
|
||||
var message by mutableStateOf(TextFieldValue(""))
|
||||
val message = TextFieldState()
|
||||
var urlPreview by mutableStateOf<String?>(null)
|
||||
val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false
|
||||
val isUploadingFile: Boolean get() = uploadState?.isUploadingFile ?: false
|
||||
@@ -274,7 +276,7 @@ open class ChannelNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
@@ -302,7 +304,7 @@ open class ChannelNewMessageViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val attachments = mutableSetOf<Event>()
|
||||
@@ -365,14 +367,14 @@ open class ChannelNewMessageViewModel :
|
||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor(it.toNostrUri())
|
||||
message.insertUrlAtCursor(it.toNostrUri())
|
||||
}
|
||||
|
||||
urlPreview = findUrlInMessage()
|
||||
} else if (upload.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
iMetaAttachments.add(upload.result, uploadState.caption, uploadState.contentWarningReason)
|
||||
|
||||
message = message.insertUrlAtCursor(upload.result.url)
|
||||
message.insertUrlAtCursor(upload.result.url)
|
||||
urlPreview = findUrlInMessage()
|
||||
}
|
||||
}
|
||||
@@ -392,18 +394,19 @@ open class ChannelNewMessageViewModel :
|
||||
|
||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||
val channel = channel ?: return null
|
||||
val messageText = message.text.toString()
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message = message.text,
|
||||
message = messageText,
|
||||
pTags = listOfNotNull(replyTo.value?.author),
|
||||
eTags = listOfNotNull(replyTo.value),
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
|
||||
val urls = findURLs(message.text)
|
||||
val urls = findURLs(messageText)
|
||||
val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet())
|
||||
val emojis = findEmoji(message.text, accountViewModel.account.emoji.myEmojis.value)
|
||||
val emojis = findEmoji(messageText, accountViewModel.account.emoji.myEmojis.value)
|
||||
|
||||
val channelRelays = channel.relays()
|
||||
val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null
|
||||
@@ -533,7 +536,7 @@ open class ChannelNewMessageViewModel :
|
||||
open fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
replyTo.value = null
|
||||
|
||||
@@ -561,18 +564,18 @@ open class ChannelNewMessageViewModel :
|
||||
emojiSuggestions?.reset()
|
||||
}
|
||||
|
||||
open fun findUrlInMessage(): String? = UrlParser().parseValidUrls(message.text).withScheme.firstOrNull()
|
||||
open fun findUrlInMessage(): String? = UrlParser().parseValidUrls(message.text.toString()).withScheme.firstOrNull()
|
||||
|
||||
open fun addToMessage(it: String) {
|
||||
updateMessage(TextFieldValue(message.text + " " + it))
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||
onMessageChanged()
|
||||
}
|
||||
|
||||
open fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
open fun onMessageChanged() {
|
||||
urlPreview = findUrlInMessage()
|
||||
|
||||
if (newMessage.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
@@ -600,7 +603,7 @@ open class ChannelNewMessageViewModel :
|
||||
userSuggestions?.let {
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = it.replaceCurrentWord(message, lastWord, item)
|
||||
it.replaceCurrentWord(message, lastWord, item)
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.addItem(item)
|
||||
forwardZapToEditting = TextFieldValue("")
|
||||
@@ -615,7 +618,7 @@ open class ChannelNewMessageViewModel :
|
||||
|
||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -631,7 +634,7 @@ open class ChannelNewMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -648,7 +651,7 @@ open class ChannelNewMessageViewModel :
|
||||
uploadState?.multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
@@ -675,7 +678,7 @@ open class ChannelNewMessageViewModel :
|
||||
|
||||
fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.items.any { it.key == taggedUser }) {
|
||||
|
||||
@@ -38,7 +38,7 @@ import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
@@ -114,8 +114,8 @@ fun EditFieldRow(
|
||||
}
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = channelScreenModel.message,
|
||||
onValueChange = { channelScreenModel.updateMessage(it) },
|
||||
state = channelScreenModel.message,
|
||||
onTextChanged = { channelScreenModel.onMessageChanged() },
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
@@ -151,7 +151,7 @@ fun EditFieldRow(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,12 +62,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||
@@ -293,7 +294,7 @@ private fun MarkdownPostScreenBody(
|
||||
.padding(Size10dp),
|
||||
) {
|
||||
RenderContentAsMarkdown(
|
||||
content = postViewModel.message.text,
|
||||
content = postViewModel.message.text.toString(),
|
||||
tags = EmptyTagList,
|
||||
canPreview = true,
|
||||
quotesLeft = 1,
|
||||
@@ -304,9 +305,9 @@ private fun MarkdownPostScreenBody(
|
||||
}
|
||||
} else {
|
||||
// Markdown editor
|
||||
OutlinedTextField(
|
||||
value = postViewModel.message,
|
||||
onValueChange = postViewModel::updateMessage,
|
||||
ThinPaddingTextField(
|
||||
state = postViewModel.message,
|
||||
onTextChanged = postViewModel::onMessageChanged,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -328,7 +329,7 @@ private fun MarkdownPostScreenBody(
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
),
|
||||
visualTransformation = UrlUserTagTransformation(MaterialTheme.colorScheme.primary),
|
||||
outputTransformation = UrlUserTagOutputTransformation(MaterialTheme.colorScheme.primary),
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -147,7 +149,7 @@ class LongFormPostViewModel :
|
||||
|
||||
var isUploadingCoverImage by mutableStateOf(false)
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
var showPreview by mutableStateOf(false)
|
||||
|
||||
@@ -241,7 +243,7 @@ class LongFormPostViewModel :
|
||||
summary = TextFieldValue(noteEvent.summary() ?: "")
|
||||
publishedAt = noteEvent.publishedAt() ?: noteEvent.createdAt
|
||||
coverImageUrl = noteEvent.image() ?: ""
|
||||
message = TextFieldValue(noteEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(noteEvent.content)
|
||||
existingDTag = noteEvent.dTag()
|
||||
}
|
||||
|
||||
@@ -265,7 +267,7 @@ class LongFormPostViewModel :
|
||||
summary = TextFieldValue(draftEvent.summary() ?: "")
|
||||
publishedAt = draftEvent.publishedAt() ?: draftEvent.createdAt
|
||||
coverImageUrl = draftEvent.image() ?: ""
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
existingDTag = draftEvent.dTag()
|
||||
|
||||
canAddInvoice = accountViewModel.userProfile().lnAddress() != null
|
||||
@@ -335,7 +337,7 @@ class LongFormPostViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank() && title.text.isBlank()) {
|
||||
if (message.text.toString().isBlank() && title.text.isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val template = createTemplate() ?: return
|
||||
@@ -349,7 +351,7 @@ class LongFormPostViewModel :
|
||||
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message.text,
|
||||
message.text.toString(),
|
||||
null,
|
||||
null,
|
||||
accountViewModel,
|
||||
@@ -539,7 +541,7 @@ class LongFormPostViewModel :
|
||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||
|
||||
val markdownImage = ""
|
||||
message = message.insertUrlAtCursor(markdownImage)
|
||||
message.insertUrlAtCursor(markdownImage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,7 +561,7 @@ class LongFormPostViewModel :
|
||||
title = TextFieldValue("")
|
||||
summary = TextFieldValue("")
|
||||
coverImageUrl = ""
|
||||
message = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
publishedAt = TimeUtils.now()
|
||||
showPreview = false
|
||||
|
||||
@@ -594,11 +596,9 @@ class LongFormPostViewModel :
|
||||
this.multiOrchestrator?.remove(selected)
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
|
||||
override fun onMessageChanged() {
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
@@ -626,7 +626,7 @@ class LongFormPostViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
@@ -641,7 +641,7 @@ class LongFormPostViewModel :
|
||||
|
||||
fun autocompleteWithEmoji(item: EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
emojiSuggestions?.reset()
|
||||
draftTag.newVersion()
|
||||
}
|
||||
@@ -655,21 +655,21 @@ class LongFormPostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
emojiSuggestions?.reset()
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun canPost(): Boolean =
|
||||
title.text.isNotBlank() &&
|
||||
message.text.isNotBlank() &&
|
||||
message.text.toString().isNotBlank() &&
|
||||
!isUploadingImage &&
|
||||
!wantsInvoice &&
|
||||
(!wantsZapRaiser || zapRaiserAmount.value != null) &&
|
||||
multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
@@ -699,7 +699,7 @@ class LongFormPostViewModel :
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger =
|
||||
NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -41,7 +42,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -112,7 +112,8 @@ fun NewProductScreen(
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -131,7 +133,7 @@ open class NewProductViewModel :
|
||||
var productImages by mutableStateOf<List<ProductImageMeta>>(emptyList())
|
||||
val iMetaDescription = IMetaAttachments()
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
@@ -151,10 +153,10 @@ open class NewProductViewModel :
|
||||
val strippingFailureConfirmation = SuspendableConfirmation()
|
||||
|
||||
// Classifieds
|
||||
var title by mutableStateOf(TextFieldValue(""))
|
||||
var price by mutableStateOf(TextFieldValue(""))
|
||||
var locationText by mutableStateOf(TextFieldValue(""))
|
||||
var category by mutableStateOf(TextFieldValue(""))
|
||||
val title = TextFieldState()
|
||||
val price = TextFieldState()
|
||||
val locationText = TextFieldState()
|
||||
val category = TextFieldState()
|
||||
var condition by mutableStateOf(ConditionTag.CONDITION.USED_LIKE_NEW)
|
||||
|
||||
// Invoices
|
||||
@@ -226,7 +228,7 @@ open class NewProductViewModel :
|
||||
open fun quote(quote: Note) {
|
||||
val accountViewModel = accountViewModel
|
||||
|
||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||
|
||||
quote.author?.let { quotedUser ->
|
||||
if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) {
|
||||
@@ -246,7 +248,7 @@ open class NewProductViewModel :
|
||||
wantsForwardZapTo = true
|
||||
}
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
@@ -284,10 +286,10 @@ open class NewProductViewModel :
|
||||
zapRaiserAmount.value = zapraiser
|
||||
}
|
||||
|
||||
title = TextFieldValue(draftEvent.title() ?: "")
|
||||
price = TextFieldValue(draftEvent.price()?.amount ?: "")
|
||||
category = TextFieldValue(draftEvent.categories().firstOrNull() ?: "")
|
||||
locationText = TextFieldValue(draftEvent.location() ?: "")
|
||||
title.setTextAndPlaceCursorAtEnd(draftEvent.title() ?: "")
|
||||
price.setTextAndPlaceCursorAtEnd(draftEvent.price()?.amount ?: "")
|
||||
category.setTextAndPlaceCursorAtEnd(draftEvent.categories().firstOrNull() ?: "")
|
||||
locationText.setTextAndPlaceCursorAtEnd(draftEvent.location() ?: "")
|
||||
condition = draftEvent.conditionValid() ?: ConditionTag.CONDITION.USED_LIKE_NEW
|
||||
|
||||
val imageSet = draftEvent.images().toMutableSet()
|
||||
@@ -305,9 +307,9 @@ open class NewProductViewModel :
|
||||
productImages = productImages + ProductImageMeta(it)
|
||||
}
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
@@ -323,7 +325,7 @@ open class NewProductViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val template = createTemplate() ?: return
|
||||
@@ -336,7 +338,7 @@ open class NewProductViewModel :
|
||||
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message = message.text,
|
||||
message = message.text.toString(),
|
||||
dao = accountViewModel,
|
||||
)
|
||||
tagger.run()
|
||||
@@ -356,15 +358,15 @@ open class NewProductViewModel :
|
||||
|
||||
val template =
|
||||
ClassifiedsEvent.build(
|
||||
title.text,
|
||||
PriceTag(price.text, "SATS", null),
|
||||
title.text.toString(),
|
||||
PriceTag(price.text.toString(), "SATS", null),
|
||||
tagger.message,
|
||||
locationText.text.ifBlank { null },
|
||||
locationText.text.toString().ifBlank { null },
|
||||
condition,
|
||||
) {
|
||||
productImages.forEach { image(it.url) }
|
||||
|
||||
hashtags(listOfNotNull(category.text.ifBlank { null }) + findHashtags(tagger.message))
|
||||
hashtags(listOfNotNull(category.text.toString().ifBlank { null }) + findHashtags(tagger.message))
|
||||
quotes(quotes)
|
||||
|
||||
geoHash?.let { geohash(it) }
|
||||
@@ -438,8 +440,8 @@ open class NewProductViewModel :
|
||||
} else {
|
||||
iMetaDescription.add(it.result, alt, contentWarningReason)
|
||||
|
||||
message = message.insertUrlAtCursor(it.result.url)
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor(it.result.url)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,7 +460,7 @@ open class NewProductViewModel :
|
||||
open fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
multiOrchestrator = null
|
||||
mediaUploadTracker.finishUpload()
|
||||
@@ -468,10 +470,10 @@ open class NewProductViewModel :
|
||||
zapRaiserAmount.value = null
|
||||
|
||||
condition = ConditionTag.CONDITION.USED_LIKE_NEW
|
||||
locationText = TextFieldValue("")
|
||||
title = TextFieldValue("")
|
||||
category = TextFieldValue("")
|
||||
price = TextFieldValue("")
|
||||
locationText.setTextAndPlaceCursorAtEnd("")
|
||||
title.setTextAndPlaceCursorAtEnd("")
|
||||
category.setTextAndPlaceCursorAtEnd("")
|
||||
price.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
wantsForwardZapTo = false
|
||||
wantsToMarkAsSensitive = false
|
||||
@@ -505,9 +507,8 @@ open class NewProductViewModel :
|
||||
this.multiOrchestrator?.remove(selected)
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreviews.update(message)
|
||||
override fun onMessageChanged() {
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = message.currentWord()
|
||||
@@ -538,8 +539,8 @@ open class NewProductViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message.text.toString())
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
@@ -555,8 +556,8 @@ open class NewProductViewModel :
|
||||
open fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -572,8 +573,8 @@ open class NewProductViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -591,7 +592,7 @@ open class NewProductViewModel :
|
||||
multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
@@ -620,7 +621,7 @@ open class NewProductViewModel :
|
||||
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
@@ -648,19 +649,11 @@ open class NewProductViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateTitle(it: TextFieldValue) {
|
||||
title = it
|
||||
fun onTitleChanged() {
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updatePrice(it: TextFieldValue) {
|
||||
runCatching {
|
||||
if (it.text.isEmpty()) {
|
||||
price = TextFieldValue("")
|
||||
} else if (it.text.toLongOrNull() != null) {
|
||||
price = it
|
||||
}
|
||||
}
|
||||
fun onPriceChanged() {
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
@@ -669,13 +662,12 @@ open class NewProductViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateCategory(value: TextFieldValue) {
|
||||
category = value
|
||||
fun updateCategory(text: String) {
|
||||
category.setTextAndPlaceCursorAtEnd(text)
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateLocation(it: TextFieldValue) {
|
||||
locationText = it
|
||||
fun onLocationChanged() {
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
|
||||
@@ -44,12 +44,12 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue // kept for readonly dropdown ThinPaddingTextField
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
@@ -109,10 +109,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = postViewModel.title,
|
||||
onValueChange = {
|
||||
postViewModel.updateTitle(it)
|
||||
},
|
||||
state = postViewModel.title,
|
||||
onTextChanged = { postViewModel.onTitleChanged() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
@@ -120,8 +118,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
visualTransformation =
|
||||
UrlUserTagTransformation(
|
||||
outputTransformation =
|
||||
UrlUserTagOutputTransformation(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
colors =
|
||||
@@ -146,10 +144,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
|
||||
ThinPaddingTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.price,
|
||||
onValueChange = {
|
||||
postViewModel.updatePrice(it)
|
||||
},
|
||||
state = postViewModel.price,
|
||||
onTextChanged = { postViewModel.onPriceChanged() },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "1000",
|
||||
@@ -275,10 +271,10 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
categoryTypes.map { TitleExplainer(it.second, null) }.toImmutableList()
|
||||
}
|
||||
TextSpinner(
|
||||
placeholder = categoryTypes.firstOrNull { it.second == postViewModel.category.text }?.second ?: "",
|
||||
placeholder = categoryTypes.firstOrNull { it.second == postViewModel.category.text.toString() }?.second ?: "",
|
||||
options = categoryOptions,
|
||||
onSelect = {
|
||||
postViewModel.updateCategory(TextFieldValue(categoryTypes[it].second))
|
||||
postViewModel.updateCategory(categoryTypes[it].second)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -313,10 +309,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = postViewModel.locationText,
|
||||
onValueChange = {
|
||||
postViewModel.updateLocation(it)
|
||||
},
|
||||
state = postViewModel.locationText,
|
||||
onTextChanged = { postViewModel.onLocationChanged() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
@@ -324,8 +318,8 @@ fun SellProduct(postViewModel: NewProductViewModel) {
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
visualTransformation =
|
||||
UrlUserTagTransformation(
|
||||
outputTransformation =
|
||||
UrlUserTagOutputTransformation(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
colors =
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash
|
||||
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
@@ -67,7 +67,8 @@ fun GeoHashPostScreen(
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag
|
||||
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
@@ -67,7 +67,8 @@ fun HashtagPostScreen(
|
||||
postViewModel.quote(it)
|
||||
}
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Poll
|
||||
@@ -54,7 +55,6 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.IntentCompat
|
||||
@@ -150,7 +150,8 @@ fun ShortNotePostScreen(
|
||||
val draft = draftId?.let { accountViewModel.getNoteIfExists(it) }
|
||||
postViewModel.load(baseReplyTo, quote, fork, version, draft)
|
||||
message?.ifBlank { null }?.let {
|
||||
postViewModel.updateMessage(TextFieldValue(it))
|
||||
postViewModel.message.setTextAndPlaceCursorAtEnd(it)
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
attachment?.ifBlank { null }?.toUri()?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -322,6 +323,13 @@ private fun NewPostScreenBody(
|
||||
MessageField(
|
||||
R.string.what_s_on_your_mind,
|
||||
postViewModel,
|
||||
onContentReceived = { uri, mimeType ->
|
||||
postViewModel.selectImage(
|
||||
persistentListOf(
|
||||
SelectedMedia(uri, mimeType),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -183,7 +185,7 @@ open class ShortNotePostViewModel :
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
@@ -367,7 +369,7 @@ open class ShortNotePostViewModel :
|
||||
multiOrchestrator = null
|
||||
|
||||
quote?.let { quotedNote ->
|
||||
message = TextFieldValue(message.text + "\nnostr:${quotedNote.toNEvent()}")
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quotedNote.toNEvent()}")
|
||||
|
||||
quotedNote.author?.let { quotedUser ->
|
||||
if (quotedUser.pubkeyHex != user.pubkeyHex) {
|
||||
@@ -385,7 +387,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
fork?.let { forkedNoted ->
|
||||
message = TextFieldValue(version?.event?.content ?: forkedNoted.event?.content ?: "")
|
||||
message.setTextAndPlaceCursorAtEnd(version?.event?.content ?: forkedNoted.event?.content ?: "")
|
||||
|
||||
forkedNoted.event?.isSensitiveOrNSFW()?.let {
|
||||
if (it) wantsToMarkAsSensitive = true
|
||||
@@ -436,7 +438,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
@@ -533,11 +535,11 @@ open class ShortNotePostViewModel :
|
||||
wantsPoll = false
|
||||
wantsZapPoll = false
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draftEvent: PollEvent) {
|
||||
@@ -605,11 +607,11 @@ open class ShortNotePostViewModel :
|
||||
pollType = draftEvent.pollType()
|
||||
closedAt = draftEvent.endsAt() ?: TimeUtils.oneDayAhead()
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draftEvent: ZapPollEvent) {
|
||||
@@ -679,11 +681,11 @@ open class ShortNotePostViewModel :
|
||||
zapPollConsensusThreshold = draftEvent.consensusThreshold()?.let { (it * 100).toInt() }
|
||||
zapPollClosedAt = draftEvent.closedAt() ?: TimeUtils.oneDayAhead()
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
|
||||
urlPreviews.update(message)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
@@ -750,7 +752,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val attachments = mutableSetOf<Event>()
|
||||
@@ -811,7 +813,7 @@ open class ShortNotePostViewModel :
|
||||
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
message.text,
|
||||
message.text.toString(),
|
||||
pTags,
|
||||
eTags,
|
||||
accountViewModel,
|
||||
@@ -1000,8 +1002,8 @@ open class ShortNotePostViewModel :
|
||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
val iMeta =
|
||||
@@ -1024,8 +1026,8 @@ open class ShortNotePostViewModel :
|
||||
|
||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||
|
||||
message = message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1042,7 +1044,7 @@ open class ShortNotePostViewModel :
|
||||
open fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
message = TextFieldValue("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
|
||||
forkedFromNote = null
|
||||
|
||||
@@ -1104,15 +1106,15 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
open fun addToMessage(it: String) {
|
||||
updateMessage(TextFieldValue(message.text + " " + it))
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||
onMessageChanged()
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreviews.update(message)
|
||||
override fun onMessageChanged() {
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
@@ -1140,8 +1142,8 @@ open class ShortNotePostViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message.text.toString())
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
@@ -1157,8 +1159,8 @@ open class ShortNotePostViewModel :
|
||||
open fun autocompleteWithEmoji(item: EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -1174,8 +1176,8 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -1201,7 +1203,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
// Regular text/media posts require text
|
||||
return message.text.isNotBlank() &&
|
||||
return message.text.toString().isNotBlank() &&
|
||||
!mediaUploadTracker.isUploading &&
|
||||
!isUploadingVoice &&
|
||||
!wantsInvoice &&
|
||||
@@ -1226,7 +1228,7 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
@@ -1378,7 +1380,7 @@ open class ShortNotePostViewModel :
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger =
|
||||
NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
@@ -56,7 +56,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
|
||||
@@ -415,8 +415,8 @@ fun SendDirectMessageTo(
|
||||
)
|
||||
|
||||
ThinPaddingTextField(
|
||||
value = postViewModel.toUsers,
|
||||
onValueChange = postViewModel::updateToUsers,
|
||||
state = postViewModel.toUsers,
|
||||
onTextChanged = postViewModel::onToUsersChanged,
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
@@ -432,8 +432,8 @@ fun SendDirectMessageTo(
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
visualTransformation =
|
||||
UrlUserTagTransformation(
|
||||
outputTransformation =
|
||||
UrlUserTagOutputTransformation(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
colors =
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
@@ -147,7 +149,7 @@ class NewPublicMessageViewModel :
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(emptyList())
|
||||
|
||||
override var message by mutableStateOf(TextFieldValue(""))
|
||||
override val message = TextFieldState()
|
||||
|
||||
val urlPreviews = PreviewState()
|
||||
|
||||
@@ -160,7 +162,7 @@ class NewPublicMessageViewModel :
|
||||
|
||||
var emojiSuggestions: EmojiSuggestionState? = null
|
||||
|
||||
var toUsers by mutableStateOf(TextFieldValue(""))
|
||||
val toUsers = TextFieldState()
|
||||
|
||||
// Images and Videos
|
||||
var multiOrchestrator by mutableStateOf<MultiOrchestrator?>(null)
|
||||
@@ -218,10 +220,9 @@ class NewPublicMessageViewModel :
|
||||
fun load(users: Set<HexKey>) {
|
||||
val userSet = users - account.userProfile().pubkeyHex
|
||||
|
||||
toUsers =
|
||||
TextFieldValue(
|
||||
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
toUsers.setTextAndPlaceCursorAtEnd(
|
||||
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
}
|
||||
|
||||
fun reply(post: Note) {
|
||||
@@ -229,8 +230,8 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
|
||||
fun quote(quote: Note) {
|
||||
message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}")
|
||||
urlPreviews.update(message)
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + "\nnostr:${quote.toNEvent()}")
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
// creates a split with that author.
|
||||
val quotedAuthor = quote.author ?: return
|
||||
@@ -306,13 +307,12 @@ class NewPublicMessageViewModel :
|
||||
|
||||
val userSet = draftEvent.groupKeys() - account.userProfile().pubkeyHex
|
||||
|
||||
toUsers =
|
||||
TextFieldValue(
|
||||
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
toUsers.setTextAndPlaceCursorAtEnd(
|
||||
userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" },
|
||||
)
|
||||
|
||||
message = TextFieldValue(draftEvent.content)
|
||||
urlPreviews.update(message)
|
||||
message.setTextAndPlaceCursorAtEnd(draftEvent.content)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
iMetaAttachments.addAll(draftEvent.imetas())
|
||||
}
|
||||
@@ -349,7 +349,7 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.isBlank()) {
|
||||
if (message.text.toString().isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current)
|
||||
} else {
|
||||
val broadcast = mutableSetOf<Event>()
|
||||
@@ -364,13 +364,14 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
|
||||
private suspend fun createTemplate(): EventTemplate<PublicMessageEvent> {
|
||||
val toUsersTagger = NewMessageTagger(this@NewPublicMessageViewModel.toUsers.text, null, null, accountViewModel)
|
||||
val toUsersTagger = NewMessageTagger(this@NewPublicMessageViewModel.toUsers.text.toString(), null, null, accountViewModel)
|
||||
toUsersTagger.run()
|
||||
|
||||
val messageText = message.text.toString()
|
||||
val msg =
|
||||
replyingTo?.let {
|
||||
it.toNostrUri() + "\n" + message.text
|
||||
} ?: message.text
|
||||
it.toNostrUri() + "\n" + messageText
|
||||
} ?: messageText
|
||||
|
||||
val tagger = NewMessageTagger(msg, null, null, accountViewModel)
|
||||
tagger.run()
|
||||
@@ -470,8 +471,8 @@ class NewPublicMessageViewModel :
|
||||
val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) }
|
||||
|
||||
note?.let {
|
||||
message = message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor("nostr:" + it.toNEvent())
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
} else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) {
|
||||
val iMeta =
|
||||
@@ -494,8 +495,8 @@ class NewPublicMessageViewModel :
|
||||
|
||||
iMetaAttachments.replace(iMeta.url, iMeta)
|
||||
|
||||
message = message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message)
|
||||
message.insertUrlAtCursor(state.result.url)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,8 +513,8 @@ class NewPublicMessageViewModel :
|
||||
fun cancel() {
|
||||
draftTag.rotate()
|
||||
|
||||
toUsers = TextFieldValue("")
|
||||
message = TextFieldValue("")
|
||||
toUsers.setTextAndPlaceCursorAtEnd("")
|
||||
message.setTextAndPlaceCursorAtEnd("")
|
||||
multiOrchestrator = null
|
||||
|
||||
wantsInvoice = false
|
||||
@@ -546,15 +547,15 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
|
||||
fun addToMessage(it: String) {
|
||||
updateMessage(TextFieldValue(message.text + " " + it))
|
||||
message.setTextAndPlaceCursorAtEnd(message.text.toString() + " " + it)
|
||||
onMessageChanged()
|
||||
}
|
||||
|
||||
override fun updateMessage(newMessage: TextFieldValue) {
|
||||
message = newMessage
|
||||
urlPreviews.update(newMessage)
|
||||
override fun onMessageChanged() {
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
if (message.selection.collapsed) {
|
||||
val lastWord = newMessage.currentWord()
|
||||
val lastWord = message.currentWord()
|
||||
if (lastWord.startsWith("@")) {
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
@@ -569,11 +570,9 @@ class NewPublicMessageViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
fun updateToUsers(newToUsersValue: TextFieldValue) {
|
||||
toUsers = newToUsersValue
|
||||
|
||||
if (newToUsersValue.selection.collapsed) {
|
||||
val lastWord = newToUsersValue.currentWord()
|
||||
fun onToUsersChanged() {
|
||||
if (toUsers.selection.collapsed) {
|
||||
val lastWord = toUsers.currentWord()
|
||||
userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS
|
||||
userSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
@@ -594,14 +593,14 @@ class NewPublicMessageViewModel :
|
||||
userSuggestions?.let { userSuggestions ->
|
||||
if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) {
|
||||
val lastWord = message.currentWord()
|
||||
message = userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message)
|
||||
userSuggestions.replaceCurrentWord(message, lastWord, item)
|
||||
urlPreviews.update(message.text.toString())
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
|
||||
forwardZapTo.value.addItem(item)
|
||||
forwardZapToEditting.value = TextFieldValue("")
|
||||
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
|
||||
val lastWord = toUsers.currentWord()
|
||||
toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||
userSuggestions.replaceCurrentWord(toUsers, lastWord, item)
|
||||
}
|
||||
|
||||
userSuggestionsMainMessage = null
|
||||
@@ -614,8 +613,8 @@ class NewPublicMessageViewModel :
|
||||
fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) {
|
||||
val wordToInsert = ":${item.code}:"
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -631,8 +630,8 @@ class NewPublicMessageViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
message = message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message)
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
urlPreviews.update(message.text.toString())
|
||||
|
||||
emojiSuggestions?.reset()
|
||||
|
||||
@@ -648,7 +647,7 @@ class NewPublicMessageViewModel :
|
||||
multiOrchestrator == null
|
||||
|
||||
fun insertAtCursor(newElement: String) {
|
||||
message = message.insertUrlAtCursor(newElement)
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
fun selectImage(uris: ImmutableList<SelectedMedia>) {
|
||||
@@ -671,7 +670,7 @@ class NewPublicMessageViewModel :
|
||||
|
||||
override fun updateZapFromText() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel)
|
||||
val tagger = NewMessageTagger(message.text.toString(), emptyList(), emptyList(), accountViewModel)
|
||||
tagger.run()
|
||||
tagger.pTags?.forEach { taggedUser ->
|
||||
if (!forwardZapTo.value.items.any { it.key == taggedUser }) {
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -29,10 +31,13 @@ import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Block
|
||||
@@ -67,28 +72,39 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.nip86RelayManagement.Nip86Retriever
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.AboutDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ClearTextIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.AnimateOnNewSearch
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kindDisplayName
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.nip05
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -96,6 +112,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Method
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun RelayManagementScreen(
|
||||
@@ -398,26 +416,26 @@ private fun PubkeysTab(
|
||||
}
|
||||
|
||||
if (showBanDialog) {
|
||||
HexInputDialog(
|
||||
UserSearchDialog(
|
||||
title = stringResource(R.string.relay_management_ban_pubkey),
|
||||
label = stringResource(R.string.relay_management_pubkey_hex),
|
||||
onConfirm = { hex, reason ->
|
||||
viewModel.banPubkey(hex, reason.ifBlank { null })
|
||||
onConfirm = { user, reason ->
|
||||
viewModel.banPubkey(user.pubkeyHex, reason.ifBlank { null })
|
||||
showBanDialog = false
|
||||
},
|
||||
onDismiss = { showBanDialog = false },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
if (showAllowDialog) {
|
||||
HexInputDialog(
|
||||
UserSearchDialog(
|
||||
title = stringResource(R.string.relay_management_allow_pubkey),
|
||||
label = stringResource(R.string.relay_management_pubkey_hex),
|
||||
onConfirm = { hex, reason ->
|
||||
viewModel.allowPubkey(hex, reason.ifBlank { null })
|
||||
onConfirm = { user, reason ->
|
||||
viewModel.allowPubkey(user.pubkeyHex, reason.ifBlank { null })
|
||||
showAllowDialog = false
|
||||
},
|
||||
onDismiss = { showAllowDialog = false },
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -973,6 +991,187 @@ private fun SettingsField(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserSearchDialog(
|
||||
title: String,
|
||||
onConfirm: (User, String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val userSuggestions =
|
||||
remember {
|
||||
UserSuggestionState(accountViewModel.account, accountViewModel.nip05ClientBuilder())
|
||||
}
|
||||
|
||||
var selectedUser by remember { mutableStateOf<User?>(null) }
|
||||
var reasonValue by remember { mutableStateOf("") }
|
||||
|
||||
UserSearchDataSourceSubscription(userSuggestions, accountViewModel)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect {
|
||||
userSuggestions.invalidateData()
|
||||
}
|
||||
}
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.live.deletedEventBundles.collect {
|
||||
userSuggestions.invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(title) },
|
||||
text = {
|
||||
Column {
|
||||
val selected = selectedUser
|
||||
if (selected != null) {
|
||||
SelectedUserRow(selected, accountViewModel) {
|
||||
selectedUser = null
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = reasonValue,
|
||||
onValueChange = { reasonValue = it },
|
||||
label = { Text(stringResource(R.string.relay_management_reason_optional)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
} else {
|
||||
var userName by remember { mutableStateOf(TextFieldValue("")) }
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.search_and_add_a_user)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = userName,
|
||||
onValueChange = {
|
||||
userName = it
|
||||
userSuggestions.processCurrentWord(it.text)
|
||||
},
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (userName.text.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
userName = TextFieldValue("")
|
||||
userSuggestions.processCurrentWord("")
|
||||
},
|
||||
) {
|
||||
ClearTextIcon()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
ShowUserSuggestions(
|
||||
userSuggestions = userSuggestions,
|
||||
onSelect = { user ->
|
||||
selectedUser = user
|
||||
userName = TextFieldValue("")
|
||||
userSuggestions.processCurrentWord("")
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { selectedUser?.let { onConfirm(it, reasonValue.trim()) } },
|
||||
enabled = selectedUser != null,
|
||||
) {
|
||||
Text(stringResource(R.string.relay_management_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.relay_management_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectedUserRow(
|
||||
user: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
onClear: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(SmallBorder)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ClickableUserPicture(user, 40.dp, accountViewModel = accountViewModel, onClick = null)
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f),
|
||||
) {
|
||||
UsernameDisplay(user, accountViewModel = accountViewModel)
|
||||
}
|
||||
IconButton(onClick = onClear) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.relay_management_remove),
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowUserSuggestions(
|
||||
userSuggestions: UserSuggestionState,
|
||||
onSelect: (User) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
AnimateOnNewSearch(userSuggestions, listState)
|
||||
|
||||
val suggestions by userSuggestions.results.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (suggestions.isNotEmpty()) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(top = 10.dp),
|
||||
modifier = Modifier.heightIn(0.dp, 200.dp),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, baseUser ->
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { onSelect(baseUser) })
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ClickableUserPicture(baseUser, 40.dp, accountViewModel, Modifier, null)
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f),
|
||||
) {
|
||||
UsernameDisplay(baseUser, accountViewModel = accountViewModel)
|
||||
AboutDisplay(baseUser, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HexInputDialog(
|
||||
title: String,
|
||||
|
||||
@@ -1267,6 +1267,10 @@
|
||||
<string name="git_repository">Repozytorium Git: %1$s</string>
|
||||
<string name="git_web_address">Strona internetowa:</string>
|
||||
<string name="git_clone_address">Klonuj:</string>
|
||||
<string name="nsite_title">Statyczna Witryna: %1$s</string>
|
||||
<string name="nsite_root_site">Strona główna</string>
|
||||
<string name="nsite_source">Źródło:</string>
|
||||
<string name="nsite_servers">Serwery:</string>
|
||||
<string name="existed_since">OTS: %1$s</string>
|
||||
<string name="ots_info_title">Potwierdzenie znacznika czasu</string>
|
||||
<string name="ots_info_description">Istnieje dowód na to, że ten post został podpisany przed %1$s. Dowód został opatrzony pieczęcią w łańcuchu bloków Bitcoin w tym dniu i czasie.</string>
|
||||
|
||||
@@ -226,7 +226,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="copy_public_key_npub_to_the_clipboard">Kopiraj javni ključ (NPub) v odložišče</string>
|
||||
<string name="send_a_direct_message">Pošlji direktno sporočilo</string>
|
||||
<string name="edits_the_user_s_metadata">Uredi uporabnikove metapodatke</string>
|
||||
<string name="follow">Že sledim</string>
|
||||
<string name="follow">Sledi</string>
|
||||
<string name="follow_back">Sledi nazaj</string>
|
||||
<string name="unblock">Deblokiraj</string>
|
||||
<string name="copy_user_id">Kopiraj uporabnikov ID</string>
|
||||
@@ -1282,6 +1282,10 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem</string>
|
||||
<string name="git_repository">Git skladišče: %1$s</string>
|
||||
<string name="git_web_address">Splet:</string>
|
||||
<string name="git_clone_address">Klon:</string>
|
||||
<string name="nsite_title">Statična spletna stran: %1$s</string>
|
||||
<string name="nsite_root_site">Izhodiščno spletišče</string>
|
||||
<string name="nsite_source">Vir:</string>
|
||||
<string name="nsite_servers">Strežniki:</string>
|
||||
<string name="existed_since">OTS: %1$s</string>
|
||||
<string name="ots_info_title">Dokaz časovnega žiga</string>
|
||||
<string name="ots_info_description">Obstaja dokaz, da je bil ta zapisek podpisan pred %1$s. Dokaz je bil ožigosan v Bitcoin verigi blokov na ta datum in čas.</string>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -22,6 +22,14 @@ package com.vitorpamplona.amethyst.commons.utils
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Compares two [ImmutableList] instances by element identity (`===`), not value equality (`==`).
|
||||
*
|
||||
* Returns `true` only when both lists have the same size and each element at the same index
|
||||
* is the exact same object reference.
|
||||
*
|
||||
* Useful for cheap change detection in UI/state pipelines where object identity is meaningful.
|
||||
*/
|
||||
fun <T> equalImmutableLists(
|
||||
list1: ImmutableList<T>,
|
||||
list2: ImmutableList<T>,
|
||||
|
||||
@@ -315,7 +315,7 @@ mavenPublishing {
|
||||
coordinates(
|
||||
groupId = "com.vitorpamplona.quartz",
|
||||
artifactId = "quartz",
|
||||
version = "1.07.1",
|
||||
version = "1.07.2",
|
||||
)
|
||||
|
||||
// Configure publishing to Maven Central
|
||||
|
||||
@@ -52,12 +52,7 @@ data class IntentResult(
|
||||
result = data.getStringExtra("result"),
|
||||
event = data.getStringExtra("event"),
|
||||
`package` = data.getStringExtra("package"),
|
||||
rejected =
|
||||
if (data.extras?.containsKey("rejected") == true) {
|
||||
data.getBooleanExtra("rejected", false)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
rejected = data.extras?.containsKey("rejected"),
|
||||
)
|
||||
|
||||
fun fromJson(json: String): IntentResult = JsonMapperNip55.fromJsonTo<IntentResult>(json)
|
||||
|
||||
Reference in New Issue
Block a user