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

14
src/components/Note/SuperchatPaymentMethodLabel.tsx

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

13
src/components/Note/Zap.tsx

@ -100,6 +100,17 @@ export default function Zap({ @@ -100,6 +100,17 @@ export default function Zap({
showAt
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>
) : (
<>
@ -129,6 +140,7 @@ export default function Zap({ @@ -129,6 +140,7 @@ export default function Zap({
)}
</div>
) : null}
{!isProfileWall ? (
<div
className={cn(
'flex flex-wrap items-center gap-x-2 gap-y-1',
@ -147,6 +159,7 @@ export default function Zap({ @@ -147,6 +159,7 @@ export default function Zap({
</span>
) : null}
</div>
) : null}
{comment ? (
<SuperchatCommentMarkdown event={event} comment={comment} className="mt-2" />
) : null}

18
src/components/Profile/ProfileWallSuperchats.tsx

@ -1,10 +1,16 @@ @@ -1,10 +1,16 @@
import Superchat from '@/components/Note/Superchat'
import Zap from '@/components/Note/Zap'
import { ExtendedKind } from '@/constants'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'
import { Event } from 'nostr-tools'
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({
superchats,
isLoading
@ -24,12 +30,20 @@ export default function ProfileWallSuperchats({ @@ -24,12 +30,20 @@ export default function ProfileWallSuperchats({
if (superchats.length === 0) return null
const scrollable = superchats.length > PROFILE_WALL_SUPERCHAT_VISIBLE_CAP
return (
<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')}
</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) =>
event.kind === ExtendedKind.PAYMENT_NOTIFICATION ? (
<Superchat key={event.id} event={event} variant="profileWall" />

2
src/components/Titlebar/index.tsx

@ -12,7 +12,7 @@ export function Titlebar({ @@ -12,7 +12,7 @@ export function Titlebar({
return (
<div
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',
className
)}

16
src/components/ZapDialog/SuperchatRequestForm.tsx

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
import { Button } from '@/components/ui/button'
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 { ExtendedKind } from '@/constants'
import { createPaymentNotificationDraftEvent } from '@/lib/draft-event'
@ -30,6 +32,7 @@ export default function SuperchatRequestForm({ @@ -30,6 +32,7 @@ export default function SuperchatRequestForm({
const { t } = useTranslation()
const { publish, checkLogin, pubkey: selfPubkey } = useNostr()
const [message, setMessage] = useState('')
const [minPow, setMinPow] = useState(0)
const [sending, setSending] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement>(null)
@ -67,7 +70,7 @@ export default function SuperchatRequestForm({ @@ -67,7 +70,7 @@ export default function SuperchatRequestForm({
referencedEvent: paymentContext?.referencedEvent,
addClientTag: true
})
await publish(draft, { disableFallbacks: true })
await publish(draft, { disableFallbacks: true, minPow })
showSimplePublishSuccess(t('Superchat request sent'))
onDone()
} catch (error) {
@ -103,6 +106,17 @@ export default function SuperchatRequestForm({ @@ -103,6 +106,17 @@ export default function SuperchatRequestForm({
aria-label={t('Superchat message')}
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() ? (
<div className="mt-4 min-w-0">
<p className="text-xs font-medium text-muted-foreground">{t('Preview')}</p>

254
src/hooks/useProfileWall.tsx

@ -127,6 +127,130 @@ async function hydrateProfileWallSuperchatTargets( @@ -127,6 +127,130 @@ async function hydrateProfileWallSuperchatTargets(
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<
string,
{ badges: ResolvedProfileBadge[]; comments: Event[]; superchats: Event[]; lastUpdated: number }
@ -181,6 +305,29 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -181,6 +305,29 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
const [superchats, setSuperchats] = useState<Event[]>(hasUsefulWallCache ? (cached!.superchats ?? []) : [])
const [isLoading, setIsLoading] = useState(!hasUsefulWallCache)
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(
() => relayListsContentKey(favoriteRelays, blockedRelays),
@ -199,10 +346,10 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -199,10 +346,10 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
const bumpWallRefetch = useCallback(() => {
wallCacheByKey.delete(cacheKey)
queueMicrotask(() => {
setIsLoading(true)
setLoadingUnlessWallVisible()
setRefreshToken((t) => t + 1)
})
}, [cacheKey])
}, [cacheKey, setLoadingUnlessWallVisible])
const scheduleManualWallRefetch = useCallback(() => {
if (manualRefreshBumpScheduledRef.current) return
@ -210,23 +357,26 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -210,23 +357,26 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
wallCacheByKey.delete(cacheKey)
queueMicrotask(() => {
manualRefreshBumpScheduledRef.current = false
setIsLoading(true)
setLoadingUnlessWallVisible()
setRefreshToken((t) => t + 1)
})
}, [cacheKey])
}, [cacheKey, setLoadingUnlessWallVisible])
useEffect(() => {
if (!isValidPubkey(pkNormForHydrate)) return
let cancelled = false
void hydrateProfileBadgesFromLocalCache(pkNormForHydrate).then((local) => {
if (cancelled || local.length === 0) return
setBadges((prev) => (prev.length > 0 ? prev : local))
setIsLoading(false)
void hydrateProfileWallFromLocalCache(
pkNormForHydrate,
profileEventId,
isEventDeletedRef.current
).then((local) => {
if (cancelled) return
applyLocalWallHydrate(local)
})
return () => {
cancelled = true
}
}, [pkNormForHydrate])
}, [pkNormForHydrate, profileEventId, applyLocalWallHydrate])
useEffect(() => {
const pk = normalizeWallRefreshPubkey(pkNormForHydrate)
@ -309,6 +459,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -309,6 +459,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
try {
const pkNorm = userIdToPubkey(pubkey) || pubkey
if (!isValidPubkey(pkNorm)) {
if (!cancelled && runGen === runGenRef.current) setIsLoading(false)
return
}
@ -331,22 +482,26 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -331,22 +482,26 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
useGlobalRelayBootstrapRef.current
)
const localBadges = await hydrateProfileBadgesFromLocalCache(pkNorm)
if (!cancelled && localBadges.length > 0) {
setBadges(localBadges)
setIsLoading(false)
} else if (!cancelled) {
const localWall = await hydrateProfileWallFromLocalCache(
pkNorm,
profileEventId,
isEventDeletedRef.current
)
if (!cancelled) {
applyLocalWallHydrate(localWall)
if (localWall.badges.length === 0 && localWall.superchats.length === 0) {
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, {
foreground: true,
cacheFirst: false
cacheFirst: true
})
if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) {
const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls, {
cacheFirst: false
cacheFirst: true
})
if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy
}
@ -366,40 +521,36 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -366,40 +521,36 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
)
if (cancelled) return
if (resolvedBadges.length > 0 || localBadges.length === 0) {
if (resolvedBadges.length > 0 || localWall.badges.length === 0) {
setBadges(resolvedBadges)
}
setIsLoading(false)
// --- Wall comments (kind 1111) and attested superchats (9735 / 9740 + 9741) ---
let wallComments: Event[] = []
let wallSuperchats: Event[] = []
const profileId =
profileEventId?.trim().toLowerCase() && /^[0-9a-f]{64}$/.test(profileEventId.trim())
? 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 }
)
}
let wallSuperchats: Event[] = localWall.superchats
const profileId = normalizeProfileEventId(profileEventId)
const filters = buildProfileWallSuperchatFilters(pkNorm, profileId)
const pool = new Map<string, Event>()
for (const e of localWall.superchats) pool.set(e.id, e)
try {
const idbPayments = await indexedDb.getPaymentNotificationsForRecipient(pkNorm, 200)
for (const e of idbPayments) pool.set(e.id, e)
for (const e of await indexedDb.getPaymentNotificationsForRecipient(pkNorm, 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 */
}
try {
for (const e of await indexedDb.getPaymentSuperchatEventsMatchingFilters(filters, 800)) {
pool.set(e.id, e)
}
} catch {
/* optional */
}
@ -410,8 +561,10 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -410,8 +561,10 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
)
for (const e of localMatches) pool.set(e.id, e)
} catch {
/* ignore */
/* optional */
}
if (relayUrls.length > 0) {
try {
const rows = await Promise.all(
filters.map((filter) =>
@ -427,15 +580,17 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -427,15 +580,17 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
for (const e of batch) pool.set(e.id, e)
}
} catch {
/* ignore */
/* optional */
}
}
const attestations = [...pool.values()].filter(
(e) => e.kind === ExtendedKind.PAYMENT_ATTESTATION
)
const attestedIds = await resolveAttestedPaymentIdSet(pkNorm, attestations)
const hydratedTargets = await hydrateProfileWallSuperchatTargets(attestedIds, relayUrls)
for (const e of hydratedTargets) pool.set(e.id, e)
for (const e of await hydrateProfileWallSuperchatTargets(attestedIds, relayUrls)) {
pool.set(e.id, e)
}
if (profileId) {
wallComments = [...pool.values()]
@ -462,7 +617,6 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -462,7 +617,6 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
profileId,
attestedIds
)
}
if (cancelled) return
setComments(wallComments)
@ -487,7 +641,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -487,7 +641,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
return () => {
cancelled = true
}
}, [pubkey, profileEventId, cacheKey, refreshToken])
}, [pubkey, profileEventId, cacheKey, refreshToken, applyLocalWallHydrate])
const refresh = useCallback(() => {
scheduleManualWallRefetch()

12
src/layouts/SecondaryPageLayout/index.tsx

@ -176,20 +176,23 @@ function SecondaryPageTitlebar({ @@ -176,20 +176,23 @@ function SecondaryPageTitlebar({
if (titlebar) {
return (
<Titlebar
className={cn('flex min-w-0 items-center gap-2', titlebarInset, stickyClass)}
className={cn(titlebarInset, stickyClass)}
hideBottomBorder={hideBottomBorder}
>
<div className="flex w-full min-w-0 items-center gap-2">
<ReadOnlySessionIndicator variant="titlebar" />
<div className="min-w-0 flex-1">{titlebar}</div>
{isSmallScreen ? <ActiveRelaysTitlebarButton /> : null}
</div>
</Titlebar>
)
}
return (
<Titlebar
className={cn('flex min-w-0 gap-1 items-center font-semibold', titlebarInset, stickyClass)}
className={cn(titlebarInset, stickyClass)}
hideBottomBorder={hideBottomBorder}
>
<div className="flex w-full min-w-0 items-center gap-2 font-semibold">
<ReadOnlySessionIndicator variant="titlebar" />
<div className="flex min-w-0 flex-1 items-center justify-between gap-1">
{hideBackButton ? (
@ -199,15 +202,16 @@ function SecondaryPageTitlebar({ @@ -199,15 +202,16 @@ function SecondaryPageTitlebar({
</div>
) : null
) : (
<div className="flex min-w-0 flex-1 items-center">
<div className="flex min-w-0 items-center">
<BackButton>{title ?? t('back')}</BackButton>
</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">
{controls}
{isSmallScreen ? <ActiveRelaysTitlebarButton /> : null}
</div>
</div>
{isSmallScreen ? <ActiveRelaysTitlebarButton /> : null}
</div>
</Titlebar>
)
}

Loading…
Cancel
Save