feat: enhance zap integration with backend stats and UI improvements
- Updated ContentDetailModal to display zap statistics from both Nostr and backend sources, improving visibility of zap activities. - Refactored zap data handling to merge Nostr relay and backend zap stats, ensuring accurate totals and recent zapper information. - Introduced a new function to fetch backend zap stats, enhancing the modal's responsiveness to user interactions. - Enhanced error handling and added mock data support in the IndeehubApiService for development purposes. These changes improve the user experience by providing comprehensive zap insights and ensuring the UI reflects real-time data accurately.
This commit is contained in:
@@ -143,8 +143,8 @@
|
||||
<span class="text-white font-medium">{{ content.creator }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Zaps Section (Primal/Yakihonne style: profile pics + amounts) -->
|
||||
<div v-if="zapsList.length > 0" class="mb-6">
|
||||
<!-- Zaps Section (Nostr + backend BTCPay zaps: profile pics + amounts) -->
|
||||
<div v-if="totalZapSats > 0" class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<svg class="w-5 h-5 text-[#F7931A]" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M13 3l-2 7h5l-6 11 2-7H7l6-11z"/>
|
||||
@@ -157,7 +157,7 @@
|
||||
v-for="(zap, idx) in displayZaps"
|
||||
:key="zap.pubkey + '-' + zap.timestamp + '-' + idx"
|
||||
class="zap-avatar-pill"
|
||||
:title="getZapperName(zap.pubkey) + ' — ' + zap.amount.toLocaleString() + ' sats'"
|
||||
:title="zap.amount > 0 ? getZapperName(zap.pubkey) + ' — ' + zap.amount.toLocaleString() + ' sats' : getZapperName(zap.pubkey) + ' zapped'"
|
||||
>
|
||||
<img
|
||||
:src="getZapperPicture(zap.pubkey)"
|
||||
@@ -287,6 +287,7 @@ import { useAuth } from '../composables/useAuth'
|
||||
import { useAccounts } from '../composables/useAccounts'
|
||||
import { useNostr } from '../composables/useNostr'
|
||||
import { libraryService } from '../services/library.service'
|
||||
import { indeehubApiService } from '../services/indeehub-api.service'
|
||||
import type { Content } from '../types/content'
|
||||
import VideoPlayer from './VideoPlayer.vue'
|
||||
import SubscriptionModal from './SubscriptionModal.vue'
|
||||
@@ -378,10 +379,35 @@ const reactionCounts = computed(() => nostr.reactionCounts.value)
|
||||
const isLoadingComments = computed(() => nostr.isLoading.value)
|
||||
const commentCount = computed(() => nostr.commentCount.value)
|
||||
|
||||
// Zap data from relay
|
||||
const zapsList = computed(() => nostr.zaps.value)
|
||||
// Backend zap stats (BTCPay in-app zaps) so modal shows total + who zapped
|
||||
const backendZapStats = ref<{
|
||||
zapCount: number
|
||||
zapAmountSats: number
|
||||
recentZapperPubkeys: string[]
|
||||
} | null>(null)
|
||||
|
||||
// Zap data: merge Nostr relay (9735) + backend so in-app zaps show too
|
||||
const zapsList = computed(() => {
|
||||
const fromNostr = nostr.zaps.value
|
||||
const backend = backendZapStats.value
|
||||
const nostrPubkeys = new Set(fromNostr.map((z) => z.pubkey))
|
||||
const fromBackend: { pubkey: string; amount: number; timestamp: number }[] = []
|
||||
if (backend?.recentZapperPubkeys?.length) {
|
||||
for (const pk of backend.recentZapperPubkeys) {
|
||||
if (!nostrPubkeys.has(pk)) {
|
||||
fromBackend.push({ pubkey: pk, amount: 0, timestamp: 0 })
|
||||
}
|
||||
}
|
||||
}
|
||||
const merged = [...fromNostr, ...fromBackend]
|
||||
return merged.sort((a, b) => b.timestamp - a.timestamp)
|
||||
})
|
||||
const displayZaps = computed(() => zapsList.value.slice(0, 8))
|
||||
const totalZapSats = computed(() => zapsList.value.reduce((sum, z) => sum + z.amount, 0))
|
||||
const totalZapSats = computed(() => {
|
||||
const fromNostr = nostr.zaps.value.reduce((sum, z) => sum + z.amount, 0)
|
||||
const fromBackend = backendZapStats.value?.zapAmountSats ?? 0
|
||||
return fromNostr + fromBackend
|
||||
})
|
||||
|
||||
// User's existing reaction read from relay (not local state)
|
||||
const userReaction = computed(() => nostr.userContentReaction.value)
|
||||
@@ -400,6 +426,7 @@ watch(() => props.content?.id, (newId) => {
|
||||
if (newId && props.isOpen) {
|
||||
loadSocialData(newId)
|
||||
checkRentalAccess()
|
||||
fetchBackendZapStats(newId)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -407,13 +434,23 @@ watch(() => props.isOpen, (open) => {
|
||||
if (open && props.content?.id) {
|
||||
loadSocialData(props.content.id)
|
||||
checkRentalAccess()
|
||||
fetchBackendZapStats(props.content.id)
|
||||
} else if (!open) {
|
||||
// Reset rental state when modal closes
|
||||
hasActiveRental.value = false
|
||||
rentalExpiresAt.value = null
|
||||
backendZapStats.value = null
|
||||
}
|
||||
})
|
||||
|
||||
async function fetchBackendZapStats(contentId: string) {
|
||||
try {
|
||||
const data = await indeehubApiService.getZapStats([contentId])
|
||||
backendZapStats.value = data[contentId] ?? null
|
||||
} catch {
|
||||
backendZapStats.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function loadSocialData(contentId: string) {
|
||||
nostr.cleanup()
|
||||
nostr.subscribeToContent(contentId)
|
||||
@@ -505,8 +542,8 @@ function handleZap() {
|
||||
}
|
||||
|
||||
function handleZapped(_amount: number) {
|
||||
// The zap was confirmed — the relay subscription will pick up
|
||||
// the zap receipt automatically and update zapsList.
|
||||
// In-app zaps go through BTCPay; refetch backend stats so modal updates.
|
||||
if (props.content?.id) fetchBackendZapStats(props.content.id)
|
||||
}
|
||||
|
||||
function getZapperName(pubkey: string): string {
|
||||
@@ -522,6 +559,7 @@ function getZapperPicture(pubkey: string): string {
|
||||
}
|
||||
|
||||
function formatZapAmount(sats: number): string {
|
||||
if (sats <= 0) return '—'
|
||||
if (sats >= 1_000_000) return (sats / 1_000_000).toFixed(1) + 'M'
|
||||
if (sats >= 1_000) return (sats / 1_000).toFixed(sats >= 10_000 ? 0 : 1) + 'k'
|
||||
return sats.toLocaleString()
|
||||
|
||||
@@ -45,21 +45,7 @@
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
||||
{{ getCommentCount(content.id) }}
|
||||
</span>
|
||||
<!-- Zaps: avatar stack + count (Primal/Yakihonne style) -->
|
||||
<span
|
||||
v-if="getZapCount(content.id) > 0"
|
||||
class="social-badge zap-badge flex items-center gap-1"
|
||||
:title="getZapCount(content.id) + ' zap(s)'"
|
||||
>
|
||||
<span class="flex -space-x-1.5">
|
||||
<img
|
||||
v-for="pk in getZapperPubkeys(content.id).slice(0, 3)"
|
||||
:key="pk"
|
||||
:src="zapperAvatarUrl(pk)"
|
||||
:alt="''"
|
||||
class="w-4 h-4 rounded-full border border-black/40 object-cover ring-1 ring-white/20"
|
||||
/>
|
||||
</span>
|
||||
<span class="social-badge" v-if="getZapCount(content.id) > 0">
|
||||
<svg class="w-3 h-3 text-[#F7931A]" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M13 3l-2 7h5l-6 11 2-7H7l6-11z"/>
|
||||
</svg>
|
||||
@@ -143,23 +129,6 @@ function getZapCount(contentId: string): number {
|
||||
return discovery + backend
|
||||
}
|
||||
|
||||
function getZapperPubkeys(contentId: string): string[] {
|
||||
const discovery = getStats(contentId).recentZapperPubkeys ?? []
|
||||
const backend = backendZapStats.value[contentId]?.recentZapperPubkeys ?? []
|
||||
const seen = new Set<string>()
|
||||
const merged: string[] = []
|
||||
for (const pk of [...discovery, ...backend]) {
|
||||
if (seen.has(pk) || merged.length >= 5) continue
|
||||
seen.add(pk)
|
||||
merged.push(pk)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
function zapperAvatarUrl(pubkey: string): string {
|
||||
return `https://robohash.org/${pubkey}.png`
|
||||
}
|
||||
|
||||
const sliderRef = ref<HTMLElement | null>(null)
|
||||
const canScrollLeft = ref(false)
|
||||
const canScrollRight = ref(true)
|
||||
|
||||
@@ -206,15 +206,48 @@ class IndeehubApiService {
|
||||
|
||||
/**
|
||||
* Get zap stats for film cards (count, amount, recent zapper pubkeys) by project id.
|
||||
* In dev, merges mock zap data for the first few projects so the UI is visible without real zaps.
|
||||
*/
|
||||
async getZapStats(projectIds: string[]): Promise<Record<string, { zapCount: number; zapAmountSats: number; recentZapperPubkeys: string[] }>> {
|
||||
if (projectIds.length === 0) return {}
|
||||
const ids = projectIds.join(',')
|
||||
const response = await this.client.get<Record<string, { zapCount: number; zapAmountSats: number; recentZapperPubkeys: string[] }>>(
|
||||
'/zaps/stats',
|
||||
{ params: { projectIds: ids } },
|
||||
)
|
||||
return response.data ?? {}
|
||||
let data: Record<string, { zapCount: number; zapAmountSats: number; recentZapperPubkeys: string[] }> = {}
|
||||
try {
|
||||
const response = await this.client.get<Record<string, { zapCount: number; zapAmountSats: number; recentZapperPubkeys: string[] }>>(
|
||||
'/zaps/stats',
|
||||
{ params: { projectIds: ids } },
|
||||
)
|
||||
data = response.data ?? {}
|
||||
} catch {
|
||||
data = {}
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
Object.assign(data, this.getMockZapStats(projectIds))
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock zap stats for dev so cards and modal show the zap UI.
|
||||
* Uses fake pubkeys so robohash avatars display.
|
||||
*/
|
||||
private getMockZapStats(projectIds: string[]): Record<string, { zapCount: number; zapAmountSats: number; recentZapperPubkeys: string[] }> {
|
||||
const mockPubkeys = [
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
'c7937c1d0f8d0a2e8a0e8a0e8a0e8a0e8a0e8a0e8a0e8a0e8a0e8a0e8a0e8a',
|
||||
]
|
||||
const result: Record<string, { zapCount: number; zapAmountSats: number; recentZapperPubkeys: string[] }> = {}
|
||||
const mockCounts = [3, 7, 1, 12, 2]
|
||||
const mockSats = [2100, 50000, 1000, 100000, 420]
|
||||
projectIds.slice(0, 5).forEach((id, i) => {
|
||||
result[id] = {
|
||||
zapCount: mockCounts[i % mockCounts.length],
|
||||
zapAmountSats: mockSats[i % mockSats.length],
|
||||
recentZapperPubkeys: mockPubkeys.slice(0, (i % 3) + 1),
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user