feat: screener for message requests #24
@@ -27,16 +27,19 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.toShape
|
import androidx.compose.material3.toShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coop.composeapp.generated.resources.Res
|
import coop.composeapp.generated.resources.Res
|
||||||
import coop.composeapp.generated.resources.ic_arrow_back
|
import coop.composeapp.generated.resources.ic_arrow_back
|
||||||
import coop.composeapp.generated.resources.ic_chat
|
import coop.composeapp.generated.resources.ic_chat
|
||||||
@@ -44,6 +47,7 @@ import coop.composeapp.generated.resources.ic_share
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
|
import rust.nostr.sdk.Timestamp
|
||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
import su.reya.coop.LocalNostrViewModel
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
@@ -64,9 +68,9 @@ fun ProfileScreen(pubkey: String, screening: Boolean = false) {
|
|||||||
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
|
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
|
||||||
|
|
||||||
val metadataFlow = remember(pubkey) { viewModel.getMetadata(pubkey) }
|
val metadataFlow = remember(pubkey) { viewModel.getMetadata(pubkey) }
|
||||||
val metadata by metadataFlow.collectAsState(initial = null)
|
val metadata by metadataFlow.collectAsStateWithLifecycle()
|
||||||
val profile = metadata?.asRecord()
|
|
||||||
|
|
||||||
|
val profile = metadata?.asRecord()
|
||||||
val displayName = profile?.displayName ?: profile?.name ?: "No name"
|
val displayName = profile?.displayName ?: profile?.name ?: "No name"
|
||||||
val nip05 = profile?.nip05 ?: pubkey.short()
|
val nip05 = profile?.nip05 ?: pubkey.short()
|
||||||
val picture = profile?.picture
|
val picture = profile?.picture
|
||||||
@@ -79,6 +83,22 @@ fun ProfileScreen(pubkey: String, screening: Boolean = false) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var addressVerified by remember { mutableStateOf(false) }
|
||||||
|
var lastActivity by remember { mutableStateOf<Timestamp?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(screening) {
|
||||||
|
if (screening) {
|
||||||
|
scope.launch {
|
||||||
|
// Verify NIP-05 address if present
|
||||||
|
profile?.nip05?.let { it ->
|
||||||
|
viewModel.verifyAddress(pubkey, it).let { addressVerified = it }
|
||||||
|
}
|
||||||
|
// Get the last activity
|
||||||
|
viewModel.verifyActivity(pubkey)?.let { lastActivity = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
|||||||
@@ -947,4 +947,34 @@ class Nostr {
|
|||||||
throw IllegalStateException("Failed to search nostr: ${e.message}", e)
|
throw IllegalStateException("Failed to search nostr: ${e.message}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun verifyAddress(pubkey: PublicKey, address: String): Boolean {
|
||||||
|
try {
|
||||||
|
val address = Nip05Address.parse(address)
|
||||||
|
val profile = profileFromAddress(HttpClient(), address)
|
||||||
|
|
||||||
|
return profile.publicKey() == pubkey
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("Failed to verify address: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun verifyActivity(pubkey: PublicKey): Timestamp? {
|
||||||
|
try {
|
||||||
|
val filter = Filter().author(pubkey).limit(3u)
|
||||||
|
val target = mutableMapOf<RelayUrl, List<Filter>>()
|
||||||
|
NostrManager.BOOTSTRAP_RELAYS.forEach { relay ->
|
||||||
|
target[RelayUrl.parse(relay)] = listOf(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
val events = client?.fetchEvents(
|
||||||
|
target = ReqTarget.manual(target),
|
||||||
|
timeout = Duration.parse("3s")
|
||||||
|
)
|
||||||
|
|
||||||
|
return events?.first()?.createdAt()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("Failed to get latest activity: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import rust.nostr.sdk.PublicKey
|
|||||||
import rust.nostr.sdk.RelayMetadata
|
import rust.nostr.sdk.RelayMetadata
|
||||||
import rust.nostr.sdk.RelayUrl
|
import rust.nostr.sdk.RelayUrl
|
||||||
import rust.nostr.sdk.Tag
|
import rust.nostr.sdk.Tag
|
||||||
|
import rust.nostr.sdk.Timestamp
|
||||||
import rust.nostr.sdk.UnsignedEvent
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
import su.reya.coop.blossom.BlossomClient
|
import su.reya.coop.blossom.BlossomClient
|
||||||
import su.reya.coop.storage.SecretStorage
|
import su.reya.coop.storage.SecretStorage
|
||||||
@@ -820,6 +821,24 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun verifyAddress(pubkey: PublicKey, address: String): Boolean {
|
||||||
|
return try {
|
||||||
|
nostr.verifyAddress(pubkey, address)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun verifyActivity(pubkey: PublicKey): Timestamp? {
|
||||||
|
return try {
|
||||||
|
nostr.verifyActivity(pubkey)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PublicKey.short(): String {
|
fun PublicKey.short(): String {
|
||||||
|
|||||||
Reference in New Issue
Block a user