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:
@@ -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+"), "_")
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user