Merge pull request #553 from dmnyc/feat/nsec-paste-guard

feat: nsec paste guard — block accidental private key pasting app-wide
This commit is contained in:
Barry Deen
2026-05-20 10:39:06 -04:00
committed by GitHub
40 changed files with 335 additions and 78 deletions
@@ -18,6 +18,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import com.wisp.app.repo.InterfacePreferences
import com.wisp.app.ui.component.NsecPasteGuard
import com.wisp.app.repo.LocaleRepository
import com.wisp.app.ui.component.LocalMediaSettings
import com.wisp.app.ui.component.MediaSettings
@@ -37,6 +38,7 @@ class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
NsecPasteGuard.setActivity(this)
enableEdgeToEdge()
deepLinkUri.value = intent?.data?.toString()
setContent {
@@ -94,6 +94,7 @@ import com.wisp.app.ui.component.FloatingVideoPlayer
import com.wisp.app.ui.component.PipController
import com.wisp.app.ui.component.FullScreenVideoPlayer
import com.wisp.app.ui.component.FullScreenVideoState
import com.wisp.app.ui.component.NsecPasteWarningOverlay
import com.wisp.app.ui.screen.OnboardingSuggestionsScreen
import com.wisp.app.ui.screen.OnboardingTopicsScreen
import com.wisp.app.ui.screen.OnboardingFirstPostScreen
@@ -3427,6 +3428,7 @@ fun WispNavHost(
.align(Alignment.BottomCenter)
.padding(bottom = 16.dp)
)
NsecPasteWarningOverlay()
} // CompositionLocalProvider
} // Box
@@ -194,3 +194,6 @@ fun String.hexToByteArray(): ByteArray {
((Character.digit(this[i * 2], 16) shl 4) + Character.digit(this[i * 2 + 1], 16)).toByte()
}
}
/** Encodes a 32-byte hex pubkey as a bech32 npub string. */
fun String.toNpub(): String = Nip19.npubEncode(hexToByteArray())
@@ -22,7 +22,7 @@ data class ProfileData(
val displayString: String
get() = displayName.takeUnlessBlank()
?: name.takeUnlessBlank()
?: "${pubkey.take(8)}...${pubkey.takeLast(4)}"
?: pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
companion object {
private val json = Json { ignoreUnknownKeys = true }
@@ -103,7 +103,7 @@ fun AddNoteToListDialog(
) {
OutlinedTextField(
value = newListName,
onValueChange = { newListName = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(newListName, new)) newListName = new },
placeholder = { Text("New list name") },
singleLine = true,
modifier = Modifier.weight(1f)
@@ -85,6 +85,7 @@ import androidx.compose.foundation.Image
import androidx.compose.ui.graphics.asImageBitmap
import com.wisp.app.R
import com.wisp.app.nostr.DmMessage
import com.wisp.app.nostr.toNpub
import com.wisp.app.ui.screen.GroupChatHorizontalChipStrip
import com.wisp.app.nostr.DmReaction
import com.wisp.app.nostr.EncryptedMedia
@@ -131,7 +132,7 @@ fun DmBubble(
val density = LocalDensity.current
val swipeThresholdPx = with(density) { 72.dp.toPx() }
val senderProfile = remember(message.senderPubkey) { eventRepo?.getProfileData(message.senderPubkey) }
val senderName = senderProfile?.displayString ?: (message.senderPubkey.take(8) + "")
val senderName = senderProfile?.displayString ?: message.senderPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
val animatedSwipe by animateFloatAsState(
targetValue = swipeOffsetPx,
@@ -539,7 +540,7 @@ private fun QuotedDmPreview(
) {
val senderName = remember(message.senderPubkey) {
eventRepo?.getProfileData(message.senderPubkey)?.displayString
?: message.senderPubkey.take(8) + ""
?: message.senderPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
}
val accentColor = if (parentIsSent) MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.5f)
else MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
@@ -83,6 +83,7 @@ import coil3.compose.AsyncImage
import androidx.compose.material3.CircularProgressIndicator
import com.wisp.app.R
import com.wisp.app.nostr.Nip13
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.Nip19
import com.wisp.app.nostr.Nip30
import com.wisp.app.nostr.Nip68
@@ -229,7 +230,7 @@ fun GalleryCard(
val displayName = remember(event.pubkey, profile?.displayString) {
profile?.displayString
?: event.pubkey.take(8) + "..." + event.pubkey.takeLast(4)
?: event.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
}
val timestamp = remember(event.created_at) { formatGalleryTimestamp(event.created_at) }
@@ -288,7 +289,7 @@ fun GalleryCard(
// Label text
val labelText = if (repostPubkeys.size == 1) {
val name = eventRepo?.getProfileData(repostPubkeys.first())?.displayString
?: (repostPubkeys.first().take(8) + "...")
?: repostPubkeys.first().toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
"$name reposted"
} else if (overflow > 0) {
"and $overflow others reposted"
@@ -605,7 +606,7 @@ fun GalleryCard(
if (topZap != null) {
val zapperProfile = eventRepo?.getProfileData(topZap.pubkey)
val zapperName = zapperProfile?.displayString
?: (topZap.pubkey.take(8) + "...")
?: topZap.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
TopZapperBanner(
avatarUrl = zapperProfile?.picture,
name = zapperName,
@@ -0,0 +1,200 @@
package com.wisp.app.ui.component
import android.app.Activity
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import kotlinx.coroutines.delay
import java.lang.ref.WeakReference
private val NSEC_REGEX = Regex("nsec1[a-z0-9]{58}")
object NsecPasteGuard {
/**
* Set to true while a screen that intentionally accepts nsec input (e.g. login/import)
* is visible, so key entry continues to work normally.
*/
var nsecPasteAllowed: Boolean = false
var warningVisible by mutableStateOf(false)
private set
private var activityRef: WeakReference<Activity>? = null
fun setActivity(activity: Activity) {
activityRef = WeakReference(activity)
}
fun containsNsec(text: String): Boolean = NSEC_REGEX.containsMatchIn(text)
/**
* Returns true if the paste was blocked (caller should NOT apply the new value).
* Only triggers when an nsec appears in newText that wasn't in currentText.
*/
fun blockIfNsec(currentText: String, newText: String): Boolean {
if (nsecPasteAllowed) return false
if (containsNsec(newText) && !containsNsec(currentText)) {
warningVisible = true
hideKeyboard()
return true
}
return false
}
fun dismissWarning() {
warningVisible = false
}
fun hideKeyboard() {
val activity = activityRef?.get() ?: return
val imm = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
// Try the currently focused window first (works when a dialog has focus),
// fall back to the decor view token.
val token = activity.currentFocus?.windowToken
?: activity.window.decorView.windowToken
imm.hideSoftInputFromWindow(token, 0)
}
/** InputTransformation for BasicTextField(state = TextFieldState) fields. */
val inputTransformation = InputTransformation {
if (!nsecPasteAllowed && containsNsec(asCharSequence().toString())) {
revertAllChanges()
warningVisible = true
hideKeyboard()
}
}
}
/**
* Red warning pill that floats above ALL content including dialogs, by rendering in a
* non-touchable, non-focusable Dialog window. Place once at the root of the composition.
*/
@Composable
fun NsecPasteWarningOverlay(modifier: Modifier = Modifier) {
var popupOpen by remember { mutableStateOf(false) }
var showPill by remember { mutableStateOf(false) }
// Use snapshotFlow + collectLatest so that dismissWarning() setting warningVisible=false
// doesn't cancel this coroutine (it's filtered out), allowing popupOpen = false to run.
LaunchedEffect(Unit) {
snapshotFlow { NsecPasteGuard.warningVisible }
.filter { it }
.collectLatest {
popupOpen = true
showPill = true
delay(3500)
showPill = false
NsecPasteGuard.dismissWarning()
delay(250)
popupOpen = false
}
}
if (popupOpen) {
Dialog(
onDismissRequest = {},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false,
usePlatformDefaultWidth = false
)
) {
// Inside a Compose Dialog, LocalView.current IS the DialogWindowProvider.
val dialogView = LocalView.current
val keyboardController = LocalSoftwareKeyboardController.current
SideEffect {
val window = (dialogView as? DialogWindowProvider)?.window
window?.addFlags(
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
)
window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
}
// Hide keyboard from within the overlay window's context — this uses the
// applicationWindowToken which works regardless of which window has IME focus.
LaunchedEffect(Unit) {
keyboardController?.hide()
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
AnimatedVisibility(
visible = showPill,
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut()
) {
Surface(
color = Color(0xFFB71C1C),
shape = RoundedCornerShape(24.dp),
modifier = Modifier
.padding(horizontal = 20.dp)
// Enough clearance for nav bar + compose toolbar / bottom bars
.padding(bottom = 100.dp)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp)
) {
Icon(
imageVector = Icons.Outlined.Warning,
contentDescription = null,
tint = Color.White,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = "Paste blocked — your nsec is your private key and must never be shared.",
color = Color.White,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
lineHeight = 18.sp
)
}
}
}
}
}
}
}
@@ -69,6 +69,7 @@ import androidx.compose.material.icons.outlined.Warning
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Surface
import com.wisp.app.nostr.Nip05
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.Nip10
import com.wisp.app.nostr.Nip13
import com.wisp.app.nostr.Nip19
@@ -177,7 +178,7 @@ fun PostCard(
) {
val displayName = remember(event.pubkey, profile?.displayString) {
profile?.displayString
?: event.pubkey.take(8) + "..." + event.pubkey.takeLast(4)
?: event.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
}
val timestamp = remember(event.created_at) {
@@ -212,7 +213,7 @@ fun PostCard(
val profileVersion by eventRepo?.profileVersion?.collectAsState() ?: remember { mutableIntStateOf(0) }
val replyToName = remember(replyToPubkey, profileVersion) {
replyToPubkey?.let { pk ->
eventRepo?.getProfileData(pk)?.displayString ?: (pk.take(8) + "...")
eventRepo?.getProfileData(pk)?.displayString ?: pk.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
}
}
@@ -269,7 +270,7 @@ fun PostCard(
// Label text
val labelText = if (repostPubkeys.size == 1) {
val name = eventRepo?.getProfileData(repostPubkeys.first())?.displayString
?: (repostPubkeys.first().take(8) + "...")
?: repostPubkeys.first().toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
"$name reposted"
} else if (overflow > 0) {
"and $overflow others reposted"
@@ -774,7 +775,7 @@ fun PostCard(
if (topZap != null) {
val zapperProfile = eventRepo?.getProfileData(topZap.pubkey)
val zapperName = zapperProfile?.displayString
?: (topZap.pubkey.take(8) + "...")
?: topZap.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
TopZapperBanner(
avatarUrl = zapperProfile?.picture,
name = zapperName,
@@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalContext
import com.wisp.app.R
import com.wisp.app.ui.util.AmountFormatter
import com.wisp.app.nostr.NostrEvent
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.ProfileData
import com.wisp.app.repo.EventRepository
import com.wisp.app.repo.ZapDetail
@@ -156,7 +157,7 @@ fun ZapRow(
)
Spacer(Modifier.width(8.dp))
Text(
text = message.ifBlank { profile?.displayString ?: (pubkey.take(8) + "...") },
text = message.ifBlank { profile?.displayString ?: pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" } },
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -106,6 +106,7 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.TextButton
import com.wisp.app.R
import com.wisp.app.nostr.Bolt11
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.Nip19
import com.wisp.app.nostr.Nip30
import com.wisp.app.nostr.toHex
@@ -763,7 +764,7 @@ fun RichContent(
for (pubkey in profilePubkeys) {
val profile = eventRepo?.getProfileData(pubkey)
names[pubkey] = profile?.displayString
?: "${pubkey.take(8)}...${pubkey.takeLast(4)}"
?: pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
}
names
}
@@ -839,7 +840,7 @@ fun RichContent(
}
is ContentSegment.NostrProfileSegment -> {
val pubkey = seg.pubkey
val displayName = profileNames[seg.pubkey] ?: seg.pubkey.take(8)
val displayName = profileNames[seg.pubkey] ?: seg.pubkey.toNpub().take(12)
withLink(
LinkAnnotation.Clickable("profile") {
onProfileClick?.invoke(pubkey)
@@ -1259,7 +1260,7 @@ fun QuotedNote(
Column(modifier = Modifier.weight(1f)) {
Text(
text = profile?.displayString
?: event.pubkey.take(8) + "..." + event.pubkey.takeLast(4),
?: event.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" },
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -1507,7 +1508,7 @@ private fun ArticleCard(
ProfilePicture(url = profile?.picture, size = 20)
Spacer(Modifier.width(6.dp))
val displayName = profile?.displayString
?: "${author.take(8)}...${author.takeLast(4)}"
?: author.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
Text(
text = displayName,
style = MaterialTheme.typography.labelMedium,
@@ -1686,7 +1687,7 @@ private fun LiveStreamCardContent(
ProfilePicture(url = profile?.picture, size = 20)
Spacer(Modifier.width(6.dp))
val displayName = profile?.displayString
?: "${author.take(8)}...${author.takeLast(4)}"
?: author.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
Text(
text = displayName,
style = MaterialTheme.typography.labelMedium,
@@ -73,6 +73,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.wisp.app.nostr.Nip05
import com.wisp.app.nostr.ProfileData
import com.wisp.app.nostr.toNpub
import com.wisp.app.repo.AccountInfo
import com.wisp.app.ui.util.LocalCanSign
@@ -199,7 +200,7 @@ fun WispDrawerContent(
)
} else if (pubkey != null) {
Text(
text = pubkey.take(16) + "...",
text = pubkey.toNpub().let { it.take(16) + "..." },
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
@@ -257,7 +258,7 @@ fun WispDrawerContent(
text = {
androidx.compose.material3.OutlinedTextField(
value = statusText,
onValueChange = { statusText = it },
onValueChange = { new -> if (!com.wisp.app.ui.component.NsecPasteGuard.blockIfNsec(statusText, new)) statusText = new },
label = { Text("What are you up to?") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
@@ -300,9 +301,9 @@ fun WispDrawerContent(
ProfilePicture(url = pictureUrl, size = 36)
Spacer(modifier = Modifier.width(12.dp))
val displayText = if (isActive) {
profile?.displayString ?: account.displayName ?: account.pubkeyHex.take(16) + "..."
profile?.displayString ?: account.displayName ?: account.pubkeyHex.toNpub().let { it.take(16) + "..." }
} else {
account.displayName ?: account.pubkeyHex.take(16) + "..."
account.displayName ?: account.pubkeyHex.toNpub().let { it.take(16) + "..." }
}
Text(
text = displayText,
@@ -458,7 +458,7 @@ fun ZapDialog(
// Message input
OutlinedTextField(
value = message,
onValueChange = { message = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(message, new)) message = new },
label = { Text(stringResource(R.string.placeholder_message_optional)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
@@ -974,7 +974,7 @@ private fun SaveZapPresetDialog(
Spacer(Modifier.height(12.dp))
OutlinedTextField(
value = presetMessage,
onValueChange = { presetMessage = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(presetMessage, new)) presetMessage = new },
label = { Text(stringResource(R.string.placeholder_message_optional)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
@@ -57,6 +57,7 @@ import coil3.compose.AsyncImage
import com.wisp.app.repo.EventRepository
import com.wisp.app.ui.component.ProfilePicture
import com.wisp.app.nostr.Nip30
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.NostrEvent
import com.wisp.app.ui.component.ActionBar
import com.wisp.app.ui.component.NoteActions
@@ -200,7 +201,7 @@ fun ArticleScreen(
Spacer(Modifier.width(8.dp))
Column {
val displayName = profile?.displayString
?: "${authorPubkey.take(8)}...${authorPubkey.takeLast(4)}"
?: authorPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
Text(
text = displayName,
style = MaterialTheme.typography.titleSmall,
@@ -27,11 +27,13 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.wisp.app.ui.component.NsecPasteGuard
import com.wisp.app.ui.component.QrScanner
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -85,6 +87,11 @@ fun AuthScreen(
return
}
DisposableEffect(Unit) {
NsecPasteGuard.nsecPasteAllowed = true
onDispose { NsecPasteGuard.nsecPasteAllowed = false }
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -45,6 +45,7 @@ import androidx.compose.ui.zIndex
import androidx.compose.ui.res.stringResource
import com.wisp.app.R
import com.wisp.app.relay.RelayPool
import com.wisp.app.ui.component.NsecPasteGuard
import com.wisp.app.viewmodel.BlossomServersViewModel
import kotlin.math.roundToInt
@@ -95,7 +96,7 @@ fun BlossomServersScreen(
) {
OutlinedTextField(
value = newServerUrl,
onValueChange = { viewModel.updateNewServerUrl(it) },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(newServerUrl, new)) viewModel.updateNewServerUrl(new) },
label = { Text("https://...") },
singleLine = true,
modifier = Modifier.weight(1f)
@@ -123,6 +123,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.wisp.app.nostr.Nip19
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.Nip88
import com.wisp.app.nostr.NostrEvent
import com.wisp.app.relay.RelayPool
@@ -356,7 +357,11 @@ fun ComposeScreen(
}
OutlinedTextField(
value = content,
onValueChange = { viewModel.updateContent(it) },
onValueChange = { new ->
if (!com.wisp.app.ui.component.NsecPasteGuard.blockIfNsec(content.text, new.text)) {
viewModel.updateContent(new)
}
},
placeholder = { Text(stringResource(R.string.compose_gallery_placeholder)) },
enabled = !publishing && countdownSeconds == null,
visualTransformation = galleryEmojiVisual,
@@ -564,7 +569,7 @@ fun ComposeScreen(
replyTo?.let {
val replyProfile = profileRepo?.get(it.pubkey)
val replyAuthorName = replyProfile?.displayString
?: "${it.pubkey.take(8)}..."
?: it.pubkey.toNpub().let { npub -> "${npub.take(12)}...${npub.takeLast(4)}" }
var replyExpanded by remember { mutableStateOf(false) }
Surface(
shape = RoundedCornerShape(8.dp),
@@ -623,7 +628,7 @@ fun ComposeScreen(
// Quote context with resolved display names
quoteTo?.let {
val quoteAuthorName = profileRepo?.get(it.pubkey)?.displayString
?: "${it.pubkey.take(8)}..."
?: it.pubkey.toNpub().let { npub -> "${npub.take(12)}...${npub.takeLast(4)}" }
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
@@ -742,6 +747,7 @@ fun ComposeScreen(
}),
enabled = enabled,
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
inputTransformation = com.wisp.app.ui.component.NsecPasteGuard.inputTransformation,
lineLimits = TextFieldLineLimits.MultiLine(),
outputTransformation = outputTransformation,
textStyle = MaterialTheme.typography.bodyLarge.copy(
@@ -40,6 +40,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.wisp.app.R
import com.wisp.app.nostr.Nip05
import com.wisp.app.nostr.toNpub
import com.wisp.app.repo.ContactRepository
import com.wisp.app.repo.EventRepository
import com.wisp.app.ui.component.ProfilePicture
@@ -149,7 +150,7 @@ fun ContactPickerScreen(
Column(modifier = Modifier.weight(1f)) {
Text(
text = profile?.displayString
?: entry.pubkey.take(8) + "..." + entry.pubkey.takeLast(4),
?: entry.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" },
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -63,6 +63,7 @@ import com.wisp.app.nostr.CustomEmoji
import com.wisp.app.nostr.EmojiSet
import com.wisp.app.repo.CustomEmojiRepository
import com.wisp.app.ui.component.EmojiLibrarySheet
import com.wisp.app.ui.component.NsecPasteGuard
import com.wisp.app.ui.component.pendingEmojiReactCallback
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@@ -403,7 +404,7 @@ fun CustomEmojiScreen(
) {
OutlinedTextField(
value = url,
onValueChange = { url = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(url, new)) url = new },
placeholder = { Text("Image URL") },
singleLine = true,
modifier = Modifier.weight(1f)
@@ -562,7 +563,7 @@ private fun CreateEmojiSetDialog(
text = {
OutlinedTextField(
value = name,
onValueChange = { name = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(name, new)) name = new },
placeholder = { Text("Set name") },
singleLine = true
)
@@ -611,7 +612,7 @@ private fun EditEmojiSetDialog(
Column {
OutlinedTextField(
value = title,
onValueChange = { title = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(title, new)) title = new },
label = { Text("Set name") },
singleLine = true
)
@@ -628,7 +629,7 @@ private fun EditEmojiSetDialog(
Spacer(Modifier.width(4.dp))
OutlinedTextField(
value = url,
onValueChange = { url = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(url, new)) url = new },
placeholder = { Text("URL") },
singleLine = true,
modifier = Modifier.weight(1f)
@@ -72,6 +72,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.wisp.app.nostr.DmMessage
import com.wisp.app.nostr.toNpub
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -201,7 +202,7 @@ fun DmConversationScreen(
Column {
val names = participants.take(3).joinToString(", ") { pk ->
eventRepo?.getProfileData(pk)?.displayString
?: pk.take(8) + ""
?: pk.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
}
val suffix = if (participants.size > 3) " +${participants.size - 3}" else ""
Text(
@@ -302,7 +303,7 @@ fun DmConversationScreen(
ProfilePicture(url = participantProfile?.picture, size = 20)
Spacer(Modifier.width(8.dp))
Text(
text = participantProfile?.displayString ?: pubkey.take(8) + "",
text = participantProfile?.displayString ?: pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" },
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -489,7 +490,7 @@ fun DmConversationScreen(
val senderName = remember(replyMsg.senderPubkey) {
if (replyMsg.senderPubkey == userPubkey) "You"
else eventRepo?.getProfileData(replyMsg.senderPubkey)?.displayString
?: replyMsg.senderPubkey.take(8) + ""
?: replyMsg.senderPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
}
Row(
modifier = Modifier.fillMaxWidth(),
@@ -557,7 +558,12 @@ fun DmConversationScreen(
}
androidx.compose.foundation.text.BasicTextField(
value = dmTfv,
onValueChange = { dmTfv = it; viewModel.updateMessageText(it.text) },
onValueChange = { new ->
if (!com.wisp.app.ui.component.NsecPasteGuard.blockIfNsec(dmTfv.text, new.text)) {
dmTfv = new
viewModel.updateMessageText(new.text)
}
},
modifier = Modifier.weight(1f).heightIn(min = 28.dp).padding(top = 4.dp),
enabled = uploadProgress == null,
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
@@ -61,6 +61,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.wisp.app.R
import com.wisp.app.nostr.DmConversation
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.NostrSigner
import com.wisp.app.repo.EventRepository
import com.wisp.app.repo.GroupRoom
@@ -640,11 +641,11 @@ private fun ConversationRow(
Column(modifier = Modifier.weight(1f)) {
val displayName = if (convo.isGroup) {
convo.participants.take(3).joinToString(", ") { pk ->
eventRepo.getProfileData(pk)?.displayString ?: pk.take(8) + ""
eventRepo.getProfileData(pk)?.displayString ?: pk.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
}.let { if (convo.participants.size > 3) "$it +${convo.participants.size - 3}" else it }
} else {
eventRepo.getProfileData(convo.peerPubkey)?.displayString
?: convo.peerPubkey.take(8) + "..." + convo.peerPubkey.takeLast(4)
?: convo.peerPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
}
Text(
@@ -46,6 +46,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.wisp.app.nostr.Nip05
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.Nip30
import com.wisp.app.nostr.Nip37
import com.wisp.app.nostr.NostrEvent
@@ -283,7 +284,7 @@ private fun ScheduledPostItem(
onDelete: () -> Unit
) {
val displayName = remember(post.pubkey, userProfile?.displayString) {
userProfile?.displayString ?: (post.pubkey.take(8) + "" + post.pubkey.takeLast(4))
userProfile?.displayString ?: post.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
}
val publishTime = post.created_at
val isFuture = publishTime > System.currentTimeMillis() / 1000
@@ -76,6 +76,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.wisp.app.nostr.FollowSet
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.Nip05
import com.wisp.app.nostr.Nip10
import com.wisp.app.nostr.Nip69
@@ -1549,7 +1550,7 @@ private fun FeedArticleItem(
}
val displayName = profileData?.displayString
?: "${event.pubkey.take(8)}...${event.pubkey.takeLast(4)}"
?: event.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
Surface(
shape = RoundedCornerShape(12.dp),
@@ -64,6 +64,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.wisp.app.R
import com.wisp.app.nostr.Nip29
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.NostrSigner
import com.wisp.app.relay.RelayPool
import com.wisp.app.repo.EventRepository
@@ -721,7 +722,7 @@ private fun MemberRow(
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = profile?.displayString ?: (pubkey.take(8) + ""),
text = profile?.displayString ?: pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" },
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -135,6 +135,7 @@ import androidx.compose.ui.zIndex
import coil3.compose.AsyncImage
import com.wisp.app.R
import com.wisp.app.nostr.Nip19
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.NostrSigner
import com.wisp.app.nostr.hexToByteArray
import com.wisp.app.nostr.ProfileData
@@ -578,6 +579,7 @@ fun GroupRoomScreen(
.then(pasteImagesModifier),
enabled = uploadProgress == null,
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
inputTransformation = com.wisp.app.ui.component.NsecPasteGuard.inputTransformation,
outputTransformation = groupEmojiTransformation,
lineLimits = lineLimits,
textStyle = MaterialTheme.typography.bodyLarge.copy(
@@ -776,7 +778,7 @@ fun GroupRoomScreen(
eventRepo.getProfileData(rt.senderPubkey)
}
val replyName =
replyProfile?.displayString ?: rt.senderPubkey.take(8) + ""
replyProfile?.displayString ?: rt.senderPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
GroupChatQuotedMessage(
authorPubkey = rt.senderPubkey,
authorName = replyName,
@@ -1218,7 +1220,7 @@ private fun GroupMessageBubble(
noteActions: com.wisp.app.ui.component.NoteActions? = null
) {
val profile = remember(message.senderPubkey) { eventRepo.getProfileData(message.senderPubkey) }
val displayName = profile?.displayString ?: (message.senderPubkey.take(8) + "")
val displayName = profile?.displayString ?: message.senderPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
val background = MaterialTheme.colorScheme.background
val isDarkTheme = (background.red * 0.299f + background.green * 0.587f + background.blue * 0.114f) < 0.5f
val nameColor = remember(message.senderPubkey, isDarkTheme) { groupMemberColor(message.senderPubkey, isDarkTheme) }
@@ -1397,7 +1399,7 @@ private fun GroupMessageBubble(
val replyProfile = remember(replyToMessage.senderPubkey) {
eventRepo.getProfileData(replyToMessage.senderPubkey)
}
val replyName = replyProfile?.displayString ?: replyToMessage.senderPubkey.take(8) + ""
val replyName = replyProfile?.displayString ?: replyToMessage.senderPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
GroupChatQuotedMessage(
authorPubkey = replyToMessage.senderPubkey,
authorName = replyName,
@@ -49,6 +49,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.wisp.app.R
import com.wisp.app.nostr.FollowSet
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.Nip19
import com.wisp.app.nostr.Nip51
import com.wisp.app.nostr.toHex
@@ -240,7 +241,7 @@ fun ListScreen(
eventRepo.getProfileData(pubkey)
}
val displayName = profile?.displayString
?: pubkey.take(8) + "..." + pubkey.takeLast(4)
?: pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -340,7 +341,7 @@ private fun FollowPickerDialog(
items(filteredFollows, key = { it.pubkey }) { entry ->
val profile = eventRepo.getProfileData(entry.pubkey)
val displayName = profile?.displayString
?: entry.pubkey.take(8) + "..." + entry.pubkey.takeLast(4)
?: entry.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
val isMember = entry.pubkey in existingMembers
Row(
@@ -86,6 +86,7 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.wisp.app.R
import com.wisp.app.nostr.Nip19
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.NostrSigner
import com.wisp.app.nostr.hexToByteArray
import com.wisp.app.repo.EventRepository
@@ -284,7 +285,7 @@ fun LiveStreamScreen(
AnimatedVisibility(visible = replyTarget != null) {
replyTarget?.let { reply ->
val replyProfile = remember(reply.senderPubkey) { eventRepo.getProfileData(reply.senderPubkey) }
val replyName = replyProfile?.displayString ?: (reply.senderPubkey.take(8) + "")
val replyName = replyProfile?.displayString ?: reply.senderPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.fillMaxWidth()
@@ -463,7 +464,7 @@ private fun StreamInfoBar(
isZapAnimating: Boolean = false
) {
val profile = remember(hostPubkey) { eventRepo.getProfileData(hostPubkey) }
val displayName = profile?.displayString ?: (hostPubkey.take(8) + "")
val displayName = profile?.displayString ?: hostPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
val zapColor = com.wisp.app.ui.theme.WispThemeColors.zapColor
Row(
@@ -593,7 +594,7 @@ private fun ZapAnnouncementBubble(
onProfileClick: (String) -> Unit
) {
val profile = remember(message.senderPubkey) { eventRepo.getProfileData(message.senderPubkey) }
val displayName = profile?.displayString ?: (message.senderPubkey.take(8) + "")
val displayName = profile?.displayString ?: message.senderPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
val zapColor = com.wisp.app.ui.theme.WispThemeColors.zapColor
Surface(
@@ -685,7 +686,7 @@ private fun LiveChatBubble(
isZapInProgress: Boolean
) {
val profile = remember(message.senderPubkey) { eventRepo.getProfileData(message.senderPubkey) }
val displayName = profile?.displayString ?: (message.senderPubkey.take(8) + "")
val displayName = profile?.displayString ?: message.senderPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
val isOwnMessage = message.senderPubkey == myPubkey
val nameColor = if (isOwnMessage) {
MaterialTheme.colorScheme.primary
@@ -866,7 +867,7 @@ private fun LiveChatBubble(
val replyProfile = remember(replyToMessage.senderPubkey) {
eventRepo.getProfileData(replyToMessage.senderPubkey)
}
val replyName = replyProfile?.displayString ?: (replyToMessage.senderPubkey.take(8) + "")
val replyName = replyProfile?.displayString ?: replyToMessage.senderPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
Row(
modifier = Modifier
.fillMaxWidth()
@@ -88,6 +88,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.res.stringResource
import coil3.compose.AsyncImage
import com.wisp.app.nostr.FlatNotificationItem
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.Nip10
import com.wisp.app.nostr.Nip30
import com.wisp.app.nostr.Nip69
@@ -648,7 +649,7 @@ private fun ZenNotificationRow(
) {
val profile = remember(profileVersion, item.actorPubkey) { resolveProfile(item.actorPubkey) }
val displayName = profile?.displayString
?: item.actorPubkey.take(8) + "..." + item.actorPubkey.takeLast(4)
?: item.actorPubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
Column(modifier = Modifier.fillMaxWidth()) {
// Compact row — always visible
@@ -166,7 +166,7 @@ fun OnboardingScreen(
Text(
when {
!relaysReady -> stringResource(R.string.onboarding_please_wait)
publishing -> stringResource(R.string.onboarding_publishing)
publishing -> stringResource(R.string.onboarding_creating_account)
else -> stringResource(R.string.btn_continue)
}
)
@@ -39,6 +39,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.wisp.app.R
import com.wisp.app.nostr.ProfileData
import com.wisp.app.nostr.toNpub
import com.wisp.app.ui.component.FollowToggleButton
import com.wisp.app.ui.component.ProfilePicture
import com.wisp.app.ui.component.StackedAvatars
@@ -203,7 +204,7 @@ private fun CreatorCards(
ProfilePicture(url = profile?.picture, size = 56)
Spacer(Modifier.height(8.dp))
Text(
text = profile?.displayString ?: pubkey.take(8) + "...",
text = profile?.displayString ?: pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" },
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -44,6 +44,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import com.wisp.app.R
import com.wisp.app.relay.RelayPool
import com.wisp.app.ui.component.NsecPasteGuard
import com.wisp.app.viewmodel.ProfileViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -114,7 +115,7 @@ fun ProfileEditScreen(
) {
OutlinedTextField(
value = name,
onValueChange = { viewModel.updateName(it) },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(name, new)) viewModel.updateName(new) },
label = { Text(stringResource(R.string.placeholder_name)) },
singleLine = true,
modifier = Modifier.fillMaxWidth().scrollOnFocus()
@@ -122,7 +123,7 @@ fun ProfileEditScreen(
Spacer(Modifier.height(12.dp))
OutlinedTextField(
value = about,
onValueChange = { viewModel.updateAbout(it) },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(about, new)) viewModel.updateAbout(new) },
label = { Text(stringResource(R.string.placeholder_about)) },
minLines = 3,
modifier = Modifier.fillMaxWidth().scrollOnFocus()
@@ -134,7 +135,7 @@ fun ProfileEditScreen(
) {
OutlinedTextField(
value = picture,
onValueChange = { viewModel.updatePicture(it) },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(picture, new)) viewModel.updatePicture(new) },
label = { Text(stringResource(R.string.placeholder_profile_picture_url)) },
singleLine = true,
modifier = Modifier.weight(1f).scrollOnFocus()
@@ -161,7 +162,7 @@ fun ProfileEditScreen(
) {
OutlinedTextField(
value = banner,
onValueChange = { viewModel.updateBanner(it) },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(banner, new)) viewModel.updateBanner(new) },
label = { Text(stringResource(R.string.placeholder_banner_url)) },
singleLine = true,
modifier = Modifier.weight(1f).scrollOnFocus()
@@ -184,7 +185,7 @@ fun ProfileEditScreen(
Spacer(Modifier.height(12.dp))
OutlinedTextField(
value = nip05,
onValueChange = { viewModel.updateNip05(it) },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(nip05, new)) viewModel.updateNip05(new) },
label = { Text(stringResource(R.string.placeholder_nip05)) },
singleLine = true,
modifier = Modifier.fillMaxWidth().scrollOnFocus()
@@ -192,7 +193,7 @@ fun ProfileEditScreen(
Spacer(Modifier.height(12.dp))
OutlinedTextField(
value = lud16,
onValueChange = { viewModel.updateLud16(it) },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(lud16, new)) viewModel.updateLud16(new) },
label = { Text(stringResource(R.string.placeholder_lightning_address)) },
singleLine = true,
modifier = Modifier.fillMaxWidth().scrollOnFocus()
@@ -50,6 +50,7 @@ import com.wisp.app.relay.RelayHealthTracker
import com.wisp.app.repo.RelayInfoRepository
import com.wisp.app.nostr.ProfileData
import com.wisp.app.nostr.RelaySet
import com.wisp.app.ui.component.NsecPasteGuard
import com.wisp.app.ui.component.ProfilePicture
import com.wisp.app.ui.component.RelayIcon
import com.wisp.app.ui.theme.WispThemeColors
@@ -377,7 +378,7 @@ private fun RelayHeader(
// Create new set
androidx.compose.material3.OutlinedTextField(
value = newSetName,
onValueChange = { newSetName = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(newSetName, new)) newSetName = new },
placeholder = { Text(stringResource(R.string.placeholder_new_set_name)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
@@ -41,6 +41,7 @@ import com.wisp.app.relay.RelayConfig
import com.wisp.app.relay.RelayPool
import com.wisp.app.relay.RelaySetType
import com.wisp.app.R
import com.wisp.app.ui.component.NsecPasteGuard
import com.wisp.app.viewmodel.RelayViewModel
@OptIn(ExperimentalMaterial3Api::class)
@@ -102,7 +103,7 @@ fun RelayScreen(
) {
OutlinedTextField(
value = newRelayUrl,
onValueChange = { viewModel.updateNewRelayUrl(it) },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(newRelayUrl, new)) viewModel.updateNewRelayUrl(new) },
label = { Text(stringResource(R.string.placeholder_relay_url)) },
singleLine = true,
modifier = Modifier.weight(1f)
@@ -40,9 +40,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import com.wisp.app.R
import com.wisp.app.repo.ExtendedNetworkCache
import com.wisp.app.nostr.toNpub
import com.wisp.app.repo.MuteRepository
import com.wisp.app.repo.ProfileRepository
import com.wisp.app.repo.SafetyPreferences
import com.wisp.app.ui.component.NsecPasteGuard
import com.wisp.app.ui.component.ProfilePicture
import kotlinx.coroutines.flow.StateFlow
@@ -145,7 +147,7 @@ private fun MutedWordsTab(
) {
OutlinedTextField(
value = newWord,
onValueChange = { newWord = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(newWord, new)) newWord = new },
placeholder = { Text(stringResource(R.string.placeholder_add_word_phrase)) },
singleLine = true,
modifier = Modifier.weight(1f)
@@ -249,7 +251,7 @@ private fun MutedUsersTab(
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = profile?.displayString ?: (pubkey.take(16) + "..."),
text = profile?.displayString ?: (pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -257,7 +259,7 @@ private fun MutedUsersTab(
)
if (profile != null) {
Text(
text = pubkey.take(16) + "...",
text = pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" },
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
@@ -76,6 +76,7 @@ import com.wisp.app.repo.TranslationRepository
import com.wisp.app.repo.TranslationState
import com.wisp.app.ui.component.FollowButton
import com.wisp.app.ui.component.NoteActions
import com.wisp.app.ui.component.NsecPasteGuard
import com.wisp.app.ui.component.PostCard
import com.wisp.app.ui.component.ProfilePicture
import com.wisp.app.viewmodel.RelayOption
@@ -213,7 +214,7 @@ fun SearchScreen(
) {
TextField(
value = query,
onValueChange = { viewModel.updateQuery(it) },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(query, new)) viewModel.updateQuery(new) },
singleLine = true,
textStyle = MaterialTheme.typography.bodyLarge,
trailingIcon = {
@@ -640,7 +641,7 @@ private fun AddRelayDialog(
text = {
OutlinedTextField(
value = url,
onValueChange = { url = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(url, new)) url = new },
placeholder = { Text(stringResource(R.string.placeholder_add_relay)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
@@ -714,7 +715,7 @@ private fun AuthorFilter(
) {
OutlinedTextField(
value = authorQuery,
onValueChange = { authorQuery = it },
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(authorQuery, new)) authorQuery = new },
placeholder = { Text("Search for author") },
singleLine = true,
modifier = Modifier.weight(1f),
@@ -70,6 +70,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.ui.res.stringResource
import com.wisp.app.R
import com.wisp.app.nostr.Nip19
import com.wisp.app.nostr.toNpub
import com.wisp.app.nostr.hexToByteArray
import com.wisp.app.repo.DiscoveryState
import com.wisp.app.repo.ExtendedNetworkCache
@@ -608,7 +609,7 @@ private fun TopAccountRow(
Text(
text = profile?.displayName
?: profile?.name
?: "${account.pubkey.take(8)}...",
?: account.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" },
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -657,7 +658,7 @@ private fun NodeDetailSheet(
Text(
text = profile?.displayName
?: profile?.name
?: "${node.pubkey.take(8)}...",
?: node.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" },
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface
)
@@ -41,6 +41,7 @@ import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.CurrencyBitcoin
import com.wisp.app.nostr.Nip30
import com.wisp.app.nostr.toNpub
import com.wisp.app.ui.component.Nip05Badge
import com.wisp.app.ui.component.RichContent
import com.wisp.app.ui.component.parseImetaTags
@@ -1393,7 +1394,7 @@ private fun FollowEntryRow(
) {
val profile = eventRepo?.getProfileData(entry.pubkey)
val displayName = profile?.displayString
?: entry.pubkey.take(8) + "..." + entry.pubkey.takeLast(4)
?: entry.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -148,6 +148,7 @@ import com.wisp.app.repo.BalanceUnit
import com.wisp.app.repo.FiatPreferences
import com.wisp.app.repo.WalletMode
import com.wisp.app.repo.WalletTransaction
import com.wisp.app.ui.component.NsecPasteGuard
import com.wisp.app.ui.component.SatsNumpad
import com.wisp.app.ui.util.AmountFormatter
import com.wisp.app.viewmodel.AutoCheckState
@@ -657,7 +658,7 @@ private fun WalletConnectionContent(
OutlinedTextField(
value = connectionString,
onValueChange = onConnectionStringChange,
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(connectionString, new)) onConnectionStringChange(new) },
label = { Text(stringResource(R.string.wallet_nwc_connection_string)) },
placeholder = { Text("nostr+walletconnect://...") },
modifier = Modifier.fillMaxWidth(),
@@ -1252,7 +1253,7 @@ private fun SendInputContent(
OutlinedTextField(
value = input,
onValueChange = onInputChange,
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(input, new)) onInputChange(new) },
label = { Text(stringResource(R.string.wallet_lightning_address_invoice)) },
placeholder = { Text(stringResource(R.string.placeholder_user_domain)) },
modifier = Modifier.fillMaxWidth(),
@@ -1260,7 +1261,7 @@ private fun SendInputContent(
maxLines = 4,
trailingIcon = {
IconButton(onClick = {
clipboardManager.getText()?.text?.let { onInputChange(it) }
clipboardManager.getText()?.text?.let { new -> if (!NsecPasteGuard.blockIfNsec(input, new)) onInputChange(new) }
}) {
Icon(Icons.Default.ContentPaste, contentDescription = stringResource(R.string.wallet_paste))
}
@@ -2612,7 +2613,7 @@ private fun SparkRestoreSeedContent(
OutlinedTextField(
value = restoreMnemonic,
onValueChange = onRestoreMnemonicChange,
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(restoreMnemonic, new)) onRestoreMnemonicChange(new) },
label = { Text("Recovery phrase") },
placeholder = { Text("Enter 12 or 24 words...") },
modifier = Modifier.fillMaxWidth(),
@@ -3638,7 +3639,7 @@ private fun DeleteWalletConfirmContent(
OutlinedTextField(
value = confirmText,
onValueChange = onConfirmTextChange,
onValueChange = { new -> if (!NsecPasteGuard.blockIfNsec(confirmText, new)) onConfirmTextChange(new) },
label = { Text("Type DELETE to confirm") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
@@ -27,6 +27,7 @@ import com.wisp.app.nostr.Nip88
import com.wisp.app.nostr.NostrEvent
import com.wisp.app.nostr.NostrSigner
import com.wisp.app.nostr.toHex
import com.wisp.app.nostr.toNpub
import com.wisp.app.relay.OutboxRouter
import com.wisp.app.relay.RelayPool
import com.wisp.app.repo.BlossomRepository
@@ -514,7 +515,7 @@ class ComposeViewModel(app: Application, private val savedStateHandle: SavedStat
private fun sanitizeMentionDisplay(candidate: MentionCandidate): String {
val raw = candidate.profile.displayName?.takeIf { it.isNotBlank() }
?: candidate.profile.name?.takeIf { it.isNotBlank() }
?: return candidate.profile.pubkey.take(8) + "" + candidate.profile.pubkey.takeLast(4)
?: return candidate.profile.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }
// Strip whitespace and leading @ so the mention remains a single token and mention detection
// can't re-trigger on a name that itself contains spaces.
return raw.trim().removePrefix("@").replace(Regex("\\s+"), "_")
+1
View File
@@ -473,6 +473,7 @@
<string name="onboarding_almost_there">Almost there…</string>
<string name="onboarding_please_wait">Please wait…</string>
<string name="onboarding_publishing">Publishing…</string>
<string name="onboarding_creating_account">Creating account…</string>
<string name="onboarding_wisp_logo">Wisp logo</string>
<string name="onboarding_follow_all_x">Follow All (%d)</string>