feat: add design-time previews for the activity cards

Five previews covering the gradient cards: like (+ heart), custom emoji
reaction, lightning zap, cashu nutzap, and onchain zap — each rendered in
the light/dark ThemeComparisonColumn with a hand-built target note so the
embedded makeItShort post shows. RenderLnZap splits into a thin resolver
plus a stateless RenderLnZapCard so the preview can feed the decrypted
sender/amount/comment directly instead of waiting on async decryption.

https://claude.ai/code/session_01LM3KTECMMAdNBHZfs1dANa
This commit is contained in:
Claude
2026-06-12 23:37:43 +00:00
parent 5e75fd1aae
commit 953f1fe66c
2 changed files with 247 additions and 1 deletions
@@ -0,0 +1,225 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent
/**
* Design-time fixtures for the activity cards (reaction / lightning zap /
* cashu nutzap / onchain zap). The notes are built by hand and never consumed
* into [LocalCache], except for the users behind the avatars.
*/
private object ActivityCardPreviewData {
val senderHex = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
val authorHex = "ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b"
const val SIG = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
const val TIME = 1_700_000_000L
val targetId = "a".repeat(64)
fun targetNote(): Note =
Note(targetId).apply {
event =
TextNoteEvent(
targetId,
authorHex,
TIME,
emptyArray(),
"Just finished the new release — zaps, nests, and a faster feed. What should we build next?",
SIG,
)
author = LocalCache.getOrCreateUser(authorHex)
replyTo = emptyList()
}
fun reactionNote(reaction: String): Note {
val id = "b".repeat(64)
return Note(id).apply {
event =
ReactionEvent(
id,
senderHex,
TIME,
arrayOf(arrayOf("e", targetId), arrayOf("p", authorHex)),
reaction,
SIG,
)
author = LocalCache.getOrCreateUser(senderHex)
replyTo = listOf(targetNote())
}
}
fun nutzapNote(): Note {
val id = "c".repeat(64)
return Note(id).apply {
event =
NutzapEvent(
id,
senderHex,
TIME,
arrayOf(
arrayOf("proof", """{"amount":2100,"id":"00ad268c4d1f5826","secret":"s","C":"c"}"""),
arrayOf("u", "https://mint.example.com"),
arrayOf("e", targetId),
arrayOf("p", authorHex),
),
"Have some cashu, great post!",
SIG,
)
author = LocalCache.getOrCreateUser(senderHex)
replyTo = listOf(targetNote())
}
}
fun lnZapNote(): Note {
val id = "d".repeat(64)
// Receipts are signed by the lightning provider; the preview skips the
// embedded request and feeds the decrypted card to RenderLnZapCard directly.
return Note(id).apply {
event = null
author = null
replyTo = listOf(targetNote())
}
}
fun onchainZapNote(): Note {
val id = "e".repeat(64)
return Note(id).apply {
event =
OnchainZapEvent(
id,
senderHex,
TIME,
arrayOf(
arrayOf("i", "bitcoin:tx:" + "ab".repeat(32)),
arrayOf("amount", "150000"),
arrayOf("e", targetId),
arrayOf("p", authorHex),
),
"Welcome to the timechain!",
SIG,
)
author = LocalCache.getOrCreateUser(senderHex)
replyTo = listOf(targetNote())
}
}
}
@Preview
@Composable
fun LikeActivityCardPreview() {
val accountViewModel = mockAccountViewModel()
ThemeComparisonColumn {
RenderReaction(
note = ActivityCardPreviewData.reactionNote("+"),
quotesLeft = 1,
backgroundColor = remember { mutableStateOf(Color.Transparent) },
accountViewModel = accountViewModel,
nav = EmptyNav(),
)
}
}
@Preview
@Composable
fun EmojiReactionActivityCardPreview() {
val accountViewModel = mockAccountViewModel()
ThemeComparisonColumn {
RenderReaction(
note = ActivityCardPreviewData.reactionNote("🤙"),
quotesLeft = 1,
backgroundColor = remember { mutableStateOf(Color.Transparent) },
accountViewModel = accountViewModel,
nav = EmptyNav(),
)
}
}
@Preview
@Composable
fun LnZapActivityCardPreview() {
val accountViewModel = mockAccountViewModel()
ThemeComparisonColumn {
RenderLnZapCard(
note = ActivityCardPreviewData.lnZapNote(),
card =
ZapAmountCommentNotification(
user = LocalCache.getOrCreateUser(ActivityCardPreviewData.senderHex),
comment = "Great post! ⚡",
amount = "100k",
),
recipientKey = ActivityCardPreviewData.authorHex,
quotesLeft = 1,
backgroundColor = remember { mutableStateOf(Color.Transparent) },
accountViewModel = accountViewModel,
nav = EmptyNav(),
)
}
}
@Preview
@Composable
fun NutzapActivityCardPreview() {
val accountViewModel = mockAccountViewModel()
ThemeComparisonColumn {
RenderNutzap(
note = ActivityCardPreviewData.nutzapNote(),
quotesLeft = 1,
backgroundColor = remember { mutableStateOf(Color.Transparent) },
accountViewModel = accountViewModel,
nav = EmptyNav(),
)
}
}
@Preview
@Composable
fun OnchainZapActivityCardPreview() {
val accountViewModel = mockAccountViewModel()
ThemeComparisonColumn {
RenderOnchainZap(
note = ActivityCardPreviewData.onchainZapNote(),
quotesLeft = 1,
backgroundColor = remember { mutableStateOf(Color.Transparent) },
accountViewModel = accountViewModel,
nav = EmptyNav(),
)
}
}
@@ -103,7 +103,28 @@ fun RenderLnZap(
val zapEvent = note.event as? LnZapEvent ?: return
val card by parseAuthorCommentAndAmount(note, accountViewModel)
val recipientKey = zapEvent.zappedAuthor().firstOrNull()
RenderLnZapCard(
note = note,
card = card,
recipientKey = zapEvent.zappedAuthor().firstOrNull(),
quotesLeft = quotesLeft,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
@Composable
fun RenderLnZapCard(
note: Note,
card: ZapAmountCommentNotification,
recipientKey: String?,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val orange = MaterialTheme.colorScheme.bitcoinColor
ActivityCardFrame(orange) {