diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index 1f6c679d..45d90ae0 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -92,7 +92,8 @@ export default function PostContent({ close, openFrom, initialHighlightData, - initialPublicMessageTo + initialPublicMessageTo, + onPublishSuccess }: { defaultContent?: string parentEvent?: Event @@ -101,6 +102,8 @@ export default function PostContent({ initialHighlightData?: HighlightData /** When set, opens in public message mode with this pubkey in the mention list. */ initialPublicMessageTo?: string + /** Called after a reply/post is successfully published, before closing. */ + onPublishSuccess?: () => void }) { const { t } = useTranslation() const { pubkey, publish, checkLogin } = useNostr() @@ -927,6 +930,7 @@ export default function PostContent({ mergePublishedReplyIntoThread(cleanEvent, relayStatuses) } + onPublishSuccess?.() close() } catch (error) { // AggregateError = "Failed to publish to any relay" is already logged in NostrProvider with relayStatuses; avoid duplicate noise @@ -968,6 +972,7 @@ export default function PostContent({ } postEditorCache.clearPostCache({ defaultContent, parentEvent }) if (draftEvent) deleteDraftEventCache(draftEvent) + onPublishSuccess?.() close() } } else { diff --git a/src/components/PostEditor/index.tsx b/src/components/PostEditor/index.tsx index 571df99f..8445eb2a 100644 --- a/src/components/PostEditor/index.tsx +++ b/src/components/PostEditor/index.tsx @@ -27,7 +27,8 @@ export default function PostEditor({ setOpen, openFrom, initialHighlightData, - initialPublicMessageTo + initialPublicMessageTo, + onPublishSuccess }: { defaultContent?: string parentEvent?: Event @@ -37,6 +38,8 @@ export default function PostEditor({ initialHighlightData?: import('./HighlightEditor').HighlightData /** When set, opens in public message mode with this pubkey in the mention list. */ initialPublicMessageTo?: string + /** Called after a reply/post is successfully published, before closing. */ + onPublishSuccess?: () => void }) { const { isSmallScreen } = useScreenSize() @@ -58,9 +61,10 @@ export default function PostEditor({ openFrom={openFrom} initialHighlightData={initialHighlightData} initialPublicMessageTo={initialPublicMessageTo} + onPublishSuccess={onPublishSuccess} /> ) - }, [effectiveDefaultContent, parentEvent, openFrom, setOpen, initialHighlightData, initialPublicMessageTo]) + }, [effectiveDefaultContent, parentEvent, openFrom, setOpen, initialHighlightData, initialPublicMessageTo, onPublishSuccess]) if (isSmallScreen) { return ( diff --git a/src/components/Profile/ProfileHeaderInteractions.tsx b/src/components/Profile/ProfileHeaderInteractions.tsx new file mode 100644 index 00000000..0c1a3085 --- /dev/null +++ b/src/components/Profile/ProfileHeaderInteractions.tsx @@ -0,0 +1,179 @@ +import Content from '@/components/Content' +import UserAvatar from '@/components/UserAvatar' +import Username from '@/components/Username' +import { formatAmount } from '@/lib/lightning' +import { toNote, toProfile } from '@/lib/link' +import { useSecondaryPage } from '@/PageManager' +import Emoji from '@/components/Emoji' +import { getEmojiInfosFromEmojiTags } from '@/lib/tag' +import type { TProfileZap } from '@/hooks/useProfileInteractions' +import type { TProfileBadge } from '@/hooks/useProfileBadges' +import { Zap, MessageCircle, ThumbsUp } from 'lucide-react' +import { Skeleton } from '@/components/ui/skeleton' +import { useTranslation } from 'react-i18next' +import { Event } from 'nostr-tools' + +type Props = { + zaps: TProfileZap[] + reactions: Event[] + comments: Event[] + badges: TProfileBadge[] + loading: boolean + badgesLoading: boolean +} + +const ZAPS_PER_ROW = 4 +const ZAP_ROWS = 3 +const MAX_ZAPS = ZAPS_PER_ROW * ZAP_ROWS +const BADGES_PER_ROW = 4 +const BADGE_ROWS = 2 +const MAX_BADGES = BADGES_PER_ROW * BADGE_ROWS + +function ZapBadge({ zap }: { zap: TProfileZap }) { + const { push } = useSecondaryPage() + return ( + + ) +} + +function ReactionBadge({ event }: { event: Event }) { + const { push } = useSecondaryPage() + const emojiInfos = getEmojiInfosFromEmojiTags(event.tags) + const displayContent = event.content.trim() || (emojiInfos[0] ? emojiInfos[0].shortcode : '+') + const isPlus = displayContent === '+' + return ( + + ) +} + +function CommentBadge({ event }: { event: Event }) { + const { push } = useSecondaryPage() + return ( + + ) +} + +function BadgeItem({ badge }: { badge: TProfileBadge }) { + const imageUrl = badge.thumb ?? badge.image + const label = badge.name ?? badge.a.split(':').pop() ?? '' + if (!imageUrl) { + return ( +
+ {label.slice(0, 2)} +
+ ) + } + return ( +
+ {label} { + e.currentTarget.style.display = 'none' + const fallback = e.currentTarget.nextElementSibling as HTMLElement + if (fallback) fallback.classList.remove('hidden') + }} + /> +
+ {label.slice(0, 2)} +
+
+ ) +} + +export default function ProfileHeaderInteractions({ zaps, reactions, comments, badges, loading, badgesLoading }: Props) { + const { t } = useTranslation() + const displayZaps = zaps.slice(0, MAX_ZAPS) + const displayBadges = badges.slice(0, MAX_BADGES) + + const Section = ({ title, isEmpty, isLoading, children, skeletonCount = 6 }: { + title: string + isEmpty: boolean + isLoading: boolean + children: React.ReactNode + skeletonCount?: number + }) => ( +
+
{title}
+ {isLoading && isEmpty ? ( +
+ {Array.from({ length: skeletonCount }).map((_, i) => ( + + ))} +
+ ) : isEmpty ? ( +
{t('None')}
+ ) : ( + children + )} +
+ ) + + return ( +
+
+
+ {displayZaps.map((item) => ( + + ))} +
+
+
+
+ {reactions.map((item) => ( + + ))} +
+
+
+
+ {comments.map((item) => ( + + ))} +
+
+
+
+ {displayBadges.map((badge) => ( + + ))} +
+
+
+ ) +} diff --git a/src/components/Profile/SmartFollowings.tsx b/src/components/Profile/SmartFollowings.tsx index d255c19c..7f47aa3d 100644 --- a/src/components/Profile/SmartFollowings.tsx +++ b/src/components/Profile/SmartFollowings.tsx @@ -1,7 +1,7 @@ import { useFetchFollowings } from '@/hooks' import { toFollowingList } from '@/lib/link' import { useSmartFollowingListNavigation } from '@/PageManager' -import { useFollowList } from '@/providers/FollowListProvider' +import { useFollowListOptional } from '@/providers/FollowListProvider' import { useNostr } from '@/providers/NostrProvider' import { Skeleton } from '@/components/ui/skeleton' import { useTranslation } from 'react-i18next' @@ -9,7 +9,8 @@ import { useTranslation } from 'react-i18next' export default function SmartFollowings({ pubkey }: { pubkey: string }) { const { t } = useTranslation() const { pubkey: accountPubkey } = useNostr() - const { followings: selfFollowings } = useFollowList() + const followList = useFollowListOptional() + const selfFollowings = followList?.followings ?? [] const { followings, isFetching } = useFetchFollowings(pubkey) const { navigateToFollowingList } = useSmartFollowingListNavigation() diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 4932758e..7e68a454 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -13,7 +13,9 @@ import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { useFetchProfile } from '@/hooks' import { kinds, type NostrEvent } from 'nostr-tools' +import { createReactionDraftEvent } from '@/lib/draft-event' import { getPaymentInfoFromEvent } from '@/lib/event-metadata' +import { showSimplePublishSuccess } from '@/lib/publishing-feedback' import { toProfileEditor } from '@/lib/link' import { generateImageByPubkey } from '@/lib/pubkey' import { usePrimaryPage } from '@/contexts/primary-page-context' @@ -28,7 +30,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' -import { Copy, Ellipsis, Calendar, MapPin, Pencil, SatelliteDish, Code, Gift, Link } from 'lucide-react' +import { Copy, Ellipsis, Calendar, MapPin, Pencil, SatelliteDish, Code, Gift, Link, MessageCircle, ThumbsUp } from 'lucide-react' import { useEffect, useLayoutEffect, @@ -47,6 +49,7 @@ import ProfileFeedWithPins from './ProfileFeedWithPins' import ProfileMediaFeed from './ProfileMediaFeed' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import type { TNoteListRef } from '@/components/NoteList' +import ProfileHeaderInteractions from './ProfileHeaderInteractions' import SmartFollowings from './SmartFollowings' import SmartMuteLink from './SmartMuteLink' import SmartRelays from './SmartRelays' @@ -59,6 +62,8 @@ import { } from '@/components/ScheduleVideoCallDialog' import RawEventDialog from '@/components/NoteOptions/RawEventDialog' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { useProfileInteractions } from '@/hooks/useProfileInteractions' +import { useProfileBadges } from '@/hooks/useProfileBadges' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' import { nip66Service } from '@/services/nip66.service' @@ -182,7 +187,7 @@ export default function Profile({ const mediaFeedRef = useRef(null) const { profile, isFetching } = useFetchProfile(id) - const { pubkey: accountPubkey } = useNostr() + const { pubkey: accountPubkey, publish, checkLogin } = useNostr() const [paymentInfo, setPaymentInfo] = useState | null>(null) const [profileEvent, setProfileEvent] = useState(undefined) const [openZapDialog, setOpenZapDialog] = useState(false) @@ -191,6 +196,8 @@ export default function Profile({ const [openScheduleOwnCall, setOpenScheduleOwnCall] = useState(false) const [openScheduleInPersonMeeting, setOpenScheduleInPersonMeeting] = useState(false) const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false) + const [openSelfReply, setOpenSelfReply] = useState(false) + const [selfReacting, setSelfReacting] = useState(false) const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays() const { relaySets, favoriteRelays } = useFavoriteRelays() @@ -280,6 +287,10 @@ export default function Profile({ [profile] ) const isSelf = accountPubkey === profile?.pubkey + const { zaps: profileZaps, reactions: profileReactions, comments: profileComments, loading: profileInteractionsLoading, refresh: refreshProfileInteractions } = + useProfileInteractions(profile?.pubkey, profileEvent) + const { badges: profileBadges, loading: profileBadgesLoading, refresh: refreshProfileBadges } = + useProfileBadges(profile?.pubkey) /** All available relays: current feed, favorites, relay sets, defaults (FAST_READ, FAST_WRITE). */ const allAvailableRelayUrls = useMemo(() => { @@ -343,6 +354,8 @@ export default function Profile({ const m = r as MutableRefObject<{ refresh: () => void } | null> m.current = { refresh: () => { + refreshProfileInteractions() + refreshProfileBadges() postsFeedRef.current?.refresh() mediaFeedRef.current?.refresh() } @@ -350,7 +363,7 @@ export default function Profile({ return () => { m.current = null } - }, []) + }, [refreshProfileInteractions, refreshProfileBadges]) useEffect(() => { if (!profile?.pubkey) return @@ -414,6 +427,7 @@ export default function Profile({ ? (url) => setOpenCallInviteTo({ pubkey, url }) : undefined } + onProfileInteractionsRefresh={refreshProfileInteractions} /> {isSelf ? ( @@ -423,6 +437,38 @@ export default function Profile({ + {profileEvent && ( + <> + setOpenSelfReply(true)}> + + {t('Reply')} + + { + if (!profileEvent) return + checkLogin(async () => { + if (selfReacting) return + setSelfReacting(true) + try { + const reaction = createReactionDraftEvent(profileEvent, '+') + const evt = await publish(reaction) + if (evt) { + showSimplePublishSuccess(t('Reaction published')) + refreshProfileInteractions() + } + } finally { + setSelfReacting(false) + } + }) + }} + disabled={selfReacting} + > + + {selfReacting ? t('Publishing...') : t('Like')} + + + + )} setOpenScheduleOwnCall(true)}> {t('Schedule a video call')} @@ -458,14 +504,23 @@ export default function Profile({ )} - ) : ( + ) : null} + {profileEvent && isSelf && ( + + )} + {!isSelf ? ( <> {mergedPaymentMethods.some((m) => m.type === 'lightning') && ( )} - )} + ) : null}
@@ -485,6 +540,14 @@ export default function Profile({
+ void /** Opens the post editor to send the call invite URL as a public message to this profile. */ onSendCallInvite?: (url: string) => void + /** Called after Like or Reply to refresh profile header interactions. */ + onProfileInteractionsRefresh?: () => void }) { const { t } = useTranslation() - const { pubkey: accountPubkey, profile } = useNostr() + const { pubkey: accountPubkey, profile, publish, checkLogin } = useNostr() const { mutePubkeySet, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = useMuteList() const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays() const { relaySets, favoriteRelays } = useFavoriteRelays() const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false) + const [openReply, setOpenReply] = useState(false) + const [reacting, setReacting] = useState(false) const [monitoringListRelayCount, setMonitoringListRelayCount] = useState(null) const [localProfileEvent, setLocalProfileEvent] = useState(profileEvent) @@ -143,6 +151,26 @@ export default function ProfileOptions({ }) } + const eventToUse = localProfileEvent || profileEvent + + const handleLike = () => { + if (!eventToUse) return + checkLogin(async () => { + if (reacting) return + setReacting(true) + try { + const reaction = createReactionDraftEvent(eventToUse, '+') + const evt = await publish(reaction) + if (evt) { + showSimplePublishSuccess(t('Reaction published')) + onProfileInteractionsRefresh?.() + } + } finally { + setReacting(false) + } + }) + } + if (pubkey === accountPubkey) return null const callInviteUrl = @@ -160,6 +188,19 @@ export default function ProfileOptions({ + {eventToUse && ( + <> + setOpenReply(true)}> + + {t('Reply')} + + + + {reacting ? t('Publishing...') : t('Like')} + + + + )} {onSendPublicMessage && ( @@ -244,8 +285,16 @@ export default function ProfileOptions({ )} - {(localProfileEvent || profileEvent) && ( - + )} + {(localProfileEvent || profileEvent) && ( + setIsRawEventDialogOpen(false)} diff --git a/src/constants.ts b/src/constants.ts index 350153be..3655d7fe 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -296,7 +296,11 @@ export const ExtendedKind = { /** NIP-52 Calendar event RSVP */ CALENDAR_EVENT_RSVP: 31925, /** NIP-A7 Spells: portable relay query filters (kind 777) */ - SPELL: 777 + SPELL: 777, + /** NIP-58 Badges: profile badges list (addressable, d=profile_badges) */ + PROFILE_BADGES: 30008, + /** NIP-58 Badges: badge definition (addressable) */ + BADGE_DEFINITION: 30009 } /** NIP-52 calendar event kinds (addressable by d-tag); use in isReplaceableEvent. */ diff --git a/src/hooks/useProfileBadges.tsx b/src/hooks/useProfileBadges.tsx new file mode 100644 index 00000000..2a859375 --- /dev/null +++ b/src/hooks/useProfileBadges.tsx @@ -0,0 +1,128 @@ +import { E_TAG_FILTER_BLOCKED_RELAY_URLS, ExtendedKind } from '@/constants' +import { queryService, replaceableEventService } from '@/services/client.service' +import { useCallback, useEffect, useRef, useState } from 'react' +import { tagNameEquals } from '@/lib/tag' +import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { useNostr } from '@/providers/NostrProvider' + +export type TProfileBadge = { + /** Badge definition coordinate (e.g. "30009:alice:bravery") */ + a: string + /** Badge award event id */ + awardId: string + /** Human-readable name from definition */ + name?: string + /** High-res image URL */ + image?: string + /** Thumbnail URL (prefer thumb over image for grid display) */ + thumb?: string +} + +/** Parse a-tag "30009:pubkey:d" into { kind, pubkey, d } */ +function parseATag(aTag: string): { kind: number; pubkey: string; d: string } | null { + const parts = aTag.split(':') + if (parts.length < 3) return null + const kind = parseInt(parts[0], 10) + if (isNaN(kind)) return null + return { kind, pubkey: parts[1], d: parts[2] } +} + +/** NIP-58: Fetches profile badges (kind 30008) and resolves badge definitions (kind 30009). */ +export function useProfileBadges(pubkey: string | undefined) { + const { pubkey: accountPubkey } = useNostr() + const { blockedRelays } = useFavoriteRelays() + const [badges, setBadges] = useState([]) + const [loading, setLoading] = useState(false) + const fetchIdRef = useRef(0) + + const fetchBadges = useCallback(async () => { + if (!pubkey) { + setBadges([]) + return + } + + const myFetchId = (fetchIdRef.current += 1) + setLoading(true) + + try { + const relayUrls = await buildComprehensiveRelayList({ + authorPubkey: pubkey, + userPubkey: accountPubkey ?? undefined, + blockedRelays: [...blockedRelays, ...E_TAG_FILTER_BLOCKED_RELAY_URLS], + includeFastReadRelays: true, + includeSearchableRelays: true, + includeProfileFetchRelays: true, + includeLocalRelays: true + }) + + const events = await queryService.fetchEvents( + relayUrls, + { authors: [pubkey], kinds: [ExtendedKind.PROFILE_BADGES], '#d': ['profile_badges'] }, + undefined + ) + const profileBadgesEvent = events.sort((a, b) => b.created_at - a.created_at)[0] + + if (!profileBadgesEvent || myFetchId !== fetchIdRef.current) { + if (myFetchId === fetchIdRef.current) setBadges([]) + return + } + + const tags = profileBadgesEvent.tags + const pairs: { a: string; e: string }[] = [] + for (let i = 0; i < tags.length - 1; i++) { + const [tagNameA, aVal] = tags[i] + const [tagNameE, eVal] = tags[i + 1] + if (tagNameA === 'a' && tagNameE === 'e' && aVal && eVal && /^[a-f0-9]{64}$/i.test(eVal)) { + pairs.push({ a: aVal, e: eVal }) + } + } + + if (pairs.length === 0) { + setBadges([]) + return + } + + const result: TProfileBadge[] = [] + for (const { a, e } of pairs) { + const parsed = parseATag(a) + if (!parsed || parsed.kind !== ExtendedKind.BADGE_DEFINITION) { + result.push({ a, awardId: e }) + continue + } + + const defEvent = await replaceableEventService.fetchReplaceableEvent( + parsed.pubkey, + parsed.kind, + parsed.d + ) + + const name = defEvent?.tags.find(tagNameEquals('name'))?.[1] + const image = defEvent?.tags.find(tagNameEquals('image'))?.[1] + const thumb = defEvent?.tags.find(tagNameEquals('thumb'))?.[1] + + result.push({ + a, + awardId: e, + name: name ?? parsed.d, + image, + thumb: thumb ?? image + }) + } + + if (myFetchId !== fetchIdRef.current) return + setBadges(result) + } catch { + if (myFetchId !== fetchIdRef.current) return + setBadges([]) + } finally { + if (myFetchId === fetchIdRef.current) setLoading(false) + } + }, [pubkey, accountPubkey, blockedRelays]) + + useEffect(() => { + fetchBadges() + }, [fetchBadges]) + + return { badges, loading, refresh: fetchBadges } +} diff --git a/src/hooks/useProfileInteractions.tsx b/src/hooks/useProfileInteractions.tsx new file mode 100644 index 00000000..8b098a48 --- /dev/null +++ b/src/hooks/useProfileInteractions.tsx @@ -0,0 +1,116 @@ +import { E_TAG_FILTER_BLOCKED_RELAY_URLS, ExtendedKind } from '@/constants' +import { getZapInfoFromEvent } from '@/lib/event-metadata' +import { queryService } from '@/services/client.service' +import { Event, Filter, kinds } from 'nostr-tools' +import { useCallback, useEffect, useRef, useState } from 'react' +import { buildComprehensiveRelayList } from '@/lib/relay-list-builder' +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { useNostr } from '@/providers/NostrProvider' + +export type TProfileZap = { + pr: string + pubkey: string + amount: number + created_at: number + comment?: string +} + +/** Fetches zaps, reactions (likes), and comments for a profile. */ +export function useProfileInteractions(pubkey: string | undefined, profileEvent: Event | undefined) { + const { pubkey: accountPubkey } = useNostr() + const { blockedRelays } = useFavoriteRelays() + const [zaps, setZaps] = useState([]) + const [reactions, setReactions] = useState([]) + const [comments, setComments] = useState([]) + const [loading, setLoading] = useState(false) + const fetchIdRef = useRef(0) + + const fetchAll = useCallback(async () => { + if (!pubkey) { + setZaps([]) + setReactions([]) + setComments([]) + return + } + + const myFetchId = (fetchIdRef.current += 1) + setLoading(true) + + try { + const relayUrls = await buildComprehensiveRelayList({ + authorPubkey: pubkey, + userPubkey: accountPubkey ?? undefined, + blockedRelays: [...blockedRelays, ...E_TAG_FILTER_BLOCKED_RELAY_URLS], + includeFastReadRelays: true, + includeSearchableRelays: true, + includeProfileFetchRelays: true, + includeLocalRelays: true + }) + + const filters: Filter[] = [{ '#p': [pubkey], kinds: [kinds.Zap], limit: 100 }] + if (profileEvent) { + filters.push({ + '#e': [profileEvent.id], + kinds: [kinds.Reaction, ExtendedKind.COMMENT], + limit: 50 + }) + } + + const collectedZaps: TProfileZap[] = [] + const collectedReactions: Event[] = [] + const collectedComments: Event[] = [] + const seenZaps = new Set() + const seenReactions = new Set() + + await queryService.fetchEvents(relayUrls, filters, { + onevent: (evt) => { + if (evt.kind === kinds.Zap) { + const info = getZapInfoFromEvent(evt) + if (!info || info.recipientPubkey !== pubkey || !info.amount || info.amount <= 0) return + if (seenZaps.has(evt.id)) return + seenZaps.add(evt.id) + collectedZaps.push({ + pr: evt.id, + pubkey: info.senderPubkey ?? evt.pubkey, + amount: info.amount, + created_at: evt.created_at, + comment: info.comment + }) + } else if (evt.kind === kinds.Reaction || evt.kind === ExtendedKind.COMMENT) { + if (seenReactions.has(evt.id)) return + seenReactions.add(evt.id) + if (evt.kind === kinds.Reaction) { + collectedReactions.push(evt) + } else { + collectedComments.push(evt) + } + } + } + }) + + if (myFetchId !== fetchIdRef.current) return + collectedZaps.sort((a, b) => b.amount - a.amount) + collectedReactions.sort((a, b) => b.created_at - a.created_at) + collectedComments.sort((a, b) => b.created_at - a.created_at) + setZaps(collectedZaps) + setReactions(collectedReactions) + setComments(collectedComments) + } catch { + if (myFetchId !== fetchIdRef.current) return + } finally { + if (myFetchId === fetchIdRef.current) setLoading(false) + } + }, [pubkey, profileEvent?.id, accountPubkey, blockedRelays]) + + useEffect(() => { + fetchAll() + }, [fetchAll]) + + return { zaps, reactions, comments, loading, refresh: fetchAll } +} + +/** @deprecated Use useProfileInteractions instead. Returns zaps only for compatibility. */ +export function useProfileZaps(pubkey: string | undefined) { + const result = useProfileInteractions(pubkey, undefined) + return { zaps: result.zaps, loading: result.loading, refresh: result.refresh } +} diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 33e83a49..328102ef 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -422,6 +422,7 @@ export default { All: 'Alle', Reactions: 'Reaktionen', Zaps: 'Zaps', + Badges: 'Abzeichen', 'Enjoying Jumble?': 'Gefรคllt dir Jumble?', 'Your donation helps me maintain Jumble and make it better! ๐Ÿ˜Š': 'Deine Spende hilft mir, Jumble zu pflegen und zu verbessern! ๐Ÿ˜Š', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index b1e93422..052d0b72 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -415,6 +415,7 @@ export default { All: 'All', Reactions: 'Reactions', Zaps: 'Zaps', + Badges: 'Badges', 'Enjoying Jumble?': 'Enjoying Jumble?', 'Your donation helps me maintain Jumble and make it better! ๐Ÿ˜Š': 'Your donation helps me maintain Jumble and make it better! ๐Ÿ˜Š',