diff --git a/app/src/main/kotlin/com/wisp/app/MainActivity.kt b/app/src/main/kotlin/com/wisp/app/MainActivity.kt index 6db1d98..ae347a1 100644 --- a/app/src/main/kotlin/com/wisp/app/MainActivity.kt +++ b/app/src/main/kotlin/com/wisp/app/MainActivity.kt @@ -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 { diff --git a/app/src/main/kotlin/com/wisp/app/Navigation.kt b/app/src/main/kotlin/com/wisp/app/Navigation.kt index 93a919f..356a303 100644 --- a/app/src/main/kotlin/com/wisp/app/Navigation.kt +++ b/app/src/main/kotlin/com/wisp/app/Navigation.kt @@ -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 diff --git a/app/src/main/kotlin/com/wisp/app/nostr/Event.kt b/app/src/main/kotlin/com/wisp/app/nostr/Event.kt index edeec4f..d060da5 100644 --- a/app/src/main/kotlin/com/wisp/app/nostr/Event.kt +++ b/app/src/main/kotlin/com/wisp/app/nostr/Event.kt @@ -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()) diff --git a/app/src/main/kotlin/com/wisp/app/nostr/ProfileData.kt b/app/src/main/kotlin/com/wisp/app/nostr/ProfileData.kt index 3842d5a..c021227 100644 --- a/app/src/main/kotlin/com/wisp/app/nostr/ProfileData.kt +++ b/app/src/main/kotlin/com/wisp/app/nostr/ProfileData.kt @@ -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 } diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/AddToListDialog.kt b/app/src/main/kotlin/com/wisp/app/ui/component/AddToListDialog.kt index dea2506..e2fa720 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/AddToListDialog.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/AddToListDialog.kt @@ -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) diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/DmBubble.kt b/app/src/main/kotlin/com/wisp/app/ui/component/DmBubble.kt index ba298af..f35b9a8 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/DmBubble.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/DmBubble.kt @@ -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) diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/GalleryCard.kt b/app/src/main/kotlin/com/wisp/app/ui/component/GalleryCard.kt index 4b9ef15..753fe5e 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/GalleryCard.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/GalleryCard.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/NsecPasteGuard.kt b/app/src/main/kotlin/com/wisp/app/ui/component/NsecPasteGuard.kt new file mode 100644 index 0000000..a2e5a27 --- /dev/null +++ b/app/src/main/kotlin/com/wisp/app/ui/component/NsecPasteGuard.kt @@ -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? = 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 + ) + } + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/PostCard.kt b/app/src/main/kotlin/com/wisp/app/ui/component/PostCard.kt index 335dbb5..7dd9912 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/PostCard.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/PostCard.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/ReactionDetailsSection.kt b/app/src/main/kotlin/com/wisp/app/ui/component/ReactionDetailsSection.kt index 6153225..e26896f 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/ReactionDetailsSection.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/ReactionDetailsSection.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/RichContent.kt b/app/src/main/kotlin/com/wisp/app/ui/component/RichContent.kt index 978a9e1..4d44a95 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/RichContent.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/RichContent.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/WispDrawerContent.kt b/app/src/main/kotlin/com/wisp/app/ui/component/WispDrawerContent.kt index ce0a30a..d2f400d 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/WispDrawerContent.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/WispDrawerContent.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/ZapDialog.kt b/app/src/main/kotlin/com/wisp/app/ui/component/ZapDialog.kt index 7394281..bb888fa 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/ZapDialog.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/ZapDialog.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ArticleScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ArticleScreen.kt index c2ca953..cdb3180 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ArticleScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ArticleScreen.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/AuthScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/AuthScreen.kt index c3ada99..ba70846 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/AuthScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/AuthScreen.kt @@ -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() diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/BlossomServersScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/BlossomServersScreen.kt index 89267fc..cc7f206 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/BlossomServersScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/BlossomServersScreen.kt @@ -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) diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ComposeScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ComposeScreen.kt index 6e309ee..168c623 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ComposeScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ComposeScreen.kt @@ -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( diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ContactPickerScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ContactPickerScreen.kt index 6b57b1e..4ab05a5 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ContactPickerScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ContactPickerScreen.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/CustomEmojiScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/CustomEmojiScreen.kt index 0eb4ee6..bac08d0 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/CustomEmojiScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/CustomEmojiScreen.kt @@ -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) diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/DmConversationScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/DmConversationScreen.kt index 56ad42c..5c02d20 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/DmConversationScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/DmConversationScreen.kt @@ -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), diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/DmListScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/DmListScreen.kt index 9203fe4..80d26b6 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/DmListScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/DmListScreen.kt @@ -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( diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/DraftsScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/DraftsScreen.kt index 2438719..1da5049 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/DraftsScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/DraftsScreen.kt @@ -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 diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/FeedScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/FeedScreen.kt index 83cf901..43df05d 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/FeedScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/FeedScreen.kt @@ -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), diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/GroupDetailScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/GroupDetailScreen.kt index 960e41f..1c2f936 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/GroupDetailScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/GroupDetailScreen.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/GroupRoomScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/GroupRoomScreen.kt index c64bc6a..33bc91e 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/GroupRoomScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/GroupRoomScreen.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ListScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ListScreen.kt index 11bbf46..93da2e1 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ListScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ListScreen.kt @@ -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( diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/LiveStreamScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/LiveStreamScreen.kt index 99b626e..2fde73b 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/LiveStreamScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/LiveStreamScreen.kt @@ -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() diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/NotificationsScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/NotificationsScreen.kt index 3a1d51f..98372d5 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/NotificationsScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/NotificationsScreen.kt @@ -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 diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/OnboardingScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/OnboardingScreen.kt index abab699..8310b3a 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/OnboardingScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/OnboardingScreen.kt @@ -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) } ) diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/OnboardingSuggestionsScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/OnboardingSuggestionsScreen.kt index 9dc688e..720123c 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/OnboardingSuggestionsScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/OnboardingSuggestionsScreen.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ProfileEditScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ProfileEditScreen.kt index f42a11a..2564973 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ProfileEditScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ProfileEditScreen.kt @@ -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() diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/RelayDetailScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/RelayDetailScreen.kt index d67358e..56135bb 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/RelayDetailScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/RelayDetailScreen.kt @@ -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(), diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/RelayScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/RelayScreen.kt index 3b49700..df0a564 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/RelayScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/RelayScreen.kt @@ -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) diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/SafetyScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/SafetyScreen.kt index 53b3eaa..8ad2448 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/SafetyScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/SafetyScreen.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/SearchScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/SearchScreen.kt index 427c59d..6891aa0 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/SearchScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/SearchScreen.kt @@ -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), diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/SocialGraphScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/SocialGraphScreen.kt index ce29649..bf751c0 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/SocialGraphScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/SocialGraphScreen.kt @@ -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 ) diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/UserProfileScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/UserProfileScreen.kt index 47ea226..2e5b4e1 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/UserProfileScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/UserProfileScreen.kt @@ -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, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt index 01ecc9e..4a7fe06 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt @@ -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 diff --git a/app/src/main/kotlin/com/wisp/app/viewmodel/ComposeViewModel.kt b/app/src/main/kotlin/com/wisp/app/viewmodel/ComposeViewModel.kt index 7e2e57b..5fb8114 100644 --- a/app/src/main/kotlin/com/wisp/app/viewmodel/ComposeViewModel.kt +++ b/app/src/main/kotlin/com/wisp/app/viewmodel/ComposeViewModel.kt @@ -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+"), "_") diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ff492f..d8b8557 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -473,6 +473,7 @@ Almost there… Please wait… Publishing… + Creating account… Wisp logo Follow All (%d)