diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
index 8512b8d51..305da3fcf 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
@@ -1541,6 +1541,8 @@ object LocalCache : ILocalCache, ICacheProvider {
repliesTo.forEach { it.addZap(zapRequest, note) }
+ attachZapToLiveActivityChannel(event, note, relay)
+
refreshNewNoteObservers(note)
return true
@@ -1549,6 +1551,24 @@ object LocalCache : ILocalCache, ICacheProvider {
return false
}
+ private fun attachZapToLiveActivityChannel(
+ event: LnZapEvent,
+ note: Note,
+ relay: NormalizedRelayUrl?,
+ ) {
+ val address =
+ event.tags
+ .asSequence()
+ .mapNotNull(ATag::parseAddress)
+ .firstOrNull { it.kind == LiveActivitiesEvent.KIND }
+ ?: return
+
+ // Match zap.stream: only show zaps whose receiver is the live activity host
+ if (event.zappedAuthor().none { it == address.pubKeyHex }) return
+
+ getOrCreateLiveChannel(address).addNote(note, relay)
+ }
+
fun consume(
event: LnZapRequestEvent,
relay: NormalizedRelayUrl?,
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt
index e2d5fac54..753f5cf90 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChangeChannelMetadataNote
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatZap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderCreateChannelNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderDraftEvent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderEncryptedFile
@@ -84,6 +85,7 @@ import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEven
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
+import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
@@ -109,20 +111,24 @@ fun ChatroomMessageCompose(
accountViewModel = accountViewModel,
nav = nav,
) { canPreview ->
- NormalChatNote(
- baseNote,
- routeForLastRead,
- innerQuote,
- canPreview,
- parentBackgroundColor,
- accountViewModel,
- nav,
- onWantsToReply,
- onWantsToEditDraft,
- onScrollToNote,
- shouldHighlight,
- onHighlightFinished,
- )
+ if (baseNote.event is LnZapEvent) {
+ RenderChatZap(baseNote, accountViewModel, nav)
+ } else {
+ NormalChatNote(
+ baseNote,
+ routeForLastRead,
+ innerQuote,
+ canPreview,
+ parentBackgroundColor,
+ accountViewModel,
+ nav,
+ onWantsToReply,
+ onWantsToEditDraft,
+ onScrollToNote,
+ shouldHighlight,
+ onHighlightFinished,
+ )
+ }
}
}
}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt
new file mode 100644
index 000000000..449d500bc
--- /dev/null
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.screen.loggedIn.chats.feed.types
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.vitorpamplona.amethyst.R
+import com.vitorpamplona.amethyst.model.Note
+import com.vitorpamplona.amethyst.ui.navigation.navs.INav
+import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment
+import com.vitorpamplona.amethyst.ui.note.UserPicture
+import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
+import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
+import com.vitorpamplona.amethyst.ui.note.ZapIcon
+import com.vitorpamplona.amethyst.ui.note.showAmountInteger
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
+import com.vitorpamplona.amethyst.ui.stringRes
+import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
+import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
+import com.vitorpamplona.amethyst.ui.theme.Size20dp
+import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
+import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
+import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
+
+private const val BIG_ZAP_THRESHOLD_SATS = 50_000L
+
+@Composable
+fun RenderChatZap(
+ baseNote: Note,
+ accountViewModel: AccountViewModel,
+ nav: INav,
+) {
+ val zapEvent = baseNote.event as? LnZapEvent ?: return
+
+ val card by produceState(
+ ZapAmountCommentNotification(user = null, comment = null, amount = null),
+ baseNote,
+ ) {
+ value = accountViewModel.innerDecryptAmountMessage(baseNote) ?: value
+ }
+
+ val isBigZap =
+ remember(zapEvent) {
+ (zapEvent.amount?.toLong() ?: 0L) >= BIG_ZAP_THRESHOLD_SATS
+ }
+
+ val containerColor =
+ if (isBigZap) {
+ BitcoinOrange.copy(alpha = 0.18f)
+ } else {
+ BitcoinOrange.copy(alpha = 0.08f)
+ }
+
+ val backgroundColor = remember(containerColor) { mutableStateOf(containerColor) }
+
+ val amountText = card.amount ?: showAmountInteger(zapEvent.amount)
+
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 2.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .background(containerColor)
+ .padding(horizontal = 10.dp, vertical = 6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ ) {
+ ZapIcon(
+ if (isBigZap) Size24Modifier else Size20Modifier,
+ BitcoinOrange,
+ )
+
+ Column(modifier = Modifier.weight(1f)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ val sender = card.user
+ if (sender != null) {
+ UserPicture(sender, Size20dp, Modifier, accountViewModel, nav)
+ Spacer(StdHorzSpacer)
+ UsernameDisplay(
+ baseUser = sender,
+ fontWeight = FontWeight.Bold,
+ accountViewModel = accountViewModel,
+ )
+ } else {
+ Text(
+ text = stringRes(R.string.chat_zap_anonymous),
+ fontWeight = FontWeight.Bold,
+ )
+ }
+
+ Spacer(StdHorzSpacer)
+ Text(
+ text = stringRes(R.string.chat_zap_amount_suffix, amountText),
+ color = BitcoinOrange,
+ fontWeight = if (isBigZap) FontWeight.Bold else FontWeight.Normal,
+ )
+ }
+
+ card.comment?.takeIf { it.isNotBlank() }?.let { comment ->
+ Spacer(Modifier.padding(top = 2.dp))
+ CrossfadeToDisplayComment(
+ comment = comment,
+ backgroundColor = backgroundColor,
+ nav = nav,
+ accountViewModel = accountViewModel,
+ )
+ }
+ }
+ }
+}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt
index 6ce98ef3b..10da3f52c 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
+import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
fun filterMessagesToLiveActivities(
channel: LiveActivitiesChannel,
@@ -35,7 +36,7 @@ fun filterMessagesToLiveActivities(
relay = it,
filter =
Filter(
- kinds = listOf(LiveActivitiesChatMessageEvent.KIND),
+ kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LnZapEvent.KIND),
tags = mapOf("a" to listOfNotNull(channel.address.toValue())),
limit = 200,
since = since?.get(it)?.time,
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index b537cbcf6..63b4ad22c 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -53,6 +53,8 @@
You are using a public key and public keys are read-only. Login with a Private key to be able to boost posts
You are using a public key and public keys are read-only. Login with a Private key to like posts
No Zap Amount Setup. Long Press to change
+ zapped %1$s sats
+ Anonymous
You are using a public key and public keys are read-only. Login with a Private key to be able to send zaps
You are using a public key and public keys are read-only. Login with a Private key to be able to follow
You are using a public key and public keys are read-only. Login with a Private key to be able to unfollow