Content is user-generated and unverified.
package com.example.quartzfeed import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.quartz.experimental.bgcommands.BasicOkHttpWebSocket import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import okhttp3.OkHttpClient // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- private val DEFAULT_RELAYS = listOf( "wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band", ) private const val FEED_PAGE_SIZE = 50 private const val MAX_NOTES_IN_MEMORY = 200 // --------------------------------------------------------------------------- // Activity // --------------------------------------------------------------------------- /** * A minimal Nostr client built on Amethyst's Quartz library. * * Lifecycle: onResume connects to relays, onPause disconnects to save battery * — the subscription registered in onCreate survives across reconnects. */ class MainActivity : ComponentActivity() { private val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private val client = buildNostrClient(appScope) private val signer = NostrSignerInternal(KeyPair()) /** Compose-observable feed. The relay subscription appends; the UI reads. */ private val notes = mutableStateListOf<Event>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) subscribeToTextNotes() setContent { MaterialTheme { FeedScreen(notes = notes, onPublish = ::publish) } } } override fun onResume() { super.onResume() client.connect() } override fun onPause() { client.disconnect() super.onPause() } // -- Nostr operations --------------------------------------------------- private fun subscribeToTextNotes() { val filter = Filter( kinds = listOf(TextNoteEvent.KIND), limit = FEED_PAGE_SIZE, ) val perRelay = DEFAULT_RELAYS.associateWith { listOf(filter) } client.req(filters = { perRelay }, onEvent = ::onIncomingNote) } private fun onIncomingNote(event: Event) { if (notes.any { it.id == event.id }) return notes.add(0, event) if (notes.size > MAX_NOTES_IN_MEMORY) { notes.removeAt(notes.lastIndex) } } private fun publish(content: String) { appScope.launch { val signed = signer.sign(TextNoteEvent.build(content)) client.send(signed) } } } private fun buildNostrClient(scope: CoroutineScope): NostrClient { val http = OkHttpClient() val sockets = BasicOkHttpWebSocket.Builder { _ -> http } return NostrClient(sockets, scope) } // --------------------------------------------------------------------------- // UI // --------------------------------------------------------------------------- @OptIn(ExperimentalMaterial3Api::class) @Composable private fun FeedScreen( notes: List<Event>, onPublish: (String) -> Unit, ) { Scaffold( topBar = { TopAppBar(title = { Text("QuartzFeed") }) }, ) { padding -> Column( modifier = Modifier .padding(padding) .fillMaxSize(), ) { Composer(onPublish = onPublish) HorizontalDivider() NoteList(notes = notes) } } } @Composable private fun Composer(onPublish: (String) -> Unit) { var draft by remember { mutableStateOf("") } Row( modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically, ) { OutlinedTextField( value = draft, onValueChange = { draft = it }, modifier = Modifier.weight(1f), placeholder = { Text("Post a note…") }, ) Spacer(Modifier.width(8.dp)) Button( onClick = { onPublish(draft) draft = "" }, enabled = draft.isNotBlank(), ) { Text("Send") } } } @Composable private fun NoteList(notes: List<Event>) { LazyColumn( contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { items(notes, key = { it.id }) { note -> NoteCard(note = note) } } } @Composable private fun NoteCard(note: Event) { ElevatedCard(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(12.dp)) { Text( text = note.pubKey.take(12) + "…", style = MaterialTheme.typography.labelSmall, ) Spacer(Modifier.height(4.dp)) Text(text = note.content) } } }
Content is user-generated and unverified.
    MainActivity.kt: Build a Nostr Client with Jetpack Compose | Claude