mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat: add compose signature pre-filled in text-based post screens
Adds a Signature field to Compose Settings (global UI settings, DataStore persisted). When opening any text-based composer — new note, reply, quote, poll, NIP-22 comment (reply/hashtag/geohash/url), or a new long-form article — the signature is appended to the message with a blank line, keeping the cursor at the start so the user types above it. Drafts, forks, and version edits are skipped since their content already carries (or deliberately omits) a signature, and an untouched signature-only message is treated as blank so closing the composer never auto-saves a junk draft. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vq4JQPB9m62nJ8Vdp7xVLN
This commit is contained in:
@@ -57,6 +57,7 @@ data class UiSettings(
|
||||
val accentColor: AccentColorType = AccentColorType.PURPLE,
|
||||
val fontFamily: FontFamilyType = FontFamilyType.SYSTEM,
|
||||
val fontSize: FontSizeType = FontSizeType.NORMAL,
|
||||
val composeSignature: String = "",
|
||||
)
|
||||
|
||||
enum class ThemeType(
|
||||
|
||||
@@ -57,6 +57,7 @@ class UiSettingsFlow(
|
||||
val accentColor: MutableStateFlow<AccentColorType> = MutableStateFlow(AccentColorType.PURPLE),
|
||||
val fontFamily: MutableStateFlow<FontFamilyType> = MutableStateFlow(FontFamilyType.SYSTEM),
|
||||
val fontSize: MutableStateFlow<FontSizeType> = MutableStateFlow(FontSizeType.NORMAL),
|
||||
val composeSignature: MutableStateFlow<String> = MutableStateFlow(""),
|
||||
) {
|
||||
val listOfFlows: List<Flow<Any?>> =
|
||||
listOf<Flow<Any?>>(
|
||||
@@ -88,6 +89,7 @@ class UiSettingsFlow(
|
||||
accentColor,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
composeSignature,
|
||||
)
|
||||
|
||||
// emits at every change in any of the propertyes.
|
||||
@@ -123,6 +125,7 @@ class UiSettingsFlow(
|
||||
flows[25] as AccentColorType,
|
||||
flows[26] as FontFamilyType,
|
||||
flows[27] as FontSizeType,
|
||||
flows[28] as String,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,6 +159,7 @@ class UiSettingsFlow(
|
||||
accentColor.value,
|
||||
fontFamily.value,
|
||||
fontSize.value,
|
||||
composeSignature.value,
|
||||
)
|
||||
|
||||
fun update(torSettings: UiSettings): Boolean {
|
||||
@@ -273,6 +277,10 @@ class UiSettingsFlow(
|
||||
fontSize.tryEmit(torSettings.fontSize)
|
||||
any = true
|
||||
}
|
||||
if (composeSignature.value != torSettings.composeSignature) {
|
||||
composeSignature.tryEmit(torSettings.composeSignature)
|
||||
any = true
|
||||
}
|
||||
|
||||
return any
|
||||
}
|
||||
@@ -326,6 +334,7 @@ class UiSettingsFlow(
|
||||
MutableStateFlow(uiSettings.accentColor),
|
||||
MutableStateFlow(uiSettings.fontFamily),
|
||||
MutableStateFlow(uiSettings.fontSize),
|
||||
MutableStateFlow(uiSettings.composeSignature),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -126,6 +126,7 @@ class UiSharedPreferences(
|
||||
val UI_ACCENT_COLOR = stringPreferencesKey("ui.accent_color")
|
||||
val UI_FONT_FAMILY = stringPreferencesKey("ui.font_family")
|
||||
val UI_FONT_SIZE = stringPreferencesKey("ui.font_size")
|
||||
val UI_COMPOSE_SIGNATURE = stringPreferencesKey("ui.compose_signature")
|
||||
|
||||
suspend fun uiPreferences(context: Context): UiSettings? =
|
||||
try {
|
||||
@@ -166,6 +167,7 @@ class UiSharedPreferences(
|
||||
accentColor = preferences[UI_ACCENT_COLOR]?.let { AccentColorType.valueOf(it) } ?: AccentColorType.PURPLE,
|
||||
fontFamily = preferences[UI_FONT_FAMILY]?.let { FontFamilyType.valueOf(it) } ?: FontFamilyType.SYSTEM,
|
||||
fontSize = preferences[UI_FONT_SIZE]?.let { FontSizeType.valueOf(it) } ?: FontSizeType.NORMAL,
|
||||
composeSignature = preferences[UI_COMPOSE_SIGNATURE] ?: "",
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
@@ -218,6 +220,7 @@ class UiSharedPreferences(
|
||||
preferences[UI_ACCENT_COLOR] = sharedSettings.accentColor.name
|
||||
preferences[UI_FONT_FAMILY] = sharedSettings.fontFamily.name
|
||||
preferences[UI_FONT_SIZE] = sharedSettings.fontSize.name
|
||||
preferences[UI_COMPOSE_SIGNATURE] = sharedSettings.composeSignature
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
+21
-1
@@ -34,6 +34,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.appendSignature
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.currentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
@@ -160,6 +161,10 @@ open class CommentPostViewModel :
|
||||
var externalIdentity by mutableStateOf<ExternalId?>(null)
|
||||
var replyingTo: Note? by mutableStateOf(null)
|
||||
|
||||
// The signature pre-filled by applySignature(), so an untouched signature-only
|
||||
// message is treated as blank instead of auto-saved as a junk draft.
|
||||
private var appliedSignature: String? = null
|
||||
|
||||
val iMetaAttachments = IMetaAttachments()
|
||||
var nip95attachments by mutableStateOf<List<Pair<FileStorageEvent, FileStorageHeaderEvent>>>(
|
||||
emptyList(),
|
||||
@@ -407,6 +412,20 @@ open class CommentPostViewModel :
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fills the signature from Compose Settings at the end of the message. Skipped by
|
||||
* callers when loading a draft (the draft content already carries — or deliberately
|
||||
* omits — a signature).
|
||||
*/
|
||||
fun applySignature() {
|
||||
val signature = accountViewModel.settings.composeSignature()
|
||||
if (signature.isEmpty()) return
|
||||
|
||||
appliedSignature = signature
|
||||
message.appendSignature(signature)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
val draftEvent = draft.event ?: return
|
||||
if (draftEvent !is CommentEvent) return
|
||||
@@ -513,7 +532,8 @@ open class CommentPostViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.toString().isBlank()) {
|
||||
val text = message.text.toString()
|
||||
if (text.isBlank() || text.trim() == appliedSignature) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftNote)
|
||||
} else if (accountViewModel.settings.automaticallyCreateDrafts()) {
|
||||
val attachments = mutableSetOf<Event>()
|
||||
|
||||
+3
@@ -136,6 +136,9 @@ fun ReplyCommentPostScreen(
|
||||
postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType)))
|
||||
}
|
||||
}
|
||||
if (draftId == null) {
|
||||
postViewModel.applySignature()
|
||||
}
|
||||
}
|
||||
|
||||
GenericCommentPostScreen(postViewModel, accountViewModel, nav)
|
||||
|
||||
@@ -135,6 +135,8 @@ class UiSettingsState(
|
||||
|
||||
fun automaticallyCreateDrafts() = uiSettingsFlow.automaticallyCreateDrafts.value == BooleanType.ALWAYS
|
||||
|
||||
fun composeSignature() = uiSettingsFlow.composeSignature.value.trim()
|
||||
|
||||
fun isImmersiveScrollingActive() = uiSettingsFlow.automaticallyHideNavigationBars.value == BooleanType.ALWAYS
|
||||
|
||||
fun showProfilePictures() = showProfilePictures.value
|
||||
|
||||
+3
@@ -140,6 +140,9 @@ fun LongFormPostScreen(
|
||||
val draft = draftId?.let { accountViewModel.getNoteIfExists(it) }
|
||||
val version = versionId?.let { accountViewModel.getNoteIfExists(it) }
|
||||
postViewModel.load(draft, version)
|
||||
if (draftId == null && versionId == null) {
|
||||
postViewModel.applySignature()
|
||||
}
|
||||
}
|
||||
|
||||
WatchAndLoadMyEmojiList(accountViewModel)
|
||||
|
||||
+20
-1
@@ -35,6 +35,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.appendSignature
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.currentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
@@ -151,6 +152,10 @@ class LongFormPostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
// The signature pre-filled by applySignature(), so an untouched signature-only
|
||||
// body is treated as blank instead of auto-saved as a junk draft.
|
||||
private var appliedSignature: String? = null
|
||||
|
||||
var title by mutableStateOf(TextFieldValue(""))
|
||||
var summary by mutableStateOf(TextFieldValue(""))
|
||||
var coverImageUrl by mutableStateOf("")
|
||||
@@ -271,6 +276,19 @@ class LongFormPostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fills the signature from Compose Settings at the end of the article body. Skipped
|
||||
* by callers when loading a draft or editing an existing version (the content already
|
||||
* carries — or deliberately omits — a signature).
|
||||
*/
|
||||
fun applySignature() {
|
||||
val signature = accountViewModel.settings.composeSignature()
|
||||
if (signature.isEmpty()) return
|
||||
|
||||
appliedSignature = signature
|
||||
message.appendSignature(signature)
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
val draftEvent = draft.event ?: return
|
||||
if (draftEvent is LongTextNoteEvent) {
|
||||
@@ -355,7 +373,8 @@ class LongFormPostViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.toString().isBlank() && title.text.isBlank()) {
|
||||
val text = message.text.toString()
|
||||
if ((text.isBlank() || text.trim() == appliedSignature) && title.text.isBlank()) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftNote)
|
||||
} else if (accountViewModel.settings.automaticallyCreateDrafts()) {
|
||||
val template = createTemplate() ?: return
|
||||
|
||||
+3
@@ -76,6 +76,9 @@ fun GeoHashPostScreen(
|
||||
postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType)))
|
||||
}
|
||||
}
|
||||
if (draftId == null) {
|
||||
postViewModel.applySignature()
|
||||
}
|
||||
}
|
||||
|
||||
GenericCommentPostScreen(postViewModel, accountViewModel, nav)
|
||||
|
||||
+3
@@ -76,6 +76,9 @@ fun HashtagPostScreen(
|
||||
postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType)))
|
||||
}
|
||||
}
|
||||
if (draftId == null) {
|
||||
postViewModel.applySignature()
|
||||
}
|
||||
}
|
||||
|
||||
GenericCommentPostScreen(postViewModel, accountViewModel, nav)
|
||||
|
||||
+3
@@ -180,6 +180,9 @@ fun ShortNotePostScreen(
|
||||
postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType)))
|
||||
}
|
||||
}
|
||||
if (draftId == null && forkId == null) {
|
||||
postViewModel.applySignature()
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(nav, activity) {
|
||||
|
||||
+21
-1
@@ -36,6 +36,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.appendSignature
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.currentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
@@ -209,6 +210,10 @@ open class ShortNotePostViewModel :
|
||||
var originalNote: Note? by mutableStateOf(null)
|
||||
var forkedFromNote: Note? by mutableStateOf(null)
|
||||
|
||||
// The signature pre-filled by applySignature(), so an untouched signature-only
|
||||
// message is treated as blank instead of auto-saved as a junk draft.
|
||||
private var appliedSignature: String? = null
|
||||
|
||||
var pTags by mutableStateOf<List<User>?>(null)
|
||||
var eTags by mutableStateOf<List<Note>?>(null)
|
||||
|
||||
@@ -609,6 +614,20 @@ open class ShortNotePostViewModel :
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fills the signature from Compose Settings at the end of the message. Skipped by
|
||||
* callers when loading a draft or forking (the existing content already carries — or
|
||||
* deliberately omits — a signature).
|
||||
*/
|
||||
fun applySignature() {
|
||||
val signature = accountViewModel.settings.composeSignature()
|
||||
if (signature.isEmpty()) return
|
||||
|
||||
appliedSignature = signature
|
||||
message.appendSignature(signature)
|
||||
urlPreviews.update(message.text.toString())
|
||||
}
|
||||
|
||||
private fun loadFromDraft(draft: Note) {
|
||||
val draftEvent = draft.event ?: return
|
||||
if (draftEvent is TextNoteEvent) {
|
||||
@@ -974,7 +993,8 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
|
||||
suspend fun sendDraftSync() {
|
||||
if (message.text.toString().isBlank()) {
|
||||
val text = message.text.toString()
|
||||
if (text.isBlank() || text.trim() == appliedSignature) {
|
||||
accountViewModel.account.deleteDraftIgnoreErrors(draftNote)
|
||||
} else if (accountViewModel.settings.automaticallyCreateDrafts()) {
|
||||
val attachments = mutableSetOf<Event>()
|
||||
|
||||
+3
@@ -51,6 +51,9 @@ fun PollPostScreen(
|
||||
postViewModel.onMessageChanged()
|
||||
}
|
||||
postViewModel.wantsPoll = true
|
||||
if (draftId == null) {
|
||||
postViewModel.applySignature()
|
||||
}
|
||||
}
|
||||
|
||||
NewPostScreenInner(postViewModel, accountViewModel, nav)
|
||||
|
||||
+23
@@ -23,11 +23,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -109,10 +112,30 @@ fun ComposeSettingsContent(
|
||||
)
|
||||
SettingsDivider()
|
||||
AddClientTagTile(accountViewModel)
|
||||
SettingsDivider()
|
||||
SignatureTile(sharedPrefs.composeSignature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SignatureTile(flow: MutableStateFlow<String>) {
|
||||
val value by flow.collectAsState()
|
||||
|
||||
SettingsBlockTile(
|
||||
icon = MaterialSymbols.EditNote,
|
||||
title = stringRes(R.string.compose_signature_setting_title),
|
||||
description = stringRes(R.string.compose_signature_setting_description),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = { flow.tryEmit(it) },
|
||||
placeholder = { Text(stringRes(R.string.compose_signature_setting_hint)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddClientTagTile(accountViewModel: AccountViewModel) {
|
||||
val addClientTag by accountViewModel.account.settings.syncedSettings.security
|
||||
|
||||
+3
@@ -78,6 +78,9 @@ fun UrlPostScreen(
|
||||
postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType)))
|
||||
}
|
||||
}
|
||||
if (draftId == null) {
|
||||
postViewModel.applySignature()
|
||||
}
|
||||
}
|
||||
|
||||
GenericCommentPostScreen(postViewModel, accountViewModel, nav)
|
||||
|
||||
@@ -1782,7 +1782,7 @@
|
||||
<string name="ots_explorer_search_keywords" translatable="false">opentimestamps, timestamp, ots, proof</string>
|
||||
<string name="namecoin_search_keywords" translatable="false">namecoin, dns, identity, name</string>
|
||||
<string name="calendar_reminder_search_keywords" translatable="false">calendar, events, reminders, rsvp</string>
|
||||
<string name="compose_search_keywords" translatable="false">draft, posting, editor, auto-save</string>
|
||||
<string name="compose_search_keywords" translatable="false">draft, posting, editor, auto-save, signature</string>
|
||||
<string name="reactions_settings_search_keywords" translatable="false">emoji, reactions, like</string>
|
||||
<string name="bottom_bar_search_keywords" translatable="false">navigation, tabs, nav bar</string>
|
||||
<string name="home_tabs_search_keywords" translatable="false">tabs, feeds, threads, conversations</string>
|
||||
@@ -3622,6 +3622,9 @@
|
||||
<string name="compose_settings">Compose Settings</string>
|
||||
<string name="auto_create_drafts_setting_title">Automatically create drafts</string>
|
||||
<string name="auto_create_drafts_setting_description">Saves a draft event automatically when you type or leave a composer with unsent text and sends to your private outbox relays.</string>
|
||||
<string name="compose_signature_setting_title">Signature</string>
|
||||
<string name="compose_signature_setting_description">Added at the end of the message when opening a new post, reply, quote, or article. Leave empty to disable.</string>
|
||||
<string name="compose_signature_setting_hint">Your signature</string>
|
||||
<string name="ai_writing_use_this">Use This</string>
|
||||
<string name="ai_writing_dismiss">Dismiss</string>
|
||||
<string name="ai_tone_correct">Correct</string>
|
||||
|
||||
+15
@@ -32,6 +32,21 @@ fun TextFieldState.setTextAndPlaceCursorAtBeginning(text: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends [signature] to the end of the field, separated by a blank line, keeping the
|
||||
* cursor where it was so the user starts typing above it. No-op when the signature is
|
||||
* already present, so re-running a screen's load logic never duplicates it.
|
||||
*/
|
||||
fun TextFieldState.appendSignature(signature: String) {
|
||||
if (text.contains(signature)) return
|
||||
edit {
|
||||
val cursor = selection
|
||||
append("\n\n")
|
||||
append(signature)
|
||||
selection = cursor
|
||||
}
|
||||
}
|
||||
|
||||
fun TextFieldState.insertUrlAtCursor(url: String) {
|
||||
edit {
|
||||
var toInsert = url.trim()
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.ui.text
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class AppendSignatureTest {
|
||||
@Test
|
||||
fun appendsToEmptyFieldKeepingCursorAtStart() {
|
||||
val state = TextFieldState()
|
||||
|
||||
state.appendSignature("— me")
|
||||
|
||||
assertEquals("\n\n— me", state.text.toString())
|
||||
assertEquals(0, state.selection.start)
|
||||
assertEquals(0, state.selection.end)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun appendsAfterExistingContentKeepingCursor() {
|
||||
val state = TextFieldState("nostr:quote")
|
||||
state.setTextAndPlaceCursorAtBeginning("nostr:quote")
|
||||
|
||||
state.appendSignature("— me")
|
||||
|
||||
assertEquals("nostr:quote\n\n— me", state.text.toString())
|
||||
assertEquals(0, state.selection.start)
|
||||
assertEquals(0, state.selection.end)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotDuplicateWhenSignatureAlreadyPresent() {
|
||||
val state = TextFieldState()
|
||||
|
||||
state.appendSignature("— me")
|
||||
state.appendSignature("— me")
|
||||
|
||||
assertEquals("\n\n— me", state.text.toString())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user