Browse Source

bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
1789a58d30
  1. 8
      src/components/Note/Superchat.tsx
  2. 14
      src/components/Note/SuperchatPaymentMethodLabel.tsx
  3. 13
      src/components/Note/Zap.tsx
  4. 18
      src/components/Profile/ProfileWallSuperchats.tsx
  5. 2
      src/components/Titlebar/index.tsx
  6. 16
      src/components/ZapDialog/SuperchatRequestForm.tsx
  7. 254
      src/hooks/useProfileWall.tsx
  8. 12
      src/layouts/SecondaryPageLayout/index.tsx

8
src/components/Note/Superchat.tsx

@ -90,6 +90,12 @@ export default function Superchat({
showAt showAt
className="min-w-0 font-medium text-foreground/85 hover:text-foreground" className="min-w-0 font-medium text-foreground/85 hover:text-foreground"
/> />
<SuperchatPaymentMethodLabel
paytoType={paytoType}
iconOnly
className="shrink-0"
imgClassName="size-5"
/>
</div> </div>
) : ( ) : (
<> <>
@ -115,6 +121,7 @@ export default function Superchat({
)} )}
</div> </div>
) : null} ) : null}
{!isProfileWall ? (
<div <div
className={cn( className={cn(
'flex flex-wrap items-center gap-x-2 gap-y-1', 'flex flex-wrap items-center gap-x-2 gap-y-1',
@ -128,6 +135,7 @@ export default function Superchat({
/> />
<span className="text-xl font-semibold text-yellow-400/90">{t('Superchat')}</span> <span className="text-xl font-semibold text-yellow-400/90">{t('Superchat')}</span>
</div> </div>
) : null}
{comment ? ( {comment ? (
<SuperchatCommentMarkdown event={event} comment={comment} className="mt-2" /> <SuperchatCommentMarkdown event={event} comment={comment} className="mt-2" />
) : null} ) : null}

14
src/components/Note/SuperchatPaymentMethodLabel.tsx

@ -5,26 +5,32 @@ import { cn } from '@/lib/utils'
export default function SuperchatPaymentMethodLabel({ export default function SuperchatPaymentMethodLabel({
paytoType, paytoType,
className, className,
imgClassName imgClassName,
iconOnly = false
}: { }: {
/** Canonical or alias payto type (`lightning`, `monero`, `geyser`, …). */ /** Canonical or alias payto type (`lightning`, `monero`, `geyser`, …). */
paytoType: string paytoType: string
className?: string className?: string
imgClassName?: string imgClassName?: string
/** Profile wall: icon only (label in `title` for hover). */
iconOnly?: boolean
}) { }) {
const canonical = getCanonicalPaytoType(paytoType) const canonical = getCanonicalPaytoType(paytoType)
const label = getPaytoEditorTypeLabel(canonical) const label = getPaytoEditorTypeLabel(canonical)
return ( return (
<span <span
title={iconOnly ? label : undefined}
className={cn( className={cn(
'inline-flex shrink-0 items-center gap-1.5 rounded-md border border-border/60 bg-muted/40', 'inline-flex shrink-0 items-center rounded-md border border-border/60 bg-muted/40',
'px-2 py-1 text-sm font-semibold leading-none text-muted-foreground', iconOnly
? 'p-1.5 leading-none text-muted-foreground'
: 'gap-1.5 px-2 py-1 text-sm font-semibold leading-none text-muted-foreground',
className className
)} )}
> >
<PaytoTypeIcon type={paytoType} imgClassName={imgClassName} /> <PaytoTypeIcon type={paytoType} imgClassName={imgClassName} />
<span className="truncate">{label}</span> {iconOnly ? null : <span className="truncate">{label}</span>}
</span> </span>
) )
} }

13
src/components/Note/Zap.tsx

@ -100,6 +100,17 @@ export default function Zap({
showAt showAt
className="min-w-0 font-medium text-foreground/85 hover:text-foreground" className="min-w-0 font-medium text-foreground/85 hover:text-foreground"
/> />
{amount != null ? (
<span className="shrink-0 text-sm font-bold tabular-nums tracking-tight text-foreground">
{formatAmount(amount)} {t('sats')}
</span>
) : null}
<SuperchatPaymentMethodLabel
paytoType={paytoType}
iconOnly
className="shrink-0"
imgClassName="size-5"
/>
</div> </div>
) : ( ) : (
<> <>
@ -129,6 +140,7 @@ export default function Zap({
)} )}
</div> </div>
) : null} ) : null}
{!isProfileWall ? (
<div <div
className={cn( className={cn(
'flex flex-wrap items-center gap-x-2 gap-y-1', 'flex flex-wrap items-center gap-x-2 gap-y-1',
@ -147,6 +159,7 @@ export default function Zap({
</span> </span>
) : null} ) : null}
</div> </div>
) : null}
{comment ? ( {comment ? (
<SuperchatCommentMarkdown event={event} comment={comment} className="mt-2" /> <SuperchatCommentMarkdown event={event} comment={comment} className="mt-2" />
) : null} ) : null}

18
src/components/Profile/ProfileWallSuperchats.tsx

@ -1,10 +1,16 @@
import Superchat from '@/components/Note/Superchat' import Superchat from '@/components/Note/Superchat'
import Zap from '@/components/Note/Zap' import Zap from '@/components/Note/Zap'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
/** Roughly five profile-wall superchat rows before scrolling. */
const PROFILE_WALL_SUPERCHAT_SCROLL_MAX_HEIGHT = 'max-h-[28rem]'
const PROFILE_WALL_SUPERCHAT_VISIBLE_CAP = 5
export default function ProfileWallSuperchats({ export default function ProfileWallSuperchats({
superchats, superchats,
isLoading isLoading
@ -24,12 +30,20 @@ export default function ProfileWallSuperchats({
if (superchats.length === 0) return null if (superchats.length === 0) return null
const scrollable = superchats.length > PROFILE_WALL_SUPERCHAT_VISIBLE_CAP
return ( return (
<section className="mt-4 min-w-0" aria-label={t('Profile wall superchats')}> <section className="mt-4 min-w-0" aria-label={t('Profile wall superchats')}>
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground"> <h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-yellow-400/90">
{t('Superchats')} {t('Superchats')}
</h3> </h3>
<div className="space-y-2"> <div
className={cn(
'space-y-2',
scrollable &&
cn(PROFILE_WALL_SUPERCHAT_SCROLL_MAX_HEIGHT, 'overflow-y-auto overscroll-y-contain pr-1')
)}
>
{superchats.map((event) => {superchats.map((event) =>
event.kind === ExtendedKind.PAYMENT_NOTIFICATION ? ( event.kind === ExtendedKind.PAYMENT_NOTIFICATION ? (
<Superchat key={event.id} event={event} variant="profileWall" /> <Superchat key={event.id} event={event} variant="profileWall" />

2
src/components/Titlebar/index.tsx

@ -12,7 +12,7 @@ export function Titlebar({
return ( return (
<div <div
className={cn( className={cn(
'imwald-titlebar-fog sticky top-0 z-40 flex w-full min-h-12 shrink-0 flex-col justify-center overflow-visible bg-background py-1.5 [&_svg]:size-5 [&_svg]:shrink-0 select-none', 'imwald-titlebar-fog sticky top-0 z-40 flex w-full min-h-12 shrink-0 flex-row items-center justify-start overflow-visible bg-background py-1.5 [&_svg]:size-5 [&_svg]:shrink-0 select-none',
!hideBottomBorder && 'border-b border-border', !hideBottomBorder && 'border-b border-border',
className className
)} )}

16
src/components/ZapDialog/SuperchatRequestForm.tsx

@ -1,5 +1,7 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DialogFooter } from '@/components/ui/dialog' import { DialogFooter } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { Slider } from '@/components/ui/slider'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { createPaymentNotificationDraftEvent } from '@/lib/draft-event' import { createPaymentNotificationDraftEvent } from '@/lib/draft-event'
@ -30,6 +32,7 @@ export default function SuperchatRequestForm({
const { t } = useTranslation() const { t } = useTranslation()
const { publish, checkLogin, pubkey: selfPubkey } = useNostr() const { publish, checkLogin, pubkey: selfPubkey } = useNostr()
const [message, setMessage] = useState('') const [message, setMessage] = useState('')
const [minPow, setMinPow] = useState(0)
const [sending, setSending] = useState(false) const [sending, setSending] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
@ -67,7 +70,7 @@ export default function SuperchatRequestForm({
referencedEvent: paymentContext?.referencedEvent, referencedEvent: paymentContext?.referencedEvent,
addClientTag: true addClientTag: true
}) })
await publish(draft, { disableFallbacks: true }) await publish(draft, { disableFallbacks: true, minPow })
showSimplePublishSuccess(t('Superchat request sent')) showSimplePublishSuccess(t('Superchat request sent'))
onDone() onDone()
} catch (error) { } catch (error) {
@ -103,6 +106,17 @@ export default function SuperchatRequestForm({
aria-label={t('Superchat message')} aria-label={t('Superchat message')}
placeholder={t('Superchat message placeholder')} placeholder={t('Superchat message placeholder')}
/> />
<div className="mt-4 grid gap-2">
<Label htmlFor="superchat-pow">{t('Proof of Work (difficulty {{minPow}})', { minPow })}</Label>
<Slider
id="superchat-pow"
value={[minPow]}
onValueChange={([pow]) => setMinPow(pow)}
max={28}
step={1}
disabled={sending}
/>
</div>
{previewEvent && message.trim() ? ( {previewEvent && message.trim() ? (
<div className="mt-4 min-w-0"> <div className="mt-4 min-w-0">
<p className="text-xs font-medium text-muted-foreground">{t('Preview')}</p> <p className="text-xs font-medium text-muted-foreground">{t('Preview')}</p>

254
src/hooks/useProfileWall.tsx

@ -127,6 +127,130 @@ async function hydrateProfileWallSuperchatTargets(
return [...byId.values()] return [...byId.values()]
} }
function normalizeProfileEventId(profileEventId: string | undefined): string | undefined {
const id = profileEventId?.trim().toLowerCase()
return id && /^[0-9a-f]{64}$/.test(id) ? id : undefined
}
function buildProfileWallSuperchatFilters(pkNorm: string, profileId: string | undefined): Filter[] {
const filters: Filter[] = [
{ kinds: [ExtendedKind.PAYMENT_NOTIFICATION], '#p': [pkNorm], limit: 200 },
{ kinds: [kinds.Zap], '#p': [pkNorm], limit: 200 },
{ kinds: [ExtendedKind.PAYMENT_ATTESTATION], authors: [pkNorm], limit: 500 }
]
if (profileId) {
const profileCoord = getReplaceableCoordinate(kinds.Metadata, pkNorm, '')
filters.unshift(
{ kinds: [ExtendedKind.COMMENT], '#e': [profileId], limit: 200 },
{ kinds: [ExtendedKind.COMMENT], '#a': [profileCoord], limit: 200 }
)
filters.push(
{ kinds: [ExtendedKind.PAYMENT_NOTIFICATION], '#e': [profileId], limit: 200 },
{ kinds: [ExtendedKind.PAYMENT_NOTIFICATION], '#a': [profileCoord], limit: 200 },
{ kinds: [kinds.Zap], '#e': [profileId], limit: 200 }
)
}
return filters
}
/** IndexedDB + session only — no relay REQ (profile wall must paint immediately when cached). */
async function hydrateProfileWallSuperchatsFromLocalCache(
pkNorm: string,
profileEventId: string | undefined,
isEventDeleted: (event: Event) => boolean
): Promise<Event[]> {
if (!isValidPubkey(pkNorm)) return []
const profileId = normalizeProfileEventId(profileEventId)
const pool = new Map<string, Event>()
try {
for (const e of await indexedDb.getPaymentNotificationsForRecipient(pkNorm, 200)) {
pool.set(e.id, e)
}
} catch {
/* optional */
}
if (profileId) {
try {
for (const e of await indexedDb.getPaymentNotificationsForReferencedEvent(profileId, 200)) {
pool.set(e.id, e)
}
} catch {
/* optional */
}
try {
const profileCoord = getReplaceableCoordinate(kinds.Metadata, pkNorm, '')
for (const e of await indexedDb.getPaymentNotificationsForReferencedCoordinate(profileCoord, 200)) {
pool.set(e.id, e)
}
} catch {
/* optional */
}
}
try {
for (const e of await indexedDb.getPaymentAttestationsForAuthor(pkNorm, 500)) {
pool.set(e.id, e)
}
} catch {
/* optional */
}
const filters = buildProfileWallSuperchatFilters(pkNorm, profileId)
try {
for (const e of await indexedDb.getPaymentSuperchatEventsMatchingFilters(filters, 800)) {
pool.set(e.id, e)
}
} catch {
/* optional */
}
try {
const localMatches = await client.getLocalFeedEvents(
filters.map((filter) => ({ urls: [], filter: filter as TSubRequestFilter })),
{ maxMatches: 800 }
)
for (const e of localMatches) pool.set(e.id, e)
} catch {
/* optional */
}
const attestations = [...pool.values()].filter((e) => e.kind === ExtendedKind.PAYMENT_ATTESTATION)
const attestedIds = await resolveAttestedPaymentIdSet(pkNorm, attestations)
for (const e of await hydrateProfileWallSuperchatTargets(attestedIds, [])) {
pool.set(e.id, e)
}
const paymentEvents = [...pool.values()].filter(
(e) =>
(e.kind === ExtendedKind.PAYMENT_NOTIFICATION ||
e.kind === kinds.Zap ||
e.kind === ExtendedKind.ZAP_RECEIPT) &&
!isEventDeleted(e)
)
return filterAttestedProfileWallSuperchats(
paymentEvents,
attestations,
pkNorm,
profileId,
attestedIds
)
}
async function hydrateProfileWallFromLocalCache(
pkNorm: string,
profileEventId: string | undefined,
isEventDeleted: (event: Event) => boolean
): Promise<{ badges: ResolvedProfileBadge[]; superchats: Event[] }> {
const [badges, superchats] = await Promise.all([
hydrateProfileBadgesFromLocalCache(pkNorm),
hydrateProfileWallSuperchatsFromLocalCache(pkNorm, profileEventId, isEventDeleted)
])
return { badges, superchats }
}
const wallCacheByKey = new Map< const wallCacheByKey = new Map<
string, string,
{ badges: ResolvedProfileBadge[]; comments: Event[]; superchats: Event[]; lastUpdated: number } { badges: ResolvedProfileBadge[]; comments: Event[]; superchats: Event[]; lastUpdated: number }
@ -181,6 +305,29 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
const [superchats, setSuperchats] = useState<Event[]>(hasUsefulWallCache ? (cached!.superchats ?? []) : []) const [superchats, setSuperchats] = useState<Event[]>(hasUsefulWallCache ? (cached!.superchats ?? []) : [])
const [isLoading, setIsLoading] = useState(!hasUsefulWallCache) const [isLoading, setIsLoading] = useState(!hasUsefulWallCache)
const [refreshToken, setRefreshToken] = useState(0) const [refreshToken, setRefreshToken] = useState(0)
const badgesRef = useRef(badges)
const superchatsRef = useRef(superchats)
badgesRef.current = badges
superchatsRef.current = superchats
const setLoadingUnlessWallVisible = useCallback(() => {
setIsLoading(badgesRef.current.length === 0 && superchatsRef.current.length === 0)
}, [])
const applyLocalWallHydrate = useCallback(
(local: { badges: ResolvedProfileBadge[]; superchats: Event[] }) => {
if (local.badges.length > 0) {
setBadges((prev) => (prev.length > 0 ? prev : local.badges))
}
if (local.superchats.length > 0) {
setSuperchats((prev) => (prev.length > 0 ? prev : local.superchats))
}
if (local.badges.length > 0 || local.superchats.length > 0) {
setIsLoading(false)
}
},
[]
)
const relayListsKey = useMemo( const relayListsKey = useMemo(
() => relayListsContentKey(favoriteRelays, blockedRelays), () => relayListsContentKey(favoriteRelays, blockedRelays),
@ -199,10 +346,10 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
const bumpWallRefetch = useCallback(() => { const bumpWallRefetch = useCallback(() => {
wallCacheByKey.delete(cacheKey) wallCacheByKey.delete(cacheKey)
queueMicrotask(() => { queueMicrotask(() => {
setIsLoading(true) setLoadingUnlessWallVisible()
setRefreshToken((t) => t + 1) setRefreshToken((t) => t + 1)
}) })
}, [cacheKey]) }, [cacheKey, setLoadingUnlessWallVisible])
const scheduleManualWallRefetch = useCallback(() => { const scheduleManualWallRefetch = useCallback(() => {
if (manualRefreshBumpScheduledRef.current) return if (manualRefreshBumpScheduledRef.current) return
@ -210,23 +357,26 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
wallCacheByKey.delete(cacheKey) wallCacheByKey.delete(cacheKey)
queueMicrotask(() => { queueMicrotask(() => {
manualRefreshBumpScheduledRef.current = false manualRefreshBumpScheduledRef.current = false
setIsLoading(true) setLoadingUnlessWallVisible()
setRefreshToken((t) => t + 1) setRefreshToken((t) => t + 1)
}) })
}, [cacheKey]) }, [cacheKey, setLoadingUnlessWallVisible])
useEffect(() => { useEffect(() => {
if (!isValidPubkey(pkNormForHydrate)) return if (!isValidPubkey(pkNormForHydrate)) return
let cancelled = false let cancelled = false
void hydrateProfileBadgesFromLocalCache(pkNormForHydrate).then((local) => { void hydrateProfileWallFromLocalCache(
if (cancelled || local.length === 0) return pkNormForHydrate,
setBadges((prev) => (prev.length > 0 ? prev : local)) profileEventId,
setIsLoading(false) isEventDeletedRef.current
).then((local) => {
if (cancelled) return
applyLocalWallHydrate(local)
}) })
return () => { return () => {
cancelled = true cancelled = true
} }
}, [pkNormForHydrate]) }, [pkNormForHydrate, profileEventId, applyLocalWallHydrate])
useEffect(() => { useEffect(() => {
const pk = normalizeWallRefreshPubkey(pkNormForHydrate) const pk = normalizeWallRefreshPubkey(pkNormForHydrate)
@ -309,6 +459,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
try { try {
const pkNorm = userIdToPubkey(pubkey) || pubkey const pkNorm = userIdToPubkey(pubkey) || pubkey
if (!isValidPubkey(pkNorm)) { if (!isValidPubkey(pkNorm)) {
if (!cancelled && runGen === runGenRef.current) setIsLoading(false)
return return
} }
@ -331,22 +482,26 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
useGlobalRelayBootstrapRef.current useGlobalRelayBootstrapRef.current
) )
const localBadges = await hydrateProfileBadgesFromLocalCache(pkNorm) const localWall = await hydrateProfileWallFromLocalCache(
if (!cancelled && localBadges.length > 0) { pkNorm,
setBadges(localBadges) profileEventId,
setIsLoading(false) isEventDeletedRef.current
} else if (!cancelled) { )
if (!cancelled) {
applyLocalWallHydrate(localWall)
if (localWall.badges.length === 0 && localWall.superchats.length === 0) {
setIsLoading(true) setIsLoading(true)
} }
}
// --- Badges (NIP-58): show cache first; relay refresh may upgrade list/definitions --- // --- Badges (NIP-58): IndexedDB first; relay refresh may upgrade list/definitions ---
let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls, { let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls, {
foreground: true, foreground: true,
cacheFirst: false cacheFirst: true
}) })
if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) { if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) {
const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls, { const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls, {
cacheFirst: false cacheFirst: true
}) })
if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy
} }
@ -366,40 +521,36 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
) )
if (cancelled) return if (cancelled) return
if (resolvedBadges.length > 0 || localBadges.length === 0) { if (resolvedBadges.length > 0 || localWall.badges.length === 0) {
setBadges(resolvedBadges) setBadges(resolvedBadges)
} }
setIsLoading(false)
// --- Wall comments (kind 1111) and attested superchats (9735 / 9740 + 9741) --- // --- Wall comments (kind 1111) and attested superchats (9735 / 9740 + 9741) ---
let wallComments: Event[] = [] let wallComments: Event[] = []
let wallSuperchats: Event[] = [] let wallSuperchats: Event[] = localWall.superchats
const profileId = const profileId = normalizeProfileEventId(profileEventId)
profileEventId?.trim().toLowerCase() && /^[0-9a-f]{64}$/.test(profileEventId.trim()) const filters = buildProfileWallSuperchatFilters(pkNorm, profileId)
? profileEventId.trim().toLowerCase()
: undefined
if (relayUrls.length > 0) {
const profileCoord = getReplaceableCoordinate(kinds.Metadata, pkNorm, '')
const filters: Filter[] = [
{ kinds: [ExtendedKind.PAYMENT_NOTIFICATION], '#p': [pkNorm], limit: 200 },
{ kinds: [kinds.Zap], '#p': [pkNorm], limit: 200 },
{ kinds: [ExtendedKind.PAYMENT_ATTESTATION], authors: [pkNorm], limit: 500 }
]
if (profileId) {
filters.unshift(
{ kinds: [ExtendedKind.COMMENT], '#e': [profileId], limit: 200 },
{ kinds: [ExtendedKind.COMMENT], '#a': [profileCoord], limit: 200 }
)
filters.push(
{ kinds: [ExtendedKind.PAYMENT_NOTIFICATION], '#e': [profileId], limit: 200 },
{ kinds: [ExtendedKind.PAYMENT_NOTIFICATION], '#a': [profileCoord], limit: 200 },
{ kinds: [kinds.Zap], '#e': [profileId], limit: 200 }
)
}
const pool = new Map<string, Event>() const pool = new Map<string, Event>()
for (const e of localWall.superchats) pool.set(e.id, e)
try { try {
const idbPayments = await indexedDb.getPaymentNotificationsForRecipient(pkNorm, 200) for (const e of await indexedDb.getPaymentNotificationsForRecipient(pkNorm, 200)) {
for (const e of idbPayments) pool.set(e.id, e) pool.set(e.id, e)
}
} catch {
/* optional */
}
try {
for (const e of await indexedDb.getPaymentAttestationsForAuthor(pkNorm, 500)) {
pool.set(e.id, e)
}
} catch {
/* optional */
}
try {
for (const e of await indexedDb.getPaymentSuperchatEventsMatchingFilters(filters, 800)) {
pool.set(e.id, e)
}
} catch { } catch {
/* optional */ /* optional */
} }
@ -410,8 +561,10 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
) )
for (const e of localMatches) pool.set(e.id, e) for (const e of localMatches) pool.set(e.id, e)
} catch { } catch {
/* ignore */ /* optional */
} }
if (relayUrls.length > 0) {
try { try {
const rows = await Promise.all( const rows = await Promise.all(
filters.map((filter) => filters.map((filter) =>
@ -427,15 +580,17 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
for (const e of batch) pool.set(e.id, e) for (const e of batch) pool.set(e.id, e)
} }
} catch { } catch {
/* ignore */ /* optional */
}
} }
const attestations = [...pool.values()].filter( const attestations = [...pool.values()].filter(
(e) => e.kind === ExtendedKind.PAYMENT_ATTESTATION (e) => e.kind === ExtendedKind.PAYMENT_ATTESTATION
) )
const attestedIds = await resolveAttestedPaymentIdSet(pkNorm, attestations) const attestedIds = await resolveAttestedPaymentIdSet(pkNorm, attestations)
const hydratedTargets = await hydrateProfileWallSuperchatTargets(attestedIds, relayUrls) for (const e of await hydrateProfileWallSuperchatTargets(attestedIds, relayUrls)) {
for (const e of hydratedTargets) pool.set(e.id, e) pool.set(e.id, e)
}
if (profileId) { if (profileId) {
wallComments = [...pool.values()] wallComments = [...pool.values()]
@ -462,7 +617,6 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
profileId, profileId,
attestedIds attestedIds
) )
}
if (cancelled) return if (cancelled) return
setComments(wallComments) setComments(wallComments)
@ -487,7 +641,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
return () => { return () => {
cancelled = true cancelled = true
} }
}, [pubkey, profileEventId, cacheKey, refreshToken]) }, [pubkey, profileEventId, cacheKey, refreshToken, applyLocalWallHydrate])
const refresh = useCallback(() => { const refresh = useCallback(() => {
scheduleManualWallRefetch() scheduleManualWallRefetch()

12
src/layouts/SecondaryPageLayout/index.tsx

@ -176,20 +176,23 @@ function SecondaryPageTitlebar({
if (titlebar) { if (titlebar) {
return ( return (
<Titlebar <Titlebar
className={cn('flex min-w-0 items-center gap-2', titlebarInset, stickyClass)} className={cn(titlebarInset, stickyClass)}
hideBottomBorder={hideBottomBorder} hideBottomBorder={hideBottomBorder}
> >
<div className="flex w-full min-w-0 items-center gap-2">
<ReadOnlySessionIndicator variant="titlebar" /> <ReadOnlySessionIndicator variant="titlebar" />
<div className="min-w-0 flex-1">{titlebar}</div> <div className="min-w-0 flex-1">{titlebar}</div>
{isSmallScreen ? <ActiveRelaysTitlebarButton /> : null} {isSmallScreen ? <ActiveRelaysTitlebarButton /> : null}
</div>
</Titlebar> </Titlebar>
) )
} }
return ( return (
<Titlebar <Titlebar
className={cn('flex min-w-0 gap-1 items-center font-semibold', titlebarInset, stickyClass)} className={cn(titlebarInset, stickyClass)}
hideBottomBorder={hideBottomBorder} hideBottomBorder={hideBottomBorder}
> >
<div className="flex w-full min-w-0 items-center gap-2 font-semibold">
<ReadOnlySessionIndicator variant="titlebar" /> <ReadOnlySessionIndicator variant="titlebar" />
<div className="flex min-w-0 flex-1 items-center justify-between gap-1"> <div className="flex min-w-0 flex-1 items-center justify-between gap-1">
{hideBackButton ? ( {hideBackButton ? (
@ -199,15 +202,16 @@ function SecondaryPageTitlebar({
</div> </div>
) : null ) : null
) : ( ) : (
<div className="flex min-w-0 flex-1 items-center"> <div className="flex min-w-0 items-center">
<BackButton>{title ?? t('back')}</BackButton> <BackButton>{title ?? t('back')}</BackButton>
</div> </div>
)} )}
<div className="flex shrink-0 flex-wrap items-center justify-end gap-0.5 min-w-0 max-w-[min(100%,14rem)] sm:max-w-none"> <div className="flex shrink-0 flex-wrap items-center justify-end gap-0.5 min-w-0 max-w-[min(100%,14rem)] sm:max-w-none">
{controls} {controls}
{isSmallScreen ? <ActiveRelaysTitlebarButton /> : null}
</div> </div>
</div> </div>
{isSmallScreen ? <ActiveRelaysTitlebarButton /> : null}
</div>
</Titlebar> </Titlebar>
) )
} }

Loading…
Cancel
Save