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)
}
}
}